<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged next-mdx-remote</title>
<link>https://coderlegion.com/tag/next-mdx-remote</link>
<description>Powered by Question2Answer</description>
<item>
<title>Complete MDX Blog Setup with next-mdx-remote and Next.js 16 (2026 Beginner Guide)</title>
<link>https://coderlegion.com/23799/complete-mdx-blog-setup-with-next-mdx-remote-and-next-js-16-2026-beginner-guide</link>
<description>&lt;p&gt;If you've ever tried writing blog posts in plain Markdown inside a Next.js app, you've probably hit a wall pretty fast. Plain Markdown can't render React components, can't handle custom styled code blocks, and definitely can't drop in an interactive widget in the middle of a paragraph.&lt;/p&gt;
&lt;p&gt;That's exactly the gap MDX fills — and &lt;code&gt;next-mdx-remote&lt;/code&gt; is the easiest way to load MDX content that lives outside your &lt;code&gt;app&lt;/code&gt; folder (like a &lt;code&gt;content/blogs&lt;/code&gt; directory) and render it as real React.&lt;/p&gt;
&lt;p&gt;In this guide, you'll build a complete, production-ready blog setup using &lt;code&gt;next-mdx-remote&lt;/code&gt; in Next.js 16 — the same setup powering this very blog. By the end, you'll have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MDX files stored in a &lt;code&gt;content/blogs&lt;/code&gt; folder&lt;/li&gt;
&lt;li&gt;A reusable &lt;code&gt;getAllPosts()&lt;/code&gt; / &lt;code&gt;getPostBySlug()&lt;/code&gt; data layer&lt;/li&gt;
&lt;li&gt;Custom-styled MDX components (headings, code blocks, links, tables)&lt;/li&gt;
&lt;li&gt;Syntax highlighting with &lt;code&gt;sugar-high&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;remark-gfm&lt;/code&gt; for GitHub-flavored Markdown (tables, strikethrough, task lists)&lt;/li&gt;
&lt;li&gt;Dynamic SEO metadata and JSON-LD structured data per post&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-mdxremote-instead-of-next-mdx-&quot;&gt;Why MDXRemote Instead of @next/mdx?&lt;/h3&gt;
&lt;p&gt;Next.js offers two different ways to work with MDX, and beginners often mix them up.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;@next/mdx&lt;/code&gt;&lt;/strong&gt; treats &lt;code&gt;.mdx&lt;/code&gt; files as actual pages/routes. You'd create a file like &lt;code&gt;app/blog/my-post/page.mdx&lt;/code&gt; and Next.js compiles it into a route at build time. This works, but it means every blog post is a route file living inside &lt;code&gt;app/&lt;/code&gt; — awkward if you want a &lt;code&gt;content/&lt;/code&gt; folder, dynamic &lt;code&gt;[slug]&lt;/code&gt; routes, or to pull posts from a CMS/database later.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;next-mdx-remote&lt;/code&gt;&lt;/strong&gt; lets you store MDX content &lt;em&gt;anywhere&lt;/em&gt; — a &lt;code&gt;content/blogs&lt;/code&gt; folder, a database, a CMS — and render it through a single dynamic route like &lt;code&gt;app/blog/[slug]/page.tsx&lt;/code&gt;. You read the raw MDX string yourself (usually with the filesystem), then hand it to the &lt;code&gt;&amp;lt;MDXRemote /&amp;gt;&lt;/code&gt; component to compile and render.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; If you want a handful of static pages (like a docs page), &lt;code&gt;@next/mdx&lt;/code&gt; is simpler. If you're building a blog with many posts and dynamic routing, &lt;code&gt;next-mdx-remote&lt;/code&gt; is the better fit — it's what we'll use here.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-we-re-building&quot;&gt;What We're Building&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;content/
  blogs/
    hello-world.mdx
    another-post.mdx

lib/
  posts.ts        → reads MDX files, parses frontmatter, sorts posts

app/
  blog/
    [slug]/
      page.tsx              → dynamic route rendering MDXRemote
      opengraph-image.tsx    → auto-generated OG image per post

