This blog is plain MDX files on disk, rendered by Next.js 15 with the App Router. There is no CMS and no database for content. The posts are .mdx files in content/blog, with a small data layer on top.
Wiring up @next/mdx
The setup starts in next.config.mjs. It registers the MDX file extensions and wraps the config with createMDX, passing the remark and rehype plugins:
import createMDX from "@next/mdx";
const withMDX = createMDX({
options: {
remarkPlugins: ["remark-gfm", "remark-frontmatter"],
rehypePlugins: ["rehype-slug"],
},
});remark-gfm adds GitHub-flavored markdown: tables, task lists and autolinks. remark-frontmatter strips the YAML block at the top of each file, so it never shows on the page. rehype-slug adds id attributes to headings, and rehype-pretty-code handles syntax highlighting.
Turbopack needs plugin names as strings
This part cost me an afternoon. In development the project runs next dev --turbopack. Turbopack's engine is written in Rust, and it cannot pass JavaScript functions across the boundary between JavaScript and Rust. An imported plugin function works under webpack but fails under Turbopack.
So every plugin is passed as a string, or a [string, options] tuple, instead of an imported function:
- Do this:
remarkPlugins: ["remark-gfm"] - Not this:
remarkPlugins: [remarkGfm]
Next.js resolves the string to the real module itself. A string can cross the boundary, so Turbopack accepts it. The same config works unchanged when next build runs for production.
Static pages from generateStaticParams
Every post is static. The dynamic route at app/blog/[slug]/page.tsx lists every slug at build time:
generateStaticParamsreads the slugs from disk and returns one{ slug }per published post.- Next.js renders each route to static HTML.
- The page imports the matching
.mdxfile and renders it as a React Server Component.
export async function generateStaticParams() {
return getPostSlugs().map((slug) => ({ slug }));
}Everything is known at build time. Nothing compiles MDX at runtime and nothing is fetched in the browser. Posts are read from the filesystem during the build and ship as static HTML. I still get components, syntax highlighting and inline code while writing.
The Next.js development page describes the kind of sites I build this way.