Sanity CMS pra blog headless: setup completo
Sanity é o CMS headless mais flexível do mercado. Schema em TypeScript, queries GROQ potentes e CDN de imagem embutido. Setup completo com Astro e Next.js.
Equipe Máximo do Marketing
Máximo do Marketing
Equipe de marketing pede mudança no layout do blog. Dev vai pra fila do sprint. Duas semanas depois, mudança ainda não saiu. Post que devia ir segunda vai na quinta. Isso é WordPress gerenciando conteúdo junto com código — os dois acoplados, o editor dependendo do dev pra qualquer coisa.
Sanity CMS resolve esse problema na raiz: conteúdo separado do frontend, com API que qualquer framework consome. O time de conteúdo edita pelo Sanity Studio (interface própria), o site consome via API — e publicar conteúdo não precisa de dev. Deploy de código e deploy de post são coisas diferentes e independentes.
Esse artigo cobre o setup completo: desde a conta até integração com Astro ou Next.js, com código funcional em cada etapa.
O que é CMS headless e por que importa
CMS tradicional (WordPress, Ghost, Drupal) une backend e frontend no mesmo sistema. Conteúdo e template vivem juntos. Mudar um afeta o outro. Plugin novo pode quebrar o layout. Atualização de tema pode apagar customizações.
CMS headless separa os dois:
- Backend (head-less): onde conteúdo é criado e armazenado — no caso, o Sanity Studio
- Frontend: onde conteúdo é exibido — seu Astro, Next.js, app mobile, o que for
A conexão é via API. O frontend pergunta “me dá os últimos 10 posts com título, slug e resumo” — Sanity devolve JSON. Como você renderiza é problema do frontend.
Resultado prático: mesmo conteúdo alimenta site, app mobile e newsletter. Troca de framework não toca no conteúdo. E a equipe de conteúdo tem interface própria, sem depender de acesso ao servidor.
Se você está construindo site headless com Jamstack, Sanity é a escolha de CMS mais madura e flexível disponível.
Sanity vs os concorrentes
Você vai esbarrar em Contentful, Strapi e TinaCMS. Comparação direta:
| Critério | Sanity | Contentful | Strapi | TinaCMS |
|---|---|---|---|---|
| Schema | Código TypeScript | Interface gráfica | Código ou UI | Código |
| Linguagem de query | GROQ (própria, potente) | REST / GraphQL | REST / GraphQL | GraphQL |
| Self-hostable | ❌ (cloud only) | ❌ (cloud only) | ✅ | ✅ |
| Plano gratuito | 3 usuários, 100k docs | Muito limitado | Grátis (self-hosted) | Grátis (self-hosted) |
| Image CDN embutido | ✅ | ✅ | ❌ | ❌ |
| Melhor pra | Equipe com dev, volume médio | Empresas grandes | Dev-heavy customizado | Blog simples |
O diferencial do Sanity: schema em código TypeScript. Você define estrutura de conteúdo em .ts, versionado no Git. Não existe divergência entre o que o dev configurou e o que o editor está vendo — ambos leem o mesmo arquivo. Refatorar um campo, renomear um tipo, adicionar validação: tudo com Pull Request revisado pelo time.
GROQ (Graph-Relational Object Queries), a linguagem de query própria do Sanity, é outro diferencial. Mais expressiva que REST, mais prática que GraphQL pra queries de conteúdo.
Instalação e configuração inicial
Precisa de Node.js 18+. Roda dentro da pasta do projeto ou em nova pasta:
npm create sanity@latest
O assistente pergunta:
- Nome do projeto: qualquer nome (ex:
meu-blog-studio) - Dataset:
production(padrão) - Package manager: npm ou pnpm
- Template: “Blog” pra começar com schema pronto, “Clean” se vai criar do zero
Estrutura gerada:
studio/
sanity.config.ts ← configuração central
schemaTypes/
index.ts ← exporta todos os schemas
post.ts ← schema de post (no template Blog)
author.ts ← schema de autor
category.ts ← schema de categoria
package.json
Roda o studio localmente:
cd studio
npm run dev
Abre http://localhost:3333. Login com conta Sanity (gratuita). O Studio já funciona com o schema do template — você pode criar posts ali agora mesmo.
Definindo schemas: o coração do Sanity
Schema é onde você define quais campos um documento tem. Em vez de configurar via interface (Contentful) ou no banco (SQL), Sanity usa TypeScript. Isso significa autocompletar no editor, validação em tempo de compilação e histórico de mudanças no Git.
Schema completo de post de blog:
// studio/schemaTypes/post.ts
import { defineField, defineType } from 'sanity'
export const postType = defineType({
name: 'post',
title: 'Post',
type: 'document',
fields: [
defineField({
name: 'title',
title: 'Título',
type: 'string',
validation: (rule) => rule.required().min(10).max(100),
}),
defineField({
name: 'slug',
title: 'Slug (URL)',
type: 'slug',
options: { source: 'title', maxLength: 96 },
validation: (rule) => rule.required(),
}),
defineField({
name: 'publishedAt',
title: 'Data de publicação',
type: 'datetime',
}),
defineField({
name: 'excerpt',
title: 'Resumo (até 220 chars)',
type: 'text',
rows: 3,
validation: (rule) => rule.max(220),
}),
defineField({
name: 'mainImage',
title: 'Imagem principal',
type: 'image',
options: { hotspot: true }, // recorte inteligente por ponto focal
fields: [
defineField({
name: 'alt',
title: 'Texto alternativo',
type: 'string',
validation: (rule) => rule.required(),
}),
],
}),
defineField({
name: 'categories',
title: 'Categorias',
type: 'array',
of: [{ type: 'reference', to: { type: 'category' } }],
}),
defineField({
name: 'author',
title: 'Autor',
type: 'reference',
to: { type: 'author' },
}),
defineField({
name: 'body',
title: 'Conteúdo',
type: 'array',
of: [
{ type: 'block' }, // rich text padrão (Portable Text)
{ type: 'image', options: { hotspot: true } }, // imagens inline
],
}),
],
preview: {
select: { title: 'title', author: 'author.name', media: 'mainImage' },
prepare(sel) {
return { ...sel, subtitle: sel.author ? `Por ${sel.author}` : '' }
},
},
})
Tipos de campo mais usados:
| Tipo | Quando usar |
|---|---|
string | Texto curto (título, nome) |
text | Texto longo sem formatação |
slug | URL com geração automática |
image | Upload com CDN automático |
array | Lista de qualquer coisa |
reference | Relacionamento com outro documento |
block | Rich text (Portable Text) |
datetime | Data e hora |
boolean | Toggle (ex: featured, active) |
Validação é embutida: required(), min(), max(), regex, até funções customizadas. Erros aparecem no Studio antes de publicar.
GROQ: a linguagem de query que diferencia o Sanity
GROQ (Graph-Relational Object Queries) é a linguagem de busca do Sanity. Mais expressiva que REST puro, mais simples que GraphQL pra queries de conteúdo.
Listagem de posts publicados, ordem cronológica reversa:
*[_type == "post" && defined(publishedAt)] | order(publishedAt desc) {
title,
"slug": slug.current,
publishedAt,
excerpt,
"authorName": author->name,
"categories": categories[]->title
}
O operador -> resolve referências inline. author->name traz o nome do autor sem uma segunda chamada de API — GROQ faz o join internamente. Isso diferencia bastante de REST onde você faria uma query pra post e outra pra author.
Post individual por slug (com todos os campos):
*[_type == "post" && slug.current == $slug][0] {
title,
publishedAt,
excerpt,
"image": mainImage {
"url": asset->url,
alt
},
"author": author->{ name, "imageUrl": image.asset->url },
"categories": categories[]->title,
body
}
Paginação simples:
// posts 0-9 (primeira página de 10)
*[_type == "post"] | order(publishedAt desc) [0...10] {
title,
"slug": slug.current,
publishedAt
}
O Sanity tem GROQ Playground online pra testar queries antes de usar no código. Essencial durante desenvolvimento.
Integração com Astro
Astro é o framework mais popular pra sites de marketing headless. Integração com Sanity é direta.
Instala o client:
npm install @sanity/client @sanity/image-url @portabletext/react
Configura o client:
// src/lib/sanity.ts
import { createClient } from '@sanity/client'
export const client = createClient({
projectId: import.meta.env.SANITY_PROJECT_ID,
dataset: import.meta.env.SANITY_DATASET ?? 'production',
apiVersion: '2025-01-01',
useCdn: true, // CDN pra content publicado (mais rápido)
})
Variáveis no .env:
SANITY_PROJECT_ID=abc123def456
SANITY_DATASET=production
O projectId está em manage.sanity.io → seu projeto → Settings.
Página de listagem de posts:
---
// src/pages/blog/index.astro
import { client } from '../../lib/sanity'
const posts = await client.fetch(`
*[_type == "post" && defined(publishedAt)] | order(publishedAt desc) {
title,
"slug": slug.current,
publishedAt,
excerpt,
"imageUrl": mainImage.asset->url
}
`)
---
<ul>
{posts.map(post => (
<li>
{post.imageUrl && <img src={post.imageUrl} alt={post.title} />}
<a href={`/blog/${post.slug}/`}>{post.title}</a>
<p>{post.excerpt}</p>
</li>
))}
</ul>
Página de post individual com rotas estáticas (geradas no build):
---
// src/pages/blog/[slug].astro
import { client } from '../../lib/sanity'
export async function getStaticPaths() {
const slugs = await client.fetch(
`*[_type == "post"] { "slug": slug.current }`
)
return slugs.map(p => ({ params: { slug: p.slug } }))
}
const { slug } = Astro.params
const post = await client.fetch(`
*[_type == "post" && slug.current == $slug][0] {
title,
publishedAt,
body,
"author": author->name
}
`, { slug })
---
<h1>{post.title}</h1>
<p>Por {post.author}</p>
<!-- renderiza Portable Text aqui -->
Pra renderizar o Portable Text (rich text do Sanity) no Astro, usa o pacote @portabletext/astro:
npm install @portabletext/astro
---
import { PortableText } from '@portabletext/astro'
---
<PortableText value={post.body} />
Imagens com CDN automático
O Sanity tem CDN de imagem com transformações via parâmetro de URL. Sem Cloudinary, Sharp ou ImageKit.
import imageUrlBuilder from '@sanity/image-url'
const builder = imageUrlBuilder(client)
export function urlFor(source: any) {
return builder.image(source)
}
// Uso em qualquer componente:
const src = urlFor(post.mainImage)
.width(800)
.height(450)
.format('webp')
.quality(80)
.url()
Resultado: URL com imagem redimensionada, convertida pra WebP e comprimida — tudo por parâmetro de URL, sem processamento local. Isso impacta diretamente nas métricas de PageSpeed e Core Web Vitals, especialmente LCP.
O hotspot: true no schema de imagem habilita ponto focal: editor clica na região importante da imagem (o rosto, o produto) e o CDN recorta inteligente pra qualquer proporção sem cortar o que importa.
Deploy do Studio e do frontend
Hospedar o Studio: deploy grátis na hospedagem própria do Sanity:
npx sanity deploy
Gera URL tipo https://meu-blog.sanity.studio. Compartilha com toda a equipe de conteúdo. Acesso por login Sanity — sem precisar de servidor próprio.
Rebuild automático quando conteúdo muda: configura webhook no Sanity Studio que dispara rebuild no Vercel, Netlify ou Railway sempre que um post é publicado ou atualizado:
- Sanity Dashboard → API → Webhooks → Add Webhook
- URL: seu hook de deploy da Vercel/Netlify
- Dataset:
production - Triggers: On create, On update, On delete
A cada publicação, a plataforma de hosting recebe sinal e inicia novo build. Conteúdo novo no ar em 30-60 segundos sem nenhuma ação manual de dev.
Quanto custa
| Plano | Preço | Inclui |
|---|---|---|
| Free | $0 | 3 usuários, 100k documentos, 20 GB assets, 500k requests/mês |
| Growth | $15/mês por projeto | Usuários ilimitados, 500k docs, 100 GB assets |
| Team | $90/mês | Datasets múltiplos, features avançadas |
| Enterprise | Custom | SLA, SAML, compliance |
Pra maioria dos blogs e sites de marketing: Free serve por muito tempo. 100k documentos cobre anos de conteúdo. 20 GB de assets dá pra centenas de posts com imagem.
Comparando com os concorrentes:
| CMS | Tier básico pago | Custo |
|---|---|---|
| Sanity Growth | Usuários ilimitados | $15/mês |
| DatoCMS Basic | 3 usuários | $99/mês |
| Contentful Basic | 5 usuários | $300/mês |
| Strapi Cloud Starter | 1 ambiente | $29/mês |
Sanity é o mais barato em feature-parity pra equipes pequenas e médias.
Quando não usar Sanity
Sanity exige desenvolvedor pra configurar schemas e integração inicial. Se sua equipe não tem dev, será frustrante.
Considere alternativas se:
- Sem dev e conteúdo simples → TinaCMS ou Notion-as-CMS
- Precisa de self-hosted por requisito regulatório → Strapi ou Directus
- Quer e-commerce integrado ao CMS → Shopify resolve os dois
- Time usa o CMS como ferramenta de gestão de dados complexos → Airtable ou Notion são mais indicados
Sanity é a escolha certa se:
- Tem dev (mesmo que somente você)
- Schema precisa ser flexível com relacionamentos entre tipos
- Vai consumir conteúdo em mais de um frontend (site + app + email)
- Quer queries potentes sem o boilerplate do GraphQL
- Quer image CDN embutido sem serviço extra
Dicas que economizam tempo
1. Tipos compartilhados: cria um tipo seoType com title, description e openGraph, e reutiliza em todos os schemas com {type: 'seo'}. Sem duplicar campos.
2. Preview ao vivo: Sanity tem integração de live preview com Next.js e Astro. O editor vê mudanças antes de publicar, em tempo real, no design real do site. Configuração de 30 minutos, mas elimina o loop “publica, espera build, verifica, edita”.
3. Portable Text customizado: você define blocos customizados dentro do rich text — CTA embutido, card de produto, embed de vídeo, callout. Tudo em um campo body, sem precisar de campo separado pra cada componente especial.
4. Ordernamento por _updatedAt pra cache: queries pra listagem sempre incluem _updatedAt na projeção. Serve como cache busting — quando o conteúdo muda, o timestamp muda, e queries com revalidação baseada em tempo funcionam corretamente.
5. Dataset de staging: no plano Growth cria um dataset staging separado de production. Time de conteúdo produz no staging, você valida, depois publica pra production. Pra blog isso pode ser excessivo, mas pra e-commerce headless vale muito.
A Máximo implementa Sanity CMS integrado ao Astro e Next.js em projetos de site e blog headless. Quer migrar do WordPress sem perder a capacidade de editar conteúdo facilmente? Fala com a gente.
Quer aplicar isso na sua empresa?
A gente faz um diagnóstico gratuito e mostra o caminho mais curto pra você crescer com previsibilidade.
Quero meu diagnóstico