mdx-components.tsx  → custom styled components used inside MDX
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Prerequisites: Next.js 16 (App Router), TypeScript in strict mode, and Tailwind CSS for styling. If your project doesn't already have these, scaffold one with &lt;code&gt;npx create-next-app@latest&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-1-install-dependencies&quot;&gt;Step 1: Install Dependencies&lt;/h3&gt;
&lt;p&gt;Run the following command to install everything this setup needs:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install next-mdx-remote gray-matter remark-gfm reading-time sugar-high
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's what each package actually does:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Package &lt;/th&gt;&lt;th&gt;Purpose&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;next-mdx-remote&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Compiles and renders MDX strings as React on the server&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;gray-matter&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Parses frontmatter (the &lt;code&gt;---&lt;/code&gt; block at the top of your &lt;code&gt;.mdx&lt;/code&gt; files)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;remark-gfm&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Adds GitHub-flavored Markdown — tables, strikethrough, task lists&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;reading-time&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Calculates estimated reading time from post content&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;sugar-high&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Lightweight syntax highlighter for code blocks (no client JS needed)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Don't confuse &lt;code&gt;next-mdx-remote&lt;/code&gt; with &lt;code&gt;next-mdx-remote/rsc&lt;/code&gt;. The base package is for the Pages Router / client components. For the App Router with Server Components (which is what we want in Next.js 16), you import from &lt;code&gt;next-mdx-remote/rsc&lt;/code&gt; instead. We'll use that import throughout this guide.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-2-configure-next-config-mjs-optional-&quot;&gt;Step 2: Configure next.config.mjs (Optional)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// next.config.mjs

/** @type {import('next').NextConfig} */
const nextConfig = {
    pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
    transpilePackages: ['next-mdx-remote'],
}

export default nextConfig
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pageExtensions&lt;/code&gt; tells Next.js that &lt;code&gt;.md&lt;/code&gt; and &lt;code&gt;.mdx&lt;/code&gt; files can also be treated as pages, alongside the usual &lt;code&gt;.ts&lt;/code&gt;/&lt;code&gt;.tsx&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;transpilePackages: ['next-mdx-remote']&lt;/code&gt; makes sure Next.js properly transpiles the package rather than trying to treat it as pre-built.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-3-create-the-content-folder&quot;&gt;Step 3: Create the Content Folder&lt;/h3&gt;
&lt;p&gt;This is where your actual blog posts will live as plain &lt;code&gt;.mdx&lt;/code&gt; files.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;content/blogs/hello-world.mdx
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;---
title: &quot;Hello World: My First MDX Post&quot;
description: &quot;A short example post to test out the MDXRemote setup in Next.js 16.&quot;
publishedAt: &quot;2026-07-01&quot;
tags: [&quot;MDX&quot;, &quot;Next.js&quot;]
featured: false
---

## Hello World

This is my **first** MDX post rendered with `next-mdx-remote`.

- It supports Markdown
- It supports *React components*
- It even supports tables (thanks to remark-gfm)

| Feature            | Supported |
| ------------------ | --------- |
| Tables             | ✅       |
| Code blocks        | ✅       |
| Custom components  | ✅       |

const greeting: string = &quot;Hello from MDX!&quot;;
console.log(greeting);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Understanding the frontmatter block:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The section between the &lt;code&gt;---&lt;/code&gt; lines at the top is called &lt;em&gt;frontmatter&lt;/em&gt;. It's plain YAML metadata that describes the post — title, description, publish date, tags — without being part of the rendered content itself. &lt;code&gt;gray-matter&lt;/code&gt; (installed earlier) is the library that separates this metadata from the actual MDX body.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Keep your frontmatter field names consistent across every post (&lt;code&gt;title&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, &lt;code&gt;publishedAt&lt;/code&gt;, &lt;code&gt;tags&lt;/code&gt;, &lt;code&gt;featured&lt;/code&gt;). If one file has &lt;code&gt;date&lt;/code&gt; and another has &lt;code&gt;publishedAt&lt;/code&gt;, your sorting and metadata logic will silently break for that post.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-4-build-the-data-layer-lib-posts-ts-&quot;&gt;Step 4: Build the Data Layer (lib/posts.ts)&lt;/h3&gt;
&lt;p&gt;This file is the heart of the whole setup — it reads every &lt;code&gt;.mdx&lt;/code&gt; file from disk, parses the frontmatter, and returns clean, typed post objects your pages can use.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/posts.ts
import fs from &quot;fs&quot;;
import path from &quot;path&quot;;
import matter from &quot;gray-matter&quot;;
import readingTime from &quot;reading-time&quot;;

