
July 23, 2026
A Markdown-first blog starter is still one of the best ways to ship content sites in Next.js because it balances developer ergonomics, performance, and long-term maintainability. For blogs, docs sites, changelogs, and editorial landing pages, local Markdown or MDX gives you a content workflow that is easy to version, easy to review in Git, and easy to deploy as static pages. Next.js’s App Router is now the recommended routing model for new applications, and it brings server components, nested layouts, streaming, and better integration with modern metadata APIs. In practice, that makes it an especially strong foundation for content-heavy starters. (nextjs.org)
The goal of this guide is to show how to build a clean, production-friendly Markdown blog starter with the App Router, TypeScript, and MDX. You will see how to structure content, render posts from the file system, build dynamic routes, add SEO primitives, and prepare the project for static generation and deployment. The implementation approach stays intentionally simple: keep content local, keep rendering server-side, and layer in enhancements only where they improve the reading experience. That gives you a starter that is lightweight enough for small sites and scalable enough for real projects. (nextjs.org)
Markdown remains a strong default for blog content because it minimizes operational overhead. Writers can focus on content without learning a CMS interface, and developers can manage posts as plain files under version control. That is especially useful for technical blogs, engineering documentation, and product updates, where the source of truth should be diffable and reviewable in Git. Next.js supports local Markdown and MDX content directly, and the App Router allows those files to participate in file-based routing and metadata generation. (nextjs.org)
A Markdown starter is also an excellent fit when you want predictable deployment behavior. If your posts are local files, the build step can statically generate pages once, cache them well, and serve them efficiently. That reduces runtime dependencies and avoids the failure modes that often come with external CMS integrations, such as rate limits, preview complexity, or schema drift. For teams that care about shipping quickly, the workflow is simple: edit Markdown, commit changes, and redeploy. (nextjs.org)
MDX adds a practical upgrade path. It lets you keep the authoring simplicity of Markdown while embedding React components where needed—examples include callouts, diagrams, custom code blocks, or interactive demos. Next.js explicitly supports MDX in App Router projects, and its docs show how MDX content can be rendered as pages or imported into server components. That means a blog starter can begin as plain Markdown and evolve into richer editorial experiences without changing the underlying architecture. (nextjs.org)

