Разработка сайта на CMS Sanity
Sanity — headless CMS с реальным временем, написанный на TypeScript. Контентная схема описывается в коде (TypeScript/JavaScript), Sanity Studio — кастомизируемый React-редактор. GROQ — собственный язык запросов, мощнее REST для сложных структур. Контент хранится в облаке Sanity (Content Lake), локальное хранение не поддерживается.
Архитектура
Sanity Studio (React SPA) Next.js Frontend
↕ ↕
Content Lake (Sanity Cloud)
↕ GROQ / REST API
CDN (image CDN встроен)
Sanity Studio — это React-приложение, которое деплоится отдельно или embed в Next.js. Studio — это не "admin panel из коробки" — она конфигурируется под каждый проект.
Создание проекта
npm create sanity@latest -- --template clean --create-project "My Site" --dataset production
# Создаст проект в sanity.io и локальную Studio
cd my-site
npm run dev # Studio: http://localhost:3333
Схема документа
// schemas/post.ts
import { defineType, defineField } from 'sanity'
export const postType = defineType({
name: 'post',
title: 'Статья',
type: 'document',
fields: [
defineField({
name: 'title',
title: 'Заголовок',
type: 'string',
validation: rule => rule.required().min(5).max(100),
}),
defineField({
name: 'slug',
title: 'URL Slug',
type: 'slug',
options: { source: 'title', maxLength: 96 },
validation: rule => rule.required(),
}),
defineField({
name: 'author',
type: 'reference',
to: [{ type: 'author' }],
}),
defineField({
name: 'mainImage',
type: 'image',
options: { hotspot: true },
fields: [
defineField({ name: 'alt', type: 'string', title: 'Alt text' }),
],
}),
defineField({
name: 'categories',
type: 'array',
of: [{ type: 'reference', to: [{ type: 'category' }] }],
}),
defineField({
name: 'publishedAt',
type: 'datetime',
}),
defineField({
name: 'body',
type: 'blockContent', // Portable Text
}),
defineField({
name: 'excerpt',
type: 'text',
rows: 4,
}),
],
preview: {
select: { title: 'title', author: 'author.name', media: 'mainImage' },
prepare: ({ title, author, media }) => ({
title,
subtitle: author ? `by ${author}` : '',
media,
}),
},
})
Конфигурация Studio
// sanity.config.ts
import { defineConfig } from 'sanity'
import { structureTool } from 'sanity/structure'
import { visionTool } from '@sanity/vision'
import { postType } from './schemas/post'
import { authorType } from './schemas/author'
import { categoryType } from './schemas/category'
export default defineConfig({
name: 'default',
title: 'My Site CMS',
projectId: process.env.SANITY_STUDIO_PROJECT_ID!,
dataset: 'production',
plugins: [
structureTool({
structure: S => S.list()
.title('Контент')
.items([
S.documentTypeListItem('post').title('Статьи'),
S.documentTypeListItem('author').title('Авторы'),
S.divider(),
S.documentTypeListItem('category').title('Категории'),
]),
}),
visionTool(), // GROQ playground
],
schema: { types: [postType, authorType, categoryType] },
})
GROQ запросы
// Все опубликованные статьи с populate
*[_type == "post" && publishedAt < now() && !(_id in path("drafts.**"))] | order(publishedAt desc) [0...12] {
_id,
title,
"slug": slug.current,
publishedAt,
excerpt,
"mainImage": mainImage { ..., "url": asset->url, "blurHash": asset->metadata.blurHash },
"author": author->{ name, "avatar": image.asset->url },
"categories": categories[]->{ title, slug }
}
Интеграция с Next.js
npm install @sanity/client next-sanity @portabletext/react
// lib/sanity.ts
import { createClient } from '@sanity/client'
import imageUrlBuilder from '@sanity/image-url'
export const sanityClient = createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET || 'production',
apiVersion: '2024-01-01',
useCdn: process.env.NODE_ENV === 'production',
token: process.env.SANITY_API_TOKEN, // для черновиков
})
const builder = imageUrlBuilder(sanityClient)
export const urlFor = (source: any) => builder.image(source)
// app/blog/[slug]/page.tsx
import { sanityClient, urlFor } from '@/lib/sanity'
import { PortableText } from '@portabletext/react'
import { notFound } from 'next/navigation'
const query = `*[_type == "post" && slug.current == $slug][0] {
_id, title, publishedAt, body,
"mainImage": mainImage { ..., "url": asset->url },
"author": author->{ name },
"categories": categories[]->{ title }
}`
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await sanityClient.fetch(query, { slug: params.slug })
if (!post) notFound()
return (
<article>
<h1>{post.title}</h1>
{post.mainImage?.url && (
<img src={urlFor(post.mainImage).width(1200).height(630).url()} alt={post.title} />
)}
<PortableText value={post.body} />
</article>
)
}
export async function generateStaticParams() {
const slugs = await sanityClient.fetch(`*[_type == "post"].slug.current`)
return slugs.map((slug: string) => ({ slug }))
}
Типичный стек
| Слой | Технология |
|---|---|
| CMS | Sanity 3.x |
| Studio | Embedded в Next.js или отдельный деплой |
| Frontend | Next.js 14 App Router |
| Изображения | Sanity Image CDN (встроен) |
| Поиск | GROQ / Algolia |
| Деплой | Vercel (Next.js) + sanity.io (Studio) |
Отличия от Strapi/Directus
- Контент хранится только в Sanity Cloud (SaaS), нет self-hosted варианта с БД
- GROQ-запросы мощнее REST для сложных структур данных
- Real-time коллаборация в Studio из коробки
- Portable Text — формат rich text с кастомными блоками
- Платные планы: бесплатный (3 пользователя, 5 датасетов), платный от $15/мес
Сроки
Базовый сайт с 3–5 типами документов и Next.js фронтендом — 2–3 недели. Сложный проект с кастомными Studio плагинами и Visual Editing — 4–6 недель.







