Настройка GROQ-запросов для Sanity
GROQ (Graph-Relational Object Queries) — язык запросов Sanity. Не REST, не SQL, не GraphQL. Гибче REST для сложных структур: проекции, join через ->, условные выборки, агрегации — всё в одном запросе без N+1 проблем.
Базовый синтаксис
// Все документы типа post
*[_type == "post"]
// С условием
*[_type == "post" && status == "published"]
// Сортировка и пагинация
*[_type == "post"] | order(publishedAt desc) [0...10]
// Проекция — выбрать конкретные поля
*[_type == "post"] {
_id,
title,
"slug": slug.current,
publishedAt
}
// Переименование поля
*[_type == "post"] {
"id": _id,
"createdAt": _createdAt,
title
}
Resolve (join через ->)
// Заполнить связанные документы
*[_type == "post"] {
title,
"author": author->{
name,
"avatar": image.asset->url
},
"categories": categories[]->{ title, "slug": slug.current }
}
// Вложенный resolve
*[_type == "product"] {
name, price,
"category": category->{
title,
"parent": parent->{ title }
}
}
Условные проекции
// Разные поля в зависимости от _type (для динамических зон)
*[_type == "page"] {
title,
"sections": sections[] {
_type,
_type == "hero" => {
heading, subtitle,
"backgroundImage": background.asset->url
},
_type == "textBlock" => {
content,
columns
},
_type == "gallery" => {
"images": images[].asset->url
}
}
}
Параметризованные запросы
// Всегда используйте параметры вместо конкатенации строк
const query = `*[_type == "post" && slug.current == $slug][0] { title, body }`
const post = await client.fetch(query, { slug: 'my-post-slug' })
// Безопасная пагинация
const paginatedQuery = `*[_type == "post"] | order(publishedAt desc) [$start...$end] { title }`
const posts = await client.fetch(paginatedQuery, { start: 0, end: 12 })
Агрегации и функции
// Count
count(*[_type == "post" && status == "published"])
// Условный count
{
"total": count(*[_type == "post"]),
"published": count(*[_type == "post" && defined(publishedAt)]),
"drafts": count(*[_type == "post" && !defined(publishedAt)])
}
// Уникальные значения
array::unique(*[_type == "post"].status)
// Строковые функции
*[_type == "post"] { title, "truncated": string::slice(excerpt, 0, 200) }
// Дата
*[_type == "post" && publishedAt > now() - 60*60*24*7] // последние 7 дней
Portable Text — извлечение текста
// Все тексты из Portable Text блоков
*[_type == "post"] {
title,
"plainText": pt::text(body),
"bodyLength": length(pt::text(body))
}
Fulltext поиск
// Встроенный fulltext (работает только в Sanity Cloud)
*[_type == "post" && [title, pt::text(body)] match $q] {
_id, title, "slug": slug.current
}
Для продвинутого поиска рекомендован Algolia — индексация через Sanity webhooks.
Связанные документы в обе стороны
// Найти все статьи в категории (обратная связь)
*[_type == "post" && references($categoryId)] {
title, "slug": slug.current
}
// Автор и все его статьи
*[_type == "author" && _id == $authorId][0] {
name,
"posts": *[_type == "post" && references(^._id)] | order(publishedAt desc) {
title, "slug": slug.current, publishedAt
}
}
GROQ в TypeScript с типизацией
import { createClient } from '@sanity/client'
import { groq } from 'next-sanity'
const postQuery = groq`
*[_type == "post" && slug.current == $slug][0] {
_id,
title,
"slug": slug.current,
publishedAt,
body,
"author": author->{ name, "image": image.asset->url },
"relatedPosts": *[_type == "post" && references(^.categories[]._ref) && _id != ^._id] | order(publishedAt desc) [0...3] {
title, "slug": slug.current
}
}
`
type PostResult = {
_id: string
title: string
slug: string
publishedAt: string
body: any[]
author: { name: string; image: string }
relatedPosts: { title: string; slug: string }[]
}
const post = await client.fetch<PostResult>(postQuery, { slug: params.slug })
Отладка через Vision
В Sanity Studio, если установлен @sanity/vision, есть вкладка Vision — интерактивный GROQ playground с историей запросов.
Сроки
Разработка набора GROQ запросов для 4–6 типов страниц — 1–2 дня.







