Awwwards Nominee Awwwards Nominee

Next.js with a Headless CMS: Architecture, Content Modelling and Choosing Strapi, Directus or WordPress

by : deepak-chauhan Category : Nextjs Date :
Request a Quote

A Next.js site with a headless CMS keeps content in one system (Strapi, Directus, WordPress or something custom) and serves it through an API to a Next.js front end that renders pages on the server, caches them and refreshes them when editors publish. The decisions that matter most are how you model content, how pages get revalidated after a change, and how editors preview drafts. The CMS brand matters less than most teams expect.

This guide is for CTOs and founders planning a content-heavy Next.js build, from marketing sites to directories with thousands of programmatic pages.

What the architecture looks like

At its simplest there are three layers:

  • The CMS — where editors create and publish content. It owns the content model, the database, media uploads, user roles and workflows.
  • The API — REST or GraphQL, exposed by the CMS. Next.js reads from it with a server-side token that never reaches the browser.
  • The Next.js front end — Server Components fetch content and render HTML, which is cached and served from a CDN. Client Components are used only for interactivity.

Around that core you usually add object storage and a CDN for media, a search index, CMS webhooks, and sometimes a separate application back end for things that are not content, such as user accounts or payments. Keeping those out of the CMS pays off over time.

Build-time vs request-time fetching

In the App Router, a Server Component can simply await a fetch to the CMS. What changes is when that fetch runs:

  • At build time — pages listed by generateStaticParams are rendered during the build. Fast to serve, but a full rebuild for every edit does not scale once you have thousands of pages.
  • On first request, then cached — pages not generated at build are rendered the first time someone visits and cached afterwards. Most large sites pre-build key pages and let the long tail fill in on demand.
  • On every request — for personalised or real-time content. It costs more server time and CMS API load, so use it deliberately.

If you want the SEO trade-offs of static versus server rendering in more depth, see our comparison of SSG vs SSR in Next.js for SEO. A typical dynamic route for CMS content looks like this:

// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation';
import { getPost, getPopularSlugs } from '@/lib/cms';

export const revalidate = 3600; // fallback: refresh at most once an hour

export async function generateStaticParams() {
  const slugs = await getPopularSlugs(); // pre-build the pages that matter most
  return slugs.map((slug) => ({ slug }));
}

