cms
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseHeadless CMS Integration
无头CMS集成
You are an expert in integrating headless CMS platforms with Vercel-deployed applications — covering Sanity (native Vercel Marketplace), Contentful, DatoCMS, Storyblok, and Builder.io.
您是将无头CMS平台与Vercel部署的应用集成的专家——涵盖Sanity(Vercel市场原生支持)、Contentful、DatoCMS、Storyblok和Builder.io。
Sanity (Native Vercel Marketplace Integration)
Sanity(Vercel市场原生集成)
Sanity is the primary CMS integration on the Vercel Marketplace with first-class Visual Editing support.
Sanity是Vercel市场上的主要CMS集成,提供一流的可视化编辑支持。
Install via Marketplace
通过市场安装
bash
undefinedbash
undefinedInstall Sanity from Vercel Marketplace (auto-provisions env vars)
Install Sanity from Vercel Marketplace (auto-provisions env vars)
vercel integration add sanity
Auto-provisioned environment variables:
- `SANITY_PROJECT_ID` — Sanity project identifier
- `SANITY_DATASET` — dataset name (usually `production`)
- `SANITY_API_TOKEN` — read/write API token
- `NEXT_PUBLIC_SANITY_PROJECT_ID` — client-side project ID
- `NEXT_PUBLIC_SANITY_DATASET` — client-side dataset namevercel integration add sanity
自动配置的环境变量:
- `SANITY_PROJECT_ID` — Sanity项目标识符
- `SANITY_DATASET` — 数据集名称(通常为`production`)
- `SANITY_API_TOKEN` — 读写API令牌
- `NEXT_PUBLIC_SANITY_PROJECT_ID` — 客户端项目ID
- `NEXT_PUBLIC_SANITY_DATASET` — 客户端数据集名称SDK Setup
SDK设置
bash
undefinedbash
undefinedInstall Sanity packages for Next.js
Install Sanity packages for Next.js
npm install next-sanity @sanity/client @sanity/image-url
npm install next-sanity @sanity/client @sanity/image-url
For embedded studio (optional)
For embedded studio (optional)
npm install sanity @sanity/vision
undefinednpm install sanity @sanity/vision
undefinedClient Configuration
客户端配置
ts
// lib/sanity.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: "2026-03-01",
useCdn: true,
});ts
// lib/sanity.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: "2026-03-01",
useCdn: true,
});Content Schema
内容模型
ts
// schemas/post.ts
import { defineType, defineField } from "sanity";
export const post = defineType({
name: "post",
title: "Post",
type: "document",
fields: [
defineField({ name: "title", type: "string" }),
defineField({ name: "slug", type: "slug", options: { source: "title" } }),
defineField({ name: "body", type: "array", of: [{ type: "block" }] }),
defineField({ name: "mainImage", type: "image", options: { hotspot: true } }),
defineField({ name: "publishedAt", type: "datetime" }),
],
});ts
// schemas/post.ts
import { defineType, defineField } from "sanity";
export const post = defineType({
name: "post",
title: "Post",
type: "document",
fields: [
defineField({ name: "title", type: "string" }),
defineField({ name: "slug", type: "slug", options: { source: "title" } }),
defineField({ name: "body", type: "array", of: [{ type: "block" }] }),
defineField({ name: "mainImage", type: "image", options: { hotspot: true } }),
defineField({ name: "publishedAt", type: "datetime" }),
],
});Embedded Studio (App Router)
嵌入式工作室(App Router)
ts
// app/studio/[[...tool]]/page.tsx
"use client";
import { NextStudio } from "next-sanity/studio";
import config from "@/sanity.config";
export default function StudioPage() {
return <NextStudio config={config} />;
}ts
// sanity.config.ts
import { defineConfig } from "sanity";
import { structureTool } from "sanity/structure";
import { visionTool } from "@sanity/vision";
import { post } from "./schemas/post";
export default defineConfig({
name: "default",
title: "My Studio",
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,
plugins: [structureTool(), visionTool()],
schema: { types: [post] },
});ts
// app/studio/[[...tool]]/page.tsx
"use client";
import { NextStudio } from "next-sanity/studio";
import config from "@/sanity.config";
export default function StudioPage() {
return <NextStudio config={config} />;
}ts
// sanity.config.ts
import { defineConfig } from "sanity";
import { structureTool } from "sanity/structure";
import { visionTool } from "@sanity/vision";
import { post } from "./schemas/post";
export default defineConfig({
name: "default",
title: "My Studio",
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,
plugins: [structureTool(), visionTool()],
schema: { types: [post] },
});Live Content with defineLive()
(next-sanity v12)
defineLive()使用defineLive()
实现实时内容(next-sanity v12)
defineLive()Use for automatic real-time content updates without manual revalidation. In next-sanity v11+, must be imported from the subpath:
defineLive()defineLivenext-sanity/livets
// lib/sanity.ts
import { createClient } from "next-sanity";
import { defineLive } from "next-sanity/live";
const client = createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,
apiVersion: "2026-03-01",
useCdn: true,
});
export const { sanityFetch, SanityLive } = defineLive({
client,
// Required for draft content in Visual Editing — use a Viewer role token
serverToken: process.env.SANITY_API_TOKEN,
// Optional but recommended for faster live preview
browserToken: process.env.SANITY_BROWSER_TOKEN,
});tsx
// app/page.tsx
import { sanityFetch, SanityLive } from "@/lib/sanity";
export default async function Page() {
const { data: posts } = await sanityFetch({ query: `*[_type == "post"]` });
return (
<>
{posts.map((post) => <div key={post._id}>{post.title}</div>)}
<SanityLive />
</>
);
}Breaking change in v12:has been removed.defineLive({fetchOptions: {revalidate}})is deprecated.defineLive({stega})
使用实现自动实时内容更新,无需手动重新验证。在next-sanity v11+中,必须从子路径导入:
defineLive()next-sanity/livedefineLivets
// lib/sanity.ts
import { createClient } from "next-sanity";
import { defineLive } from "next-sanity/live";
const client = createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,
apiVersion: "2026-03-01",
useCdn: true,
});
export const { sanityFetch, SanityLive } = defineLive({
client,
// 可视化编辑中的草稿内容需要——使用Viewer角色令牌
serverToken: process.env.SANITY_API_TOKEN,
// 可选但推荐用于更快的实时预览
browserToken: process.env.SANITY_BROWSER_TOKEN,
});tsx
// app/page.tsx
import { sanityFetch, SanityLive } from "@/lib/sanity";
export default async function Page() {
const { data: posts } = await sanityFetch({ query: `*[_type == "post"]` });
return (
<>
{posts.map((post) => <div key={post._id}>{post.title}</div>)}
<SanityLive />
</>
);
}v12中的重大变更:已被移除。defineLive({fetchOptions: {revalidate}})已废弃。defineLive({stega})
Visual Editing (Presentation Mode)
可视化编辑(演示模式)
Sanity Visual Editing lets content editors click-to-edit content directly on the live site preview. Requires Sanity Studio v5+ (React 19.2) and v5+.
@sanity/visual-editingbash
npm install @sanity/visual-editingIn next-sanity v11+, must be imported from the subpath:
VisualEditingnext-sanity/visual-editingts
// app/layout.tsx
import { VisualEditing } from "next-sanity/visual-editing";
import { draftMode } from "next/headers";
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const { isEnabled } = await draftMode();
return (
<html>
<body>
{children}
{isEnabled && <VisualEditing />}
</body>
</html>
);
}Sanity可视化编辑允许内容编辑器直接在实时站点预览上点击编辑内容。需要Sanity Studio v5+(React 19.2)和 v5+。
@sanity/visual-editingbash
npm install @sanity/visual-editing在next-sanity v11+中,必须从子路径导入:
next-sanity/visual-editingVisualEditingts
// app/layout.tsx
import { VisualEditing } from "next-sanity/visual-editing";
import { draftMode } from "next/headers";
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const { isEnabled } = await draftMode();
return (
<html>
<body>
{children}
{isEnabled && <VisualEditing />}
</body>
</html>
);
}On-Demand Revalidation Webhook
按需重新验证Webhook
ts
// app/api/revalidate/route.ts
import { revalidateTag } from "next/cache";
import { parseBody } from "next-sanity/webhook";
export async function POST(req: Request) {
const { isValidSignature, body } = await parseBody<{
_type: string;
slug?: { current?: string };
}>(req, process.env.SANITY_REVALIDATE_SECRET);
if (!isValidSignature) {
return Response.json({ message: "Invalid signature" }, { status: 401 });
}
if (body?._type) {
revalidateTag(body._type);
}
return Response.json({ revalidated: true, now: Date.now() });
}Configure the webhook in Sanity at Settings → API → Webhooks pointing to .
https://your-site.vercel.app/api/revalidatets
// app/api/revalidate/route.ts
import { revalidateTag } from "next/cache";
import { parseBody } from "next-sanity/webhook";
export async function POST(req: Request) {
const { isValidSignature, body } = await parseBody<{
_type: string;
slug?: { current?: string };
}>(req, process.env.SANITY_REVALIDATE_SECRET);
if (!isValidSignature) {
return Response.json({ message: "Invalid signature" }, { status: 401 });
}
if (body?._type) {
revalidateTag(body._type);
}
return Response.json({ revalidated: true, now: Date.now() });
}在Sanity的设置 → API → Webhooks中配置Webhook,指向。
https://your-site.vercel.app/api/revalidateContentful
Contentful
bash
npm install contentfults
// lib/contentful.ts
import { createClient } from "contentful";
export const contentful = createClient({
space: process.env.CONTENTFUL_SPACE_ID!,
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN!,
});bash
npm install contentfults
// lib/contentful.ts
import { createClient } from "contentful";
export const contentful = createClient({
space: process.env.CONTENTFUL_SPACE_ID!,
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN!,
});Fetching Entries
获取条目
ts
// app/page.tsx
import { contentful } from "@/lib/contentful";
export default async function Page() {
const entries = await contentful.getEntries({ content_type: "blogPost" });
return (
<ul>
{entries.items.map((entry) => (
<li key={entry.sys.id}>{entry.fields.title as string}</li>
))}
</ul>
);
}ts
// app/page.tsx
import { contentful } from "@/lib/contentful";
export default async function Page() {
const entries = await contentful.getEntries({ content_type: "blogPost" });
return (
<ul>
{entries.items.map((entry) => (
<li key={entry.sys.id}>{entry.fields.title as string}</li>
))}
</ul>
);
}Draft Mode (Preview)
预览模式(Draft Mode)
All CMS integrations should use Next.js Draft Mode for preview:
ts
// app/api/draft/route.ts
import { draftMode } from "next/headers";
export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const secret = searchParams.get("secret");
if (secret !== process.env.DRAFT_SECRET) {
return Response.json({ message: "Invalid token" }, { status: 401 });
}
const draft = await draftMode();
draft.enable();
const slug = searchParams.get("slug") ?? "/";
return Response.redirect(new URL(slug, req.url));
}所有CMS集成都应使用Next.js Draft Mode进行预览:
ts
// app/api/draft/route.ts
import { draftMode } from "next/headers";
export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const secret = searchParams.get("secret");
if (secret !== process.env.DRAFT_SECRET) {
return Response.json({ message: "Invalid token" }, { status: 401 });
}
const draft = await draftMode();
draft.enable();
const slug = searchParams.get("slug") ?? "/";
return Response.redirect(new URL(slug, req.url));
}Environment Variables
环境变量
| Variable | Scope | CMS | Description |
|---|---|---|---|
| Server / Client | Sanity | Project identifier |
| Server / Client | Sanity | Dataset name |
| Server | Sanity | Read/write token |
| Server | Sanity | Webhook secret for revalidation |
| Server | Contentful | Space identifier |
| Server | Contentful | Delivery API token |
| Server | Contentful | Preview API token |
| Server | DatoCMS | Read-only API token |
| 变量 | 作用域 | CMS | 描述 |
|---|---|---|---|
| 服务端 / 客户端 | Sanity | 项目标识符 |
| 服务端 / 客户端 | Sanity | 数据集名称 |
| 服务端 | Sanity | 读写令牌 |
| 服务端 | Sanity | 用于重新验证的Webhook密钥 |
| 服务端 | Contentful | 空间标识符 |
| 服务端 | Contentful | 交付API令牌 |
| 服务端 | Contentful | 预览API令牌 |
| 服务端 | DatoCMS | 只读API令牌 |
Cross-References
交叉引用
- Marketplace install and env var provisioning →
⤳ skill: marketplace - On-demand revalidation and caching →
⤳ skill: runtime-cache - Draft mode and middleware patterns →
⤳ skill: routing-middleware - Environment variable management →
⤳ skill: env-vars - Image optimization →
⤳ skill: nextjs
- 市场安装与环境变量配置 →
⤳ skill: marketplace - 按需重新验证与缓存 →
⤳ skill: runtime-cache - 预览模式与中间件模式 →
⤳ skill: routing-middleware - 环境变量管理 →
⤳ skill: env-vars - 图片优化 →
⤳ skill: nextjs