A Step-by-Step Guide to Building Your First Next.js Blog with MDX and shadcn/ui
In this tutorial series, you'll learn how to build a modern, feature-rich blog using Next.js, MDX for content, and shadcn/ui for beautiful components. We'll start by building the blog with local MDX files to understand the fundamentals, and then in Part 4, we'll migrate to ElmapiCMS (headless CMS) to enable content management without code changes. This approach lets you learn both MDX file-based blogging and headless CMS integration. By the end of Part 1, you'll have a working blog listing page that displays posts from MDX files.
Table of Contents
- What You'll Build
- Prerequisites
- Step 1: Creating the Next.js Project
- Step 2: Installing and Configuring shadcn/ui
- Step 3: Setting Up MDX Support
- Step 4: Creating the Blog Directory Structure
- Step 5: Building the Blog Listing Page
- Step 6: Styling with Tailwind CSS
- What's Next
- Troubleshooting
- Conclusion
What You'll Build
By the end of Part 1, you'll have:
- A Next.js project with TypeScript configured
- shadcn/ui components installed and configured
- MDX support for writing blog posts
- A blog listing page that displays posts from MDX files
- Basic styling with Tailwind CSS
- A foundation ready for advanced features in later parts
Prerequisites
Before starting, make sure you have:
- Node.js 20+ installed
- npm or yarn package manager
- Basic knowledge of React and TypeScript
- A code editor like VS Code or Cursor
- Familiarity with the command line
Step 1: Creating the Next.js Project
Let's start by creating a new Next.js project with TypeScript. Open your terminal and run:
npx create-next-app@latest my-blog
When prompted "Would you like to use the recommended Next.js defaults?", select the following option:
"Yes, use recommended defaults"
This will create a new Next.js project with all the modern features we need. Once the installation is complete, navigate to your project:
cd my-blog
Project Structure Overview
Your project should now have this structure:
my-blog/
├── app/
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── public/
├── .gitignore
├── eslint.config.mjs
├── next-env.d.ts
├── next.config.ts
├── package.json
├── ...
The app/ directory is where we'll create our blog pages using Next.js App Router.
Step 2: Installing and Configuring shadcn/ui
shadcn/ui is a collection of beautifully designed, accessible React components built on top of Radix UI and Tailwind CSS. We'll use it to build our blog's UI.
Installing shadcn/ui
First, initialize shadcn/ui in your project:
npx shadcn@latest init
You'll be prompted with a question:
- Which color would you like to use as the base color? → Select one of the default colors
This creates a components.json file in your project root that configures shadcn/ui.
Installing Initial Components
Now let's install the components we'll need for Part 1:
npx shadcn@latest add button input badge
This installs:
- Button - For interactive elements
- Input - For search functionality (we'll use this in Part 2)
- Badge - For displaying categories
The components will be added to components/ui/ and you can import them like this:
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
Understanding components.json
Your components.json file should look something like this:
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}
This configuration ensures that shadcn/ui components work correctly with your project setup.
Step 3: Setting Up MDX Support
MDX allows you to write JSX in your Markdown files, making it perfect for blog posts. We'll use next-mdx-remote to render MDX content and gray-matter to parse frontmatter.
Note: We're starting with MDX files to understand how content management works at a fundamental level. In Part 4, we'll migrate to ElmapiCMS headless CMS, which will replace reading from MDX files with API calls. The content structure and rendering approach will remain similar, but content will be managed through an admin panel instead of files.
Installing Required Packages
Install the necessary packages:
npm install next-mdx-remote gray-matter globby remark-gfm
- next-mdx-remote - Renders MDX content in Next.js
- gray-matter - Parses frontmatter from MDX files
- globby - Finds MDX files in directories
- remark-gfm - GitHub Flavored Markdown support
Step 4: Creating the Blog Directory Structure
Now let's set up the directory structure for storing blog posts. Create a content/blog/ directory in your project root:
mkdir -p content/blog
Understanding Frontmatter Structure
Each MDX blog post will have frontmatter (metadata) at the top of the file. Here's the structure we'll use:
---
title: 'Your Post Title'
date: '2026-01-10'
description: 'A brief description of your post'
image: '/img/blog/your-image.webp'
image_dark: '/img/blog/your-image-dark.webp'
category: 'Tutorial'
---
- title - The post title
- date - Publication date (YYYY-MM-DD format)
- description - SEO description and post excerpt
- image - Featured image URL (optional)
- image_dark - Dark mode variant (optional)
- category - Post category (optional)
Creating Sample MDX Files
Create a few sample blog posts to test with. Create content/blog/first-post.mdx:
---
title: 'Welcome to My Blog'
date: '2026-01-10'
description: 'This is my first blog post using Next.js and MDX!'
category: 'Getting Started'
---
# Welcome to My Blog
This is my first blog post. I'm using **Next.js** and **MDX** to create a modern blog.
## What I'm Learning
- Next.js App Router
- MDX for content
- shadcn/ui components
## Next Steps
I'll be adding more features in the next parts of this tutorial series.
Create another sample post content/blog/second-post.mdx:
---
title: 'Building with Next.js'
date: '2026-01-10'
description: 'Exploring the power of Next.js for building modern web applications.'
category: 'Tutorial'
---
# Building with Next.js
Next.js is a powerful React framework that makes building web applications easy.
## Key Features
- Server-side rendering
- Static site generation
- API routes
- Image optimization
Step 5: Building the Blog Listing Page
Now let's create the utility functions to read MDX files and build the blog listing page.
Creating Posts Utility Functions
Create lib/posts.ts:
import { globby } from 'globby'
import matter from 'gray-matter'
import fs from 'fs/promises'
import path from 'path'
export interface Post {
slug: string
title: string
date: string
description: string
content: string
image?: string
image_dark?: string
category?: string
}
export type PostMatter = Omit<Post, 'content'>
const postsDirectory = path.join(process.cwd(), 'content/blog')
export async function getSortedPostsData(): Promise<PostMatter[]> {
const filePaths = await globby('*.mdx', { cwd: postsDirectory })
const allPostsData = await Promise.all(
filePaths.map(async (filePath: string) => {
const slug = filePath.replace(/\.mdx$/, '')
const fullPath = path.join(postsDirectory, filePath)
const fileContents = await fs.readFile(fullPath, 'utf8')
// Use gray-matter to parse the post metadata section
const matterResult = matter(fileContents)
// Combine the data with the id
return {
slug,
title: matterResult.data.title,
date: matterResult.data.date,
description: matterResult.data.description,
image: matterResult.data.image,
image_dark: matterResult.data.image_dark,
category: matterResult.data.category,
} as PostMatter
}),
)
// Sort posts by date (newest first)
return allPostsData.sort((a: PostMatter, b: PostMatter) => {
if (a.date < b.date) {
return 1
} else {
return -1
}
})
}
export async function getAllPostSlugs() {
const filePaths = await globby('*.mdx', { cwd: postsDirectory })
return filePaths.map((filePath) => {
return {
slug: filePath.replace(/\.mdx$/, ''),
}
})
}
This file exports:
- Post interface - TypeScript type for a complete post
- PostMatter type - Post without content (for listings)
- getSortedPostsData() - Gets all posts sorted by date
- getAllPostSlugs() - Gets all post slugs (for static generation)
Creating the Blog Page
Create app/blog/page.tsx:
import { getSortedPostsData } from '@/lib/posts'
import type { Metadata } from 'next'
import Link from 'next/link'
import { Badge } from '@/components/ui/badge'
export const metadata: Metadata = {
title: 'Blog',
description: 'Read our latest blog posts and tutorials.',
}
export default async function Blog() {
const posts = await getSortedPostsData()
return (
<div className="container mx-auto px-4 py-16">
<div className="mb-12 text-center">
<h1 className="text-4xl font-bold tracking-tight sm:text-6xl">
Blog
</h1>
<p className="mt-6 text-lg text-muted-foreground">
Read our latest posts and tutorials
</p>
</div>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{posts.map((post) => (
<Link
key={post.slug}
href={`/blog/${post.slug}`}
className="group block"
>
<article className="h-full rounded-lg border bg-card p-6 transition-colors hover:border-primary">
{post.category && (
<Badge variant="secondary" className="mb-2">
{post.category}
</Badge>
)}
<h2 className="mb-2 text-xl font-semibold group-hover:text-primary transition-colors">
{post.title}
</h2>
<p className="mb-4 text-sm text-muted-foreground">
{post.description}
</p>
<time className="text-xs text-muted-foreground">
{new Date(post.date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</time>
</article>
</Link>
))}
</div>
</div>
)
}
This page:
- Fetches all posts using
getSortedPostsData() - Displays them in a responsive grid
- Shows category badges
- Links to individual post pages (we'll create these in Part 2)
Testing the Blog Page
Start your development server:
npm run dev
Visit http://localhost:3000/blog to see your blog listing page. You should see your sample posts displayed in a grid.
Step 6: Styling with Tailwind CSS
Tailwind CSS is already configured in your Next.js project. The blog page uses Tailwind utility classes for styling.
Understanding the Styling
The blog page uses:
- container - Centers content with max-width
- grid - Creates a responsive grid layout
- gap-6 - Adds spacing between grid items
- md:grid-cols-2 - 2 columns on medium screens
- lg:grid-cols-3 - 3 columns on large screens
- hover:border-primary - Changes border color on hover
- text-muted-foreground - Uses theme-aware muted text color
Customizing Colors
shadcn/ui uses CSS variables for theming. You can customize colors in app/globals.css. The default theme includes:
- Primary colors
- Secondary colors
- Muted colors
- Accent colors
- Destructive colors
What's Next
Congratulations! You've completed Part 1. You now have:
- A Next.js project with TypeScript
- shadcn/ui components installed
- MDX support configured
- A working blog listing page
In Part 2, we'll add:
- Individual post pages with MDX rendering
- Table of contents component
- Search functionality
- Category filtering system
In Part 4, we'll migrate from MDX files to ElmapiCMS headless CMS, which will enable:
- Content management through an admin panel (no code changes needed)
- Multi-user collaboration
- API-based content delivery
- Automatic rebuilds via webhooks
This migration will replace the MDX file reading with API calls, but all the features we build will continue to work seamlessly.
Top comments (0)