Use a headless CMS with a Next.js template
Every Framebrick Next.js template is built the same way, so connecting a headless CMS works the same for all of them.
The template doesn’t need one: content lives in src/content/ and you edit it like any other file. Connect a CMS when
somebody who doesn’t use a code editor has to publish — a colleague adding posts, a client adding projects. Either way
the pages, sections and components stay exactly as they are; only where the data comes from changes.
What differs from one template to the next — its collections, their fields and the functions that read them — is on the template’s own Using a headless CMS page, in its docs under Headless CMS (for example Nitip’s).
The one file that matters
Section titled “The one file that matters”Pages never import the collections directly. They call src/lib/content.ts, which has a pair of functions for each
collection — the list and a single item:
export const getPosts = cache(async (): Promise<Post[]> => posts);export const getPost = cache(async (slug: string) => (await getPosts()).find((item) => item.slug === slug));To move to a CMS, rewrite these functions to fetch from it and return the same types (src/content/types.ts). Keep
the names, the arguments and the return types, and nothing else in the project has to change — including
sitemap.ts and every list, filter and search, which build themselves from the same functions.
export const getPosts = cache(async (): Promise<Post[]> => { const entries = await fetch(`${process.env.CMS_URL}/posts`, { headers: { … } }).then((r) => r.json()); return entries.map(toPost); // your mapping, one function per collection});The single-item functions can keep searching the list, as they do now, or ask the CMS for one entry. cache makes
each call run once per request, even when a page and its metadata ask for the same entry.
Page copy (src/content/pages/) and site details (src/content/site.ts) can stay in the repository even when the
collections come from a CMS — they change once a year, not once a week.
What to model in the CMS
Section titled “What to model in the CMS”Mirror src/content/types.ts: one collection in the CMS for each type the functions return. A few rules save trouble
later:
slugis the URL, so make it unique and required.- When an item points to another — a project to its services, a property to its agent — model a reference, and return what the type expects (the slug, sometimes a label too) in your mapping function.
- Keep the collection’s order in the CMS (a sort field): the site shows items in the order it receives them, and the home page shows the first few.
- Make a field required in the CMS when the type requires it, images included, so an editor can’t publish an item the page can’t show.
Rich text
Section titled “Rich text”Post bodies — and on some templates descriptions, legal texts or a service’s “What’s included” — are blocks, not HTML:
{ type: "heading", text: "…" }{ type: "paragraph", text: "…" }{ type: "list", ordered?: true, items: … }Each template adds a few details, such as a bold start or links inside the text; its CMS page and
src/content/types.ts have the exact shapes. Every CMS has its own rich-text format: Portable Text in Sanity, Lexical
or Slate in Payload, HTML in Strapi and Directus. Convert it to these blocks in your mapping function, or extend
src/design-system/components/patterns/RichText.tsx with the block types you need.
Images
Section titled “Images”Content refers to images as /images/<file> in public/. A CMS gives you URLs on its own domain instead:
- Return the CMS URL from your mapping function.
- Allow that domain in
next.config.ts:images: { remotePatterns: [{ protocol: "https", hostname: "cdn.sanity.io" }] },
next/image then resizes and serves them as before. While the CMS runs on your own machine, Next.js also refuses
images from a private address until you allow it (images.dangerouslyAllowLocalIP, shown in the Strapi and Directus
guides). Upload
images at least as large as the layout uses; the template’s CMS page gives the sizes.
Which CMS
Section titled “Which CMS”| CMS | How it fits | Step by step |
|---|---|---|
| Sanity | Hosted, generous free tier. Query with GROQ in your mapping functions; editors work in Sanity Studio, which can live inside the site. Good when editors want live preview. | Sanity + Next.js |
| Payload | Runs inside the same Next.js app, with a database you own. Collections map almost one to one to types.ts. Good when you want everything in one deployment. |
Payload + Next.js |
| Strapi | Free and open source, a Node app of its own next to the site. Good when the client already runs a server. | Strapi + Next.js |
| Directus | An admin on top of a SQL database you own, self-hosted in Docker. Good when the content also feeds something else. | Directus + Next.js |
The guides were tested on Nitip; the steps are the same for every template, with its own collections. Any other
headless CMS (Contentful, Storyblok, Hygraph, Prismic, Keystone, a WordPress REST API) works the same way: fetch in
src/lib/content.ts, map to the types.
Keeping the site fresh
Section titled “Keeping the site fresh”Pages are pre-rendered, which is why the site is fast. Choose how new content reaches visitors:
| Approach | What to do | Feels like |
|---|---|---|
| Rebuild on publish | Add a webhook in the CMS that calls your host’s deploy hook | New content in a minute or two |
| Revalidate on a timer | export const revalidate = 600; in the page file |
New content within 10 minutes, no rebuild |
| Revalidate on demand | The CMS tells the site after a save: a route that calls revalidatePath() (a hook, with Payload in the same app) — what the guides set up |
New content in seconds |
| Always fresh | export const dynamic = "force-dynamic"; |
Slower pages; only for something truly live |
Collection pages also list their slugs in generateStaticParams() and set dynamicParams = false. With a CMS, set
dynamicParams = true, so an item published after the build renders on its first visit; unknown slugs are still a
404.
Keeping both
Section titled “Keeping both”A useful middle ground: the CMS owns the collections that change every week — posts, roles, case studies — while the
rest and the page copy stay in the repository. src/lib/content.ts can fetch some collections and return local ones
for the others: the functions are independent.