For content sites, the main trade-off is that Markdown starters push structure into the codebase. You are responsible for frontmatter, routing conventions, and content validation. But that is often a feature, not a bug: it creates a small, explicit system that your team can understand quickly. In other words, Markdown blog starters are not “old-fashioned”; they are a deliberate choice for teams that value clarity, portability, and build-time reliability. (nextjs.org)
If you are starting a new blog starter in 2026, the App Router should usually be your default choice. Next.js documents App Router as the file-system router built around React’s latest features, including server components, Suspense, and server functions. The Pages Router is still supported, but the official docs recommend App Router for the latest capabilities. For a content site, those capabilities matter because they improve layout composition, metadata handling, and server-side rendering patterns. (nextjs.org)
The Pages Router can still work well for simple blogs, especially if you are maintaining an older codebase or depending on legacy patterns like getStaticProps. But for a greenfield starter, it is usually less future-proof. App Router gives you nested layouts out of the box, better alignment with React Server Components, and a cleaner mental model for colocating content rendering with routing structure. That makes it easier to build a scalable starter that can later support categories, tags, authors, and custom post layouts. (nextjs.org)
Another important point is metadata. Next.js’s modern metadata API is built around App Router conventions such as metadata, generateMetadata, and metadata files for Open Graph images, robots, and sitemap generation. That is ideal for blogs because SEO and social sharing are first-class requirements. In a Pages Router project, you can still manage head tags, but the App Router’s metadata system is more structured and easier to standardize across content pages. (nextjs.org)
For a blog starter, the App Router also pairs well with server-side content loading. Posts can be imported or read from disk in server components and route handlers without exposing unnecessary client-side complexity. That keeps your render path simple and your bundle size smaller. If your goal is to create a maintainable starter that other developers will fork and extend, App Router is the stronger default. (nextjs.org)
The cleanest starting point is create-next-app with TypeScript enabled. Next.js’s installation docs show the App Router workflow as the baseline setup, and TypeScript gives you immediate value for content typing, route params, metadata objects, and MDX component props. A blog starter tends to grow in surface area over time, so typing your content model early pays off quickly. (nextjs.org)
A practical starter setup might look like this:
npx create-next-app@latest markdown-blog-starter \
--ts --app --src-dir --eslintFrom there, the repo structure should emphasize separation of concerns:
src/
app/
blog/
layout.tsx
page.tsx
components/
content/
posts/
lib/
posts.ts
mdx-components.tsx
next.config.mjsThat structure keeps content in src/content/posts, application routes under src/app, and reusable content-loading logic in src/lib. By doing this, you make the starter approachable for contributors: content authors can find posts quickly, while developers can find rendering and routing logic in predictable locations. The App Router documentation also emphasizes file-system organization, layouts, and pages as core concepts of the routing model. (nextjs.org)
TypeScript is especially useful for frontmatter and metadata. You can define a PostFrontmatter interface that includes fields like title, description, date, tags, draft, and canonicalUrl. Then your loader can validate the presence of required fields before a page is built. Even without a schema validator, TypeScript helps keep the API for your starter explicit and hard to misuse. In a content site, that consistency matters because blog pages often depend on structured metadata for cards, lists, SEO tags, and sitemaps. (nextjs.org)
@next/mdx and configure next.config.mjsNext.js supports Markdown and MDX through @next/mdx, which is the official integration for turning .md and .mdx files into routable pages or importable modules. The setup requires configuring next.config.mjs and adding an mdx-components.tsx file in the project root so App Router can resolve custom MDX components. This is the foundation that lets your starter treat content as code, while still preserving a content-first workflow. (nextjs.org)
A typical next.config.mjs looks like this:
import createMDX from '@next/mdx'
const withMDX = createMDX({
extension: /\.(md|mdx)$/,
})
/** @type {import('next').NextConfig} */
const nextConfig = {
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'],
}
export default withMDX(nextConfig)This configuration tells Next.js to compile both Markdown and MDX files. The docs note that @next/mdx supports .mdx by default, and you can extend it to .md files with the extension option. That is useful for blog starters because some teams prefer plain Markdown for posts, while others want MDX for enhanced components. Supporting both gives you flexibility without complicating the rest of the stack. (nextjs.org)
You also need mdx-components.tsx to define shared components used across MDX content:
import type { MDXComponents } from 'mdx/types'
const components: MDXComponents = {
// map custom tags here
}
export function useMDXComponents(): MDXComponents {
return components
}This file is required for App Router MDX support, according to the Next.js docs. It gives you a central place to override headings, links, images, tables, code blocks, and callouts with polished UI primitives. That is especially helpful in a blog starter, because the reading experience often depends more on typography and content components than on elaborate app chrome. (nextjs.org)

