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:
commit
f440456dd0
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
# production
|
||||
/build
|
||||
public/sitemap.xml
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
},
|
||||
}
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
export const YOUTUBE_REGEX =
|
||||
/^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#\&\?]*).*/;
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:29d50bf63165133404309fea97324eff18a7cd86a3172d39e0c3d0cd635b62fe
|
||||
size 141203
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:df3e54f72d5eea93eda159366a0c5d561db0e259c3125c3d49e20c3da433b8ea
|
||||
size 1107869
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:18bf3b1fb17ea49bd7a616d992245f40e34eba33e5e49739c1bbe32cc2970054
|
||||
size 60178
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4d4728d0239c3103ca7cd6da93a0ad6d2f69f3df0f6bebeac72e44bf36105e0b
|
||||
size 27545
|
||||
|
|
@ -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();
|
||||
Loading…
Reference in New Issue