Communitygithub.com

agents-inc/skills

结构化内容平台 — GROQ 查询、Schema 定义、@sanity/client、Portable Text、图像处理、实时监听、变更操作、TypeGen

skills 是什么?

该技能为开发者使用 Sanity 结构化内容平台提供了全面的指南。它涵盖了从客户端配置、GROQ 查询、Schema 定义到内容渲染和变更操作等完整工作流。核心模式包括:始终设置 `apiVersion` 日期字符串、使用 `useCdn` 控制缓存、通过参数化 GROQ 查询防止注入、利用 `defineQuery()` 配合 TypeGen 实现端到端类型安全。技能详细描述了如何使用 `@sanity/client` 获取数据,用 `@portabletext/react` 渲染富文本,以及用 `@sanity/image-url` 生成响应式图像。同时提供了清晰的决策框架,指导开发者处理 draft 文档、选择适当的变更方法,并规避常见陷阱如遗漏 `apiVersion`、GROQ 字符串插值等。

兼容平台✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/agents-inc/skills/tree/HEAD/src/skills/api-cms-sanity

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

Sanity 模式

快速指南: 使用 Sanity 进行结构化内容管理,搭配 GROQ 查询、通过 defineType/defineField 实现类型化 schema,以及 @sanity/client 进行数据获取。始终将 apiVersion 设置为日期字符串,公开读取使用 useCdn: true,显式处理草稿文档,使用 @sanity/image-url 进行图像转换,并用 @portabletext/react 渲染富文本。通过 sanity typegen generate 生成 TypeScript 类型。


<critical_requirements>

关键:使用此技能前须知

所有代码必须遵循 CLAUDE.md 中的项目约定 (kebab-case、具名导出、导入顺序、import type、具名常量)

(你必须在 createClient 上设置 apiVersion 为日期字符串,例如 '2025-02-19' — 省略此参数将使用可能出错的旧版 API)

(你必须为公开读取查询使用 useCdn: true,并在使用 token 或需要最新数据时使用 useCdn: false)

(你必须对任何动态值使用参数化 GROQ 查询 ($param) — 绝不要将用户输入直接插入到 GROQ 字符串中)

(你必须显式处理草稿 — 草稿文档的 _id 以 drafts. 为前缀,默认情况下不会被 perspective: 'published' 返回)

(你必须使用 groq 中的 defineQuery(),并将查询赋值给具名变量以便 TypeGen 生成类型)

</critical_requirements>


自动检测: Sanity, sanity, @sanity/client, createClient, GROQ, groq, defineType, defineField, defineArrayMember, @sanity/image-url, urlFor, @portabletext/react, PortableText, portable text, block content, sanity.config, sanity.cli, typegen, sanity studio, content lake

何时使用:

  • 使用 createClient 设置 @sanity/client 进行数据获取
  • 编写 GROQ 查询 (过滤器、投影、连接、排序、切片)
  • 使用 defineType、defineField、defineArrayMember 定义内容 schema
  • 使用 @portabletext/react 渲染 Portable Text (块内容)
  • 使用 @sanity/image-url 生成图像 URL (响应式图片、裁剪、热点)
  • 执行变更操作 (创建、部分更新、删除、事务)
  • 使用 client.listen() 设置实时监听
  • 使用 Sanity TypeGen 生成 TypeScript 类型

涵盖的关键模式:

  • 使用 createClient 和 apiVersion 配置客户端设置
  • GROQ 查询语言:过滤器、投影、排序、切片、连接、引用
  • Schema 定义:文档类型、对象类型、数组、引用、图像
  • 使用自定义组件渲染 Portable Text
  • 使用响应式图像和转换的图像 URL 构建器
  • 变更操作:创建、创建或替换、部分更新、删除、事务
  • 通过 client.listen() 实现实时监听
  • 使用 defineQuery() 的 TypeGen 实现类型安全的 GROQ 查询

何时不使用:

  • 仅 GraphQL 的 API (Sanity 主要支持 GROQ;如需要请使用 GraphQL 技能)
  • 直接数据库访问 (Sanity 是一个托管的内容库,不是数据库)
  • 非 Sanity 的 CMS 平台 (请使用针对你 CMS 的专用技能)

详细资源:

  • 关于决策框架和快速参考表,请参阅 reference.md

客户端 & GROQ:

Schemas:

  • examples/schemas.md — defineType、defineField、文档类型、对象类型、引用、图像

富内容:

变更操作 & 实时:


理念

Sanity 是一个结构化内容平台,围绕实时内容库、GROQ (图关系对象查询) 作为其查询语言,以及 Sanity Studio 作为可定制的编辑环境构建。

