Back to blog
next.js blog
Topic: Next.js Blog SEO

How to Add a Blog to Next.js: MDX, CMS, and Database Approaches Compared

11 min read

How to Add a Blog to Next.js: MDX, CMS, and Database Approaches Compared

If you're building a Next.js blog for a SaaS product, the architecture decision you make in the first hour will follow you for years. There are three real options — MDX files in Git, a headless CMS, and a database-driven setup — and each one has a different breaking point. This guide compares all three, gives you a concrete folder structure, and covers the SEO implementation details that actually move rankings.


Table of Contents


What Is the Best Blog Setup for Next.js?

The short answer: it depends on who's publishing and how often. A Next.js blog can be production-ready in under a day with any of the three approaches below. The question is which one you'll still be happy with at post 80.

Here's the decision frame:

  • MDX in Git — best for solo developers publishing infrequently, who are comfortable with a PR-based workflow
  • Headless CMS — best for SaaS teams where a marketer or founder needs to publish without touching code
  • Database-driven — best when the blog is part of the product itself, not just a marketing channel

All examples in this guide use the App Router, which is the current standard. If you're still on Pages Router, the concepts transfer, but the file paths and API signatures differ.

Choosing the wrong architecture early is a real cost. Migrating 60 MDX files into a CMS mid-growth is a half-sprint of work that nobody wants to schedule. Pick correctly upfront.


Approach 1: MDX Files in Git

MDX lets you write Markdown with embedded React components. For technical blogs — documentation-style posts, code-heavy tutorials — it's genuinely useful. You get version control, no third-party dependency, and zero monthly cost.

How it works in App Router:

  1. Store posts in /content/posts/ as .mdx files with frontmatter
  2. Use gray-matter to parse frontmatter (title, date, slug, description)
  3. Use generateStaticParams() to generate all post routes at build time
  4. Use generateMetadata() in /app/blog/[slug]/page.tsx to set per-post metadata

A minimal generateMetadata export looks like this:

export async function generateMetadata({ params }) {
  const post = getPostBySlug(params.slug);
  return {
    title: post.title,
    description: post.description,
    openGraph: { title: post.title, description: post.description },
  };
}

Where MDX breaks down:

Every new post requires a Git commit, a PR, and a deploy. On a small team publishing once a month, that's fine. On a team publishing weekly, it adds up fast.

Field note: A SaaS founder shipping weekly posts via MDX will spend roughly 2–3 hours per week on Git overhead — branching, committing, waiting for deploys — before writing a single word of content. At 50+ posts, the repo itself becomes a maintenance burden: merge conflicts on the content directory, stale drafts in feature branches, and no non-developer can touch any of it.

MDX is a good starting point. It's not a good long-term system for most SaaS blogs. If you want to automate the content generation side of an MDX workflow, see Best AI Blog Writer for Next.js Websites in 2026.


Approach 2: Headless CMS Integration

A headless CMS — Contentful, Sanity, Hygraph — decouples your content from your codebase. Non-developers can publish, edit, and schedule posts without touching Git. For most SaaS marketing blogs, this is the right default.

How it works in App Router:

  • Fetch posts server-side using fetch() inside a Server Component
  • Use ISR (revalidate) so Next.js rebuilds only changed pages rather than the full site
  • Route structure stays the same: /app/blog/page.tsx for the index, /app/blog/[slug]/page.tsx for posts

A basic ISR fetch in a Server Component:

async function getPost(slug: string) {
  const res = await fetch(`https://your-cms.io/posts/${slug}`, {
    next: { revalidate: 3600 },
  });
  return res.json();
}

ISR matters for SEO. With 100+ posts, rebuilding the entire site on every publish is slow and unnecessary. ISR lets you serve cached static pages while revalidating only the updated content in the background. That keeps your Time to First Byte low — which Google measures.

Metadata from the CMS:

Your generateMetadata() function pulls title, description, and Open Graph data directly from the CMS record. This means your marketing team can update SEO metadata without a code deploy.