export default async function Page({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await getPost(slug);
  if (!post) notFound();
  return <article>{/* render post */}</article>;
}

On-demand revalidation with webhooks

Time-based revalidation is a safety net, not a publishing workflow. Editors expect changes live when they press Publish. The pattern is:

  1. Tag your CMS fetches, for example next: { tags: ['post:' + slug, 'posts'] }.
  2. Configure a webhook in the CMS that fires on create, update, publish, unpublish and delete.
  3. Point it at a Route Handler in Next.js that checks a shared secret and calls revalidateTag or revalidatePath.
// lib/cms.ts
export async function getPost(slug: string) {
  const res = await fetch(`${process.env.CMS_URL}/api/posts?slug=${slug}`, {
    headers: { Authorization: `Bearer ${process.env.CMS_TOKEN}` },
    next: { tags: [`post:${slug}`, 'posts'] },
  });
  if (!res.ok) return null;
  return res.json();
}

// app/api/revalidate/route.ts
import { revalidateTag, revalidatePath } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(req: NextRequest) {
  if (req.headers.get('x-webhook-secret') !== process.env.REVALIDATE_SECRET) {
    return NextResponse.json({ ok: false }, { status: 401 });
  }
  const { model, slug } = await req.json();
  if (model === 'post') {
    revalidateTag(`post:${slug}`);
    revalidateTag('posts'); // listing pages, related-post blocks
  } else {
    revalidatePath('/', 'layout'); // e.g. navigation or footer changed
  }
  return NextResponse.json({ ok: true });
}

Tags beat paths for content shown in many places: one author update may touch an author page, every byline and a listing. Note that the revalidateTag signature has changed in recent Next.js releases (newer versions accept a cache profile as a second argument), so check the docs for your version.

Preview and draft mode for editors

Editors need to see drafts in the real site design before publishing. Next.js Draft Mode handles this: a Route Handler sets a cookie, and while it is present pages render at request time and can fetch unpublished content.

// app/api/draft/route.ts
import { draftMode } from 'next/headers';
import { redirect } from 'next/navigation';

export async function GET(req: Request) {
  const { searchParams } = new URL(req.url);
  if (searchParams.get('secret') !== process.env.PREVIEW_SECRET) {
    return new Response('Invalid token', { status: 401 });
  }
  const slug = searchParams.get('slug') ?? '';
  (await draftMode()).enable();
  redirect(`/blog/${slug}`);
}

In your data layer, check (await draftMode()).isEnabled and, if true, ask the CMS for draft content using a separate preview token. Each CMS exposes drafts differently, so keep that logic in one data-fetching wrapper. Add a visible preview banner with an exit link.

Content modelling for programmatic pages

Content modelling is where headless projects succeed or quietly fail. For a site that generates thousands of pages from structured data (locations, categories, listings, comparisons), model the data, not the page.

  • Entities, not pages. Institutions, cities and courses are separate collections with relations. A page like “courses in a city” is a query over those relations.
  • Structured fields over rich text. Fees, addresses and facilities should be typed fields that can drive filters, schema markup and comparisons.
  • Reusable blocks for editorial pages. Components or dynamic zones (hero, FAQ, CTA, table) give marketing teams flexibility without letting them break the layout.
  • SEO fields on every routable type. Slug, meta title, meta description, canonical override, noindex flag and Open Graph image, all read by generateMetadata.
  • Templated copy with editorial overrides. Programmatic pages need default intros and headings generated from data, plus a field that lets an editor replace them on high-value pages.
  • Plan for thin pages. Decide rules up front (for example, noindex combinations with too few results) and enforce them in code.
// app/colleges/[city]/[course]/page.tsx
import type { Metadata } from 'next';
import { getCityCourse } from '@/lib/cms';

type Params = Promise<{ city: string; course: string }>;

export async function generateMetadata({ params }: { params: Params }): Promise<Metadata> {
  const { city, course } = await params;
  const data = await getCityCourse(city, course);
  return {
    title: data.seo?.title ?? `${data.course.name} colleges in ${data.city.name}`,
    description: data.seo?.description,
    alternates: { canonical: `/colleges/${city}/${course}` },
    robots: data.results.length < 3 ? { index: false } : undefined,
  };
}

Media, images and CDN

Do not store uploads on the CMS server’s local disk. Configure the CMS to write media to object storage (Amazon S3 or an equivalent) and serve it through a CDN. On the front end, use next/image with remotePatterns in next.config so images from your media domain are resized, served in modern formats and lazy-loaded. Make alt text mandatory, store dimensions with every asset to avoid layout shift, and decide whether images are optimised in Next.js, the CMS or an image CDN, not all three.

Search and filtering

CMS API filtering is fine for small datasets. When you need typo tolerance, facets and relevance ranking, sync content into a dedicated search engine such as Meilisearch, Typesense, Algolia, Elasticsearch or OpenSearch. The same CMS webhook that triggers revalidation can update the search index. Keep crawlable, indexable filter combinations as real routes rendered on the server, and handle the rest of the filtering on the client or through URL search parameters marked noindex.

Auth, roles and security

  • Editor roles live in the CMS: authors, editors, reviewers and admins with field-level or collection-level permissions where the CMS supports it.
  • API tokens should be read-only for the public site, stored as server-side environment variables and never prefixed with NEXT_PUBLIC_. Use a separate token for previews.
  • Site users (members, students, customers) are usually better handled by the application layer or a dedicated auth provider than by the CMS’s own user system.
  • Lock down the admin: put it on its own subdomain, enforce two-factor authentication, restrict by IP or VPN where practical and keep the CMS patched. A headless CMS reduces the public attack surface, but the admin and API are still internet-facing.

Hosting options

You are hosting two things. The Next.js front end can run on a managed platform such as Vercel or Netlify, on AWS (Amplify, or containers on ECS or App Runner behind CloudFront), or as a Node.js server on your own infrastructure; our guide to deploying a Next.js application on AWS walks through one route. The CMS needs a server or container, a database (PostgreSQL or MySQL are the common choices) and object storage, or you can use the vendor’s managed cloud where one exists.

Costs to plan for

Prices vary too much by scale and vendor to quote usefully, but the line items are predictable:

  • Front-end hosting, which on managed platforms often scales with bandwidth, function invocations and image optimisation usage.
  • CMS hosting (compute, managed database, backups) or a managed CMS subscription, plus any paid enterprise features such as SSO or audit logs.
  • Object storage and CDN egress for media.
  • Search service, if you use a hosted one.
  • Build time for modelling, preview, revalidation and content migration.
  • Ongoing upgrades, security patches and monitoring.

For a rough budget range on the build itself, our web application cost calculator is a sensible starting point.

Strapi vs Directus vs headless WordPress vs custom for Next.js

We have compared these platforms in detail elsewhere, in Strapi vs Directus and Strapi vs WordPress. The table below focuses only on how each fits a Next.js front end.

FactorStrapiDirectusHeadless WordPressCustom CMS
Best fitStructured content and programmatic pages with a JavaScript teamData-heavy projects, or wrapping an existing SQL databaseEditorial teams already fluent in WordPress, blog-led sitesUnusual workflows or content tightly bound to application logic
Content modellingCollection types, components and dynamic zonesMirrors your database tables and relations directlyPosts and pages, extended with custom post types and field pluginsWhatever you build
APIREST, with GraphQL available via pluginREST and GraphQLREST built in; GraphQL via a pluginYour own
Drafts and previewBuilt-in draft and publishConfigurable via status fields and content versioningNative drafts; preview needs authenticated requestsMust be built
Webhooks for revalidationBuilt inVia Flows or webhooksUsually via a plugin or custom hookMust be built
StackNode.jsNode.jsPHP and MySQLYour choice
Main trade-offMajor-version upgrades need planningCheck licence terms against your organisation’s sizePlugin sprawl and a second language in the stackYou own every feature and every bug

Our rule of thumb: choose Strapi when the content is structured and the team writes TypeScript; Directus when the data already lives in SQL or the project is closer to a data platform; headless WordPress when editors’ familiarity outweighs architectural neatness; and custom only when you can name the workflow no existing CMS supports. Our headless CMS development team works across all of these.

When not to go headless

  • A small brochure site that rarely changes. Two systems to maintain is overhead with no payoff.
  • Marketing wants a visual page builder with full layout control. Headless can do this, but it takes deliberate work.
  • No one will own the front end. Every new template needs a developer. Without that capacity, a traditional CMS is safer.
  • You depend on plugins that assume the CMS renders the pages.
  • Budget is tight and content volume is low. The benefits of headless show up at scale and over time.

The Colleges18 and Schools18 pattern

Two of our own builds show this architecture applied to large-scale programmatic SEO. Colleges18 is a college discovery portal with a server-rendered Next.js front end, Strapi as the headless CMS alongside a Node.js back end, programmatic SEO pages, dynamic search and filtering, media on AWS S3 and OpenAI used to help generate content. Schools18 applies the same Strapi and Next.js approach to a school search portal covering more than 19,000 schools, built for programmatic SEO and fast performance with a large dataset.

The common thread: institutions and locations modelled as structured entities, pages generated from their relations, media in object storage, and search treated as a core feature.

How VOCSO can help

VOCSO has been building software since 2009, with more than 600 projects delivered, ISO 27001 certification and a 5.0★ rating on Clutch. Our Next.js development services cover content modelling, CMS selection and set-up, preview and revalidation workflows, and hosting. We sign an NDA from day one. If you are planning a content-heavy Next.js site, talk to our team about your architecture.

Frequently asked questions

Is a headless CMS good for SEO with Next.js?

Yes, provided pages are rendered on the server and SEO fields are modelled in the CMS. Next.js sends full HTML to crawlers, and generateMetadata can read titles, descriptions and canonicals straight from your content.

Do I need to rebuild the site every time an editor publishes?

No. Use on-demand revalidation: a CMS webhook calls a Route Handler that runs revalidateTag or revalidatePath, and only the affected pages are refreshed.

Can editors preview unpublished content?

Yes. Next.js Draft Mode lets a secured route set a cookie so pages render at request time and fetch draft content from the CMS using a separate preview token.

Should I use REST or GraphQL?

Either works. REST is simpler to cache and debug; GraphQL helps when pages need deeply nested related content. Wrap whichever you choose in a typed data layer.

Can I keep WordPress and still use Next.js?

Yes. Editors keep the WordPress admin while Next.js renders the public site. Theme-dependent plugins need replacing with front-end code.

Related reading

Deepak Chauhan About Deepak Chauhan I am a technology strategist at VOCSO with 20 years of experience in full-stack development. Specializing in Python, the MERN stack, Node.js, and Next.js, I architect scalable, high-performance applications and custom solutions. I excel at transforming ideas into innovative digital products that drive business success.


Leave a Reply

Your email address will not be published. Required fields are marked *

Further Reading...

We use cookies to give you the best online experience. By using our website you agree to use of cookies in accordance with VOCSO cookie policy. I Accept Cookies