Communitygithub.com

agents-inc/skills

开源无头CMS — 内容类型架构、REST API、文档服务API、自定义控制器、生命周期钩子、身份验证、TypeScript

skills 是什么?

该技能为开发者使用Strapi(基于Node.js/Koa的开源无头CMS)提供全面的模式参考。它涵盖内容类型架构定义(schema.json)、REST API查询(配合qs库)、Strapi v5中的文档服务API(strapi.documents(),替代已弃用的Entity Service)、自定义控制器和服务、路由扩展、生命周期钩子、JWT身份验证以及TypeScript类型生成。核心要点包括:默认不填充关系/媒体,必须显式传递populate参数;v5中的文档ID(documentId)取代数据库ID作为主标识符;所有端点默认私有,需通过用户权限插件或API令牌显式配置权限。技能支持Strapi v5,并包含一系列模式示例,从基础CRUD到高级用例(如动态区段、多语言内容和发布/草稿工作流)。它通过reference.md、core.md、backend.md和auth.md等详细资源文件引导决策,并为权限管理和内容获取提供安全最佳实践。

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

在你喜欢的 AI 中提问

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

文档

Strapi 模式

快速指南: 使用 Strapi 作为开源无头 CMS,从内容类型架构自动生成 REST/GraphQL API。在 v5 中,使用文档服务 API (strapi.documents()) 进行后端数据访问,而非已弃用的 Entity Service。REST API 响应采用扁平格式(data.fieldName,而不是 data.attributes.fieldName)。默认情况下,关系和媒体不会被填充——始终传递 populate。使用 qs 构建复杂查询字符串。内容类型默认是私有的;通过用户权限插件或 API 令牌配置权限。


<critical_requirements>

关键:使用此技能之前

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

(在 Strapi v5 中,你必须使用 strapi.documents('api::content-type.content-type')(文档服务 API)进行所有后端数据访问——Entity Service API 已移除)

(当你需要关系、媒体、组件或动态区段时,你必须始终传递 populate——默认情况下 Strapi 不返回任何关系)

(你必须使用 qs 库构建包含过滤器、填充和排序的复杂 REST API 查询字符串——手动字符串构建会导致嵌套参数问题)

(在自定义控制器中,你必须使用 this.sanitizeQuery(ctx)、this.sanitizeOutput() 和 this.validateQuery(ctx) 对输入和输出进行清理和验证)

(你必须通过管理面板或配置为每个内容类型端点设置权限——所有路由默认是私有的)

</critical_requirements>


自动检测: Strapi, strapi, @strapi/strapi, createCoreController, createCoreService, createCoreRouter, Document Service, strapi.documents, content-type, schema.json, api::, plugin::, lifecycle hooks, Users & Permissions, /api/auth/local, populate, qs.stringify

何时使用:

  • 构建以 Strapi 作为无头 CMS 的内容管理应用程序
  • 定义包含字段、关系、组件和动态区段的内容类型架构(schema.json)
  • 使用过滤器、填充、排序和分页查询 REST API
  • 创建自定义控制器、服务、路由、策略或中间件
  • 使用文档服务 API 进行后端 CRUD 操作,支持草稿/发布工作流
  • 使用用户权限插件实现 JWT 身份验证
  • 为内容类型添加生命周期钩子以实现副作用
  • 为内容架构生成 TypeScript 类型

涵盖的关键模式:

  • 内容类型架构定义(schema.json)
  • 使用 qs 进行 REST API 查询(过滤器、填充、排序、分页)
  • 文档服务 API(findMany、findOne、create、update、delete、publish、unpublish)
  • 自定义控制器、服务、路由、策略和中间件
  • 生命周期钩子(beforeCreate、afterUpdate 等)
  • JWT 身份验证(注册、登录、经过身份验证的请求)
  • TypeScript 类型生成

何时不使用:

  • 非 Strapi 的 CMS 平台(使用专用于您的 CMS 的技能)
  • 绕过 Strapi API 层的直接数据库查询(使用 Strapi 的文档服务)
  • 需要原始 SQL 的复杂事务逻辑(Strapi 抽象了数据库)

详细资源:

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

核心与 REST API:

  • examples/core.md —— 内容类型架构、REST API 查询、文档服务 API、错误处理

后端定制:

  • examples/backend.md —— 自定义控制器、服务、路由、策略、中间件、生命周期钩子、文档服务中间件

身份验证:


理念

Strapi 是一个基于 Node.js (Koa) 的开源无头 CMS,它从内容类型架构自动生成 RESTful 和 GraphQL API。内容通过 JSON 架构定义,通过管理面板管理,并通过生成的 API 端点消费。

