DEV Community

Raşit
Raşit

Posted on • Originally published at elmapicms.com

How to Create a Next.js Blog - Part 1: Setup and Basic Structure

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

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
├── ...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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": {}
}

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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'
---
Enter fullscreen mode Exit fullscreen mode
  • 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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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$/, ''),
    }
  })
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)