tutorialNext.jsMDXBlog

How to Build a Blog with Next.js and MDX

How to Build a Blog with Next.js and MDX

Ready to build your own blog? This tutorial walks you through creating a modern, performant blog using Next.js and MDX—the same stack many developers are migrating to from WordPress.


What We're Building

By the end of this tutorial, you'll have:

  • ✅ Blog with MDX-powered posts
  • ✅ Homepage with post listing
  • ✅ Individual post pages
  • ✅ Syntax highlighting for code
  • ✅ SEO metadata
  • ✅ Responsive design
  • ✅ Deployed to Vercel

Time required: 1-2 hours


Prerequisites

Before starting:

  • Node.js 18+ installed
  • Basic React knowledge
  • A code editor (VS Code recommended)
  • A Vercel account (free) for deployment

Step 1: Create Next.js Project

Open your terminal and run:

npx create-next-app@latest my-blog --typescript --tailwind --app --src-dir --eslint

When prompted:

  • Would you like to use Turbopack? → No
  • Import alias → Use default @/*

Navigate to the project:

cd my-blog


Step 2: Install Dependencies

Install MDX and related packages:

npm install @next/mdx @mdx-js/loader @mdx-js/react gray-matter reading-time rehype-highlight rehype-slug

What these do:

  • @next/mdx - Next.js MDX support
  • @mdx-js/loader - Webpack loader for MDX
  • @mdx-js/react - React MDX provider
  • gray-matter - Parse frontmatter from MDX files
  • reading-time - Calculate reading time
  • rehype-highlight - Syntax highlighting
  • rehype-slug - Add IDs to headings

Step 3: Configure MDX

Create next.config.mjs (replace existing next.config.ts):

import createMDX from '@next/mdx';

import rehypeHighlight from 'rehype-highlight';

import rehypeSlug from 'rehype-slug';

const withMDX = createMDX({

extension: /\.mdx?$/,

options: {

remarkPlugins: [],

rehypePlugins: [rehypeHighlight, rehypeSlug],

},

});

/ @type {import('next').NextConfig} */

const nextConfig = {

pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'],

};

export default withMDX(nextConfig);


Step 4: Create Content Directory

Create a folder for your blog posts:

mkdir -p content/posts

Create your first post at content/posts/hello-world.mdx:

---

title: "Hello World: My First Post"

excerpt: "Welcome to my new blog built with Next.js and MDX!"

publishedAt: "2026-02-05"

author: "Your Name"

tags: ["welcome", "first-post"]


Hello World: My First Post

Welcome to my new blog! This is built with Next.js and MDX.

Why I Built This

I wanted a fast, modern blog where I could:

  • Write in Markdown
  • Use custom React components
  • Have full control over the design
  • Deploy for free

Code Example

Here's some JavaScript:

javascript

function greet(name) {

return Hello, ${name}!;

}

console.log(greet('World'));


What's Next

Stay tuned for more posts about web development, design, and technology.


Step 5: Create Post Utilities

Create src/lib/posts.ts to handle fetching posts:

import fs from 'fs';

import path from 'path';

import matter from 'gray-matter';

import readingTime from 'reading-time';

const postsDirectory = path.join(process.cwd(), 'content/posts');

export interface Post {

slug: string;

title: string;

excerpt: string;

publishedAt: string;

author: string;

tags: string[];

readingTime: string;

content: string;

}

export function getAllPosts(): Post[] {

const fileNames = fs.readdirSync(postsDirectory);

const posts = fileNames

.filter((fileName) => fileName.endsWith('.mdx'))

.map((fileName) => {

const slug = fileName.replace(/\.mdx$/, '');

const fullPath = path.join(postsDirectory, fileName);

const fileContents = fs.readFileSync(fullPath, 'utf8');

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

return {

slug,

title: data.title,

excerpt: data.excerpt,

publishedAt: data.publishedAt,

author: data.author,

tags: data.tags || [],

readingTime: readingTime(content).text,

content,

};

})

.sort((a, b) =>

new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime()

);

return posts;

}

export function getPostBySlug(slug: string): Post | null {

const fullPath = path.join(postsDirectory, ${slug}.mdx);

if (!fs.existsSync(fullPath)) {

return null;

}

const fileContents = fs.readFileSync(fullPath, 'utf8');

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

return {

slug,

title: data.title,

excerpt: data.excerpt,

publishedAt: data.publishedAt,

author: data.author,

tags: data.tags || [],

readingTime: readingTime(content).text,

content,

};

}


