Интеграция 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: 'ru',
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: 'ru',
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=ru, 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 дня.







