mdx · velite · blog
Blog Route, MDX, Slugs, and Velite
A concise technical walkthrough of the blog system, including MDX content files, Velite transforms, slug routes, metadata, and reusable article rendering.
3 min read

Goal#
The blog route was built to be:
- content-driven
- statically generated
- easy to maintain
- visually consistent with the portfolio
Instead of hardcoding articles, the system reads MDX files and turns them into structured data.
Content source#
All posts live in:
src/content/blog/Each post contains frontmatter like:
---
title: "Post Title"
description: "Short description"
date: "2026-04-12"
published: true
tags: ["mdx", "nextjs"]
cover: "/images/profile.png"
---This makes posts easy to add without touching route code.
Velite pipeline#
The content layer is configured in velite.config.ts.
Main job of Velite:
- read MDX files
- validate fields
- compile MDX body
- generate slug
- generate TOC
- calculate reading time
Important schema fields:
slug: s.path(),
body: s.mdx(),
raw: s.raw(),
toc: s.toc({ maxDepth: 3 }),And in transform:
slugAsParams: post.slug.replace(/^blog\//, "")
readingTime: readingTime(post.raw).textBlog list route#
The main route is src/app/blog/page.tsx.
It:
- imports posts from
#site/content - filters published posts
- sorts by newest date
- passes data to the client search component
const publishedPosts = posts
.filter((post) => post.published)
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());Search behavior#
Search is handled in blog-client.tsx.
Approach:
- read
?search= - filter by title
- keep the URL in sync
const search = searchParams.get("search") ?? "";
const filteredPosts = posts.filter((post) =>
post.title.toLowerCase().includes(search.trim().toLowerCase())
);This keeps the list route server-first while allowing lightweight client interaction.
Dynamic slug route#
Articles are rendered by:
src/app/blog/[slug]/page.tsxThis route handles:
generateStaticParams()generateMetadata()notFound()- JSON-LD
- MDX rendering
Example:
export function generateStaticParams() {
return posts
.filter((post) => post.published)
.map((post) => ({ slug: post.slugAsParams }));
}This makes all published posts statically generated.
MDX rendering layer#
Compiled MDX is rendered through custom components.
Main files:
mdx-content.tsxmdx-components.tsx
This allows support for:
- custom links
- code blocks
- callouts
- YouTube embeds
Example idea:
<MdxContent code={post.body} />Styling approach#
Prose styles are scoped in:
src/styles/mdx.cssThey are wrapped under:
.mdx-content { ... }This prevents blog typography from leaking into the rest of the site.
Short architecture summary#
The blog system is:
- MDX files for authoring
- Velite for transformation and typing
- static slug routes for rendering
- small client boundary for search
- custom MDX components for richer article content
Prompt#
If you want to build a blog system like this, use this prompt:
Build an MDX-powered blog system in Next.js App Router using Velite.
Store posts in src/content/blog as .mdx files with frontmatter.
Generate typed content with slug, reading time, and TOC.
Create a server-rendered /blog page with search handled in a small client component.
Create statically generated /blog/[slug] pages with metadata, JSON-LD, and custom MDX rendering.
Use scoped prose styles and keep the design consistent with the rest of the portfolio.