Building a Portfolio with MDX and the App Router
Why I render every page from flat MDX files, and how I keep the content pipeline boring on purpose.
I wanted a portfolio I could update by writing a file and pushing it, without a CMS or an 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, and the output is plain HTML on a CDN. There's no server to
keep alive.
Why MDX over Markdown
Plain Markdown would cover 95% of what I write. MDX covers the other 5%, like the occasional interactive demo or custom callout. 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.
Keeping the pipeline boring is what keeps the writing fun.