核心原则:

  1. 结构化内容 — 内容通过 schema (defineType、defineField) 定义,这些 schema 描述了内容的形状、验证和编辑界面。Schema 即代码,而不是配置文件。
  2. 优先使用 GROQ — GROQ 允许你在单个查询中过滤、投影、连接和重塑数据。与 REST 或 GraphQL 不同,GROQ 查询返回你在投影中定义的精确形状。
  3. 内容即数据 — 富文本以 Portable Text (一种基于 JSON 的规范) 存储,可以在任何前端框架中渲染,而无需供应商锁定。
  4. API 版本化 — 每个客户端必须指定一个 apiVersion 日期字符串。这会将你的代码固定到特定的 API 行为,防止破坏性更改影响生产环境。
  5. CDN 缓存 — 公共读取查询使用 useCdn: true 以获取边缘缓存的响应。变更操作和经过身份验证的读取使用 useCdn: false 以获取最新数据。
  6. 类型生成 — Sanity TypeGen 从你的 schema 和 GROQ 查询中生成 TypeScript 类型,实现从内容模型到前端的端到端类型安全。

何时使用 Sanity:

  • 内容驱动的网站和应用程序 (博客、营销网站、文档)
  • 需要在可定制的 Studio 中实时协作编辑的项目
  • 从单一内容源进行多渠道内容交付 (Web、移动、IoT)
  • 希望使用 GROQ 和 TypeGen 实现类型安全内容查询的团队

何时不使用:

  • 需要 ACID 保证的事务性数据 (请使用数据库)
  • 大规模用户生成内容 (Sanity 针对编辑内容优化)
  • 需要自托管 CMS 的项目 (Sanity 的内容库是托管的,但 Studio 是开源的)

核心模式

模式 1:使用 createClient 设置客户端

使用项目 ID、数据集、API 版本和 CDN 偏好配置 @sanity/client。始终将 apiVersion 设置为日期字符串,并显式设置 useCdn。

import { createClient } from "@sanity/client";

export const client = createClient({
  projectId: process.env.SANITY_PROJECT_ID!,
  dataset: process.env.SANITY_DATASET!,
  apiVersion: "2025-02-19", // 固定到特定的 API 版本日期
  useCdn: true, // 公共读取为 true,经过身份验证/最新数据为 false
});

关于双客户端设置 (公共 + 带 token 的预览),请参阅 examples/core.md。


模式 2:带过滤器和投影的 GROQ 查询

GROQ 查询结合了过滤器、投影、排序和切片。始终使用 defineQuery() 实现 TypeGen,并使用 $param 作为动态值。

import { defineQuery } from "groq";

const POST_BY_SLUG_QUERY = defineQuery(`
  *[_type == "post" && slug.current == $slug][0]{
    _id, title, body, "author": author->{name, image}
  }
`);
const post = await client.fetch(POST_BY_SLUG_QUERY, { slug: "my-post" });

永远不要将用户输入插入到 GROQ 字符串中——始终使用 $param 参数以防止 GROQ 注入。有关高级查询 (组合查询、条件投影),请参阅 examples/core.md。


模式 3:使用 defineType 和 defineField 定义 Schema

使用 "sanity" 中的 defineType、defineField 和 defineArrayMember 定义内容结构,以实现类型安全和 Studio UI。

import { defineType, defineField } from "sanity";

export const postType = defineType({
  name: "post",
  type: "document",
  fields: [
    defineField({
      name: "title",
      type: "string",
      validation: (r) => r.required(),
    }),
    defineField({ name: "slug", type: "slug", options: { source: "title" } }),
  ],
});

有关完整的 schema (带热点的图像、数组、引用、预览、对象类型),请参阅 examples/schemas.md。


模式 4:Portable Text 渲染

使用 @portabletext/react 渲染块内容。为非常见块 (图像、代码) 和标记 (链接、高亮) 定义自定义 PortableTextComponents。

import { PortableText } from "@portabletext/react";
<PortableText value={body} components={components} />;

不要使用已弃用的 @sanity/block-content-to-react 包。有关完整的组件示例,请参阅 examples/rich-content.md。


模式 5:图像 URL 构建器

使用 @sanity/image-url 生成优化、响应式的图像 URL,并支持裁剪和热点。

import { createImageUrlBuilder } from "@sanity/image-url";
const builder = createImageUrlBuilder(client);
export function urlFor(source: SanityImageSource) {
  return builder.image(source);
}
// 用法:urlFor(image).width(800).auto("format").url()

有关响应式 srcSet 模式和图像转换,请参阅 examples/rich-content.md。


模式 6:变更操作 (创建、部分更新、删除)

使用 @sanity/client 方法进行文档变更操作。始终在补丁和事务上调用 .commit()。

await client.create({ _type: "post", title: "新文章" });
await client.patch("post-123").set({ title: "已更新" }).commit();
await client.delete("post-123");

有关 createOrReplace、createIfNotExists、事务、数组插入和可见性选项,请参阅 examples/mutations.md。


模式 7:实时监听

使用 client.listen() 订阅文档变更。监听器仅使用 GROQ 的过滤器部分——投影和排序会被忽略。

const subscription = client.listen(`*[_type == "post"]`).subscribe({
  next: (update) => {
    /* update.transition: 'update' | 'appear' | 'disappear' */
  },
  error: (err) => console.error(err),
});
subscription.unsubscribe(); // 完成后清理

