Интеграция CMS Sanity для управления контентом
Sanity — headless CMS с полностью кастомизируемой схемой и редактором (Sanity Studio). Контент хранится в облачной инфраструктуре Sanity и отдаётся через CDN-backed API. Отличается форматом хранения контента — Portable Text вместо HTML — и возможностью строить редактор под конкретный проект.
Архитектура Sanity
- Sanity Content Lake — облачная NoSQL БД с версионированием, транзакциями, real-time обновлениями
- 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 ?? ''}
/>
),
},
}}
/>
Real-time обновления (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 — страница обновляется без перезагрузки.
Webhooks
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-based кроп
.format('webp')
.quality(80)
.url()
Сроки
Настройка проекта Sanity, схема, Studio, GROQ-запросы, интеграция с Next.js — 3–5 рабочих дней. Live preview, кастомные Studio-плагины, webhooks, кастомные поля — +3–4 дня.







