cms

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Headless 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
undefined
bash
undefined

Install 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 name
vercel 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
undefined
bash
undefined

Install 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
undefined
npm install sanity @sanity/vision
undefined

Client 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()
实现实时内容(next-sanity v12)

Use
defineLive()
for automatic real-time content updates without manual revalidation. In next-sanity v11+,
defineLive
must be imported from the
next-sanity/live
subpath:
ts
// 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:
defineLive({fetchOptions: {revalidate}})
has been removed.
defineLive({stega})
is deprecated.
使用
defineLive()
实现自动实时内容更新,无需手动重新验证。在next-sanity v11+中,必须从
next-sanity/live
子路径导入
defineLive
ts
// 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
@sanity/visual-editing
v5+.
bash
npm install @sanity/visual-editing
In next-sanity v11+,
VisualEditing
must be imported from the
next-sanity/visual-editing
subpath:
ts
// 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)和
@sanity/visual-editing
v5+。
bash
npm install @sanity/visual-editing
在next-sanity v11+中,必须从
next-sanity/visual-editing
子路径导入
VisualEditing
ts
// 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/revalidate
.
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() });
}
在Sanity的设置 → API → Webhooks中配置Webhook,指向
https://your-site.vercel.app/api/revalidate

Contentful

Contentful

bash
npm install contentful
ts
// 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 contentful
ts
// 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

环境变量

VariableScopeCMSDescription
SANITY_PROJECT_ID
/
NEXT_PUBLIC_SANITY_PROJECT_ID
Server / ClientSanityProject identifier
SANITY_DATASET
/
NEXT_PUBLIC_SANITY_DATASET
Server / ClientSanityDataset name
SANITY_API_TOKEN
ServerSanityRead/write token
SANITY_REVALIDATE_SECRET
ServerSanityWebhook secret for revalidation
CONTENTFUL_SPACE_ID
ServerContentfulSpace identifier
CONTENTFUL_ACCESS_TOKEN
ServerContentfulDelivery API token
CONTENTFUL_PREVIEW_TOKEN
ServerContentfulPreview API token
DATOCMS_API_TOKEN
ServerDatoCMSRead-only API token
变量作用域CMS描述
SANITY_PROJECT_ID
/
NEXT_PUBLIC_SANITY_PROJECT_ID
服务端 / 客户端Sanity项目标识符
SANITY_DATASET
/
NEXT_PUBLIC_SANITY_DATASET
服务端 / 客户端Sanity数据集名称
SANITY_API_TOKEN
服务端Sanity读写令牌
SANITY_REVALIDATE_SECRET
服务端Sanity用于重新验证的Webhook密钥
CONTENTFUL_SPACE_ID
服务端Contentful空间标识符
CONTENTFUL_ACCESS_TOKEN
服务端Contentful交付API令牌
CONTENTFUL_PREVIEW_TOKEN
服务端Contentful预览API令牌
DATOCMS_API_TOKEN
服务端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

Official Documentation

官方文档