Merge pull request #169 from life-itself/164-optimize-seo

[site/seo]: page speed optimization, keywords and sitemap for seo
This commit is contained in:
Khalil Ali 2022-06-02 15:40:31 +03:00 committed by GitHub
commit f440456dd0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 783 additions and 6564 deletions

View File

@ -3,7 +3,7 @@ title: Collective Action Problems & Climate Change
created: 2022-04-13 created: 2022-04-13
date: 2022-03-01 date: 2022-03-01
description: 'In this episode we use the example of KlimaDAO to explore the interaction between climate change and the public goods problem.' description: 'In this episode we use the example of KlimaDAO to explore the interaction between climate change and the public goods problem.'
image: /img/Collective Action Climate.png image: /img/Collective Action Climate.jpg
youtube: https://www.youtube.com/watch?v=SLXtnCL6IxE youtube: https://www.youtube.com/watch?v=SLXtnCL6IxE
podcast: https://anchor.fm/life-itself/episodes/Collective-Action-Problems--Climate-Change-e1h4o6e/a-a7gpq18 podcast: https://anchor.fm/life-itself/episodes/Collective-Action-Problems--Climate-Change-e1h4o6e/a-a7gpq18
featured: true featured: true

View File

@ -3,7 +3,7 @@ title: Web3 and Post-State Technocracy
created: 2022-03-21 created: 2022-03-21
date: 2022-02-17 date: 2022-02-17
description: "In this episode we explore the aspirational transition from the existing US-led international order to a world in which blockchain technology and technocracy are the new foundations for global human governance." description: "In this episode we explore the aspirational transition from the existing US-led international order to a world in which blockchain technology and technocracy are the new foundations for global human governance."
image: /img/technocracy.jpg image: /img/post-state-technocracy.jpg
youtube: https://www.youtube.com/watch?v=gZ0iCJkM3PU youtube: https://www.youtube.com/watch?v=gZ0iCJkM3PU
podcast: https://anchor.fm/life-itself/episodes/On-Web3-and-Post-State-Technocracy-with-Stephen-Diehl--Rufus-Pollock-e1g4cpe podcast: https://anchor.fm/life-itself/episodes/On-Web3-and-Post-State-Technocracy-with-Stephen-Diehl--Rufus-Pollock-e1g4cpe
featured: true featured: true

1
site/.gitignore vendored
View File

@ -14,6 +14,7 @@
# production # production
/build /build
public/sitemap.xml
# misc # misc
.DS_Store .DS_Store

View File

