Інтеграція CMS Contentful для управління контентом
Contentful — хмарна headless CMS з потужною екосистемою SDK, розвинутим API та вбудованою багатомовною підтримкою. Контент зберігається в інфраструктурі Contentful, доступ через Delivery API (публічний, кешований) та Management API (запис, приватний).
Структура Contentful
- Space — робочий простір, аналог проекту
- Environment — середовища всередину Space (master, staging, sandbox)
- Content Type — схема типу запису: поля, валідації
- Entry — конкретна запис певного Content Type
- Asset — медіафайл (зображення, відео, PDF)
Кожна Entry та Asset має унікальний ID, незалежний від мови. Локалізовані поля зберігаються як словник {locale: value} всередину однієї записи.
Створення Content Type через 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 (читання контенту)
import { createClient } from 'contentful';
const client = createClient({
space: process.env.CONTENTFUL_SPACE_ID!,
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN!, // Delivery API token
environment: 'master',
});
// Отримати список статей
const response = await client.getEntries<ArticleFields>({
content_type: 'article',
'fields.publishedAt[lte]': new Date().toISOString(),
order: ['-fields.publishedAt'],
limit: 10,
locale: 'uk',
include: 2, // глибина populate (автор, обкладинка)
});
response.items.forEach(entry => {
console.log(entry.fields.title); // string
console.log(entry.fields.cover?.fields); // Asset fields
});
// Конкретна запис за slug
const entries = await client.getEntries({
content_type: 'article',
'fields.slug': 'my-article',
locale: 'uk',
limit: 1,
});
const article = entries.items[0];
TypeScript-типи з Content Types
npx @contentful/cli@latest content-type export \
--space-id $CONTENTFUL_SPACE_ID \
--output-file src/types/contentful.d.ts
Або через cf-content-types-generator:
npx cf-content-types-generator -s $SPACE_ID -t $ACCESS_TOKEN -o src/types/
Rich Text рендеринг
Contentful Rich Text зберігається як JSON AST, не HTML. Для рендеринга:
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
Для попереду чорновиків використовується Preview API з окремим токеном:
const previewClient = createClient({
space: process.env.CONTENTFUL_SPACE_ID!,
accessToken: process.env.CONTENTFUL_PREVIEW_TOKEN!, // Preview API token
host: 'preview.contentful.com', // не cdn.contentful.com
});
Webhooks для 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 });
}
Багатомовність
Contentful підтримує локалізацію на рівні полів. Поле localized: true в схемі → при запиті до API додати locale=uk, locale=en.
// Отримати запис для всіх локалей одночасно
const entry = await client.getEntry(entryId, { locale: '*' });
// entry.fields.title = { 'ru': 'Заголовок', 'en': 'Title', 'de': 'Titel' }
Обмеження та вартість
Безплатний тариф Contentful (Community): 1 Space, до 25 000 записів, 2 ролі, 2 мови. Для production з командою та кількома середовищами (staging + production) потрібен платний тариф.
Альтернатива при бюджетних обмеженнях: Directus (self-hosted) або Sanity (щедріший free tier).
Терміни
Настройка Space, Content Types, SDK, інтеграція з Next.js, webhooks для ISR — 3–5 робочих днів. Багатомовність, Preview Mode, CI/CD з міграціями схеми — +2–3 дня.