核心原则:

  1. 架构驱动的内容 —— 内容类型在 schema.json 文件中定义,这些文件描述字段、关系、组件和动态区段。管理面板的内容类型构建器提供了可视化编辑器,但架构是驻留在您仓库中的代码。
  2. 自动生成的 API —— 每个内容类型都会自动获得 CRUD REST 端点(/api/:pluralApiId)和可选的 GraphQL 支持。标准操作无需手动创建路由/控制器。
  3. 文档服务 API (v5) —— 用于从自定义代码、插件和生命周期钩子访问内容的后端 API。取代了 v4 的 Entity Service。使用 documentId(而不是数据库 id)作为主标识符。
  4. 草稿与发布 —— 内容类型可以具有草稿/发布工作流。文档服务默认使用 status: 'draft';已发布内容需要 status: 'published' 或显式调用 publish()。
  5. 权限优先 —— 所有内容类型端点默认是私有的。必须通过管理面板(用户权限插件)或 API 令牌显式授予访问权限。
  6. 后端定制 —— 控制器、服务、路由、策略和中间件都可以自定义。Strapi 遵循基于 Koa 的 MVC 类似模式。

核心模式

模式 1:内容类型架构

内容类型在 ./src/api/[api-name]/content-types/[content-type-name]/schema.json 的 schema.json 文件中定义。对于多文档内容(文章、产品)使用 collectionType,对于单文档内容(站点设置、首页)使用 singleType。

{
  "kind": "collectionType",
  "info": {
    "singularName": "article",
    "pluralName": "articles",
    "displayName": "Article"
  },
  "options": { "draftAndPublish": true },
  "attributes": {
    "title": { "type": "string", "required": true },
    "slug": { "type": "uid", "targetField": "title" },
    "author": {
      "type": "relation",
      "relation": "manyToOne",
      "target": "api::author.author",
      "inversedBy": "articles"
    },
    "blocks": {
      "type": "dynamiczone",
      "components": ["blocks.hero", "blocks.rich-text"]
    }
  }
}

关键字段: uid 从 targetField 自动生成 slug,relation 类型通过 inversedBy/mappedBy 定义基数,component 嵌入可重用块,dynamiczone 允许混合组件类型。有关完整的集合和单一类型示例,请参阅 examples/core.md。


模式 2:使用 qs 进行 REST API 查询

REST API 接受复杂的查询参数,用于过滤、填充、排序和分页。始终使用 qs 库构建查询字符串。

import qs from "qs";

const query = qs.stringify(
  {
    filters: { publishedAt: { $notNull: true } },
    populate: {
      author: { fields: ["name"] },
      categories: { fields: ["name", "slug"] },
    },
    sort: ["publishedAt:desc"],
    pagination: { page: 1, pageSize: 25 },
  },
  { encodeValuesOnly: true },
);
const response = await fetch(`${STRAPI_URL}/api/articles?${query}`);
const { data, meta } = await response.json();

关键点: encodeValuesOnly 防止括号编码问题,过滤器使用运算符($eq、$notNull、$containsi、$or、$and),带有 fields 的定向 populate 避免过度获取(切勿在生产中使用 populate=*),动态区段组件使用 on 语法。有关完整的过滤、填充和分页示例,请参阅 examples/core.md。


模式 3:文档服务 API (后端)

文档服务 API 用于自定义控制器、服务、生命周期钩子和插件中,以便从服务器端访问内容。它取代了 v4 的 Entity Service(在 v5 中已移除)。

const CONTENT_TYPE_UID = "api::article.article";

// Find published content (default is 'draft')
const articles = await strapi.documents(CONTENT_TYPE_UID).findMany({
  status: "published",
  filters: { categories: { slug: { $eq: "news" } } },
  populate: { author: true },
});

// CRUD uses documentId (not database id)
const article = await strapi
  .documents(CONTENT_TYPE_UID)
  .findOne({ documentId });
const created = await strapi
  .documents(CONTENT_TYPE_UID)
  .create({ data: { title: "New" } });
await strapi.documents(CONTENT_TYPE_UID).publish({ documentId });

关键点: strapi.documents(uid) 取代 strapi.entityService(在 v5 中移除),documentId 是跨语言和草稿/已发布版本的持久标识符,默认状态是 'draft'(显式传递 status: 'published'),有专门的 publish()/unpublish()/discardDraft() 方法。有关完整的 CRUD 和发布/取消发布示例,请参阅 examples/core.md。


模式 4:自定义控制器

扩展或替换自动生成的控制器动作。控制器处理请求/响应逻辑并委托给服务。使用 factories 中的 createCoreController 来继承清理助手。

