···11+import { getPostById, getAllPosts } from '@/utils/api';
22+33+// Generate the post, note that this is a "react server component"! it is
44+// allowed to be async
55+export default async function Post({ params: { id } }: { params: { id: string } }) {
66+ const { html, title, date } = await getPostById(id);
77+ return (
88+ <article>
99+ <h1>{title}</h1>
1010+ <h4>{date}</h4>
1111+ <div dangerouslySetInnerHTML={{ __html: html }} />
1212+ </article>
1313+ );
1414+}
1515+1616+// This function can statically allow nextjs to find all the posts that you
1717+// have made, and statically generate them
1818+export async function generateStaticParams() {
1919+ const posts = await getAllPosts();
2020+2121+ return posts.map((post) => ({
2222+ id: post.id,
2323+ }));
2424+}
2525+2626+// Set the title of the page to be the post title, note that we no longer use
2727+// e.g. next/head in app dir, and this can be async just like the server
2828+// component
2929+export async function generateMetadata({ params: { id } }: { params: { id: string } }) {
3030+ const { title } = await getPostById(id);
3131+ return {
3232+ title,
3333+ };
3434+}
+71
src/utils/api.ts
···11+import fs from 'fs';
22+import matter from 'gray-matter';
33+import { join } from 'path';
44+import { unified } from 'unified';
55+import remarkGfm from 'remark-gfm';
66+import rehypeSlug from 'rehype-slug';
77+import rehypeAutolinkHeadings from 'rehype-autolink-headings';
88+import remarkParse from 'remark-parse';
99+import remarkRehype from 'remark-rehype';
1010+import rehypeStringify from 'rehype-stringify';
1111+import rehypeShiki from '@leafac/rehype-shiki';
1212+import * as shiki from 'shiki';
1313+1414+// memoize/cache the creation of the markdown parser, this sped up the
1515+// building of the blog from ~60s->~10s
1616+let p: ReturnType<typeof getParserPre> | undefined;
1717+1818+async function getParserPre() {
1919+ return unified()
2020+ .use(remarkParse)
2121+ .use(remarkRehype)
2222+ .use(remarkGfm)
2323+ .use(rehypeShiki, {
2424+ highlighter: await shiki.getHighlighter({ theme: 'poimandres' }),
2525+ })
2626+ .use(rehypeStringify)
2727+ .use(rehypeSlug)
2828+ .use(rehypeAutolinkHeadings, {
2929+ content: (arg) => ({
3030+ type: 'element',
3131+ tagName: 'a',
3232+ properties: {
3333+ href: '#' + arg.properties?.id,
3434+ style: 'margin-right: 10px',
3535+ },
3636+ children: [{ type: 'text', value: '#' }],
3737+ }),
3838+ });
3939+}
4040+4141+function getParser() {
4242+ if (!p) {
4343+ p = getParserPre().catch((e) => {
4444+ p = undefined;
4545+ throw e;
4646+ });
4747+ }
4848+ return p;
4949+}
5050+5151+export async function getPostById(id: string) {
5252+ const realId = id.replace(/\.md$/, '');
5353+ const fullPath = join('_posts', `${realId}.md`);
5454+ const { data, content } = matter(await fs.promises.readFile(fullPath, 'utf8'));
5555+5656+ const parser = await getParser();
5757+ const html = await parser.process(content);
5858+5959+ return {
6060+ ...data,
6161+ title: data.title,
6262+ id: realId,
6363+ date: `${data.date?.toISOString().slice(0, 10)}`,
6464+ html: html.value.toString(),
6565+ };
6666+}
6767+6868+export async function getAllPosts() {
6969+ const posts = await Promise.all(fs.readdirSync('_posts').map((id) => getPostById(id)));
7070+ return posts.sort((post1, post2) => (post1.date > post2.date ? -1 : 1));
7171+}