Інтеграція CMS Sanity для управління контентом
Sanity — це headless CMS з повністю налаштовуваною схемою та редактором (Sanity Studio). Контент зберігається в хмарній інфраструктурі Sanity і поставляється через API з підтримкою CDN. Відрізняється форматом зберігання контенту — Portable Text замість HTML — та можливістю побудови редактора, специфічного для вашого проекту.
Архітектура Sanity
- Sanity Content Lake — хмарна NoSQL БД з версіюванням, транзакціями, оновленнями в реальному часі
- Sanity Studio — налаштовуване React-приложення редактора, розгортається на будь-якому хостингу
- GROQ API — власна мова запитів (Graph-Relational Object Queries)
-
CDN API — кешовані запити через
cdn.sanity.ioдля продакшену
Ініціалізація проекту
npm create sanity@latest -- --project my-project --dataset production --template clean
cd my-project
npm run dev # Studio на http://localhost:3333
Для підключення до існуючого проекту:
npm install @sanity/client
// lib/sanity.ts
import { createClient } from '@sanity/client';
export const client = createClient({
projectId: 'abc123xyz',
dataset: 'production',
apiVersion: '2024-01-01',
useCdn: true, // true для публічного контенту, false для приватного/свіжого
});
Визначення схеми
Схема — TypeScript/JavaScript, описує типи документів та їхні поля:
// schemas/article.ts
import { defineType, defineField } from 'sanity';
export const article = defineType({
name: 'article',
title: 'Стаття',
type: 'document',
fields: [
defineField({
name: 'title',
title: 'Заголовок',
type: 'string',
validation: (Rule) => Rule.required().min(5).max(120),
}),
defineField({
name: 'slug',
title: 'Slug',
type: 'slug',
options: { source: 'title', maxLength: 96 },
}),
defineField({
name: 'cover',
title: 'Обкладинка',
type: 'image',
options: { hotspot: true },
fields: [
{ name: 'alt', type: 'string', title: 'Alt текст' },
],
}),
defineField({
name: 'body',
title: 'Контент',
type: 'array',
of: [
{ type: 'block' }, // Portable Text
{ type: 'image', options: { hotspot: true } },
{ type: 'code' }, // блок коду (з sanity-plugin-code-input)
],
}),
defineField({
name: 'publishedAt',
type: 'datetime',
}),
defineField({
name: 'categories',
type: 'array',
of: [{ type: 'reference', to: [{ type: 'category' }] }],
}),
],
preview: {
select: { title: 'title', media: 'cover' },
},
});
GROQ-запити
GROQ — декларативна мова запитів, специфічна для Sanity. Потужніша за REST-фільтрацію:
// Всі опубліковані статті з даними категорій
*[_type == "article" && defined(publishedAt) && publishedAt <= now()] | order(publishedAt desc) [0..9] {
_id,
title,
slug,
publishedAt,
"cover": cover.asset->url,
"categories": categories[]->{ _id, title, slug }
}
// Виконання запиту
const articles = await client.fetch(
`*[_type == "article"] | order(publishedAt desc) [0..$limit] {
_id, title, slug, publishedAt
}`,
{ limit: 10 }
);
// Конкретна стаття за slug
const article = await client.fetch(
`*[_type == "article" && slug.current == $slug][0] {
title,
body,
"author": author->{ name, image }
}`,
{ slug: 'my-article-slug' }
);
Portable Text
Замість HTML контент зберігається як структурований JSON:
[
{ "_type": "block", "style": "h2", "children": [{ "_type": "span", "text": "Введення" }] },
{ "_type": "block", "style": "normal", "children": [
{ "_type": "span", "text": "Звичайний текст з " },
{ "_type": "span", "marks": ["strong"], "text": "жирним" },
{ "_type": "span", "text": " словом." }
]},
{ "_type": "image", "asset": { "_ref": "image-abc123-800x600-jpg" } }
]
Для рендеринга в React:
import { PortableText } from '@portabletext/react';
import imageUrlBuilder from '@sanity/image-url';
const builder = imageUrlBuilder(client);
<PortableText
value={article.body}
components={{
types: {
image: ({ value }) => (
<img
src={builder.image(value).width(800).url()}
alt={value.alt ?? ''}
/>
),
},
}}
/>
Оновлення в реальному часі (Live Preview)
// next.js: app/[slug]/page.tsx з live preview
import { useLiveQuery } from '@sanity/preview-kit';
const { data: article } = useLiveQuery(initialArticle, articleQuery, { slug });
Sanity GROQ Streaming API відправляє оновлення через Server-Sent Events при зміні контенту в Studio — сторінка оновляється без перезагрузки.
Вебхуки
Sanity Console → API → Webhooks → Create
URL: https://example.com/api/revalidate
Trigger on: create, update, delete
Filter: _type == "article"
Secret: webhook-secret-key
// Next.js API route для ISR revalidation
export async function POST(req: Request) {
const signature = req.headers.get('sanity-webhook-signature');
// перевірити підпис за допомогою @sanity/webhook-toolkit
await revalidatePath('/blog');
return Response.json({ revalidated: true });
}
Управління ресурсами зображень
Sanity трансформує зображення на льоту через URL-параметри:
builder.image(source)
.width(1200)
.height(630)
.fit('crop')
.crop('focalpoint') // кроп на основі hotspot
.format('webp')
.quality(80)
.url()
Терміни
Налаштування проекту Sanity, схема, Studio, GROQ-запити, інтеграція з Next.js — 3–5 робочих днів. Live preview, користувальницькі плагіни Studio, вебхуки, користувальницькі поля — +3–4 дні.







