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.
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:
fs.readFileSync()the filegray-mattersplits frontmatter from the bodyreading-timeestimates how long it'll take to read- the body is handed to
next-mdx-remote/rscto 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.