Skip to main content
Back to writing
2 min read

Building a Portfolio with MDX and the App Router

Why I render every page from flat MDX files — and how the content pipeline stays boring on purpose.

engineeringnext.jstypescript

I wanted a portfolio I could update by writing a file and pushing it — no CMS, no database, no admin panel to babysit. MDX in the App Router gets me exactly that.

The content pipeline

Every post and project is a single .mdx file under content/. At build time the pipeline is deliberately dull:

  1. fs.readFileSync() the file
  2. gray-matter splits frontmatter from the body
  3. reading-time estimates how long it'll take to read
  4. the body is handed to next-mdx-remote/rsc to render as a server component
export function getAllPosts(): BlogPost[] {
  return readMdxFiles(BLOG_DIR)
    .map(parseBlogFile)
    .filter((post) => post.published)
    .sort((a, b) => (a.date < b.date ? 1 : -1));
}

Because it's all static generation, generateStaticParams() produces every route ahead of time. There's no server to keep alive — just HTML and a CDN.

Why MDX over Markdown

Plain Markdown would cover 95% of what I write. MDX covers the other 5%: the occasional interactive demo, a custom callout, a chart. I don't pay for that flexibility until I use it, and the authoring experience is still just writing.

The best content system is the one you'll actually keep using. For me that's a text file and git push.

That's the whole trick. Keep the pipeline boring so the writing stays fun.