Step 6: Create MDX Components

Create src/components/mdx-components.tsx:

import type { MDXComponents } from 'mdx/types';

import Link from 'next/link';

export function useMDXComponents(components: MDXComponents): MDXComponents {

return {

h1: ({ children }) => (

{children}

),

h2: ({ children }) => (

{children}

),

h3: ({ children }) => (

{children}

),

p: ({ children }) => (

{children}

),

a: ({ href, children }) => {

if (href?.startsWith('/')) {

return (

{children}

);

}

return (

href={href}

target="_blank"

rel="noopener noreferrer"

className="text-blue-600 hover:underline"

>

{children}

);

},

ul: ({ children }) => (

    {children}

),

ol: ({ children }) => (

    {children}

),

code: ({ children }) => (

{children}

),

pre: ({ children }) => (

{children}

),

blockquote: ({ children }) => (

{children}

),

...components,

};

}

Create src/mdx-components.tsx (required by Next.js MDX):

export { useMDXComponents } from '@/components/mdx-components';


Step 7: Create Blog Homepage

Replace src/app/page.tsx:

import Link from 'next/link';

import { getAllPosts } from '@/lib/posts';

export default function Home() {

const posts = getAllPosts();

return (

My Blog

Thoughts on web development, design, and technology.

Latest Posts

{posts.map((post) => (

key={post.slug}

className="border-b border-gray-200 pb-8"

>

/blog/${post.slug}}>

{post.title}

{new Date(post.publishedAt).toLocaleDateString('en-US', {

year: 'numeric',

month: 'long',

day: 'numeric',

})}

·

{post.readingTime}

{post.excerpt}

{post.tags.map((tag) => (

key={tag}

className="bg-gray-100 text-gray-700 px-2 py-1 rounded text-sm"

>

{tag}

))}

))}

);

}


Step 8: Create Blog Post Page

Create src/app/blog/[slug]/page.tsx:

import { notFound } from 'next/navigation';

import { getAllPosts, getPostBySlug } from '@/lib/posts';

import { MDXRemote } from 'next-mdx-remote/rsc';

import { useMDXComponents } from '@/components/mdx-components';

import type { Metadata } from 'next';

interface PageProps {

params: { slug: string };

}

// Generate static paths for all posts

export function generateStaticParams() {

const posts = getAllPosts();

return posts.map((post) => ({

slug: post.slug,

}));

}

// Generate metadata for SEO

export function generateMetadata({ params }: PageProps): Metadata {

const post = getPostBySlug(params.slug);

if (!post) {

return {

title: 'Post Not Found',

};

}

return {

title: ${post.title} | My Blog,

description: post.excerpt,

openGraph: {

title: post.title,

description: post.excerpt,

type: 'article',

publishedTime: post.publishedAt,

authors: [post.author],

},

};

}

export default function BlogPost({ params }: PageProps) {

const post = getPostBySlug(params.slug);

if (!post) {

notFound();

}

const components = useMDXComponents({});

return (

{post.title}

{post.author}

·

{new Date(post.publishedAt).toLocaleDateString('en-US', {

year: 'numeric',

month: 'long',

day: 'numeric',

})}

·

{post.readingTime}

{post.tags.map((tag) => (

key={tag}

className="bg-blue-100 text-blue-700 px-3 py-1 rounded-full text-sm"

>

{tag}

))}

);

}

Install the additional package:

npm install next-mdx-remote


Step 9: Add Syntax Highlighting Styles

Add to src/app/globals.css:

/ Add syntax highlighting theme /

@import 'highlight.js/styles/github-dark.css';

/ Customize prose styles /

.prose pre {

@apply bg-gray-900 rounded-lg overflow-x-auto;

}

.prose code {

@apply bg-gray-100 px-1.5 py-0.5 rounded text-sm;

}

.prose pre code {

@apply bg-transparent p-0;

}


Step 10: Add Layout and Navigation

Update src/app/layout.tsx:

import type { Metadata } from 'next';

import { Inter } from 'next/font/google';

import Link from 'next/link';

import './globals.css';

