{"id":34151,"date":"2026-09-24T10:30:29","date_gmt":"2026-09-24T05:00:29","guid":{"rendered":"https:\/\/www.vocso.com\/blog\/?p=34151"},"modified":"2026-09-24T10:40:42","modified_gmt":"2026-09-24T05:10:42","slug":"nextjs-headless-cms-architecture","status":"publish","type":"post","link":"https:\/\/www.vocso.com\/blog\/nextjs-headless-cms-architecture\/","title":{"rendered":"Next.js with a Headless CMS: Architecture, Content Modelling and Choosing Strapi, Directus or WordPress"},"content":{"rendered":"<div style=\"margin-top: 0px; margin-bottom: 0px;\" class=\"sharethis-inline-share-buttons\" ><\/div>\n<p>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.<\/p>\n\n\n\n<p>This guide is for CTOs and founders planning a content-heavy Next.js build, from marketing sites to directories with thousands of programmatic pages.<\/p>\n\n\n\n<div id=\"ez-toc-container\" class=\"ez-toc-v2_0_81 counter-hierarchy ez-toc-counter ez-toc-grey ez-toc-container-direction\">\n<div class=\"ez-toc-title-container\">\n<p class=\"ez-toc-title ez-toc-toggle\" style=\"cursor:pointer\">Table of Contents<\/p>\n<span class=\"ez-toc-title-toggle\"><\/span><\/div>\n<nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-1\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-headless-cms-architecture\/#what-the-architecture-looks-like\" >What the architecture looks like<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-2\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-headless-cms-architecture\/#build-time-vs-request-time-fetching\" >Build-time vs request-time fetching<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-headless-cms-architecture\/#on-demand-revalidation-with-webhooks\" >On-demand revalidation with webhooks<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-4\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-headless-cms-architecture\/#preview-and-draft-mode-for-editors\" >Preview and draft mode for editors<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-5\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-headless-cms-architecture\/#content-modelling-for-programmatic-pages\" >Content modelling for programmatic pages<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-6\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-headless-cms-architecture\/#media-images-and-cdn\" >Media, images and CDN<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-7\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-headless-cms-architecture\/#search-and-filtering\" >Search and filtering<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-8\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-headless-cms-architecture\/#auth-roles-and-security\" >Auth, roles and security<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-9\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-headless-cms-architecture\/#hosting-options\" >Hosting options<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-10\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-headless-cms-architecture\/#costs-to-plan-for\" >Costs to plan for<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-11\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-headless-cms-architecture\/#strapi-vs-directus-vs-headless-wordpress-vs-custom-for-nextjs\" >Strapi vs Directus vs headless WordPress vs custom for Next.js<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-12\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-headless-cms-architecture\/#when-not-to-go-headless\" >When not to go headless<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-13\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-headless-cms-architecture\/#the-colleges18-and-schools18-pattern\" >The Colleges18 and Schools18 pattern<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-14\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-headless-cms-architecture\/#how-vocso-can-help\" >How VOCSO can help<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-15\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-headless-cms-architecture\/#frequently-asked-questions\" >Frequently asked questions<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-16\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-headless-cms-architecture\/#is-a-headless-cms-good-for-seo-with-nextjs\" >Is a headless CMS good for SEO with Next.js?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-17\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-headless-cms-architecture\/#do-i-need-to-rebuild-the-site-every-time-an-editor-publishes\" >Do I need to rebuild the site every time an editor publishes?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-18\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-headless-cms-architecture\/#can-editors-preview-unpublished-content\" >Can editors preview unpublished content?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-19\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-headless-cms-architecture\/#should-i-use-rest-or-graphql\" >Should I use REST or GraphQL?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-20\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-headless-cms-architecture\/#can-i-keep-wordpress-and-still-use-nextjs\" >Can I keep WordPress and still use Next.js?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-21\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-headless-cms-architecture\/#related-reading\" >Related reading<\/a><\/li><\/ul><\/li><\/ul><\/nav><\/div>\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"what-the-architecture-looks-like\"><\/span>What the architecture looks like<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>At its simplest there are three layers:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><strong>The CMS<\/strong> \u2014 where editors create and publish content. It owns the content model, the database, media uploads, user roles and workflows.<\/li><li><strong>The API<\/strong> \u2014 REST or GraphQL, exposed by the CMS. Next.js reads from it with a server-side token that never reaches the browser.<\/li><li><strong>The Next.js front end<\/strong> \u2014 Server Components fetch content and render HTML, which is cached and served from a CDN. Client Components are used only for interactivity.<\/li><\/ul>\n\n\n\n<p>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.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"build-time-vs-request-time-fetching\"><\/span>Build-time vs request-time fetching<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>In the App Router, a Server Component can simply <code>await<\/code> a fetch to the CMS. What changes is <em>when<\/em> that fetch runs:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><strong>At build time<\/strong> \u2014 pages listed by <code>generateStaticParams<\/code> are rendered during the build. Fast to serve, but a full rebuild for every edit does not scale once you have thousands of pages.<\/li><li><strong>On first request, then cached<\/strong> \u2014 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.<\/li><li><strong>On every request<\/strong> \u2014 for personalised or real-time content. It costs more server time and CMS API load, so use it deliberately.<\/li><\/ul>\n\n\n\n<p>If you want the SEO trade-offs of static versus server rendering in more depth, see our comparison of <a href=\"https:\/\/www.vocso.com\/blog\/static-site-generation-ssg-vs-server-side-rendering-ssr-in-nextjs-which-is-best-for-seo\/\">SSG vs SSR in Next.js for SEO<\/a>. A typical dynamic route for CMS content looks like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ app\/blog\/[slug]\/page.tsx\nimport { notFound } from 'next\/navigation';\nimport { getPost, getPopularSlugs } from '@\/lib\/cms';\n\nexport const revalidate = 3600; \/\/ fallback: refresh at most once an hour\n\nexport async function generateStaticParams() {\n  const slugs = await getPopularSlugs(); \/\/ pre-build the pages that matter most\n  return slugs.map((slug) =&gt; ({ slug }));\n}\n\nexport default async function Page({\n  params,\n}: {\n  params: Promise&lt;{ slug: string }&gt;;\n}) {\n  const { slug } = await params;\n  const post = await getPost(slug);\n  if (!post) notFound();\n  return &lt;article&gt;{\/* render post *\/}&lt;\/article&gt;;\n}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"on-demand-revalidation-with-webhooks\"><\/span>On-demand revalidation with webhooks<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>Time-based revalidation is a safety net, not a publishing workflow. Editors expect changes live when they press Publish. The pattern is:<\/p>\n\n\n\n<ol class=\"wp-block-list\"><li>Tag your CMS fetches, for example <code>next: { tags: ['post:' + slug, 'posts'] }<\/code>.<\/li><li>Configure a webhook in the CMS that fires on create, update, publish, unpublish and delete.<\/li><li>Point it at a Route Handler in Next.js that checks a shared secret and calls <code>revalidateTag<\/code> or <code>revalidatePath<\/code>.<\/li><\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ lib\/cms.ts\nexport async function getPost(slug: string) {\n  const res = await fetch(`${process.env.CMS_URL}\/api\/posts?slug=${slug}`, {\n    headers: { Authorization: `Bearer ${process.env.CMS_TOKEN}` },\n    next: { tags: [`post:${slug}`, 'posts'] },\n  });\n  if (!res.ok) return null;\n  return res.json();\n}\n\n\/\/ app\/api\/revalidate\/route.ts\nimport { revalidateTag, revalidatePath } from 'next\/cache';\nimport { NextRequest, NextResponse } from 'next\/server';\n\nexport async function POST(req: NextRequest) {\n  if (req.headers.get('x-webhook-secret') !== process.env.REVALIDATE_SECRET) {\n    return NextResponse.json({ ok: false }, { status: 401 });\n  }\n  const { model, slug } = await req.json();\n  if (model === 'post') {\n    revalidateTag(`post:${slug}`);\n    revalidateTag('posts'); \/\/ listing pages, related-post blocks\n  } else {\n    revalidatePath('\/', 'layout'); \/\/ e.g. navigation or footer changed\n  }\n  return NextResponse.json({ ok: true });\n}<\/code><\/pre>\n\n\n\n<p>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 <code>revalidateTag<\/code> 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.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"preview-and-draft-mode-for-editors\"><\/span>Preview and draft mode for editors<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>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.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ app\/api\/draft\/route.ts\nimport { draftMode } from 'next\/headers';\nimport { redirect } from 'next\/navigation';\n\nexport async function GET(req: Request) {\n  const { searchParams } = new URL(req.url);\n  if (searchParams.get('secret') !== process.env.PREVIEW_SECRET) {\n    return new Response('Invalid token', { status: 401 });\n  }\n  const slug = searchParams.get('slug') ?? '';\n  (await draftMode()).enable();\n  redirect(`\/blog\/${slug}`);\n}<\/code><\/pre>\n\n\n\n<p>In your data layer, check <code>(await draftMode()).isEnabled<\/code> 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.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"content-modelling-for-programmatic-pages\"><\/span>Content modelling for programmatic pages<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>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.<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><strong>Entities, not pages.<\/strong> Institutions, cities and courses are separate collections with relations. A page like &#8220;courses in a city&#8221; is a query over those relations.<\/li><li><strong>Structured fields over rich text.<\/strong> Fees, addresses and facilities should be typed fields that can drive filters, schema markup and comparisons.<\/li><li><strong>Reusable blocks for editorial pages.<\/strong> Components or dynamic zones (hero, FAQ, CTA, table) give marketing teams flexibility without letting them break the layout.<\/li><li><strong>SEO fields on every routable type.<\/strong> Slug, meta title, meta description, canonical override, noindex flag and Open Graph image, all read by <code>generateMetadata<\/code>.<\/li><li><strong>Templated copy with editorial overrides.<\/strong> Programmatic pages need default intros and headings generated from data, plus a field that lets an editor replace them on high-value pages.<\/li><li><strong>Plan for thin pages.<\/strong> Decide rules up front (for example, noindex combinations with too few results) and enforce them in code.<\/li><\/ul>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ app\/colleges\/[city]\/[course]\/page.tsx\nimport type { Metadata } from 'next';\nimport { getCityCourse } from '@\/lib\/cms';\n\ntype Params = Promise&lt;{ city: string; course: string }&gt;;\n\nexport async function generateMetadata({ params }: { params: Params }): Promise&lt;Metadata&gt; {\n  const { city, course } = await params;\n  const data = await getCityCourse(city, course);\n  return {\n    title: data.seo?.title ?? `${data.course.name} colleges in ${data.city.name}`,\n    description: data.seo?.description,\n    alternates: { canonical: `\/colleges\/${city}\/${course}` },\n    robots: data.results.length &lt; 3 ? { index: false } : undefined,\n  };\n}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"media-images-and-cdn\"><\/span>Media, images and CDN<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>Do not store uploads on the CMS server&#8217;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 <code>next\/image<\/code> with <code>remotePatterns<\/code> in <code>next.config<\/code> 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.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"search-and-filtering\"><\/span>Search and filtering<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>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.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"auth-roles-and-security\"><\/span>Auth, roles and security<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<ul class=\"wp-block-list\"><li><strong>Editor roles<\/strong> live in the CMS: authors, editors, reviewers and admins with field-level or collection-level permissions where the CMS supports it.<\/li><li><strong>API tokens<\/strong> should be read-only for the public site, stored as server-side environment variables and never prefixed with <code>NEXT_PUBLIC_<\/code>. Use a separate token for previews.<\/li><li><strong>Site users<\/strong> (members, students, customers) are usually better handled by the application layer or a dedicated auth provider than by the CMS&#8217;s own user system.<\/li><li><strong>Lock down the admin<\/strong>: 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.<\/li><\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"hosting-options\"><\/span>Hosting options<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>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 <a href=\"https:\/\/www.vocso.com\/blog\/deploying-a-next-js-application-on-aws-step-by-step-guide\/\">deploying a Next.js application on AWS<\/a> 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&#8217;s managed cloud where one exists.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"costs-to-plan-for\"><\/span>Costs to plan for<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>Prices vary too much by scale and vendor to quote usefully, but the line items are predictable:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Front-end hosting, which on managed platforms often scales with bandwidth, function invocations and image optimisation usage.<\/li><li>CMS hosting (compute, managed database, backups) or a managed CMS subscription, plus any paid enterprise features such as SSO or audit logs.<\/li><li>Object storage and CDN egress for media.<\/li><li>Search service, if you use a hosted one.<\/li><li>Build time for modelling, preview, revalidation and content migration.<\/li><li>Ongoing upgrades, security patches and monitoring.<\/li><\/ul>\n\n\n\n<p>For a rough budget range on the build itself, our <a href=\"https:\/\/www.vocso.com\/web-application-cost-calculator\">web application cost calculator<\/a> is a sensible starting point.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"strapi-vs-directus-vs-headless-wordpress-vs-custom-for-nextjs\"><\/span>Strapi vs Directus vs headless WordPress vs custom for Next.js<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>We have compared these platforms in detail elsewhere, in <a href=\"https:\/\/www.vocso.com\/blog\/strapi-vs-directus-comparing-top-headless-cms\/\">Strapi vs Directus<\/a> and <a href=\"https:\/\/www.vocso.com\/blog\/strapi-vs-wordpress\/\">Strapi vs WordPress<\/a>. The table below focuses only on how each fits a Next.js front end.<\/p>\n\n\n\n<figure class=\"wp-block-table table table-bordered\"><table><tbody><tr><td><strong>Factor<\/strong><\/td><td><strong>Strapi<\/strong><\/td><td><strong>Directus<\/strong><\/td><td><strong>Headless WordPress<\/strong><\/td><td><strong>Custom CMS<\/strong><\/td><\/tr><tr><td><strong>Best fit<\/strong><\/td><td>Structured content and programmatic pages with a JavaScript team<\/td><td>Data-heavy projects, or wrapping an existing SQL database<\/td><td>Editorial teams already fluent in WordPress, blog-led sites<\/td><td>Unusual workflows or content tightly bound to application logic<\/td><\/tr><tr><td><strong>Content modelling<\/strong><\/td><td>Collection types, components and dynamic zones<\/td><td>Mirrors your database tables and relations directly<\/td><td>Posts and pages, extended with custom post types and field plugins<\/td><td>Whatever you build<\/td><\/tr><tr><td><strong>API<\/strong><\/td><td>REST, with GraphQL available via plugin<\/td><td>REST and GraphQL<\/td><td>REST built in; GraphQL via a plugin<\/td><td>Your own<\/td><\/tr><tr><td><strong>Drafts and preview<\/strong><\/td><td>Built-in draft and publish<\/td><td>Configurable via status fields and content versioning<\/td><td>Native drafts; preview needs authenticated requests<\/td><td>Must be built<\/td><\/tr><tr><td><strong>Webhooks for revalidation<\/strong><\/td><td>Built in<\/td><td>Via Flows or webhooks<\/td><td>Usually via a plugin or custom hook<\/td><td>Must be built<\/td><\/tr><tr><td><strong>Stack<\/strong><\/td><td>Node.js<\/td><td>Node.js<\/td><td>PHP and MySQL<\/td><td>Your choice<\/td><\/tr><tr><td><strong>Main trade-off<\/strong><\/td><td>Major-version upgrades need planning<\/td><td>Check licence terms against your organisation&#8217;s size<\/td><td>Plugin sprawl and a second language in the stack<\/td><td>You own every feature and every bug<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>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&#8217; familiarity outweighs architectural neatness; and custom only when you can name the workflow no existing CMS supports. Our <a href=\"https:\/\/www.vocso.com\/headless-cms-development-services\">headless CMS development<\/a> team works across all of these.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"when-not-to-go-headless\"><\/span>When not to go headless<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<ul class=\"wp-block-list\"><li><strong>A small brochure site<\/strong> that rarely changes. Two systems to maintain is overhead with no payoff.<\/li><li><strong>Marketing wants a visual page builder<\/strong> with full layout control. Headless can do this, but it takes deliberate work.<\/li><li><strong>No one will own the front end.<\/strong> Every new template needs a developer. Without that capacity, a traditional CMS is safer.<\/li><li><strong>You depend on plugins<\/strong> that assume the CMS renders the pages.<\/li><li><strong>Budget is tight and content volume is low.<\/strong> The benefits of headless show up at scale and over time.<\/li><\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"the-colleges18-and-schools18-pattern\"><\/span>The Colleges18 and Schools18 pattern<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>Two of our own builds show this architecture applied to large-scale programmatic SEO. <a href=\"https:\/\/www.vocso.com\/case-study\/colleges18-college-discovery-web-application-development-with-strapi-nextjs\">Colleges18<\/a> 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. <a href=\"https:\/\/www.vocso.com\/case-study\/schools18-schools-search-website-application-development\">Schools18<\/a> 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.<\/p>\n\n\n\n<p>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.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"how-vocso-can-help\"><\/span>How VOCSO can help<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>VOCSO has been building software since 2009, with more than 600 projects delivered, ISO 27001 certification and a 5.0\u2605 rating on Clutch. Our <a href=\"https:\/\/www.vocso.com\/nextjs-development-services\">Next.js development services<\/a> 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, <a href=\"https:\/\/www.vocso.com\/contact-us\">talk to our team<\/a> about your architecture.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"frequently-asked-questions\"><\/span>Frequently asked questions<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"is-a-headless-cms-good-for-seo-with-nextjs\"><\/span>Is a headless CMS good for SEO with Next.js?<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>Yes, provided pages are rendered on the server and SEO fields are modelled in the CMS. Next.js sends full HTML to crawlers, and <code>generateMetadata<\/code> can read titles, descriptions and canonicals straight from your content.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"do-i-need-to-rebuild-the-site-every-time-an-editor-publishes\"><\/span>Do I need to rebuild the site every time an editor publishes?<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>No. Use on-demand revalidation: a CMS webhook calls a Route Handler that runs <code>revalidateTag<\/code> or <code>revalidatePath<\/code>, and only the affected pages are refreshed.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"can-editors-preview-unpublished-content\"><\/span>Can editors preview unpublished content?<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>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.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"should-i-use-rest-or-graphql\"><\/span>Should I use REST or GraphQL?<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>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.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"can-i-keep-wordpress-and-still-use-nextjs\"><\/span>Can I keep WordPress and still use Next.js?<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>Yes. Editors keep the WordPress admin while Next.js renders the public site. Theme-dependent plugins need replacing with front-end code.<\/p>\n\n\n<!-- vocso-related -->\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"related-reading\"><\/span>Related reading<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<ul class=\"wp-block-list\"><li><a href=\"https:\/\/www.vocso.com\/blog\/nextjs-pages-router-to-app-router-migration\/\">Migrating a Next.js App from the Pages Router to the App Router<\/a><\/li><li><a href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/\">Next.js Performance: How to Pass Core Web Vitals (LCP, INP, CLS)<\/a><\/li><li><a href=\"https:\/\/www.vocso.com\/blog\/nextjs-seo-checklist\/\">Next.js SEO: A Technical Checklist for the App Router<\/a><\/li><\/ul>\n\n<!-- \/vocso-related -->","protected":false},"excerpt":{"rendered":"<p>A practical guide for CTOs and founders planning a content-heavy Next.js site: how the CMS, API and App Router fit together, and how to choose between Strapi, Directus, headless WordPress or a custom CMS.<\/p>\n","protected":false},"author":23,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1415],"tags":[],"class_list":["post-34151","post","type-post","status-publish","format-standard","hentry","category-nextjs"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/www.vocso.com\/blog\/wp-json\/wp\/v2\/posts\/34151","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.vocso.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.vocso.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.vocso.com\/blog\/wp-json\/wp\/v2\/users\/23"}],"replies":[{"embeddable":true,"href":"https:\/\/www.vocso.com\/blog\/wp-json\/wp\/v2\/comments?post=34151"}],"version-history":[{"count":2,"href":"https:\/\/www.vocso.com\/blog\/wp-json\/wp\/v2\/posts\/34151\/revisions"}],"predecessor-version":[{"id":34308,"href":"https:\/\/www.vocso.com\/blog\/wp-json\/wp\/v2\/posts\/34151\/revisions\/34308"}],"wp:attachment":[{"href":"https:\/\/www.vocso.com\/blog\/wp-json\/wp\/v2\/media?parent=34151"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.vocso.com\/blog\/wp-json\/wp\/v2\/categories?post=34151"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.vocso.com\/blog\/wp-json\/wp\/v2\/tags?post=34151"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}