.md/.mdx files, frontmatter, and metadata fieldsA strong blog starter begins with a clear content schema. The most practical approach is to store each post as a local .md or .mdx file and use frontmatter for structured metadata. Next.js’s MDX documentation explicitly mentions frontmatter as a key/value structure for storing page data, and it also notes that metadata can be extracted from local files using Node’s file system APIs. That makes local content a natural fit for static blog generation. (nextjs.org)
A good frontmatter model usually includes:
title
description
date
updatedAt
slug
tags
author
draft
coverImage
canonicalUrl
You do not need every field on every post, but you should decide which fields are required and which are optional. For example, title, description, date, and slug are usually required because they drive navigation, SEO, and page generation. coverImage and tags are optional but useful for post cards and category pages. Keeping the schema explicit avoids content drift as your starter grows. (nextjs.org)
A simple post file might look like this:
---
title: "Getting Started with App Router"
description: "Build your first content site with Next.js App Router and MDX."
date: "2026-07-01"
slug: "getting-started-with-app-router"
tags:
- nextjs
- mdx
- blogging
draft: false
---
# Getting Started with App Router
Your post content goes here.For maintainability, create a loader module such as src/lib/posts.ts that reads the posts directory, parses frontmatter, and returns typed objects. That module becomes the single source of truth for list pages, detail pages, RSS feeds, and sitemaps. If you later move to remote content or a CMS, you can swap out the loader implementation while keeping the same post shape. That abstraction layer is one of the biggest advantages of designing the content model early. (nextjs.org)
The blog index page is where content browsing starts, so it should be simple, fast, and visually scannable. With the App Router, your /blog route can be created as src/app/blog/page.tsx, and the page can render post data pulled from the file system at build time. Next.js routing is file-system based, which means the directory structure itself becomes part of your architecture. For a starter, that is ideal because the route hierarchy mirrors the content hierarchy. (nextjs.org)
A practical index page should render responsive cards with title, description, date, and tags. Each card should link to the post route using next/link, and the card layout should adapt cleanly across screen sizes. Since App Router pages are server-rendered by default, the index page can load posts on the server, sort them by date, and send a lean HTML response to the browser. Next.js also provides built-in prefetching and client-side transitions for fast navigation between routes. (nextjs.org)
Here is a simplified example:
import Link from 'next/link'
import { getAllPosts } from '@/lib/posts'
export default async function BlogIndexPage() {
const posts = await getAllPosts()
return (
<main className="mx-auto max-w-5xl px-6 py-16">
<h1 className="text-4xl font-bold">Blog</h1>
<div className="mt-10 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{posts.map((post) => (
<article key={post.slug} className="rounded-2xl border p-6">
<p className="text-sm text-gray-500">{post.date}</p>
<h2 className="mt-2 text-xl font-semibold">
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
</h2>
<p className="mt-3 text-gray-600">{post.description}</p>
</article>
))}
</div>
</main>
)
}The key design principle here is that the index page should be content-focused, not feature-heavy. A blog starter is more useful when it ships with polished defaults than when it tries to solve every possible editorial pattern. Responsive cards, good spacing, visible dates, and meaningful previews are usually enough to make the index page feel complete. If needed later, you can extend it with tag filters, featured posts, or pagination without redesigning the content model. (nextjs.org)
Dynamic post pages are the core of the starter. In App Router, this usually means creating a route like src/app/blog/[slug]/page.tsx. The slug segment maps directly to the post file name or frontmatter slug, and the page loads the associated Markdown or MDX file on the server. Next.js documentation shows that MDX content can be rendered via file-based routing and dynamic imports, which makes it a natural match for slug-based blog pages. (nextjs.org)
The typical architecture is straightforward:
Read all post slugs from the content directory.
Implement generateStaticParams() so Next.js knows which routes to build.
Load a specific post by slug in the page component.
Render the MDX content with shared components and layout wrappers.
That workflow is especially powerful because it keeps rendering server-side by default. The page can import or read the .mdx module, extract metadata, and stream the final content without pushing the parsing logic to the client. This is one of the main strengths of the App Router for content sites: the browser receives the rendered result, not the entire content-processing pipeline. (nextjs.org)
A basic route might look like this:
import { notFound } from 'next/navigation'
import { getAllSlugs, getPostBySlug } from '@/lib/posts'
export async function generateStaticParams() {
return getAllSlugs().map((slug) => ({ slug }))
}
export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await getPostBySlug(slug)
if (!post) notFound()
const { Content } = post
return (
<article className="mx-auto max-w-3xl px-6 py-16">
<h1 className="text-4xl font-bold">{post.title}</h1>
<p className="mt-4 text-gray-500">{post.date}</p>
<div className="prose mt-10 max-w-none">
<Content />
</div>
</article>
)
}From an architectural standpoint, this is a classic separation of responsibilities: the route file handles routing and rendering, while the loader handles content discovery. That makes the starter testable and extensible. If you later add related posts, reading-time estimates, or heading anchors, those enhancements can live in the loader or shared MDX components without changing the slug routing pattern. (nextjs.org)
A blog starter becomes memorable through its reading experience. MDX makes this easier because it lets you map semantic elements to custom React components, so headings, links, images, blockquotes, and code fences all use the same visual system. Next.js’s MDX docs explicitly call out custom styles and components, shared layouts, and the mdx-components.tsx convention as the place to configure them. (nextjs.org)
A good reading experience starts with typography. The body text should have comfortable line height, sufficient contrast, and a readable measure around 60–75 characters per line. Headings should have clear hierarchy, and code blocks should be distinct enough to support technical articles. If your starter targets developers, code styling is not optional; it is part of the product. Consider adding syntax highlighting and a monospaced font stack, then wrap markdown content in a prose container or a typography utility class. Next.js’s MDX docs also reference the Tailwind typography plugin as a common path for styling MDX content. (nextjs.org)
You can define richer components in mdx-components.tsx:
import type { MDXComponents } from 'mdx/types'
function Callout({ children }: { children: React.ReactNode }) {
return <div className="my-6 rounded-xl border-l-4 p-4">{children}</div>
}
export function useMDXComponents(): MDXComponents {
return {
h2: (props) => <h2 className="mt-10 text-3xl font-semibold" {...props} />,
a: (props) => <a className="text-blue-600 underline" {...props} />,
pre: (props) => <pre className="overflow-x-auto rounded-xl bg-black p-4 text-white" {...props} />,
Callout,
}
}This is where MDX shines. You can create editorial patterns once and reuse them everywhere: callouts, note boxes, alert banners, code preview frames, and image captions. That gives technical content a more polished feel while keeping the authoring experience almost as simple as Markdown. The result is a blog starter that feels handcrafted without requiring a full design system. (nextjs.org)
SEO for a blog starter should be systematic, not improvised. In the App Router, Next.js provides a metadata API designed for exactly this purpose, including page metadata, Open Graph images, Twitter images, robots, and sitemap support. That means you should treat metadata as part of your content model rather than as an afterthought. (nextjs.org)
At minimum, each post should have a unique title and description, and blog pages should expose canonical URLs to avoid duplication problems. You can define metadata either statically in metadata exports or dynamically with generateMetadata, which is especially useful when metadata comes from frontmatter. This approach is clean for content sites because the post file becomes the source of truth for both rendered content and SEO fields. (nextjs.org)
A dynamic metadata export might look like this:
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await getPostBySlug(slug)
if (!post) return {}
return {
title: post.title,
description: post.description,
alternates: {
canonical: `https://example.com/blog/${post.slug}`,
},
openGraph: {
title: post.title,
description: post.description,
url: `https://example.com/blog/${post.slug}`,
type: 'article',
},
}
}For discoverability, add a sitemap. Next.js supports sitemap.xml as a special metadata file convention, and the docs show that it can be generated as XML or as a JavaScript/TypeScript route depending on your needs. For a blog starter, the simplest approach is to generate a sitemap that includes the home page, blog index, and every published post. That gives search engines a clear view of your content structure. (nextjs.org)

