How to Build a Blog with Next.js and MDX
Muhammad Bilal Azhar
Co-Founder & Technical Lead · Google Cloud Certified Professional
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 (
);
},
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 (
My Blog
Home
About
{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
${siteUrl}
Thoughts on web development ${posts
.map(
(post) =>
${post.title} ${siteUrl}/blog/${post.slug}
${new Date(post.publishedAt).toUTCString()}
${post.excerpt}
)
.join('')}
;
return new Response(rss, {
headers: {
'Content-Type': 'application/xml',
},
});
}
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:
- ✅ Automatic code highlighting
- ✅ SEO metadata
- ✅ Free hosting on Vercel
- ✅ Full control over design
This is the foundation many developers are migrating to from WordPress. Enjoy your new blog!
Related guides:
Related Articles
View allGit for WordPress Developers: Getting Started
Learn Git version control coming from WordPress. Track changes, collaborate, and never lose work again.
TypeScript for JavaScript Developers: A Practical Guide
Learn TypeScript to write safer code. Types, interfaces, and practical patterns that make JavaScript development better.
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.
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.