const inter = Inter({ subsets: ['latin'] });

export const metadata: Metadata = {

title: 'My Blog',

description: 'Thoughts on web development, design, and technology.',

};

export default function RootLayout({

children,

}: {

children: React.ReactNode;

}) {

return (

{children}

© {new Date().getFullYear()} My Blog. Built with Next.js and MDX.

);

}


Step 11: Run and Test

Start the development server:

npm run dev

Visit http://localhost:3000 to see your blog!


Step 12: Add More Posts

Create more posts in content/posts/:

content/posts/getting-started-with-nextjs.mdx:

---

title: "Getting Started with Next.js"

excerpt: "A beginner's guide to building modern web applications with Next.js."

publishedAt: "2026-02-04"

author: "Your Name"

tags: ["nextjs", "react", "tutorial"]


Getting Started with Next.js

Next.js makes building React applications a breeze...


Step 13: Deploy to Vercel

Connect to Git

git init

git add .

git commit -m "Initial blog"

Push to GitHub

1. Create new repo on GitHub

2. Push your code:

git remote add origin https://github.com/yourusername/my-blog.git

git push -u origin main

Deploy to Vercel

1. Go to vercel.com

2. Import your GitHub repo

3. Click Deploy

4. Your blog is live!


Adding More Features

RSS Feed

Create src/app/rss.xml/route.ts:

import { getAllPosts } from '@/lib/posts';

export async function GET() {

const posts = getAllPosts();

const siteUrl = 'https://yourblog.com';

const rss =

My Blog

Sitemap

Create src/app/sitemap.ts:

import { getAllPosts } from '@/lib/posts';

import { MetadataRoute } from 'next';

export default function sitemap(): MetadataRoute.Sitemap {

const posts = getAllPosts();

const siteUrl = 'https://yourblog.com';

const blogUrls = posts.map((post) => ({

url: ${siteUrl}/blog/${post.slug},

lastModified: new Date(post.publishedAt),

changeFrequency: 'weekly' as const,

priority: 0.8,

}));

return [

{

url: siteUrl,

lastModified: new Date(),

changeFrequency: 'daily',

priority: 1,

},

...blogUrls,

];

}


Migrating WordPress Content

Have WordPress posts to migrate?

1. Export from WordPress using our free tool

2. Convert to MDX (automatic with our tool)

3. Place files in content/posts/

4. Done!

Try the free WordPress export tool →


FAQ

Q: Can I use this with any hosting?

Yes! While Vercel is easiest, you can use Netlify, Cloudflare Pages, or any Node.js host. Compare hosting options →

Q: How do I add comments?

Add Giscus (GitHub-based), Disqus, or any commenting service via their embed code.

Q: Can I add a newsletter signup?

Yes! Add your email provider's form (Mailchimp, ConvertKit, etc.) as a React component.

Q: What about styling?

You can use Tailwind CSS or any CSS framework. Tailwind is included by default with create-next-app.


Conclusion

You now have a modern, fast, SEO-friendly blog:

  • ✅ Zero-config MDX support
  • This is the foundation many developers are migrating to from WordPress. Enjoy your new blog!

    Related guides:

    Need to migrate existing WordPress content? →

    Share:

    Related Articles

    View all
    Main Guidetutorial

    Git for WordPress Developers: Getting Started

    Learn Git version control coming from WordPress. Track changes, collaborate, and never lose work again.

    12 min read
    tutorial

    TypeScript for JavaScript Developers: A Practical Guide

    Learn TypeScript to write safer code. Types, interfaces, and practical patterns that make JavaScript development better.

    14 min read
    tutorial

    Tailwind CSS for WordPress Developers: Complete Guide

    Learn Tailwind CSS coming from WordPress. Utility-first CSS explained for theme developers making the switch to modern frameworks.

    13 min read
    education

    React vs Vue vs Svelte for WordPress Developers

    Confused by JavaScript frameworks? This guide explains React, Vue, and Svelte from a WordPress developer's perspective.

    13 min read
    Previous

    How to Choose a Headless CMS in 2026: Decision Guide

    Next

    15 Best WordPress Alternatives in 2026: Complete Guide

    Next Steps

    Ready to Migrate Your WordPress Site?

    Use our free tool to export your WordPress content in minutes.

    Start Free Migration