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
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.'
image: /img/Collective Action Climate.png
image: /img/Collective Action Climate.jpg
youtube: https://www.youtube.com/watch?v=SLXtnCL6IxE
podcast: https://anchor.fm/life-itself/episodes/Collective-Action-Problems--Climate-Change-e1h4o6e/a-a7gpq18
featured: true

View File

@ -3,7 +3,7 @@ title: Web3 and Post-State Technocracy
created: 2022-03-21
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."
image: /img/technocracy.jpg
image: /img/post-state-technocracy.jpg
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
featured: true

1
site/.gitignore vendored
View File

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

View File

@ -1,5 +1,4 @@
import { Tooltip } from './Tooltip';
import siteConfig from '../config/siteConfig.js'
/**
* 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">
{posts && posts.map((post) => (
<div key={post.title} className="flex flex-col rounded-lg shadow-lg overflow-hidden">
<div className="flex-shrink-0">
{post.image ? <img className="h-48 w-full object-cover" src={post.image} alt={post.title} />
: <div className="h-20 w-full bg-slate-500" />
}
</div>
{post.image ?
<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="flex-1 bg-slate-800 p-6 flex flex-col justify-between">
<div className="flex-1">
<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" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<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>
<Nav />
<main>
@ -45,7 +43,7 @@ export default function Layout({ children }) {
target="_blank"
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}
{' '}
Licensed under a CC-By 4.0 International License

View File

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

View File

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

View File

@ -5,6 +5,9 @@ import rehypeSlug from 'rehype-slug'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
import wikiLinkPlugin from "remark-wiki-link-plus"
const isValidDate = dateObject => new Date(dateObject)
.toString() !== 'Invalid Date';
const ObsidianAliases = defineNestedType(() => ({
name: 'Obsidian',
filePathPattern: '**/*.md*',
@ -22,6 +25,7 @@ const OtherPage = defineDocumentType(() => ({
date: { type: "date", description: "This will be the publication date" },
image: { type: "string" },
description: { type: 'string' },
keywords: { type: "string" },
youtube: { type: "string" },
podcast: { type: "string" },
featured: { type: "boolean", default: false },
@ -31,13 +35,19 @@ const OtherPage = defineDocumentType(() => ({
computedFields: {
date: {
type: "date",
resolve: (doc) => new Date(doc.date).toLocaleDateString('en-US', {
weekday: "long", year: "numeric", month: "long", day: "numeric"
})
resolve: (doc) => {
const formattedDate = new Date(doc.date).toLocaleDateString('en-US', {
weekday: "long", year: "numeric", month: "long", day: "numeric"
})
return isValidDate(formattedDate) ? formattedDate : null
}
},
created: {
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": {
"dev": "next dev",
"build": "next build",
"postbuild": "NODE_OPTIONS='--experimental-json-modules' node ./scripts/generate-sitemap.mjs",
"export": "next export",
"start": "next start"
},
@ -10,34 +11,31 @@
"@floating-ui/react-dom-interactions": "^0.6.0",
"@headlessui/react": "^1.4.1",
"@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",
"contentlayer": "^0.1.2",
"framer-motion": "^6.3.3",
"gray-matter": "^4.0.3",
"hast-util-to-string": "^2.0.0",
"next": "^12.1.0",
"next-contentlayer": "^0.1.2",
"next-seo": "^4.28.1",
"next-themes": "^0.1.1",
"react": "^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-slug": "^5.0.1",
"remark-gfm": "^3.0.0",
"remark-parse": "^10.0.1",
"remark-slug": "^7.0.0",
"remark-toc": "^8.0.0",
"remark-wiki-link-plus": "^1.0.0",
"tailwindcss": "^3.0.0",
"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 { allOtherPages } from 'contentlayer/generated';
import { useMDXComponent } from 'next-contentlayer/hooks';
import siteConfig from "../config/siteConfig"
export default function Page({ body, ...rest }) {
@ -9,19 +8,12 @@ export default function Page({ body, ...rest }) {
const children = {
Component,
frontmatter: {
...rest,
date: rest.date === "Invalid Date" ? null : rest.date,
created: rest.created === "Invalid Date" ? null : rest.created
...rest
},
};
// 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 (
<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 '../styles/global.css'
import "react-lite-youtube-embed/dist/LiteYouTubeEmbed.css";
import siteConfig from '../config/siteConfig.js'
import Layout from '../components/Layout'
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();