const POSTS_PATH = path.join(process.cwd(), &quot;content/blogs&quot;);

export type Post = {
  slug: string;
  title: string;
  description: string;
  publishedAt: string;
  updatedAt?: string;
  tags: string[];
  coverImage?: string;
  readingTime: string;
  content: string;
};

export function getAllPosts(): Post[] {
  const files = fs.readdirSync(POSTS_PATH);

  const posts = files.map((file) =&amp;gt; {
    const filePath = path.join(POSTS_PATH, file);
    const fileContent = fs.readFileSync(filePath, &quot;utf-8&quot;);

    const { data, content } = matter(fileContent);

    const slug = file.replace(/\.mdx$/, &quot;&quot;);

    return {
      slug,
      title: data.title,
      description: data.description,
      publishedAt: data.publishedAt,
      tags: data.tags || [],
      coverImage: data.coverImage || &quot;&quot;,
      readingTime: readingTime(content).text,
      content,
    };
  });

  return posts.sort(
    (a, b) =&amp;gt;
      new Date(b.publishedAt).getTime() -
      new Date(a.publishedAt).getTime()
  );
}

export function getPostBySlug(slug: string) {
  return getAllPosts().find((post) =&amp;gt; post.slug === slug);
}

export function extractFirstImage(content: string) {
  const regex = /!\[.*?\]\((.*?)\)/;
  const match = content.match(regex);
  return match?.[1] || null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Breaking this down line by line:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fs.readdirSync(POSTS_PATH)&lt;/code&gt; reads the filenames of every &lt;code&gt;.mdx&lt;/code&gt; file inside &lt;code&gt;content/blogs&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;matter(fileContent)&lt;/code&gt; splits each file into &lt;code&gt;data&lt;/code&gt; (the frontmatter object) and &lt;code&gt;content&lt;/code&gt; (the raw MDX body below the frontmatter).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;slug&lt;/code&gt; is derived directly from the filename — &lt;code&gt;hello-world.mdx&lt;/code&gt; becomes the slug &lt;code&gt;hello-world&lt;/code&gt;, which later becomes the URL &lt;code&gt;/blog/hello-world&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;readingTime(content).text&lt;/code&gt; automatically calculates something like &lt;code&gt;&quot;3 min read&quot;&lt;/code&gt; based on word count — no manual math needed.&lt;/li&gt;
&lt;li&gt;Posts are sorted newest-first by comparing &lt;code&gt;publishedAt&lt;/code&gt; dates.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;extractFirstImage()&lt;/code&gt; is a small helper that scans the Markdown content for the first &lt;code&gt;![alt](url)&lt;/code&gt; image — handy for auto-generating Open Graph preview images later.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Because this uses &lt;code&gt;fs&lt;/code&gt; (Node's filesystem module), it can only run on the server — inside Server Components, &lt;code&gt;generateStaticParams&lt;/code&gt;, or &lt;code&gt;generateMetadata&lt;/code&gt;. Never import &lt;code&gt;lib/posts.ts&lt;/code&gt; into a &lt;code&gt;&quot;use client&quot;&lt;/code&gt; component.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-5-create-custom-mdx-components&quot;&gt;Step 5: Create Custom MDX Components&lt;/h3&gt;
&lt;p&gt;By default, MDX renders plain, unstyled HTML tags (&lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;code&amp;gt;&lt;/code&gt;, etc). To make your blog actually look good, you override these tags with your own styled versions.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// mdx-components.tsx
import React, { ComponentPropsWithoutRef } from 'react';
import Link from 'next/link';
import { highlight } from 'sugar-high';

type HeadingProps = ComponentPropsWithoutRef&amp;lt;'h1'&amp;gt;;
type ParagraphProps = ComponentPropsWithoutRef&amp;lt;'p'&amp;gt;;
type ListProps = ComponentPropsWithoutRef&amp;lt;'ul'&amp;gt;;
type ListItemProps = ComponentPropsWithoutRef&amp;lt;'li'&amp;gt;;
type AnchorProps = ComponentPropsWithoutRef&amp;lt;'a'&amp;gt;;
type BlockquoteProps = ComponentPropsWithoutRef&amp;lt;'blockquote'&amp;gt;;

const components = {
    h1: (props: HeadingProps) =&amp;gt; (
        &amp;lt;h1 className=&quot;scroll-m-20 text-4xl font-bold tracking-tight lg:text-5xl&quot; {...props} /&amp;gt;
    ),
    h2: (props: HeadingProps) =&amp;gt; (
        &amp;lt;h2 className=&quot;scroll-m-20 mt-16 pb-2 text-3xl font-semibold tracking-tight&quot; {...props} /&amp;gt;
    ),
    h3: (props: HeadingProps) =&amp;gt; (
        &amp;lt;h3 className=&quot;scroll-m-20 mt-12 text-2xl font-semibold tracking-tight&quot; {...props} /&amp;gt;
    ),
    p: (props: ParagraphProps) =&amp;gt; (
        &amp;lt;p className=&quot;text-gray-800 dark:text-zinc-300&quot; {...props} /&amp;gt;
    ),
    ul: (props: ListProps) =&amp;gt; (
        &amp;lt;ul className=&quot;text-gray-800 dark:text-zinc-300 list-disc pl-5 space-y-1&quot; {...props} /&amp;gt;
    ),
    li: (props: ListItemProps) =&amp;gt; &amp;lt;li className=&quot;pl-1&quot; {...props} /&amp;gt;,
    a: ({ href, children, ...props }: AnchorProps) =&amp;gt; {
        const className = 'text-blue-500 hover:text-blue-700';

        // Internal links use Next.js's Link for client-side navigation
        if (href?.startsWith('/')) {
            return &amp;lt;Link href={href} className={className} {...props}&amp;gt;{children}&amp;lt;/Link&amp;gt;;
        }

        // External links open in a new tab
        return (
            &amp;lt;a href={href} target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; className={className} {...props}&amp;gt;
                {children}
            &amp;lt;/a&amp;gt;
        );
    },
    code: ({ children, ...props }: ComponentPropsWithoutRef&amp;lt;'code'&amp;gt;) =&amp;gt; {
        const codeHTML = highlight(String(children));
        return &amp;lt;code dangerouslySetInnerHTML={{ __html: codeHTML }} {...props} /&amp;gt;;
    },
    blockquote: (props: BlockquoteProps) =&amp;gt; (
        &amp;lt;blockquote className=&quot;ml-[0.075em] border-l-3 border-green-400 pl-4 text-gray-700&quot; {...props} /&amp;gt;
    ),
    em: (props: ComponentPropsWithoutRef&amp;lt;'em'&amp;gt;) =&amp;gt; (
        &amp;lt;em className=&quot;font-medium&quot; {...props} /&amp;gt;
    ),
};

declare global {
    type MDXProvidedComponents = typeof components;
}

export { components };
export function useMDXComponents(): MDXProvidedComponents {
    return components;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Every key in the &lt;code&gt;components&lt;/code&gt; object (&lt;code&gt;h1&lt;/code&gt;, &lt;code&gt;p&lt;/code&gt;, &lt;code&gt;code&lt;/code&gt;, etc.) maps directly to that HTML tag name inside your MDX files. Write &lt;code&gt;# My Heading&lt;/code&gt; in MDX, and it renders through your custom &lt;code&gt;h1&lt;/code&gt; component automatically.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;code&lt;/code&gt; component is the interesting one — instead of plain unstyled text, it runs the code through &lt;code&gt;sugar-high&lt;/code&gt;'s &lt;code&gt;highlight()&lt;/code&gt; function, which returns syntax-highlighted HTML. This gives you colored code blocks with &lt;strong&gt;zero client-side JavaScript&lt;/strong&gt;, since &lt;code&gt;sugar-high&lt;/code&gt; runs entirely at render time on the server.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;a&lt;/code&gt; component checks whether a link is internal (starts with &lt;code&gt;/&lt;/code&gt;) or external. Internal links use Next.js's &lt;code&gt;&amp;lt;Link&amp;gt;&lt;/code&gt; for fast client-side navigation; external links get &lt;code&gt;target=&quot;_blank&quot;&lt;/code&gt; so readers don't lose your site.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Forgetting to style the &lt;code&gt;code&lt;/code&gt; component is the #1 reason MDX blogs look broken — code blocks renders with no formatting. Always wire up a syntax highlighter here.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-6-build-the-dynamic-blog-route&quot;&gt;Step 6: Build the Dynamic Blog Route&lt;/h3&gt;
&lt;p&gt;This is where everything comes together — reading the post, compiling the MDX, and rendering it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/blog/[slug]/page.tsx
import { notFound } from &quot;next/navigation&quot;;
import { MDXRemote } from &quot;next-mdx-remote/rsc&quot;;
import remarkGfm from &quot;remark-gfm&quot;;
import { getAllPosts, getPostBySlug } from &quot;@/lib/posts&quot;;
import { components } from &quot;@/mdx-components&quot;;

interface BlogPostPageProps {
    params: Promise&amp;lt;{ slug: string }&amp;gt;;
}

// Pre-renders every post at build time (Static Site Generation)
export async function generateStaticParams() {
    const posts = getAllPosts();
    return posts.map((post) =&amp;gt; ({ slug: post.slug }));
}

export async function generateMetadata({ params }: BlogPostPageProps) {
    const { slug } = await params;
    const post = getPostBySlug(slug);

    if (!post) return {};

    return {
        title: post.title,
        description: post.description,
    };
}

export default async function BlogPostPage({ params }: BlogPostPageProps) {
    const { slug } = await params;
    const post = getPostBySlug(slug);

    if (!post) {
        notFound();
    }

    return (
        &amp;lt;article className=&quot;prose dark:prose-invert mx-auto&quot;&amp;gt;
            &amp;lt;h1&amp;gt;{post.title}&amp;lt;/h1&amp;gt;

            &amp;lt;MDXRemote
                source={post.content}
                components={components}
                options={{
                    mdxOptions: {
                        remarkPlugins: [remarkGfm],
                    },
                }}
            /&amp;gt;
        &amp;lt;/article&amp;gt;
    );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Understanding each piece:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;generateStaticParams()&lt;/code&gt;&lt;/strong&gt; tells Next.js every possible &lt;code&gt;slug&lt;/code&gt; value ahead of time, so it can statically generate every blog post page at build time instead of rendering it on every request. This is what makes MDX blogs built this way extremely fast.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;generateMetadata()&lt;/code&gt;&lt;/strong&gt; dynamically sets the &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; and meta description per post — critical for SEO, since every post needs its own unique tags rather than a single site-wide default.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;notFound()&lt;/code&gt;&lt;/strong&gt; triggers Next.js's built-in 404 page if someone visits a slug that doesn't exist in &lt;code&gt;content/blogs&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;&amp;lt;MDXRemote source={...} /&amp;gt;&lt;/code&gt;&lt;/strong&gt; is doing the real work: it takes the raw MDX string (&lt;code&gt;post.content&lt;/code&gt;), compiles it into React on the server, and renders it — swapping in your custom &lt;code&gt;components&lt;/code&gt; along the way.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;remarkPlugins: [remarkGfm]&lt;/code&gt;&lt;/strong&gt; enables GitHub-flavored Markdown syntax &lt;em&gt;inside this specific render call&lt;/em&gt; — tables, &lt;code&gt;~~strikethrough~~&lt;/code&gt;, and &lt;code&gt;- [ ] task lists&lt;/code&gt; all now work in your MDX files.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; &lt;code&gt;MDXRemote&lt;/code&gt; from &lt;code&gt;next-mdx-remote/rsc&lt;/code&gt; is an &lt;strong&gt;async Server Component&lt;/strong&gt;. It streams and renders entirely on the server — no MDX compiler code ever ships to the browser, which keeps your client bundle small.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-7-add-json-ld-structured-data-optional-but-great-for-seo-&quot;&gt;Step 7: Add JSON-LD Structured Data (Optional, but Great for SEO)&lt;/h3&gt;
&lt;p&gt;To help Google understand each post is a blog article (and potentially show rich snippets), add structured data using Next.js's &lt;code&gt;&amp;lt;Script&amp;gt;&lt;/code&gt; component.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/blog/[slug]/page.tsx (addition)
import Script from &quot;next/script&quot;;

&amp;lt;&amp;gt;
&amp;lt;article&amp;gt;
rest of the code
&amp;lt;/article&amp;gt;
&amp;lt;Script
    type=&quot;application/ld+json&quot;
    dangerouslySetInnerHTML={{
        __html: JSON.stringify({
            &quot;@context&quot;: &quot;https://schema.org&quot;,
            &quot;@type&quot;: &quot;BlogPosting&quot;,
            headline: post.title,
            description: post.description,
            datePublished: post.publishedAt,
            author: { &quot;@type&quot;: &quot;Person&quot;, name: &quot;Your Name&quot; },
        }),
    }}
/&amp;gt;
&amp;lt;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This injects a &lt;code&gt;&amp;lt;script type=&quot;application/ld+json&quot;&amp;gt;&lt;/code&gt; tag containing structured metadata search engines can parse directly — no visible UI change, just better SEO.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-8-update-global-css&quot;&gt;Step 8: Update &lt;code&gt;global.css&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Blog content renders as plain HTML elements with no default styling, so headings, paragraphs, and lists will look unstyled out of the box. The Tailwind Typography plugin fixes this in one line.&lt;/p&gt;
&lt;p&gt;Install it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install @tailwindcss/typography
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add the plugin to your global CSS:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* app/globals.css */
@import &quot;tailwindcss&quot;;

@plugin &quot;@tailwindcss/typography&quot;;

:root{
  /* your other styles */

  /* add code blocks styling */
  --sh-class: #7aa2f7;
  --sh-sign: #89ddff;
  --sh-string: #9ece6a;
  --sh-keyword: #bb9af7;
  --sh-comment: #acacac;
  --sh-jsxliterals: #7aa2f7;
  --sh-property: #73daca;
  --sh-entity: #e0af68;
  --sh-identifier: #fff;
}

@layer base {
  * {
    @apply border-border outline-ring/50;
  }

  body {
    @apply bg-background text-foreground;
  }

  button:not(:disabled),
  [role=&quot;button&quot;]:not(:disabled) {
    cursor: pointer;
  }

  pre::-webkit-scrollbar {
    display: none;
  }

  code:not(pre code) {
    background-color: var(--color-gray-100);
  }

  code:not(pre code) span {
    font-weight: 600;
    color: black !important;
  }

  hr {
    color: var(--color-gray-200);
  }

  /* Remove Safari input shadow on mobile */
  input[type='text'],
  input[type='email'] {
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
  }

  table {
    display: block;
    max-width: fit-content;
    overflow-x: auto;
    white-space: nowrap;
    text-align: left;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;testing-your-setup&quot;&gt;Testing Your Setup&lt;/h3&gt;
&lt;p&gt;Start your dev server:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then visit:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;http://localhost:3000/blog/hello-world
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should see your MDX post fully rendered with styled headings, a working table, and a syntax-highlighted code block.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; If you get a &quot;module not found: fs&quot; error, you've likely imported &lt;code&gt;lib/posts.ts&lt;/code&gt; inside a Client Component (&lt;code&gt;&quot;use client&quot;&lt;/code&gt;). The &lt;code&gt;fs&lt;/code&gt; module only works on the server — remove the client directive or move the data fetching to a Server Component / route handler.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;bonus-dynamic-open-graph-images&quot;&gt;Bonus: Dynamic Open Graph Images&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og';

export const runtime = 'edge'; // Use Edge Runtime for faster generation
export const alt = 'Blog Post Preview';
export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';

export default async function Image({ params }: { params: Promise&amp;lt;{ slug: string; }&amp;gt; }) {
    const title = (await params).slug.replace(/-/g, ' ');
    
    return new ImageResponse(
        (
            &amp;lt;div
                style={{
                    height: &quot;100%&quot;,
                    width: &quot;100%&quot;,
                    display: &quot;flex&quot;,
                    flexDirection: &quot;column&quot;,
                    justifyContent: &quot;space-between&quot;,
                    backgroundColor: &quot;#fff&quot;,
                    padding: &quot;60px&quot;,
                }}
            &amp;gt;

                &amp;lt;div
                    style={{
                        fontSize: 44,
                        display: 'flex',
                        alignItems: 'center',
                        fontWeight: 'bold',
                        gap: '15px'
                    }}
                &amp;gt;
                    &amp;lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;64&quot; height=&quot;64&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;green&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&amp;gt;&amp;lt;path d=&quot;M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z&quot; /&amp;gt;&amp;lt;path d=&quot;M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12&quot; /&amp;gt;&amp;lt;path d=&quot;M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17&quot; /&amp;gt;&amp;lt;/svg&amp;gt;
                    &amp;lt;span&amp;gt;Website Name&amp;lt;/span&amp;gt;
                &amp;lt;/div&amp;gt;

                &amp;lt;div
                    style={{
                        fontSize: 64,
                        fontWeight: 700,
                        lineHeight: 1.1,
                        maxWidth: 900,
                        textTransform: 'capitalize'
                    }}
                &amp;gt;
                    {title}
                &amp;lt;/div&amp;gt;

                &amp;lt;div
                    style={{
                        fontSize: 28,
                        opacity: 0.7,
                    }}
                &amp;gt;
                    example.com
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        ),
        { ...size }
    );
}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;frequently-asked-questions&quot;&gt;Frequently Asked Questions&lt;/h3&gt;
&lt;h4 id=&quot;can-i-use-next-mdx-remote-with-the-pages-router-&quot;&gt;Can I use next-mdx-remote with the Pages Router?&lt;/h4&gt;
&lt;p&gt;Yes, but you'd import from &lt;code&gt;next-mdx-remote&lt;/code&gt; (not the &lt;code&gt;/rsc&lt;/code&gt; subpath) and use &lt;code&gt;getStaticProps&lt;/code&gt;/&lt;code&gt;getStaticPaths&lt;/code&gt; instead of &lt;code&gt;generateStaticParams&lt;/code&gt;. The &lt;code&gt;/rsc&lt;/code&gt; version shown in this guide is built specifically for the App Router's Server Components.&lt;/p&gt;
&lt;h4 id=&quot;do-i-need-next-mdx-if-i-m-already-using-mdxremote-&quot;&gt;Do I need @next/mdx if I'm already using MDXRemote?&lt;/h4&gt;
&lt;p&gt;Not strictly — &lt;code&gt;MDXRemote&lt;/code&gt; compiles MDX at runtime regardless of your &lt;code&gt;next.config.mjs&lt;/code&gt; setup. But registering &lt;code&gt;.mdx&lt;/code&gt; as a page extension avoids editor/tooling quirks and keeps the door open if you ever want static &lt;code&gt;.mdx&lt;/code&gt; route files too.&lt;/p&gt;
&lt;h4 id=&quot;how-do-i-add-images-inside-my-mdx-posts-&quot;&gt;How do I add images inside my MDX posts?&lt;/h4&gt;
&lt;p&gt;Just use standard Markdown image syntax: &lt;code&gt;![alt text](/path/to/image.png)&lt;/code&gt;. If you want Next.js's optimized &lt;code&gt;&amp;lt;Image&amp;gt;&lt;/code&gt; component instead, add an &lt;code&gt;img&lt;/code&gt; key to your &lt;code&gt;components&lt;/code&gt; object in &lt;code&gt;mdx-components.tsx&lt;/code&gt; that renders &lt;code&gt;next/image&lt;/code&gt; under the hood.&lt;/p&gt;
&lt;h4 id=&quot;why-is-my-code-block-not-syntax-highlighted-&quot;&gt;Why is my code block not syntax highlighted?&lt;/h4&gt;
&lt;p&gt;Double-check that your &lt;code&gt;code&lt;/code&gt; component in &lt;code&gt;mdx-components.tsx&lt;/code&gt; is running the content through &lt;code&gt;sugar-high&lt;/code&gt;'s &lt;code&gt;highlight()&lt;/code&gt; function. If you copy-pasted code but forgot this override, code blocks fall back to plain unstyled text.&lt;/p&gt;
&lt;h4 id=&quot;can-i-fetch-mdx-content-from-a-cms-instead-of-the-filesystem-&quot;&gt;Can I fetch MDX content from a CMS instead of the filesystem?&lt;/h4&gt;
&lt;p&gt;Yes — that's actually one of the biggest advantages of &lt;code&gt;next-mdx-remote&lt;/code&gt; over &lt;code&gt;@next/mdx&lt;/code&gt;. Since &lt;code&gt;MDXRemote&lt;/code&gt; just needs a raw MDX string, you can fetch that string from Contentful, Sanity, a database, or anywhere else instead of &lt;code&gt;fs.readFileSync&lt;/code&gt;. The rest of the setup stays identical.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;wrapping-up&quot;&gt;Wrapping Up&lt;/h3&gt;
&lt;p&gt;You now have a complete, production-ready MDX blog setup running on Next.js 16 — MDX files stored cleanly in &lt;code&gt;content/blogs&lt;/code&gt;, a typed data layer for reading and sorting posts, custom-styled components, GitHub-flavored Markdown support, and per-post SEO metadata.&lt;/p&gt;
&lt;p&gt;From here, natural next steps include adding tag-based filtering, a search feature, or auto-generated Open Graph images per post using &lt;code&gt;next/og&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;helpful-resources&quot;&gt;Helpful Resources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/guides/mdx&quot;&gt;Next.js MDX Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/next-mdx-remote&quot;&gt;next-mdx-remote Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/remarkjs/remark-gfm&quot;&gt;remark-gfm Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/jonschlinkert/gray-matter&quot;&gt;gray-matter Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/huozhi/sugar-high&quot;&gt;sugar-high Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/tailwindlabs/tailwindcss-typography&quot;&gt;Tailwind Typography Plugin Docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;related-guides&quot;&gt;Related Guides&lt;/h3&gt;
&lt;p&gt;If you'd like to keep building out your Next.js stack, these guides pair well with this one:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/saas-starter-architecture-nextjs&quot;&gt;SaaS Starter Architecture with Next.js 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/nextjs-16-supabase-todo-app&quot;&gt;Build a Todo App with Next.js 16 and Supabase (2026 Guide)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/how-to-add-clerk-authentication-in-nextjs-16&quot;&gt;How to Add Clerk Authentication in Next.js 16 (2026 Guide)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/resend-nextjs-16-guide&quot;&gt;How to Setup Resend Email in Next.js 16 (2026 Complete Beginner Guide)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;try-these-free-developer-tools&quot;&gt;Try These Free Developer Tools&lt;/h3&gt;
&lt;p&gt;While you're here, a couple of free tools that pair naturally with a fresh MDX blog setup like this one:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/tools/meta-tag-generator&quot;&gt;Meta Tag Generator for Next.js &amp;amp; HTML&lt;/a&gt; — generate SEO meta tags, Open Graph tags, Twitter Cards, canonical URLs, and JSON-LD structured data instantly. Handy for wiring up per-post metadata like the &lt;code&gt;generateMetadata()&lt;/code&gt; function above.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/tools/rls-policy-generator&quot;&gt;Supabase RLS Policy Generator&lt;/a&gt; — if your next project pairs this blog setup with a Supabase backend, generate secure, performance-optimized Row Level Security policies in seconds.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/tools/zod-schema-generator&quot;&gt;Zod Schema Generator&lt;/a&gt; — generate Zod schemas instantly from JSON or TypeScript. Includes smart inference, live validation, React Hook Form, Next.js API Routes, and Server Actions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;📦 &lt;strong&gt;Source Code:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Muhammad-Ali-sma/next-mdx-setup&quot;&gt;View on GitHub&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23799/complete-mdx-blog-setup-with-next-mdx-remote-and-next-js-16-2026-beginner-guide</guid>
<pubDate>Thu, 30 Jul 2026 19:00:00 +0000</pubDate>
</item>
</channel>
</rss>