Sanity
リアルタイム協調編集可能なヘッドレスCMS。構造化コンテンツとPortable Textによる柔軟なコンテンツ管理。
ヘッドレスCMS
Sanity
概要
Sanityは、リアルタイムコラボレーションと構造化コンテンツを特徴とするヘッドレスCMSです。独自のクエリ言語であるGROQ、カスタマイズ可能なSanity Studio、強力なポータブルテキスト機能を提供し、開発者に高い自由度を与えると同時に、コンテンツエディターにも優れた体験を提供します。
詳細
Sanityは、構造化コンテンツのプラットフォームとして設計され、データをポータブルテキスト、画像、ビデオなどの構造化された形式で管理します。Sanity StudioはオープンソースのReactアプリケーションで、完全にカスタマイズ可能です。
主な特徴:
- リアルタイムコラボレーション: 複数ユーザーの同時編集をサポート
- GROQ: Graph-Relational Object Queries - 強力なクエリ言語
- Portable Text: 柔軟で拡張可能なリッチテキスト形式
- Sanity Studio: カスタマイズ可能なコンテンツ管理UI
- リビジョン履歴: 完全なバージョン管理
- Asset Pipeline: 画像や動画の自動最適化
- API CDN: グローバルCDNでの高速配信
- プラグインシステム: Studioの機能拡張
メリット・デメリット
メリット
- リアルタイムコラボレーション機能
- Sanity Studioの高いカスタマイズ性
- GROQによる強力なクエリ機能
- ポータブルテキストによる柔軟なコンテンツ管理
- 完全なバージョン履歴管理
- 優れた開発者体験
- オープンソースのStudio
- 強力なTypeScriptサポート
デメリット
- 学習曲線が急(特にGROQ)
- クラウドホスティングが基本
- 初期設定が複雑
- 他のCMSと比較してテンプレートが少ない
- コミュニティが相対的に小さい
- セルフホスティングの制限
参考ページ
書き方の例
1. Hello World(基本的なセットアップ)
# Sanityプロジェクトの作成
npm create sanity@latest
# 設定オプション
# Project name: my-sanity-project
# Use default dataset: Yes
# Project output path: /path/to/project
# Select project template: Clean project
# TypeScript: Yes
# Package manager: npm
// schemas/index.ts
export const schemaTypes = [
{
name: 'post',
title: 'Post',
type: 'document',
fields: [
{
name: 'title',
title: 'Title',
type: 'string',
validation: Rule => Rule.required()
},
{
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'title',
maxLength: 96
}
},
{
name: 'content',
title: 'Content',
type: 'array',
of: [{type: 'block'}]
}
]
}
]
// Sanity Studioの起動
// npm run dev
2. コンテンツ管理
// スキーマ定義の拡張
export default {
name: 'author',
title: 'Author',
type: 'document',
fields: [
{
name: 'name',
title: 'Name',
type: 'string',
validation: Rule => Rule.required()
},
{
name: 'image',
title: 'Image',
type: 'image',
options: {
hotspot: true
}
},
{
name: 'bio',
title: 'Bio',
type: 'array',
of: [
{
type: 'block',
styles: [{title: 'Normal', value: 'normal'}],
lists: []
}
]
}
],
preview: {
select: {
title: 'name',
media: 'image'
}
}
}
// リレーションの設定
{
name: 'author',
title: 'Author',
type: 'reference',
to: {type: 'author'},
validation: Rule => Rule.required()
}
// カスタムブロックタイプ
{
type: 'block',
marks: {
decorators: [
{title: 'Strong', value: 'strong'},
{title: 'Emphasis', value: 'em'},
{title: 'Code', value: 'code'}
],
annotations: [
{
name: 'link',
type: 'object',
title: 'URL',
fields: [
{
title: 'URL',
name: 'href',
type: 'url'
}
]
}
]
}
}
3. API操作
// Sanityクライアントの設定
import {createClient} from '@sanity/client'
const client = createClient({
projectId: 'your-project-id',
dataset: 'production',
apiVersion: '2023-10-21',
useCdn: true
})
// GROQクエリ
const query = `*[_type == "post"]{
_id,
title,
slug,
author->{
name,
image
},
content,
"mainImage": mainImage.asset->url
}`
const posts = await client.fetch(query)
// パラメータ付きクエリ
const query = `*[_type == "post" && slug.current == $slug][0]{
title,
content,
author->{
name,
bio
}
}`
const params = {slug: 'my-post'}
const post = await client.fetch(query, params)
// リアルタイムリスナー
const subscription = client.listen(
'*[_type == "post"]',
{},
{includeResult: true}
).subscribe(update => {
console.log('Update:', update)
})
// ミューテーション
client
.patch('document-id')
.set({title: 'New Title'})
.inc({views: 1})
.commit()
.then(updatedDoc => {
console.log('Updated:', updatedDoc)
})
4. 認証設定
// APIトークンの設定
const client = createClient({
projectId: 'your-project-id',
dataset: 'production',
apiVersion: '2023-10-21',
token: process.env.SANITY_API_TOKEN, // 書き込みアクセス用
useCdn: false // トークン使用時はCDNを無効化
})
// Studioの認証設定
// sanity.config.ts
import {defineConfig} from 'sanity'
import {deskTool} from 'sanity/desk'
export default defineConfig({
name: 'default',
title: 'My Sanity Project',
projectId: 'your-project-id',
dataset: 'production',
plugins: [deskTool()],
schema: {
types: schemaTypes,
},
// アクセス制御
document: {
actions: (prev, {schemaType}) => {
if (schemaType === 'settings') {
return prev.filter(action => action.action !== 'delete')
}
return prev
}
}
})
// CORS設定(Sanity管理パネルで設定)
// プロジェクト設定 > API > CORS Origins
// http://localhost:3000 を追加
5. プラグイン・拡張機能
// カスタムプラグインの作成
// plugins/myPlugin.js
import {definePlugin} from 'sanity'
export const myPlugin = definePlugin({
name: 'my-plugin',
// カスタムツールの追加
tools: [
{
name: 'my-tool',
title: 'My Custom Tool',
component: MyToolComponent
}
],
// ドキュメントアクションの追加
document: {
actions: (prev, context) => {
return [...prev, {
label: 'Custom Action',
onHandle: () => {
console.log('Custom action triggered')
}
}]
}
}
})
// sanity.config.tsでプラグインを使用
import {myPlugin} from './plugins/myPlugin'
export default defineConfig({
// ...
plugins: [deskTool(), myPlugin()]
})
// カスタム入力コンポーネント
import {useFormValue} from 'sanity'
export function ConditionalField(props) {
const document = useFormValue([])
const isVisible = document?.category === 'news'
if (!isVisible) {
return null
}
return props.renderDefault(props)
}
// カスタムプレビュー
preview: {
select: {
title: 'title',
author: 'author.name',
media: 'mainImage'
},
prepare({title, author, media}) {
return {
title,
subtitle: author && `by ${author}`,
media
}
}
}
6. デプロイ・本番環境設定
// Next.jsとの統合
// lib/sanity.client.ts
import {createClient} from 'next-sanity'
export const client = createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,
apiVersion: '2023-10-21',
useCdn: process.env.NODE_ENV === 'production',
})
// プレビュー機能
// lib/sanity.preview.ts
import {definePreview} from 'next-sanity/preview'
import {projectId, dataset} from './sanity.client'
const {usePreview} = definePreview({
projectId,
dataset,
})
export default usePreview
// pages/api/preview.ts
export default function preview(req, res) {
res.setPreviewData({})
res.redirect('/')
}
// Studioのデプロイ
npm run build
# Sanity CLIでデプロイ
sanity deploy
// 環境変数の設定
// .env.local
NEXT_PUBLIC_SANITY_PROJECT_ID=your-project-id
NEXT_PUBLIC_SANITY_DATASET=production
SANITY_API_TOKEN=your-token
// Webhookの設定(Sanity管理パネル)
// API > Webhooks > Create Webhook
// URL: https://your-app.com/api/revalidate
// Trigger on: Create, Update, Delete
// Filter: _type == "post"
// ISRの実装
export async function getStaticProps({params}) {
const post = await client.fetch(
`*[_type == "post" && slug.current == $slug][0]`,
{slug: params.slug}
)
return {
props: {post},
revalidate: 60
}
}