We use cookies to enhance your experience on the site
CodeWorlds
Back to collections
Guide12 min read

Contentful, a headless CMS for content at scale

Contentful in practice: content modelling, the Delivery API, Next.js integration, the Salesforce acquisition, the free plan and Lite at 300 dollars, alternatives.

Contentful, a headless CMS for content at scale

Contentful started in Berlin in 2013 and for years was the default answer to where content should live when the front end is written in JavaScript. The model is simple: editors work in a panel, the application pulls content through an API, and the presentation layer shares nothing with the back end beyond a data contract.

In June 2026 Salesforce signed an agreement to acquire the company. Nothing changed overnight for existing deployments, but when picking a platform for the next five years that is a circumstance to weigh alongside price and capability.

The transaction has not closed yet. The buyer projected a close in the third quarter of its fiscal 2027, meaning between August and the end of October 2026, subject to regulatory approvals. Financial terms were not disclosed. Until then the two companies operate separately, and statements about the product roadmap are worth reading with that date in mind.

How it works

You describe content as types rather than pages. A type is a field definition, for instance an article with a title, rich text, an image, and a reference to an author. Editors create entries of that type, and you fetch them through an API and render them however you like.

That shift in perspective is the heart of the headless approach. In a classic content management system an entry knows how it should look. Here an entry knows only what it is, and appearance belongs to the application, so the same content feeds a website, a mobile app, and a newsletter without duplication.

The platform exposes several separate interfaces, which confuses people at the start. The Delivery API returns published content and is what the application uses. The Preview API also returns drafts and serves previews. The Management API lets you create and modify types and entries from code. The Images API scales and converts images on the fly.

A key security rule follows directly from that split. The Delivery API key is read only and may reach the browser. The Management API key can wipe an entire space, so it lives on the server alone and never lands in a variable carrying a public prefix.

Your first fetch

Code
Bash
pnpm add contentful
Code
TypeScript
import { createClient } from 'contentful'

const client = createClient({
  space: process.env.CONTENTFUL_SPACE_ID!,
  accessToken: process.env.CONTENTFUL_DELIVERY_TOKEN!
})

export async function getArticles(limit = 10) {
  const result = await client.getEntries({
    content_type: 'article',
    order: ['-fields.publishedAt'],
    limit
  })

  return result.items
}

Fetch a single entry by an identifying field rather than by the internal identifier, since the latter is unfit for a URL.

Code
TypeScript
export async function getArticle(slug: string) {
  const result = await client.getEntries({
    content_type: 'article',
    'fields.slug': slug,
    limit: 1,
    include: 2
  })

  return result.items[0] ?? null
}

The include parameter decides how many levels of references arrive with the entry. The default is one level, so an article's author arrives but the author's photo does not. Two levels usually suffice, and larger values grow the response faster than you expect.

Next.js integration

The most common arrangement generates pages at build time and refreshes them in the background. In Next.js with the app router that comes down to one option on the fetch.

Code
TypeScript
export const revalidate = 3600

export async function generateStaticParams() {
  const result = await client.getEntries({ content_type: 'article' })
  return result.items.map((entry) => ({ slug: entry.fields.slug as string }))
}

export default async function Page({ params }: { params: { slug: string } }) {
  const article = await getArticle(params.slug)
  if (!article) notFound()

  return <Article data={article} />
}

An hourly refresh is a starting point rather than a rule. If the editorial team publishes several times a day and expects an immediate effect, add a webhook that revalidates the specific path after publication. Time based refreshing then remains only as a safety net.

Rich text returns as a tree structure rather than as HTML. That is deliberate, since it lets you render paragraphs with your own components, but it needs a separate package and node mapping. The most common mistake at this stage is injecting raw HTML from a text field, which opens the attack surface the field type was meant to close.

Previewing drafts

Sooner or later the editorial team will ask to see an unpublished entry. The answer is a second client pointing at the preview host with its own token.

Code
TypeScript
const previewClient = createClient({
  space: process.env.CONTENTFUL_SPACE_ID!,
  accessToken: process.env.CONTENTFUL_PREVIEW_TOKEN!,
  host: 'preview.contentful.com'
})

Which client you use depends on the mode the request runs in. What matters is that preview mode is neither the default nor switchable through an ordinary URL parameter, since then anyone sees drafts. The standard answer is a signed draft mode activated through a dedicated route the panel opens with a secret.

Remember too that preview content is not cached on the same terms as published content. A page in preview mode should render on demand, otherwise editors see the previous version and report it as a system fault.

Content modelling, where everything is decided

The biggest costs in projects on this platform come not from the subscription price but from poorly designed types. Changing the model after a year of publishing means migrating thousands of entries.

Three rules save the most. First, do not create a type per page. A page is a layout, and a layout should assemble from components you can reuse. Second, avoid fields matching appearance, such as background colour or column width, since the layout will change within a year while the field stays. Third, validate fields from the start, because adding a required flag to a field filled on half the entries is a project of its own.

Localisation is a separate topic. The platform supports multiple languages at field level, so one entry holds every version. That is convenient for translation, but it means fetching an entry without naming a locale returns every variant at once and inflates the response for nothing.

A useful test of a model's design is asking how many types you have to change in order to add a new section to the home page. If the answer is "one", the model is fine. If the page type, the section type, and the layout type all have to move at once, the layers are woven together more tightly than they should be.

Settle early, too, what counts as content and what as configuration. The company name in the footer, the address, and the phone number are content the editorial team should be able to change. A key to an external service or a pagination threshold is configuration and belongs in environment variables rather than in a panel entry.