Also consider robots.txt and social preview images. The Next.js metadata docs cover Open Graph and Twitter image file conventions, which are useful if you want shareable cards on social platforms. Even if you do not implement every SEO enhancement on day one, building the starter around metadata exports ensures that future additions remain consistent and low-friction. (nextjs.org)
Performance is where a Markdown blog starter can really shine. If your content is local and known at build time, most or all post pages can be statically generated. The App Router supports server-rendered routes by default and works well with static route generation via generateStaticParams(). That means your pages can be prebuilt, cached aggressively, and delivered quickly without runtime database calls. (nextjs.org)
Image handling is another important part of the performance story. Blog posts often include hero images, screenshots, and diagrams, so you should standardize how images are imported or referenced. Next.js’s Image component remains the default choice for optimized images, and it pairs well with a content model that stores cover art alongside the post frontmatter. If you later add remote images, be sure to define allowed remote patterns in Next.js config so optimization remains predictable. (nextjs.org)
Deployment is straightforward on Vercel, which is the natural home for Next.js applications. The Next.js installation docs emphasize creating and running a new app locally, and the platform integration is designed so the app can be built and deployed with minimal friction. For a blog starter, that usually means:
build content at deploy time,
use environment variables only when needed,
keep dynamic logic server-side,
and avoid dependencies that make static generation brittle. (nextjs.org)
You should also think about route-level performance. Use server components wherever possible, keep client components limited to interactive widgets, and avoid turning the whole blog into a client-rendered application. Next.js’s docs highlight that routes are server-rendered by default and that navigation benefits from prefetching and streaming. For a content site, that is exactly what you want: fast first load, fast transitions, and minimal JavaScript. (nextjs.org)
A Markdown blog starter in Next.js works so well because it aligns with what content sites actually need: stable routing, simple authoring, static performance, and strong SEO. The App Router strengthens that pattern by giving you server components, modern metadata support, and a more future-facing foundation than the legacy Pages Router. Combined with @next/mdx, TypeScript, and a small file-system content layer, you get a starter that is easy to understand today and easy to extend tomorrow. (nextjs.org)
The main takeaway is to keep the architecture boring in the best possible way. Store posts locally, model metadata explicitly, render content on the server, and treat SEO and typography as first-class features. If you do that, you will end up with a starter that is fast, portable, and genuinely useful for technical publishing workflows. For many teams, that is a better long-term choice than introducing a heavy CMS too early. (nextjs.org)