Contentful CMS Integration for Content Management
Contentful — a cloud headless CMS with a strong SDK ecosystem, developed API, and built-in multi-language support. Content is stored in Contentful infrastructure, accessed via Delivery API (public, cached) and Management API (write, private).
Contentful Structure
- Space — a working space, analogous to a project
- Environment — environments within a Space (master, staging, sandbox)
- Content Type — a schema for an entry type: fields, validations
- Entry — a specific record of a defined Content Type
- Asset — a media file (image, video, PDF)
Each Entry and Asset has a unique ID, independent of language. Localized fields are stored as a dictionary {locale: value} within a single record.
Creating a Content Type via API
import Contentful from 'contentful-management';
const client = Contentful.createClient({ accessToken: process.env.CONTENTFUL_MANAGEMENT_TOKEN });
const space = await client.getSpace(process.env.CONTENTFUL_SPACE_ID);
const env = await space.getEnvironment('master');
const contentType = await env.createContentTypeWithId('article', {
name: 'Article',
displayField: 'title',
fields: [
{ id: 'title', name: 'Title', type: 'Symbol', required: true, localized: true },
{ id: 'slug', name: 'Slug', type: 'Symbol', required: true, localized: false },
{ id: 'body', name: 'Body', type: 'RichText', required: false, localized: true },
{ id: 'cover', name: 'Cover Image', type: 'Link', linkType: 'Asset' },
{ id: 'author', name: 'Author', type: 'Link', linkType: 'Entry',
validations: [{ linkContentType: ['author'] }] },
{ id: 'tags', name: 'Tags', type: 'Array', items: { type: 'Symbol' } },
{ id: 'publishedAt', name: 'Published At', type: 'Date' },
],
});
await contentType.publish();
Delivery API (Reading Content)
import { createClient } from 'contentful';
const client = createClient({
space: process.env.CONTENTFUL_SPACE_ID!,
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN!, // Delivery API token
environment: 'master',
});
// Get a list of articles
const response = await client.getEntries<ArticleFields>({
content_type: 'article',
'fields.publishedAt[lte]': new Date().toISOString(),
order: ['-fields.publishedAt'],
limit: 10,
locale: 'en',
include: 2, // populate depth (author, cover)
});
response.items.forEach(entry => {
console.log(entry.fields.title); // string
console.log(entry.fields.cover?.fields); // Asset fields
});
// Get a specific entry by slug
const entries = await client.getEntries({
content_type: 'article',
'fields.slug': 'my-article',
locale: 'en',
limit: 1,
});
const article = entries.items[0];
TypeScript Types from Content Types
npx @contentful/cli@latest content-type export \
--space-id $CONTENTFUL_SPACE_ID \
--output-file src/types/contentful.d.ts
Or via cf-content-types-generator:
npx cf-content-types-generator -s $SPACE_ID -t $ACCESS_TOKEN -o src/types/
Rich Text Rendering
Contentful Rich Text is stored as a JSON AST, not HTML. To render it:
import { documentToReactComponents } from '@contentful/rich-text-react-renderer';
import { BLOCKS, INLINES } from '@contentful/rich-text-types';
const options = {
renderNode: {
[BLOCKS.EMBEDDED_ASSET]: (node) => {
const { url, title } = node.data.target.fields.file;
return <img src={`https:${url}`} alt={title} />;
},
[INLINES.HYPERLINK]: (node, children) => (
<a href={node.data.uri} target="_blank" rel="noopener noreferrer">
{children}
</a>
),
[BLOCKS.EMBEDDED_ENTRY]: (node) => {
const entry = node.data.target;
if (entry.sys.contentType.sys.id === 'codeBlock') {
return <pre><code>{entry.fields.code}</code></pre>;
}
},
},
};
<div>{documentToReactComponents(article.fields.body, options)}</div>
Content Preview API
To preview drafts, use the Preview API with a separate token:
const previewClient = createClient({
space: process.env.CONTENTFUL_SPACE_ID!,
accessToken: process.env.CONTENTFUL_PREVIEW_TOKEN!, // Preview API token
host: 'preview.contentful.com', // not cdn.contentful.com
});
Webhooks for ISR
Space Settings → Webhooks → Add Webhook
Name: Next.js Revalidation
URL: https://example.com/api/revalidate
Events: Entry.publish, Entry.unpublish, Asset.publish
// app/api/revalidate/route.ts
export async function POST(req: Request) {
const body = await req.json();
const contentType = body.sys?.contentType?.sys?.id;
if (contentType === 'article') {
await revalidatePath('/blog');
await revalidatePath(`/blog/${body.fields?.slug?.['en-US']}`);
}
return Response.json({ revalidated: true });
}
Multi-language Support
Contentful supports field-level localization. A field with localized: true in the schema requires adding locale=en to API requests.
// Get an entry for all locales at once
const entry = await client.getEntry(entryId, { locale: '*' });
// entry.fields.title = { 'ru': 'Заголовок', 'en': 'Title', 'de': 'Titel' }
Limitations and Pricing
Contentful's free plan (Community): 1 Space, up to 25,000 entries, 2 roles, 2 languages. For production with a team and multiple environments (staging + production), a paid plan is required.
Alternative on a budget: Directus (self-hosted) or Sanity (more generous free tier).
Timeline
Setting up a Space, Content Types, SDK, Next.js integration, webhooks for ISR — 3–5 business days. Multi-language support, Preview Mode, CI/CD with schema migrations — +2–3 days.







