CodeWithSabir
HomeAIDevOpsNext.jsMobile DevelopmentWeb Development
CodeWithSabir
  • Home
  • AI
  • DevOps
  • Next.js
  • Mobile Development
  • Web Development
  • About
  • Contact
CodeWithSabir

In-depth articles, tutorials, and guides on web development, React, Next.js, AI, and modern programming practices.

Topics

  • AI
  • DevOps
  • Next.js
  • Mobile Development
  • Web Development

Company

  • About
  • Contact
  • Privacy Policy
  • Terms

© 2026 CodeWithSabir. All rights reserved.

Built with SabirSoft.com

Home/Next.js/Building a Blog with Next.js and MDX: The Complete Guide
Next.js

Building a Blog with Next.js and MDX: The Complete Guide

Learn how to build a fast, SEO-optimized blog using Next.js and MDX — covering content management with Velite, syntax highlighting, custom components, and static generation.

Sabir KhaloufiSabir KhaloufiJanuary 30, 20264 min read

A blog built with Next.js and MDX gives you the best of both worlds: the simplicity of writing in Markdown with the power of embedding React components anywhere in your content. This guide walks through building a production-ready blog using Velite for content management — the same stack powering CodeWithSabir.

Why MDX?

Regular Markdown is great for prose. MDX extends it so you can drop React components directly into your content:

mdx
# My Article
 
Here's some regular markdown text.
 
<CodePlayground lang="javascript">
  const x = 1 + 1;
  console.log(x); // 2
</CodePlayground>
 
Back to regular markdown.

This lets you embed interactive demos, callout boxes, custom video players, or anything else React can render — directly in your articles.

Project Setup

bash
npx create-next-app@latest myblog --typescript --tailwind --app
npm install velite zod
npm install -D rehype-pretty-code rehype-slug remark-gfm shiki

Configure Velite

Velite is a content management layer that processes your MDX files at build time and gives you a fully-typed collection to query:

typescript
// velite.config.ts
import { defineConfig, s } from 'velite'
import rehypeSlug from 'rehype-slug'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
import remarkGfm from 'remark-gfm'
import rehypePrettyCode from 'rehype-pretty-code'
 
export default defineConfig({
  root: 'content',
  output: {
    data: '.velite',
    assets: 'public/static',
    base: '/static/',
    name: '[name]-[hash:6].[ext]',
    clean: true,
  },
  collections: {
    posts: {
      name: 'Post',
      pattern: 'posts/**/*.mdx',
      schema: s.object({
        title: s.string().max(200),
        slug: s.slug('posts'),
        date: s.isodate(),
        excerpt: s.string().max(300).optional(),
        thumbnail: s.image().optional(),
        categories: s.array(s.object({ name: s.string(), slug: s.string() })).default([]),
        tags: s.array(s.string()).default([]),
        author: s.string().default('Your Name'),
        published: s.boolean().default(true),
        featured: s.boolean().default(false),
        metadata: s.metadata(),
        body: s.mdx(),
      }).transform(data => ({
        ...data,
        permalink: `/post/${data.slug}`,
      })),
    },
  },
  mdx: {
    rehypePlugins: [
      rehypeSlug,
      [rehypeAutolinkHeadings, { behavior: 'wrap' }],
      [rehypePrettyCode, {
        theme: 'github-dark',
        onVisitLine(node) {
          if (node.children.length === 0) {
            node.children = [{ type: 'text', value: ' ' }]
          }
        },
      }],
    ],
    remarkPlugins: [remarkGfm],
  },
})

Add Velite to your Next.js build process:

javascript
// next.config.mjs
import { build } from 'velite'
 
/** @type {import('next').NextConfig} */
export default {
  webpack: (config, { isServer }) => {
    if (isServer) {
      config.plugins.push(new VeliteWebpackPlugin())
    }
    return config
  },
}
 
class VeliteWebpackPlugin {
  static started = false
  apply(compiler) {
    compiler.hooks.beforeCompile.tapPromise('VeliteWebpackPlugin', async () => {
      if (VeliteWebpackPlugin.started) return
      VeliteWebpackPlugin.started = true
      const dev = compiler.options.mode === 'development'
      await build({ watch: dev, logLevel: dev ? 'info' : 'warn' })
    })
  }
}

Writing Your First Post

code
content/
└── posts/
    └── my-first-post.mdx
mdx
---
title: "Getting Started with Next.js 15"
slug: getting-started-nextjs-15
date: 2026-04-01
excerpt: "A beginner-friendly introduction to Next.js 15 and the App Router."
categories:
  - name: Next.js
    slug: nextjs
tags: ["next.js", "react", "beginner"]
author: Your Name
published: true
---
 
# Getting Started with Next.js 15
 
Next.js is a React framework that gives you...

The MDX Content Component

To render MDX body content, you need a component that maps HTML elements to your custom styled versions:

typescript
// components/MDXContent.tsx
import * as runtime from 'react/jsx-runtime'
 
interface MDXContentProps {
  code: string
}
 
export function MDXContent({ code }: MDXContentProps) {
  const fn = new Function(code)
  const { default: Component } = fn({ ...runtime })
  return <Component components={mdxComponents} />
}
 
// Custom components that replace default HTML elements in MDX
const mdxComponents = {
  // Custom callout component
  Callout: ({ type = 'info', children }: { type?: string; children: React.ReactNode }) => (
    <div className={`callout callout-${type} my-6 p-4 rounded-lg border-l-4`}>
      {children}
    </div>
  ),
  // Custom code block with copy button
  pre: ({ children, ...props }: any) => (
    <div className="relative group">
      <pre {...props}>{children}</pre>
      <CopyButton code={props['data-code']} />
    </div>
  ),
}

