Using a headless CMS — Findit Next.js
The template does not need a CMS: content lives in src/content/ and you edit it like any other file
(Editing the content). Connect one when somebody who does not use a code editor has to publish — an agent adding
a listing, a colleague writing posts.
Either way the pages, sections and components stay exactly as they are. Only where the data comes from changes.
The one file that matters
Section titled “The one file that matters”Pages never import the collections directly. They call src/lib/content.ts:
export const getProperties = cache(async (): Promise<Property[]> => properties);export const getProperty = cache(async (slug: string) => (await getProperties()).find((item) => item.slug === slug));export const getAgents = cache(async (): Promise<Agent[]> => agents);export const getPosts = cache(async (): Promise<Post[]> => posts);…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 the filter pages, which build themselves from the same functions.
export const getProperties = cache(async (): Promise<Property[]> => { const entries = await fetch(`${process.env.CMS_URL}/properties`, { headers: { … } }).then((r) => r.json()); return entries.map(toProperty); // your mapping, one function per collection});The functions are getProperties/getProperty, getAgents/getAgent, getLocations/getLocation,
getCategories/getCategory, getPosts/getPost, getLegalPages/getLegalPage, getTestimonials and getTeam.
The single-item ones can keep searching the list, as they do now, or ask the CMS for one 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. The demo’s shapes are:
| Collection | Key fields |
|---|---|
Property |
slug, title, propertyId, for (sell or rent), featured, category, agent, location (slugs), address, price, duration, image, gallery (four photos), video, description, features, details (size, bedrooms, bathrooms, floor, …), utility (heating, air condition, …) |
Agent |
slug, name, image, address, bio, phone, mobile, whatsapp, email |
Place (locations and categories) |
slug, name, and for locations an image |
Post |
slug, title, category, date, author, image, excerpt, body |
LegalPage |
slug, title, description, updated, body |
Testimonial, TeamMember |
name, photo, rating, title, quote / name, role, bio, photo |
Three rules save trouble later:
slugis the URL, so make it unique and required.- A property refers to its agent, location and category by their
slug— model them as references and return the slug in your mapping function. A property whose agent, location or category is missing answers “404”. - Keep the collection’s order in the CMS (a sort field) because the site shows items in the order it receives them
(the home page’s Featured section shows the first five properties that are
featured).
Rich text
Section titled “Rich text”Descriptions, features, posts and legal texts are blocks, not HTML:
{ type: "heading", text: "…" }{ type: "paragraph", text: "…" } // **bold** and [links](/path) inside the text{ type: "list", ordered?: true, items: ["…", "…"] }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. Ask the CMS for images at least as large as the layout uses
(1400 px or more for property and post photos).
The four people usually ask about
Section titled “The four people usually ask about”| CMS | How it fits |
|---|---|
| Sanity | Hosted, generous free tier. Query with GROQ in your mapping functions; convert Portable Text to the block format. Good when editors want live preview. |
| Payload | Runs inside the same Next.js app or beside it, with a Postgres or Mongo database you own. Collections map almost one to one to types.ts. Good when you want everything in one deployment. |
| Strapi | Self-hosted Node admin, REST or GraphQL. Model the content types by hand, fetch with fetch. Good when the client already runs a server. |
| Directus | Puts an admin on top of a SQL database you already have. Fetch through its REST API. Good when the listings also feed something else, such as a property database or an MLS export. |
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 | A route that calls revalidateTag() when the CMS posts to it |
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, either
leave it (new items appear on the next build or revalidation) or set dynamicParams = true so a brand-new slug is
rendered on first request.
Keeping both
Section titled “Keeping both”A useful middle ground: the CMS owns properties and posts (the things that change weekly) while agents, locations,
categories and the page copy stay in the repository. src/lib/content.ts can fetch some collections and return local
ones for the rest — the functions are independent.