Migrate blog from hardcoded TSX posts to MDX with auto-population

- Add gray-matter + next-mdx-remote for MDX rendering
- Create content/blog/ with .mdx files and frontmatter
- Add dynamic [slug] route and blog utility (src/lib/blog.ts)
- Blog index auto-populates from MDX files, sorted newest first
- Upgrade React to 19 for next-mdx-remote v6 compatibility
- Convert existing posts: website-relaunch, smirk-wallet, bauhaus-echo, crypto-checkout
This commit is contained in:
jwinterm
2026-03-10 10:46:41 -04:00
parent 3001d4bbe1
commit 2b9b1bf429
12 changed files with 1505 additions and 375 deletions
+64
View File
@@ -0,0 +1,64 @@
import fs from "fs";
import path from "path";
import matter from "gray-matter";
const CONTENT_DIR = path.join(process.cwd(), "content", "blog");
export type PostMeta = {
slug: string;
title: string;
date: string;
excerpt: string;
draft: boolean;
};
export type Post = PostMeta & {
content: string;
};
export function getAllPosts(): PostMeta[] {
if (!fs.existsSync(CONTENT_DIR)) return [];
return fs
.readdirSync(CONTENT_DIR)
.filter((f) => f.endsWith(".mdx"))
.map((filename) => {
const slug = filename.replace(/\.mdx$/, "");
const raw = fs.readFileSync(path.join(CONTENT_DIR, filename), "utf-8");
const { data } = matter(raw);
return {
slug,
title: data.title ?? slug,
date: data.date ?? "",
excerpt: data.excerpt ?? "",
draft: data.draft === true,
};
})
.filter((p) => !p.draft)
.sort((a, b) => (a.date > b.date ? -1 : 1));
}
export function getPost(slug: string): Post | null {
const filePath = path.join(CONTENT_DIR, `${slug}.mdx`);
if (!fs.existsSync(filePath)) return null;
const raw = fs.readFileSync(filePath, "utf-8");
const { data, content } = matter(raw);
return {
slug,
title: data.title ?? slug,
date: data.date ?? "",
excerpt: data.excerpt ?? "",
draft: data.draft === true,
content,
};
}
export function getAllSlugs(): string[] {
if (!fs.existsSync(CONTENT_DIR)) return [];
return fs
.readdirSync(CONTENT_DIR)
.filter((f) => f.endsWith(".mdx"))
.map((f) => f.replace(/\.mdx$/, ""));
}