The Blog List Page

typescript
// app/page.tsx
import { posts } from '#content'
import PostCard from '@/components/PostCard'
 
export default function HomePage() {
  const published = posts
    .filter(p => p.published)
    .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
 
  return (
    <main className="max-w-4xl mx-auto px-4 py-12">
      <h1 className="text-4xl font-bold mb-12">Latest Articles</h1>
      <div className="space-y-8">
        {published.map(post => (
          <PostCard key={post.slug} post={post} />
        ))}
      </div>
    </main>
  )
}

The Post Page with SEO

typescript
// app/post/[slug]/page.tsx
import { posts } from '#content'
import { notFound } from 'next/navigation'
import { MDXContent } from '@/components/MDXContent'
import type { Metadata } from 'next'
 
export function generateStaticParams() {
  return posts.filter(p => p.published).map(p => ({ slug: p.slug }))
}
 
export function generateMetadata({ params }: { params: { slug: string } }): Metadata {
  const post = posts.find(p => p.slug === params.slug && p.published)
  if (!post) return {}
 
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: 'article',
      publishedTime: post.date,
      authors: [post.author],
    },
  }
}
 
export default function PostPage({ params }: { params: { slug: string } }) {
  const post = posts.find(p => p.slug === params.slug && p.published)
  if (!post) notFound()
 
  return (
    <article className="max-w-3xl mx-auto px-4 py-12">
      <header className="mb-10">
        <div className="flex gap-2 mb-4">
          {post.categories.map(cat => (
            <a key={cat.slug} href={`/category/${cat.slug}`}
               className="text-sm font-semibold text-blue-600 uppercase tracking-wide">
              {cat.name}
            </a>
          ))}
        </div>
        <h1 className="text-4xl font-extrabold leading-tight mb-4">{post.title}</h1>
        <p className="text-gray-500 text-sm">
          By {post.author} · {new Date(post.date).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}
        </p>
      </header>
 
      <div className="prose prose-lg max-w-none">
        <MDXContent code={post.body} />
      </div>
    </article>
  )
}

Syntax Highlighting Styles

Rehype Pretty Code generates the highlighted HTML — you just need to add the CSS variables:

css
/* styles/code.css */
[data-rehype-pretty-code-figure] pre {
  overflow-x: auto;
  border-radius: 8px;
  padding: 1.25rem;
  font-size: 0.875rem;
  line-height: 1.7;
}
 
[data-line] {
  padding: 0 1rem;
}
 
[data-highlighted-line] {
  background: rgba(255, 255, 255, 0.1);
  border-left: 2px solid #3858F6;
}
 
/* File name tabs */
[data-rehype-pretty-code-title] {
  background: #1e1e2e;
  color: #cdd6f4;
  padding: 0.5rem 1rem;
  font-size: 0.8rem;
  border-radius: 8px 8px 0 0;
  font-family: monospace;
}

Sitemap and RSS Feed

typescript
// app/sitemap.ts
import { posts } from '#content'
import type { MetadataRoute } from 'next'
 
export default function sitemap(): MetadataRoute.Sitemap {
  const postEntries = posts
    .filter(p => p.published)
    .map(p => ({
      url: `https://yourdomain.com/post/${p.slug}`,
      lastModified: new Date(p.date),
      changeFrequency: 'weekly' as const,
      priority: 0.8,
    }))
 
  return [
    { url: 'https://yourdomain.com', changeFrequency: 'daily', priority: 1 },
    ...postEntries,
  ]
}

Key Takeaways

  • Velite processes MDX at build time and gives you a fully-typed, queryable content collection
  • MDX lets you embed React components in markdown — great for interactive demos and custom callouts
  • generateStaticParams + generateMetadata handles both static generation and SEO in one place
  • Rehype Pretty Code produces beautiful syntax highlighting with zero client-side JavaScript
  • Always generate a sitemap — it directly improves Google's ability to index your posts
#next.js#mdx#blog#velite#content management
Share:
Sabir Khaloufi — author photo

Written by

Sabir Khaloufi

Full-stack developer and tech blogger sharing in-depth tutorials on React, Next.js, AI, and modern web development.

On this page

  • Why MDX?
  • Project Setup
  • Configure Velite
  • Writing Your First Post
  • The MDX Content Component
  • The Blog List Page
  • The Post Page with SEO
  • Syntax Highlighting Styles
  • Sitemap and RSS Feed
  • Key Takeaways

Related Articles

Next.js

How to Build a Fullstack App with Next.js 15: Complete Guide

A hands-on guide to building a fullstack application with Next.js 15 — covering App Router, Server Actions, database integration, authentication, and deployment.

Sabir KhaloufiFebruary 25, 20265 min read
Next.js

Next.js Server Components Explained: What They Are and Why They Matter

A clear explanation of React Server Components in Next.js — what problem they solve, how they differ from Client Components, and the patterns that make them powerful.

Sabir KhaloufiFebruary 20, 20264 min read
Next.js

Authentication in Next.js with NextAuth.js v5: The Complete Setup

Learn how to implement authentication in Next.js using NextAuth.js v5 — covering credentials, OAuth providers, JWT sessions, protected routes, and role-based access control.

Sabir KhaloufiFebruary 15, 20264 min read