对于生产环境前端,请评估更新的 Live Content API 作为更简单的替代方案。有关监听器选项和注意事项,请参阅 examples/mutations.md。


模式 8:TypeGen 实现类型安全的 GROQ

在 sanity.cli.ts 中配置 TypeGen,设置 overloadClientMethods: true 以获得类型化的 client.fetch 结果。使用 "groq" 中的 defineQuery() 使查询可被 TypeGen 发现。

// sanity.cli.ts — 设置 typegen.overloadClientMethods: true
// queries/post-queries.ts — 使用 defineQuery() 包裹查询
// sanity.types.ts — 自动生成的结果类型
const posts = await client.fetch(allPostsQuery); // 类型化的结果

不使用 defineQuery() 的内联查询字符串会产生未类型化 (any) 的结果。有关完整的 TypeGen 配置和工作流程,请参阅 examples/core.md。


<decision_framework>

决策框架

useCdn: true vs false

数据是公开且非个性化的吗?
├─ 是 → useCdn: true (边缘缓存,快速)
└─ 否 →
    ├─ 使用 token 进行经过身份验证的读取? → useCdn: false
    ├─ 需要实时最新数据 (预览)? → useCdn: false
    └─ 正在执行变更操作? → useCdn: false

草稿处理

你需要草稿文档吗?
├─ 是 → 使用 perspective: 'previewDrafts' (需要 token)
├─ 否 → 使用 perspective: 'published' (自 API v2025-02-19 起为默认值)
└─ 混合 (预览模式切换)?
    └─ 创建两个客户端:一个公共 (useCdn: true),一个预览 (token + useCdn: false)

GROQ 查询返回形状

你期望多少条文档?
├─ 一条 (按 ID、slug、单例) → 在末尾添加 [0] (返回对象或 null)
├─ 多条 (列表、信息流) → 不添加切片后缀 (返回数组)
└─ 分页 → 添加 [start...end] 切片

图像处理

图像应该如何投递?
├─ 固定尺寸 (缩略图、头像) → urlFor(img).width(W).height(H).url()
├─ 响应式 (文章图片) → 使用多宽度 srcSet
├─ 格式优化 → .auto('format') 转换为 WebP/AVIF
└─ 按宽高比裁剪 → .width(W).height(H).fit('crop')

变更方法选择

你需要什么操作?
├─ 创建新文档 → client.create()
├─ 创建或完全替换 → client.createOrReplace() (用于单例)
├─ 仅当不存在时创建 → client.createIfNotExists()
├─ 更新特定字段 → client.patch(id).set({...}).commit()
├─ 删除字段 → client.patch(id).unset([...]).commit()
├─ 多个相关更改 → client.transaction()...commit()
└─ 删除文档 → client.delete(id)

</decision_framework>


<red_flags>

红旗警告

高优先级问题:

  • 客户端缺少 apiVersion — 省略 apiVersion 将使用可能随时更改的旧版 API 行为。始终固定到日期字符串。
  • 在 GROQ 查询中使用字符串插值 — 将用户输入插入 GROQ 字符串会导致 GROQ 注入。始终使用 $param 参数。
  • 将 useCdn: true 与 token 一起使用 — CDN 缓存的响应会忽略身份验证 token。经过身份验证的查询必须使用 useCdn: false。
  • 在没有 token 的情况下访问草稿文档 — 草稿 (drafts.* 文档) 需要 API token 和 perspective: 'previewDrafts'。没有 token,草稿是不可见的。

中优先级问题:

  • 在只需要几个字段时使用 {...} 获取所有字段 — 过度获取会浪费带宽和 CDN 缓存效率。仅投影你需要的字段。
  • 补丁缺少 .commit() — client.patch(id).set({...}) 如果不带 .commit() 则什么也不做——变更操作从未发送。
  • 未对 GROQ 查询使用 defineQuery() — 如果查询未包裹在 defineQuery() 中或未赋值给具名变量,TypeGen 无法为其生成类型。
  • 硬编码项目 ID 或数据集 — 使用环境变量;硬编码的值会阻止环境切换并泄露项目详细信息。
  • 使用已弃用的 @sanity/block-content-to-react — 已被 @portabletext/react 取代。旧包不再维护。

常见错误:

  • 在变更操作中忘记数组项的 _key — Sanity 数组中的每个项必须有一个唯一的 _key 字段。没有 _key 的变更操作将失败。
  • 在 client.create() 中使用 _id — create() 会生成一个随机的 _id。如果你指定了 _id 且文档已存在,则会报错。对于幂等操作,请使用 createOrReplace() 或 createIfNotExists()。
  • 未处理 [0] 返回 null 的情况 — GROQ 查询以一个 [0] 结尾,如果找不到文档则返回 null。在访问其属性之前始终检查结果是否为 null。

</red_flags>

Individual skills in this repo

This repo contains 11 individual skills — each has its own dedicated page.

相关技能