// src/api/article/controllers/article.ts
export default factories.createCoreController(
  "api::article.article",
  ({ strapi }) => ({
    async find(ctx) {
      await this.validateQuery(ctx);
      const sanitizedQuery = await this.sanitizeQuery(ctx);
      const { results, pagination } = await strapi
        .service("api::article.article")
        .find(sanitizedQuery);
      const sanitizedResults = await this.sanitizeOutput(results, ctx);
      return this.transformResponse(sanitizedResults, { pagination });
    },
  }),
);

为什么好: validateQuery + sanitizeQuery + sanitizeOutput 强制基于角色的字段访问,transformResponse() 将输出包装在 { data, meta } 信封中

有关包括自定义操作(findBySlug、incrementViews、search)在内的完整控制器示例,请参阅 examples/backend.md。


模式 5:自定义路由

定义自定义路由以公开自定义控制器操作。自定义路由文件与核心路由器放在一起。

// src/api/article/routes/custom-article.ts
export default {
  routes: [
    {
      method: "GET",
      path: "/articles/slug/:slug",
      handler: "api::article.article.findBySlug",
      config: { auth: false },
    },
  ],
};

为什么好: 与核心路由器分开的文件,handler 使用完整 UID,auth: false 用于公共访问,每条路由可附加策略/中间件

有关使用 only 进行路由限制、中间件附加和策略配置的示例,请参阅 examples/backend.md。


模式 6:生命周期钩子

在内容类型操作上注册副作用。钩子在 lifecycles.ts 文件中定义,与内容类型架构放在一起。

// src/api/article/content-types/article/lifecycles.ts
export default {
  async beforeCreate(event) {
    /* event.params.data -- mutate to transform input */
  },
  async afterCreate(event) {
    /* event.result -- the created document */
  },
  async beforeUpdate(event) {
    /* event.params.data -- mutate before save */
  },
  async afterDelete(event) {
    /* event.result -- cleanup related resources */
  },
};

为什么好: 钩子在文档服务操作时自动触发,beforeXxx 可以修改 event.params.data,afterXxx 有 event.result

注意: 对于 v5 中的横切关注点(审计日志、缓存失效),更推荐使用在 src/index.ts 中注册的文档服务中间件(strapi.documents.use((ctx, next) => { ... }))。生命周期钩子仍然可用于特定内容类型的副作用。

有关包含 slug 生成、程序化订阅和审计日志的完整生命周期示例,请参阅 examples/backend.md。


模式 7:服务

服务包含由控制器调用的可重用业务逻辑。使用 createCoreService 来继承默认的 CRUD 并添加自定义方法。

export default factories.createCoreService(
  "api::article.article",
  ({ strapi }) => ({
    async findPublished(filters = {}) {
      return strapi
        .documents("api::article.article")
        .findMany({ status: "published", filters });
    },
  }),
);
// Called via: strapi.service("api::article.article").findPublished()

为什么好: 继承默认的 CRUD,自定义方法封装可重用的查询逻辑,通过 strapi.service(uid) 从控制器调用

有关包括相关文章查询和发布附带通知模式在内的复杂服务示例,请参阅 examples/backend.md。


模式 8:策略

策略是只读函数,允许或拒绝对路由的访问。它们返回 true(允许)或 false(拒绝),不能修改请求。

// src/api/article/policies/is-owner.ts
export default async (policyContext, config, { strapi }) => {
  const article = await strapi.documents("api::article.article").findOne({
    documentId: policyContext.params.documentId,
    populate: { author: true },
  });
  return article?.author?.documentId === policyContext.state.user?.documentId;
};

为什么好: 只读检查(不修改请求),在核心路由器配置上按操作应用

有关策略应用于路由、速率限制策略和多策略链的示例,请参阅 examples/backend.md。


<decision_framework>

决策框架

REST API 与文档服务 API

您的代码在哪里运行?
+-- 客户端(浏览器、前端应用)
|   +-- 使用 REST API (fetch /api/:pluralApiId)
+-- 服务器端(自定义控制器、服务、插件、生命周期钩子)
    +-- 使用文档服务 API (strapi.documents(uid))

填充策略

您是否需要相关数据?
+-- 否 --> 不传递 populate(响应更精简)
+-- 是 --> 需要什么级别的控制?
    +-- 所有关系,1 层 --> populate: '*'(方便但过度获取)
    +-- 特定关系 --> populate: { relation: { fields: [...] } }
    +-- 嵌套关系

…

[为翻译截断]

</decision_framework>

Individual skills in this repo

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

相关技能