Trade-offs to know upfront:

  • Adds a paid third-party dependency (most CMS plans are $0–$99/month at early scale)
  • Requires a content modeling step before you can publish anything
  • Vendor lock-in is real — switching CMS later means migrating your content schema

Best fit: SaaS teams where a marketer or founder needs to publish independently. If that's not your situation, MDX is simpler.


Approach 3: Database-Driven Blog

Store posts in Postgres (or PlanetScale) and query them with Prisma or Drizzle inside Server Components. This gives you full control: drafts, scheduling, author roles, tagging, analytics — all in one system you own.

When this makes sense:

  • The blog is part of the product (e.g., user-generated content, multi-tenant blogs where each customer has their own blog)
  • You need custom publishing workflows that no CMS supports out of the box
  • You're already running a Postgres database and don't want another vendor

Route structure is identical to the other approaches — /app/blog/[slug]/page.tsx — the data source changes, not the routing.

SSR vs. ISR for database-driven blogs:

SSR (Server-Side Rendering) hits your database on every request. That adds latency and database load. ISR is usually the better default — cache the rendered page and revalidate on a schedule or on-demand when a post is updated.

You can generate canonical tags and JSON-LD structured data dynamically from the database record, which keeps your SEO metadata in sync with your content without any manual steps.

Honest assessment: For most early-stage SaaS blogs, this approach is overkill. You're adding database migrations, schema design, and backend code to a problem that a CMS solves in an afternoon. Build this when you genuinely need the control, not because it feels more engineered.


Recommended Folder Structure for a Next.js Blog

Consistency matters more than perfection here. Pick a structure and document it.

/app
  /blog
    page.tsx              ← Blog index (list of posts)
    /[slug]
      page.tsx            ← Individual post page
  sitemap.ts              ← Dynamic sitemap (served at /sitemap.xml)
  robots.ts               ← Robots rules (served at /robots.txt)

/components
  /blog
    PostCard.tsx          ← Used on the index page
    PostHeader.tsx        ← Title, date, author on post pages
    TableOfContents.tsx   ← Optional, for long posts

/lib
  blog.ts                 ← All fetch, filter, and sort logic lives here

/content
  /posts                  ← MDX files only; remove this for CMS or DB approaches

Key decisions in this structure:

  • Keep data logic in /lib/blog.ts, not in page files. Page files should be thin — they call functions and render components, nothing else.
  • /app/sitemap.ts and /app/robots.ts are native to App Router. Next.js serves them automatically at /sitemap.xml and /robots.txt. No plugin needed.
  • For CMS or database approaches, /content/posts/ disappears entirely and is replaced by API or database calls inside /lib/blog.ts.

This structure scales cleanly from 10 posts to 500. The folder shape doesn't change — only the data source does.


SEO Essentials Every Next.js Blog Needs

Next.js gives you the tools. Most teams use about 60% of them. Here's the full checklist:

Metadata per post:

  • Export generateMetadata() from every /app/blog/[slug]/page.tsx
  • Return title, description, and openGraph values pulled from your content source
  • Never use the same description for two posts — duplicate metadata is a real ranking signal problem

Sitemap:

  • Add /app/sitemap.ts and export a function that fetches all published post slugs
  • Return an array of { url, lastModified } objects
  • Next.js handles the rest — no sitemap plugin required

Canonical URLs:

  • Next.js does not auto-generate canonical tags
  • Set them explicitly in generateMetadata() using the alternates.canonical field
  • Missing canonicals on a blog with paginated or tagged archives will cause duplicate content issues

Structured data (JSON-LD):

  • Render a <script type="application/ld+json"> tag inside a Server Component with your Article schema
  • No external library needed — a plain object serialized with JSON.stringify() works fine
  • Include headline, datePublished, dateModified, author, and image at minimum

Images:

  • Use next/image for every post image — lazy loading, modern formats (WebP/AVIF), and size optimization are automatic
  • A blog page scoring below 80 on Core Web Vitals will struggle to rank regardless of content quality

Internal linking:


Scaling Your Next.js Blog for a SaaS Product

At 10 posts, any approach works. At 100 posts, your content workflow becomes the bottleneck — not your code.

What breaks first:

  • Manual metadata entry per post (someone forgets the description, Open Graph image is missing)
  • Internal linking — nobody goes back to update old posts when a new one is published
  • Slug and URL consistency — posts get published with inconsistent naming conventions

ISR configuration for blog pages:

Use a revalidate window of 60–3600 seconds depending on how often posts change. A marketing blog that publishes twice a week doesn't need a 60-second revalidation window — 3600 is fine and reduces unnecessary rebuilds. Avoid full SSR for blog pages unless you have a specific reason (e.g., personalized content per user).

Tag and category pages:

Add /app/blog/tag/[tag]/page.tsx and /app/blog/category/[category]/page.tsx. These pages add significant crawlable surface area for long-tail keywords and give Google more entry points into your content. Most SaaS blogs skip this and leave traffic on the table.

Track conversions, not just traffic:

Most SaaS blogs have 3–5 posts that drive 80% of their signups. Identify those early and invest in keeping them updated, well-linked, and technically clean. Chasing traffic volume without watching conversion attribution is how you end up with 200 posts and a flat MRR chart.

Field note: A B2B SaaS team that moved from a manual MDX workflow to a CMS-backed blog with automated metadata cut their publishing time from roughly 4 hours to under 45 minutes per post. The time savings came almost entirely from eliminating the Git overhead and manual SEO field entry — not from writing faster.

If your team is publishing more than 4 posts per month, the manual overhead of metadata, internal linking, and sitemap management adds up to a real engineering cost. A purpose-built automation layer is worth evaluating at that point. See How to Set Up Blog Automation on Next.js for SaaS and Save 20+ Hours Weekly for a practical breakdown, or Best AI Blog Writer for Next.js Websites in 2026 if you're evaluating tools to scale content production without scaling headcount.


Frequently Asked Questions: Next.js Blog Setup

Is Next.js good for SEO?
Yes. App Router with Server Components, ISR, and native metadata APIs gives you full control over every SEO signal Google measures — metadata, structured data, sitemaps, canonicals, and Core Web Vitals. The framework doesn't limit you; the implementation does.

Should I use MDX or a CMS for my Next.js blog?
MDX is fine for developer-only teams publishing infrequently. Use a headless CMS if anyone other than a developer needs to publish, or if you plan to publish more than twice a month. The Git overhead of MDX compounds quickly.

Does SSR improve SEO in Next.js?
SSR ensures content is rendered server-side and fully crawlable, but ISR is usually the better default for blog pages. ISR combines static performance with fresh content — you get fast TTFB without serving stale posts indefinitely.

How do I add metadata in Next.js App Router?
Export a generateMetadata() function from your page file. It can be async, so you can fetch post data and return dynamic title, description, and openGraph values. This replaces the <Head> component from Pages Router.

How do I create a sitemap in Next.js?
Add /app/sitemap.ts and export a default function that returns an array of URL objects with url and lastModified fields. Next.js serves it automatically at /sitemap.xml — no plugin or configuration needed.

How do I add structured data to a Next.js blog?
Render a <script type="application/ld+json"> tag inside a Server Component with your Article schema serialized as JSON. No external library is required. Include headline, datePublished, dateModified, author, and image at minimum.

What is the best folder structure for a Next.js blog?
Use /app/blog/page.tsx for the index, /app/blog/[slug]/page.tsx for posts, /lib/blog.ts for all data logic, and /components/blog/ for shared UI components. Add /app/sitemap.ts and /app/robots.ts at the app root.

How do I optimize a Next.js blog for Google?
Cover the non-negotiables: dynamic metadata per post, a generated sitemap, explicit canonical URLs, JSON-LD Article schema, next/image for all images, and deliberate internal linking between posts. Get Core Web Vitals above 80 before worrying about anything else.


Sources

Continue reading

Related Reading

Hand-picked posts that pair well with what you just read.