Shipping MDX-Powered Blog Posts with Next.js 15

Written by

Johnny Dang

Published on

February 10, 2025

View

0

Comments

0
Shipping MDX-Powered Blog Posts with Next.js 15

MDX lets your marketing stack move at the same cadence as your product UI. No CMS migrations, just authored prose that ships with your code.

Keeping blog content next to the components that render it means every deploy can bundle new stories, page metadata, and custom UI affordances. Below is the setup I use after studying the official MDX guide for the App Router and adapting it for a portfolio context.

Why MDX fits the App Router so well

MDX files compile to regular React components, which makes them a first-class citizen in the App Router. That unlocks a few advantages:

  • Co-locate stories, assets, and call-to-action logic under src/content.
  • Import JSX building blocks (cards, callouts, metrics) directly inside the markdown flow.
  • Eliminate runtime fetches by statically bundling the prose during next build.

When your content strategy evolves, you keep the feedback loop tight—add a section, run next dev, review, and commit.

Content architecture and naming

I prefer a flat src/content folder for blog posts. File names become slugs automatically, which keeps routing predictable.

text
src/
  content/
    designing-a-trustworthy-portfolio.mdx
    seo-ready-nextjs-mdx-blog.mdx

Frontmatter drives everything from listing cards to canonical URLs:

mdx
---
title: "Shipping MDX-Powered Blog Posts with Next.js 15"
description: "Field notes from wiring the Next.js App Router to MDX."
canonical: "https://nguyen-admin.com/blog/seo-ready-nextjs-mdx-blog"
image: "/blog/seo-ready-nextjs-mdx.svg"
---

The loader normalizes tags, keywords, and reading time. That means if an editor forgets a field, the build warns us during review instead of surprising production later.

Rendering with custom MDX components

The App Router guide recommends exporting a useMDXComponents helper. It transforms raw markdown elements into your design system primitives:

tsx
// src/mdx-components.tsx
export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    a: Anchor,
    code: InlineCode,
    pre: Pre,
    wrapper: Wrapper,
    ...components,
  };
}

By passing the result into compileMDX, every post inherits typography tokens, link styling, and syntax highlighting. You never copy-paste prose classes again.

SEO signals baked into content

Each post calculates readingMinutes, exposes keywords, and surfaces Open Graph data. With a canonical URL and image declared in frontmatter, generateMetadata can emit:

  • og:title, og:description, and og:image
  • twitter:card with large previews for visual posts
  • Structured authorship, publication, and update timestamps

Because the data lives beside the prose, updating canonical slugs or social cards is a single diff.

Launch checklist

Before pushing a new article, I run through this five-point audit:

  1. Verify Lighthouse scores remain green using npm run build.
  2. Skim the generated HTML to ensure headings remain nested correctly.
  3. Test the article route locally with NEXT_PUBLIC_SITE_URL set for accurate canonical links.
  4. Share the Open Graph URL in Slack to preview how the card renders.
  5. Ship—version control keeps the entire editorial history traceable.

Keeping marketing content in the same repo as the UI feels like cheating. With MDX powering the blog, the build pipeline stays simple, and every deploy ships with beautiful, SEO-friendly stories.