@ -1,5 +1,4 @@
import { Tooltip } from './Tooltip'; import { Tooltip } from './Tooltip';
import siteConfig from '../config/siteConfig.js'
/** /**
* Component for adding previews on hovering over anchor tags with relative paths * Component for adding previews on hovering over anchor tags with relative paths

View File

@ -14,11 +14,12 @@ export function Latest({ posts }) {
<div className="mt-12 max-w-lg mx-auto grid gap-5 lg:grid-cols-3 lg:max-w-none"> <div className="mt-12 max-w-lg mx-auto grid gap-5 lg:grid-cols-3 lg:max-w-none">
{posts && posts.map((post) => ( {posts && posts.map((post) => (
<div key={post.title} className="flex flex-col rounded-lg shadow-lg overflow-hidden"> <div key={post.title} className="flex flex-col rounded-lg shadow-lg overflow-hidden">
<div className="flex-shrink-0"> {post.image ?
{post.image ? <img className="h-48 w-full object-cover" src={post.image} alt={post.title} /> <div className="h-48 flex-shrink-0">
<img className="" width="100%" height="100%" src={post.image} alt={post.title} />
</div>
: <div className="h-20 w-full bg-slate-500" /> : <div className="h-20 w-full bg-slate-500" />
} }
</div>
<div className="flex-1 bg-slate-800 p-6 flex flex-col justify-between"> <div className="flex-1 bg-slate-800 p-6 flex flex-col justify-between">
<div className="flex-1"> <div className="flex-1">
<p className="text-sm font-medium text-indigo-600 dark:text-yellow-500"> <p className="text-sm font-medium text-indigo-600 dark:text-yellow-500">

View File

@ -12,8 +12,6 @@ export default function Layout({ children }) {
<meta name="viewport" content="initial-scale=1.0, width=device-width" /> <meta name="viewport" content="initial-scale=1.0, width=device-width" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" /> <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
{/* <link href="https://fonts.googleapis.com/css2?family=Nunito+Sans&display=swap" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Inter&display=swap" rel="stylesheet"></link> */}
</Head> </Head>
<Nav /> <Nav />
<main> <main>
@ -45,7 +43,7 @@ export default function Layout({ children }) {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<img src={siteConfig.authorLogo} alt={siteConfig.author} className="mx-2 h-6 inline-block" /> <img src={siteConfig.authorLogo} alt={siteConfig.author} width="20" height="20" className="mx-2 h-6 inline-block" />
{siteConfig.author} {siteConfig.author}
{' '} {' '}
Licensed under a CC-By 4.0 International License Licensed under a CC-By 4.0 International License

View File

@ -1,9 +1,15 @@
import Head from 'next/head' import Head from 'next/head'
import ReactPlayer from 'react-player/lazy' import dynamic from 'next/dynamic'
import { NextSeo } from 'next-seo' import { NextSeo } from 'next-seo'
import siteConfig from "../config/siteConfig" import siteConfig from "../config/siteConfig"
import { Paragraph } from './Paragraph' import LiteYouTubeEmbed from "react-lite-youtube-embed"
import { Anchor } from './Anchor' import { YOUTUBE_REGEX } from "../lib/constants"
const Anchor = dynamic(() => import('./Anchor').then(module => module.Anchor), {
ssr: false
})
const Paragraph = dynamic(() => import("./Paragraph").then(mod => mod.Paragraph))
const components = { const components = {
Head, Head,
@ -11,30 +17,25 @@ const components = {
a: Anchor a: Anchor
} }
export default function MdxPage({ children, editUrl }) { export default function MdxPage({ children }) {
const { Component, frontmatter: { const { Component, frontmatter: {
title, description, date, authors, youtube, podcast, image, _raw title, description, date, keywords, youtube, podcast, image, _raw
}} = children }} = children
let youtubeThumnbnail let youtubeThumnbnail
let podcastEmbed
if (youtube && !image) { const youtubeId =
youtube && YOUTUBE_REGEX.test(youtube) && youtube.split(/^|=|\//).pop();
if (youtubeId && !image) {
// get the youtube thumbnail image from https://img.youtube.com/vi/<youtube-video-id>/maxresdefault.jpg // get the youtube thumbnail image from https://img.youtube.com/vi/<youtube-video-id>/maxresdefault.jpg
const regex = youtubeThumnbnail = youtube.replace(
/\www.youtube.com\/\embed\/|youtube.com\/\embed\/|youtu.be\/|\www.youtube.com\/\watch\?v=|\youtube.com\/\watch\?v=/; YOUTUBE_REGEX,
youtubeThumnbnail = `https://img.youtube.com/vi/${youtubeId}/maxresdefault.jpg`
youtube.replace(regex, "img.youtube.com/vi/") + "/maxresdefault.jpg"; );
} }
if (podcast && podcast.includes("life-itself")) { const PodcastIcon = siteConfig.social.find((s) => s.name === "Podcast").icon;
const podcastUrl = podcast
podcastEmbed = ([
podcastUrl.slice(0, "https://anchor.fm/life-itself".length),
"/embed",
podcastUrl.slice("https://anchor.fm/life-itself".length)
].join(""))
}
const titleFromUrl = _raw.flattenedPath const titleFromUrl = _raw.flattenedPath
.split("/") .split("/")
@ -48,6 +49,11 @@ export default function MdxPage({ children, editUrl }) {
? siteConfig.url + image ? siteConfig.url + image
: youtubeThumnbnail ? youtubeThumnbnail : null : youtubeThumnbnail ? youtubeThumnbnail : null
// enable editing content only for claims, concepts, and guide for now
const editUrl = ['claims', 'concepts', 'guide'].includes(_raw.sourceFileDir)
? siteConfig.repoRoot + siteConfig.repoEditPath + _raw.sourceFilePath
: null
return ( return (
<> <>
<NextSeo <NextSeo
@ -57,6 +63,11 @@ export default function MdxPage({ children, editUrl }) {
openGraph={{ openGraph={{
title: SeoTitle, title: SeoTitle,
description: description, description: description,
url: `${siteConfig.url}/${_raw.flattenedPath}`,
type: "article",
article: {
tags: keywords ? keywords.split(",") : []
},
images: imageUrl images: imageUrl
? ([ ? ([
{ {
@ -69,16 +80,14 @@ export default function MdxPage({ children, editUrl }) {
]) ])
: siteConfig.nextSeo.openGraph.images, : siteConfig.nextSeo.openGraph.images,
}} }}
additionalMetaTags={[
{ name: "keywords", content: keywords ? keywords : "" }
]}
/> />
<article className="prose dark:prose-invert prose-a:break-all mx-auto p-6"> <article className="prose dark:prose-invert prose-a:break-all mx-auto p-6">
<header> <header>
<div className="mb-6"> <div className="mb-6">
{title && <h1 className="mb-0">{title}</h1>} {title && <h1 className="mb-0">{title}</h1>}
{authors && (
<div className="-mt-6">
<p className="opacity-60 pl-1">{authors}</p>
</div>
)}
{date && ( {date && (
<p className="text-gray-900 dark:text-gray-500 text-sm pl-2"> <p className="text-gray-900 dark:text-gray-500 text-sm pl-2">
on {date} on {date}
@ -87,36 +96,21 @@ export default function MdxPage({ children, editUrl }) {
{description && ( {description && (
<p className="">{description}</p> <p className="">{description}</p>
)} )}
{youtube && ( {youtubeId && (
<div className="relative pt-[56.25%]"> <LiteYouTubeEmbed id={youtubeId} />
<ReactPlayer
className="absolute top-0 left-0"
width="100%"
height="100%"
url={youtube}
/>
</div>
)} )}
{podcast && ( {podcast && (
<div className="pt-4"> <div className="pt-4">
<ul className="list-disc"> <ul className="list-disc">
<li> <li>
Podcast: &nbsp; <a className="flex items-center" target="_blank" rel="noopener" href={podcast}>
<a href={podcast}>{podcast}</a> <div className="w-4 mr-2">
<PodcastIcon />
</div>
<p className="m-0">Listen to this podcast</p>
</a>
</li> </li>
</ul> </ul>
{podcastEmbed && (
<div className="md:mx-4">
<iframe
src={podcastEmbed}
height="100px"
width="100%"
frameBorder="0"
scrolling="no"
className="rounded-md"
/>
</div>
)}
</div> </div>
)} )}
</div> </div>

View File

@ -1,30 +1,15 @@
import ReactPlayer from "react-player"; import LiteYouTubeEmbed from "react-lite-youtube-embed";
import { YOUTUBE_REGEX } from "../lib/constants";
const videoLinks = [
"youtube.com",
"dailymotion.com",
"vimeo.com",
"soundcloud.com",
"facebook.com/watch",
"twitch.com",
];
export const Paragraph = (props) => { export const Paragraph = (props) => {
if ( if (
typeof props.children == "object" && typeof props.children == "object" &&
props.children.props && props.children.props &&
props.children.props.href && props.children.props.href &&
videoLinks.some((str) => props.children.props.href.includes(str)) YOUTUBE_REGEX.test(props.children.props.href)
) ) {
return ( const youtubeId = props.children.props.href.split(/^|=|\//).pop();
<div className="relative pt-[56.25%]" {...props}> return <LiteYouTubeEmbed id={youtubeId} />;
<ReactPlayer }
className="absolute top-0 left-0"
width="100%"
height="100%"
url={props.children.props.href}
/>
</div>
);
return <p {...props} />; return <p {...props} />;
}; };

View File

@ -5,6 +5,9 @@ import rehypeSlug from 'rehype-slug'
import rehypeAutolinkHeadings from 'rehype-autolink-headings' import rehypeAutolinkHeadings from 'rehype-autolink-headings'
import wikiLinkPlugin from "remark-wiki-link-plus" import wikiLinkPlugin from "remark-wiki-link-plus"
const isValidDate = dateObject => new Date(dateObject)
.toString() !== 'Invalid Date';
const ObsidianAliases = defineNestedType(() => ({ const ObsidianAliases = defineNestedType(() => ({
name: 'Obsidian', name: 'Obsidian',
filePathPattern: '**/*.md*', filePathPattern: '**/*.md*',
@ -22,6 +25,7 @@ const OtherPage = defineDocumentType(() => ({
date: { type: "date", description: "This will be the publication date" }, date: { type: "date", description: "This will be the publication date" },
image: { type: "string" }, image: { type: "string" },
description: { type: 'string' }, description: { type: 'string' },
keywords: { type: "string" },
youtube: { type: "string" }, youtube: { type: "string" },
podcast: { type: "string" }, podcast: { type: "string" },
featured: { type: "boolean", default: false }, featured: { type: "boolean", default: false },
@ -31,13 +35,19 @@ const OtherPage = defineDocumentType(() => ({
computedFields: { computedFields: {
date: { date: {
type: "date", type: "date",
resolve: (doc) => new Date(doc.date).toLocaleDateString('en-US', { resolve: (doc) => {
const formattedDate = new Date(doc.date).toLocaleDateString('en-US', {
weekday: "long", year: "numeric", month: "long", day: "numeric" weekday: "long", year: "numeric", month: "long", day: "numeric"
}) })
return isValidDate(formattedDate) ? formattedDate : null
}
}, },
created: { created: {
type: "date", type: "date",
resolve: (doc) => new Date(doc.created).toLocaleDateString('en-US') resolve: (doc) => {
const formattedDate = new Date(doc.created).toLocaleDateString('en-US')
return isValidDate(formattedDate) ? formattedDate : null
}
}, },
} }
})); }));

2
site/lib/constants.js Normal file
View File

@ -0,0 +1,2 @@
export const YOUTUBE_REGEX =
/^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#\&\?]*).*/;

7095
site/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,7 @@
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"postbuild": "NODE_OPTIONS='--experimental-json-modules' node ./scripts/generate-sitemap.mjs",
"export": "next export", "export": "next export",
"start": "next start" "start": "next start"
}, },
@ -10,34 +11,31 @@
"@floating-ui/react-dom-interactions": "^0.6.0", "@floating-ui/react-dom-interactions": "^0.6.0",
"@headlessui/react": "^1.4.1", "@headlessui/react": "^1.4.1",
"@heroicons/react": "^1.0.4", "@heroicons/react": "^1.0.4",
"@mdx-js/loader": "^2.0.0",
"@mdx-js/react": "^2.0.0",
"@next/mdx": "^12.1.0",
"@silvenon/remark-smartypants": "^1.0.0",
"@tailwindcss/typography": "^0.5.2", "@tailwindcss/typography": "^0.5.2",
"contentlayer": "^0.1.2", "contentlayer": "^0.1.2",
"framer-motion": "^6.3.3", "framer-motion": "^6.3.3",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"hast-util-to-string": "^2.0.0",
"next": "^12.1.0", "next": "^12.1.0",
"next-contentlayer": "^0.1.2", "next-contentlayer": "^0.1.2",
"next-seo": "^4.28.1", "next-seo": "^4.28.1",
"next-themes": "^0.1.1", "next-themes": "^0.1.1",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-player": "^2.10.0", "react-lite-youtube-embed": "^2.2.2",
"sharp": "^0.30.5"
},
"devDependencies": {
"autoprefixer": "^10.2.6",
"globby": "^13.1.1",
"hast-util-to-string": "^2.0.0",
"postcss": "^8.3.5",
"prettier": "^2.6.2",
"rehype-autolink-headings": "^6.1.1", "rehype-autolink-headings": "^6.1.1",
"rehype-slug": "^5.0.1", "rehype-slug": "^5.0.1",
"remark-gfm": "^3.0.0", "remark-gfm": "^3.0.0",
"remark-parse": "^10.0.1", "remark-parse": "^10.0.1",
"remark-slug": "^7.0.0",
"remark-toc": "^8.0.0",
"remark-wiki-link-plus": "^1.0.0", "remark-wiki-link-plus": "^1.0.0",
"tailwindcss": "^3.0.0",
"unist-util-find": "^1.0.2" "unist-util-find": "^1.0.2"
},
"devDependencies": {
"autoprefixer": "^10.2.6",
"postcss": "^8.3.5",
"tailwindcss": "^3.0.0"
} }
} }

View File

@ -1,7 +1,6 @@
import MdxPage from '../components/MDX'; import MdxPage from '../components/MDX';
import { allOtherPages } from 'contentlayer/generated'; import { allOtherPages } from 'contentlayer/generated';
import { useMDXComponent } from 'next-contentlayer/hooks'; import { useMDXComponent } from 'next-contentlayer/hooks';
import siteConfig from "../config/siteConfig"
export default function Page({ body, ...rest }) { export default function Page({ body, ...rest }) {
@ -9,19 +8,12 @@ export default function Page({ body, ...rest }) {
const children = { const children = {
Component, Component,
frontmatter: { frontmatter: {
...rest, ...rest
date: rest.date === "Invalid Date" ? null : rest.date,
created: rest.created === "Invalid Date" ? null : rest.created
}, },
}; };
// enable editing content only for claims, concepts, and guide for now
const editUrl = ['claims', 'concepts', 'guide'].includes(rest._raw.sourceFileDir)
? siteConfig.repoRoot + siteConfig.repoEditPath + rest._raw.sourceFilePath
: null
return ( return (
<MdxPage children={children} editUrl={editUrl} /> <MdxPage children={children} />
); );
} }

View File

@ -5,6 +5,7 @@ import { DefaultSeo } from 'next-seo'
import { ThemeProvider } from 'next-themes' import { ThemeProvider } from 'next-themes'
import '../styles/global.css' import '../styles/global.css'
import "react-lite-youtube-embed/dist/LiteYouTubeEmbed.css";
import siteConfig from '../config/siteConfig.js' import siteConfig from '../config/siteConfig.js'
import Layout from '../components/Layout' import Layout from '../components/Layout'
import * as gtag from '../lib/gtag' import * as gtag from '../lib/gtag'

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:29d50bf63165133404309fea97324eff18a7cd86a3172d39e0c3d0cd635b62fe
size 141203

View File

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:df3e54f72d5eea93eda159366a0c5d561db0e259c3125c3d49e20c3da433b8ea
size 1107869

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:18bf3b1fb17ea49bd7a616d992245f40e34eba33e5e49739c1bbe32cc2970054
size 60178

View File

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4d4728d0239c3103ca7cd6da93a0ad6d2f69f3df0f6bebeac72e44bf36105e0b
size 27545

View File

@ -0,0 +1,45 @@
import { writeFileSync } from "fs";
import { globby } from "globby";
import prettier from "prettier";
async function generate() {
const prettierConfig = await prettier.resolveConfig("./.prettierrc.js");
const pages = await globby([
"pages/!(\\[**])*.js*",
"content/**/*.md*",
"!pages/_*.js*",
"!pages/api",
"!pages/404.js*",
]);
const sitemap = `
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${pages
.map((page) => {
const path = page
.replace("pages", "")
.replace("content", "")
.replace(/.jsx*/, "")
.replace(/.mdx*/, "");
const route = path === "/index" ? "" : path;
return `
<url>
<loc>${`https://web3.lifeiteself.us${route}`}</loc>
</url>
`;
})
.join("")}
</urlset>
`;
const formatted = prettier.format(sitemap, {
...prettierConfig,
parser: "html",
});
// eslint-disable-next-line no-sync
writeFileSync("public/sitemap.xml", formatted);
}
generate();