How to Build Your First Blog Using Next.js and Markdown

Himmat Regar Jun 27, 2025, 3:48 PM
nextjs
Views 282
Blog Thumbnail

Modern React projects want speed (static output), flexibility (rich components), and author-friendly content (plain text Markdown).
Next.js 15 delivers all three: its App Router produces SEO-ready pages, and MDX lets each post mix prose with interactive React. Follow this guide and launch a production-ready blog in an afternoon.


1 · Prerequisites

Tool Version (June 2025) Why you need it
Node.js ≥ 18 LTS Edge runtime & native fetch
pnpm / npm / Yarn latest package manager
Git any version control & Vercel deploy
Basic Markdown write posts

2 · Project Bootstrap

npx create-next-app@latest my-blog \ --typescript \ --tailwind \ --eslint \ --app # App Router, not pages/ cd my-blog pnpm dev

 

You now have a React 19 + Turbopack dev server with fast HMR.


3 · Add MDX Support

Next.js 15 can directly treat .mdx as a page, or you can load content from files and render anywhere. We’ll combine both approaches:

pnpm add @next/mdx gray-matter remark-gfm rehype-prism-plus

Create an MDX config (next.config.mjs):

import createMDX from '@next/mdx'; const withMDX = createMDX({ extension: /\.mdx?$/, options: { remarkPlugins: [import('remark-gfm')], rehypePlugins: [import('rehype-prism-plus')], }, }); export default withMDX({ experimental: { appDir: true }, pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'], });

 

The official guide shows how MDX pages behave like any other route in App Router. nextjs.org


4 · Create Your Post Directory

my-blog └─ content └─ posts ├─ hello-world.mdx └─ another-post.mdx

 

Each file begins with front-matter:

 
--- title: "Hello, World" date: "2025-06-27" summary: "Your first post in MDX!" tags: ["nextjs","mdx","tutorial"] --- # Hello World 👋 Welcome to **Next.js 15**…

 

gray-matter will extract that YAML block for listing pages.
This pattern is proven by the classic Markdown blog tutorial (Pages Router) and still works after migrating to App Router. nextjs.orgsinglehanded.dev


5 · Dynamic Routes for Blog Posts

Create a folder app/(blog)/posts/[slug]/page.tsx:

import { readFile, readdir } from 'node:fs/promises'; import path from 'node:path'; import matter from 'gray-matter'; import { compile } from '@mdx-js/mdx'; import { MDXRemote } from 'next-mdx-remote/rsc'; const POSTS_PATH = path.join(process.cwd(), 'content', 'posts'); export async function generateStaticParams() { const files = await readdir(POSTS_PATH); return files.map((f) => ({ slug: f.replace(/\.mdx?$/, '') })); } export default async function PostPage({ params }: { params: { slug: string } }) { const raw = await readFile(path.join(POSTS_PATH, `${params.slug}.mdx`), 'utf-8'); const { content, data } = matter(raw); const mdxSource = await compile(content, { outputFormat: 'function-body' }); return ( <article className="prose mx-auto"> <h1>{data.title}</h1> <MDXRemote source={mdxSource} /> </article> ); } export const revalidate = 60; // ISR

What’s happening

  • generateStaticParams() replaces the old getStaticPaths and runs at build.

  • The page itself is a React Server Component—no client JS cost.

  • revalidate enables Incremental Static Regeneration. hire-tim.comcoffey.codes


6 · Listing All Posts on /

app/page.tsx:

import { readFile, readdir } from 'node:fs/promises'; import path from 'node:path'; import matter from 'gray-matter'; import Link from 'next/link'; const POSTS_PATH = path.join(process.cwd(), 'content', 'posts'); export const revalidate = 60; export default async function Home() { const files = await readdir(POSTS_PATH); const posts = await Promise.all( files.map(async (file) => { const raw = await readFile(path.join(POSTS_PATH, file), 'utf-8'); const { data } = matter(raw); return { slug: file.replace(/\.mdx?$/, ''), ...data }; }), ); return ( <main className="prose mx-auto my-12"> <h1>My Blog</h1> <ul> {posts .sort((a, b) => Date.parse(b.date) - Date.parse(a.date)) .map((p) => ( <li key={p.slug}> <Link href={`/posts/${p.slug}`}>{p.title}</Link> — {p.summary} </li> ))} </ul> </main> ); }

 


7 · Styling & Components

  • Tailwind CSS: already installed; extend typography via @tailwindcss/typography.

  • Custom MDX components: create mdx-components.tsx and pass to MDXRemote so authors can drop in <YouTube id="xyz" /> right inside Markdown. nextjs.org

  • Code highlighting: rehype-prism-plus in the MDX config adds Prism themes automatically.


8 · Live Authoring Workflow

  1. Add a .mdx file under content/posts.

  2. Save—Turbopack hot-reloads the preview in < 200 ms.

  3. Push to GitHub; Vercel build caches unchanged posts and ships a new URL in ±10 s.

Articles written this month show real-world performance: a lightweight MDX blog on Next.js 15 deploys in seconds and hits 100/100 Lighthouse on mobile. mdxblog.io


9 · Deploying to Production

# 1. Commit & push repo git add . git commit -m "Launch blog" git push origin main # 2. Import in vercel.com dashboard # 3. Choose "Production branch: main", build command `next build`

 

  • Edge Network caches each prerendered page worldwide.

  • Using ISR, edits go live without a full rebuild—ideal for frequent blogging.

Self-hosting? Run next start behind Nginx or Docker.


10 · Bonus Improvements

Feature How
RSS feed Generate XML in a cron-style Server Action each day.
Search Fuse.js in the client or Algolia if content grows.
Analytics Vercel Analytics or Plausible script in layout.tsx.
Images Store in /public/images and use <Image> for optimized formats.

11 · Wrapping Up

You’ve:

  1. Bootstrapped a Next.js 15 project.

  2. Added MDX for rich Markdown writing.

  3. Created dynamic routes with App Router’s generateStaticParams.

  4. Styled with Tailwind & Prism.

  5. Deployed globally with ISR.

That’s a production-grade, component-enhanced blog entirely in React—no headless CMS or plugin maze required.

Next steps: hook up a comments provider (Giscus, Clerk), explore the new explicit caching API, or migrate old Markdown posts by dropping them into /content/posts. Happy blogging! 🚀

Comments

Please login to leave a comment.

No comments yet.

Related Posts

nextjs-explained-beginners-guide-2025
267 viewsnextjs
Himmat Regar Jun 27, 2025, 10:12 AM

Next.js Explained: A 2025 Beginner’s Guide to the React...

nextjs-vs-react-differences
268 viewsnextjs
Himmat Regar Jun 27, 2025, 11:09 AM

Next.js vs React: What’s the Difference and When to Use...

nextjs-file-based-routing-guide
284 viewsnextjs
Himmat Regar Jun 27, 2025, 11:23 AM

Understanding File-Based Routing in Next.js

nextjs-api-routes-backend-functionality
190 viewsnextjs
Himmat Regar Jun 29, 2025, 5:03 PM

How to Use API Routes in Next.js for Backend Functional...

nextjs-incremental-static-regeneration-isr-guide
207 viewsnextjs
Himmat Regar Jun 29, 2025, 5:18 PM

Incremental Static Regeneration (ISR) Explained with Ex...

image-optimization-nextjs-everything-you-should-know
200 viewsnextjs
Himmat Regar Jun 29, 2025, 5:20 PM

Image Optimization in Next.js: Everything You Should Kn...

multi-language-website-nextjs-i18n
79 viewsnextjs
Himmat Regar Jun 30, 2025, 5:14 PM

Building a Multi-Language Website with Next.js 15 & Mod...

nextjs-tailwind-css-perfect-ui-pairing
76 viewsnextjs
Himmat Regar Jun 30, 2025, 5:25 PM

Next.js 15 + Tailwind CSS 4: The Perfect UI Pairing

laravel-cookies-guide
1088 viewsLaravel
Himmat Regar Jun 1, 2025, 2:23 PM

Laravel Cookies: Secure, Encrypted & Easy (Guide 2025)