_
Why I Chose MDX for My Next.js Portfolio Blog
When I started building my portfolio website, I had a very simple goal: I wanted a place where I could write and publish content without turning my personal site into a mini CMS project. I did not want dashboards, admin panels, or a content setup that felt heavier than the blog itself. I just wanted to write, commit, and ship.
That is when I ran into MDX, and it immediately felt like the right tool for this project.
And yes, this article is also written in MDX 🙂 That feels very on-brand.
What MDX Actually Is
MDX is Markdown with React components inside it. That sounds like a small upgrade, but it changes a lot.
You still write content in a simple Markdown-like format, but now you can also import components, render custom UI, use local assets, and treat an article like a real part of your app instead of a disconnected text file.
import { Tag } from "@/components/Tag";
# My article title
Here is a regular Markdown paragraph.
<Tag label="Built with React" />
You can even import it as component in regular codebase:
import Article from "./Article.mdx";
export default function App() {
return <Article />;
}
For me, that was the whole point. I wanted writing to stay simple, but I also wanted more than plain Markdown could offer.
Why I Chose It for This Portfolio
This portfolio is built with React, Next.js App Router, TypeScript, Tailwind CSS, and local content files. In that setup, MDX feels very natural. It gives me a few things I care about:
- I can write blog posts like plain text.
- I can keep every post in the repo right next to the code.
- I can import images from the same folder as the article.
- I can render React components inside the post when I need them.
- I do not need a separate CMS just to publish a post.
That last point mattered the most. This is a personal website, not a publishing platform with editors, workflows, and content approvals. For this kind of project, MDX feels light, practical, and honestly a lot more enjoyable.
How to Set Up MDX in Your Next.js Project
The setup here is intentionally small. First, I installed the MDX packages for Next.js, plus two small plugins:
npm install rehype-slug rehype-starry-night
npm install -D @next/mdx @mdx-js/loader @mdx-js/react @types/mdx
Then I configured next.config.ts to support .mdx files:
import createMDX from "@next/mdx";
const nextConfig = {
pageExtensions: ["js", "jsx", "md", "mdx", "ts", "tsx"],
};
const withMDX = createMDX({
options: {
remarkPlugins: [],
rehypePlugins: ["rehype-starry-night", "rehype-slug"],
},
});
export default withMDX(nextConfig);
About the Plugins, very briefly:
rehype-starry-nightgives syntax highlighting for code blocks.rehype-slugadds IDs to headings, which helps the table of contents and anchor links work properly.
That is enough for this project. No plugin collection addiction required.
One More Thing That Makes It Feel Polished
Another piece I really like is mdx-components.tsx.
const components: MDXComponents = {
h2: withClassName(
"h2",
"text-main mt-8 mb-3 text-2xl font-medium tracking-tight scroll-mt-24"
),
h3: withClassName(
"h3",
"text-main mt-6 mb-2 text-xl font-medium tracking-tight scroll-mt-24"
),
p: withClassName("p", "text-secondary indent-4 mb-4 leading-relaxed"),
ul: withClassName("ul", "text-secondary mb-4 list-disc pl-5"),
a: withClassName(
"a",
"text-main underline underline-offset-2"
),
img: withClassName("img", "my-4 mx-auto block rounded-xl max-w-full"),
};
This file maps standard Markdown elements like headings, paragraphs, lists, links, and images to the styles I want across the whole blog. So instead of styling every article manually, I define that once and keep writing.
What a Post Looks Like in This Repo
In this project, every post lives in its own folder inside blog/, and that folder contains the article file plus local assets.
That means the content and images stay together, which is a very nice little quality-of-life detail.
import { CodeBlock } from "@/components/CodeBlock";
import cover from "./assets/logo.png";
export const metadata = {
title: "Why I Chose MDX for My Next.js Portfolio Blog",
description:
"How MDX became the simplest way for me to write blog content in my React and Next.js portfolio project.",
readTime: "6 min read",
tags: ["MDX", "Next.js", "React"],
image: cover.src,
};
# Why I Chose MDX for My Next.js Portfolio Blog
Bla bla bla...
Then inside the blog page, we can import the MDX file directly, render it, and reuse its metadata for SEO.
export default async function BlogPostPage({ params }) {
const { default: ArticleContent, metadata } = await import(
`@blog/${params.slug}/markdown.mdx`
);
return (
<>
<Hero title={metadata.title} />
<ArticleContent />
</>
);
}
// Powers <title>, <meta description>, and Open Graph tags
export async function generateMetadata({ params }) {
const { metadata } = await import(`@blog/${params.slug}/markdown.mdx`);
return {
title: metadata.title,
description: metadata.description,
openGraph: {
title: metadata.title,
description: metadata.description,
images: [metadata.image],
},
};
}
// Tells Next.js which slugs to pre-render at build time
export async function generateStaticParams() {
const { posts } = await getSomehowListOfArticles();
return posts.map((p) => ({ slug: p.slug }));
}
That is another reason I like MDX here: one file gives me both the content and the metadata I need for the page itself.
Final Thoughts
MDX is not the answer to every content problem, and I would not use it for every kind of product. But for a personal portfolio blog built with React and Next.js, it feels like a great fit. It gives me the simplicity of Markdown, the flexibility of React, and a workflow that stays very close to the codebase. If you are building a developer-focused site, documentation section, or personal blog and you want your content to live close to your code, MDX is absolutely worth trying.
Sometimes the best tooling choice is not the most powerful one. It is the one that lets you keep writing.