Sanity CMS: Toàn tập về Headless CMS thế hệ mới
1. Sanity CMS là gì?
Sanity là một Headless CMS API-first - nghĩa là nó chỉ lo phần quản lý & lưu trữ nội dung, còn giao diện hiển thị (frontend) bạn tự xây bằng bất kỳ framework nào (Next.js, Nuxt, SvelteKit, v.v.).
Thành lập năm 2017 tại Oslo, Na Uy. Hiện là một trong những Headless CMS phổ biến nhất trong hệ sinh thái React/Next.js.
3 trụ cột kiến trúc
Content Lake
- Backend lưu trữ nội dung hosted trên cloud của Sanity (không tự host được).
- Không phải database thông thường - đây là một document store real-time lưu JSON.
- Hỗ trợ draft/published riêng biệt, tự động lưu revision history.
- Real-time sync giữa nhiều editor cùng lúc (như Google Docs).
GROQ (Graph-Relational Object Queries)
Ngôn ngữ query riêng của Sanity, open-source, được thiết kế đặc biệt cho JSON document:
*[_type == "post" && publishedAt < now()] | order(publishedAt desc) [0...10] {
title,
slug,
"authorName": author->name,
"categoryTitles": categories[]->title,
body
}
- Dùng
->để “dereference” (join) sang document khác - giải quyết N+1 problem trong 1 query duy nhất. - Hỗ trợ projection, filter, sort, slice, aggregation.
Sanity Studio
- Giao diện quản lý nội dung - là một React app bạn tự host hoặc deploy lên Sanity hosting.
- Cấu hình qua
sanity.config.ts- toàn bộ bằng TypeScript. - Có thể embed thẳng vào Next.js app, không cần deploy riêng.
2. Tính năng nổi bật
Real-time Collaboration Nhiều người edit cùng 1 document, thấy cursor của nhau, conflict được resolve tự động ở field level.
Portable Text Thay vì lưu rich text dưới dạng HTML như WordPress, Sanity lưu dưới dạng structured JSON - render được ra bất kỳ đâu (web, mobile, PDF, email).
Image Pipeline CDN Xử lý ảnh on-the-fly qua URL params - không cần Cloudinary cho basic ops:
?w=800&h=600- resize?fm=webp&q=75- convert format + quality
Tính năng 2024-2026
- Content Releases: Bundle nhiều thay đổi thành 1 “release”, publish atomic.
- Visual Editing: Click-to-edit overlay ngay trên frontend.
- AI Assist: Generate, translate, transform content bằng LLM.
- TypeGen: Generate TypeScript types từ schema + GROQ queries.
3. Pricing (2025-2026)
| Tier | Giá | Users | API CDN Requests |
|---|---|---|---|
| Free | $0/tháng | 3 | 500K/tháng |
| Growth | ~$15/user/tháng | Unlimited | 2.5M included |
| Enterprise | Custom | Unlimited | Custom |
Free tier khá generous - nhiều production site nhỏ vẫn chạy ổn.
4. So sánh chi tiết
Sanity vs WordPress
| Tiêu chí | Sanity | WordPress |
|---|---|---|
| Kiến trúc | Headless, cloud-hosted | Monolithic (PHP + MySQL) |
| Real-time Collab | Native | Không có (lock-based) |
| Rich Text | Portable Text (JSON) | TinyMCE/Gutenberg (HTML) |
| Plugin ecosystem | ~200+ | 60,000+ |
| Learning curve | Cao (cần developer setup) | Thấp (GUI-based) |
Sanity vs Decap CMS
Xem thêm: decap-cms
| Tiêu chí | Sanity | Decap CMS |
|---|---|---|
| Lưu trữ | Content Lake (Sanity cloud) | Git repo (Markdown/JSON files) |
| Real-time Collab | Có | Không (merge-based) |
| Vendor lock-in | Trung bình | Không (data nằm trong Git) |
| Chi phí | Free tier + paid | Hoàn toàn miễn phí |
Sanity vs Payload CMS
| Tiêu chí | Sanity | Payload CMS |
|---|---|---|
| Architecture | Cloud (Content Lake) | Self-hosted, Next.js native |
| Database | Proprietary | MongoDB hoặc PostgreSQL |
| Vendor lock-in | Trung bình | Không |
| Pricing | Free + paid tiers | Miễn phí (OSS) |
5. Use Cases
Sanity tỏa sáng khi:
- Multi-channel delivery - 1 content source feed web + mobile app + kiosk + email.
- Editorial sites phức tạp - newsroom, tạp chí, cần real-time collab.
- Next.js + React ecosystem - Sanity là Headless CMS phổ biến nhất trong hệ này.
Sanity không phù hợp khi:
- Team non-technical không có dev support.
- Blog đơn giản / brochure site - overkill, dùng Ghost hoặc Markdown.
- Vendor lock-in là vấn đề nghiêm trọng.
6. Schema Definition (TypeScript)
// schemas/post.ts
import { defineType, defineField } from 'sanity'
export default defineType({
name: 'post',
title: 'Blog Post',
type: 'document',
fields: [
defineField({
name: 'title',
type: 'string',
validation: (Rule) => Rule.required().max(100),
}),
defineField({
name: 'slug',
type: 'slug',
options: { source: 'title' },
}),
defineField({
name: 'body',
type: 'array',
of: [{ type: 'block' }, { type: 'image' }], // Portable Text
}),
],
})
NateCue