The last matter is field naming. A field identifier goes straight into the code, so a name chosen on impulse stays for years, and changing it requires migrating entries and a fix on the application side. A few minutes settling a convention at the start saves considerably more later.

Pricing and what surprises people in it

PlanCostWho it suits
Free0 USDOne space, private project, learning
Lite300 USD monthlyProduct team with production traffic
Premiumquoted individuallyMultiple brands, advanced roles, add ons

The jump from the free plan to the first paid one is what surprises people most. There is no intermediate step at a hundred dollars, so a project outgrowing the free plan lands straight on a bill near three and a half thousand dollars a year. The step after that is a custom quote, with no published rate.

Separately billed items add to that. Extra spaces cost money, the visual page assembly tool is an add on, and content personalisation is another. When estimating a budget, price the plan plus expected add ons rather than the plan alone, since those are what usually double the figure.

The third thing is API call limits. The free plan caps request volume, and an application without server side caching can exhaust it on search engine crawler traffic alone. Static generation removes that problem almost entirely, since fetches happen at build time rather than on every visit.

Images and files

Graphic assets land in separate storage and return with a URL you can append transformation parameters to. Scaling, cropping, and format conversion happen on the service side, so you need no processing of your own.

Code
TypeScript
const thumbnail = `${image.fields.file.url}?w=640&fm=webp&q=75&fit=fill`

Three parameters do most of the work. Width caps download size, a modern format halves file weight against the classic one, and quality at seventy five is practically indistinguishable from a hundred at a noticeably smaller size.

The URL the API returns starts with two slashes and no protocol. The image component in Next.js will not accept that, so the protocol needs adding and the asset domain needs listing among the allowed ones in configuration. That is one of the first things a deployment trips over.

It also pays to require alternative text at the model level rather than in editorial guidelines. A description field marked required on the asset type delivers accessibility and better indexing without reminding anyone about anything.

Webhooks and automation

Publishing an entry can fire any HTTP request. The most common use is revalidating a path on the host, but the same mechanism suits notifications, application side search indexing, or synchronising with a product catalogue.

Code
TypeScript
export async function POST(request: Request) {
  const secret = request.headers.get('x-contentful-secret')
  if (secret !== process.env.WEBHOOK_SECRET) {
    return new Response('Forbidden', { status: 401 })
  }

  const payload = await request.json()
  revalidatePath(`/blog/${payload.fields.slug['en-US']}`)

  return Response.json({ ok: true })
}

Verifying the secret is mandatory. A route rebuilding pages without protection is an invitation to call it in a loop for anyone who learns the address.

The second element is filtering events on the panel side. A webhook listening to everything fires on draft saves too, refreshing the page with content nobody approved. Set the trigger to publish and unpublish alone.

The platform also added actions backed by language models, working inside the editor: draft translations, image alternative text, metadata, and summarisation. Treat them as a starting point for an editor rather than finished copy, since publishing without reading comes back as content problems.

Contentful against the alternatives

ToolStrengthWeaknessPick it when
ContentfulMaturity, roles, localisation, stable APISteep price jump, add ons billed separatelyLarge organisation, many channels, nontechnical editors
SupabaseDatabase, auth, and storage in one, low costNo editorial panel for contentTechnical team building its own panel
SanityPanel as code, flexible queriesLearning curve on a custom panelProject needing an unusual editorial layout
StrapiSelf hosted, full control over dataServer maintenance on your sideA requirement to keep content in your own infrastructure

The choice comes down to who works with content daily. If the editorial team is large, nontechnical, and works across languages, a mature panel and permission system earn their price. If three developers add the content, that same panel is a cost without a matching benefit.

Common mistakes

The first is fetching content client side on a page that could be static. Every visit then means an API call, slower first render, and a faster route to the request limit.

The second is a management token in a public variable. A prefix exposing the variable to the browser on a write token means anybody can modify content. This one happens when copying configuration from one project to another.

The third is not handling a missing entry. A query for a nonexistent identifier returns an empty list rather than an error, so code reaching for the first element without checking breaks only in production.

The fourth is references nested too deep. An include value set to the maximum returns a tree whose size grows exponentially and slows both the response and rendering.

The fifth is treating rich text as HTML. The tree structure needs mapping onto components, and the shortcut of injecting raw markup into the page defeats the safety a typed field provides.

The sixth is having no plan for model migration. Before publishing the first hundred entries, check whether the types survive the next stage of the project, since every later change means a script and a maintenance window.

FAQ

Does Contentful have a free plan?

Yes, the free plan covers one space with its own content model and suffices for learning, a prototype, and a small project. Limits apply to user count, entries, and API calls. The next step is the Lite plan at 300 dollars a month, with nothing in between.

How does Contentful fit with Next.js?

Very well with static generation and background revalidation. Pages build ahead of time, the application does not query the API on every visit, and editors see changes after a webhook triggered path revalidation. That arrangement is both the fastest and the cheapest in terms of limits.

What does the Salesforce acquisition mean?

The agreement was signed in June 2026. It changes nothing immediately for running deployments, but when planning several years ahead it is worth assuming possible changes to pricing and product direction, both typical after a change of owner.

When is something else the better pick?

When only a technical team adds content, when the budget does not cover several thousand dollars a year, or when data must stay in your own infrastructure. In those cases Strapi or a database with a custom panel comes out cheaper and gives more control.

Do I have to use GraphQL?

No, the REST interface covers the same ground with a simpler client. GraphQL wins when one page needs several related types at once, since it fetches them in a single query rather than several, and it also trims the response to the fields you actually use.

Interface details are covered in the Contentful documentation, and current price tiers sit on the pricing page.