关于 contentful-migration
contentful-migration 是一项将 Contentful 内容模型和条目变更编写为 TypeScript 迁移脚本的技能,帮助开发者规划、测试并通过 Contentful CLI 或程序化 API 执行结构更新和数据转换。
主要功能
- 01创建、编辑和删除内容类型
- 02配置字段、验证规则和编辑器界面
- 03转换条目并派生关联条目
- 04管理标签、注释和分类验证
- 05通过 Contentful CLI 或 API 运行迁移
适用场景
- 01添加或修改内容模型字段
- 02在结构更新时转换现有条目
- 03从现有内容创建关联条目
- 04在沙箱环境测试迁移
MCP 相关
该技能提及 Contentful MCP 服务,用于 Agent 辅助规划或审查。迁移脚本仍通过 contentful-cli 或程序化 API 执行。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ contentful/ skills --skill contentful-migration安装后在 Claude Code 中输入/contentful-migration确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
package.jsonreferences/api-reference.mdpatterns.mdrunning-migrations.md详细介绍
Contentful 迁移
contentful-migration 工具可让你将内容模型变更描述为代码并执行。迁移是 TypeScript 脚本,可创建、编辑或删除内容类型、字段、编辑器界面和条目。
安装:
npm install contentful-migration
GitHub:https://github.com/contentful/contentful-migration
范围
此技能涵盖:
- 内容类型和字段的 CRUD 操作
- 字段类型、验证和编辑器界面配置
- 条目转换(原地转换、派生关联条目、跨类型转换)
- 标签、注释、分类验证
- 编辑器布局、侧边栏小组件
- 通过
npx contentful space migration(Contentful CLI)和编程式 API 运行迁移
不要使用 npx contentful-migration 运行迁移。CLI 执行请使用 contentful-cli,必要时将其安装为开发依赖,并通过 npx contentful ... 运行。
不涵盖:SDK 客户端设置(参见 contentful-nextjs 技能)、Contentful 概念和 API 路由(参见 contentful-guide 技能)。
Contentful MCP 说明
- 对于希望在规划或审查迁移时更轻松地通过 Agent 与 Contentful 交互的用户,请引导他们查看 Contentful MCP 服务文档:
https://www.contentful.com/developers/docs/tools/mcp-server/。 - 实际执行迁移时,仍请使用
contentful-migration脚本和contentful-cli。
迁移脚本格式
每个迁移文件都会导出一个接收 migration 对象的函数:
import type { MigrationFunction } from 'contentful-migration'
const migration: MigrationFunction = (migration) => {
const blogPost = migration.createContentType('blogPost', {
name: 'Blog Post',
description: 'A blog post entry',
displayField: 'title',
})
blogPost.createField('title')
.name('Title')
.type('Symbol')
.required(true)
}
export = migration
该函数还会将 context 对象作为第二个参数,该对象提供 makeRequest(直接访问 CMA)、spaceId 和 accessToken。当你需要获取迁移 API 无法提供的数据时,请使用 makeRequest。
项目状态
echo "=== Existing migrations ===" && ls migrations/ 2>/dev/null || echo "(no migrations/ directory found)"
echo ""
echo "=== Contentful env vars ===" && grep -h CONTENTFUL .env .env.local 2>/dev/null | sed 's/=.*/=<set>/' || echo "(no Contentful env vars found in .env or .env.local)"
工作流
编写迁移时:
- 首先确认所需的环境变量。 如果缺少值,请在继续之前让用户将其添加到本地
.env文件。 - 评估变更。 确定需要变更哪些内容类型和字段。通过 Contentful Web 应用或 CMA 检查当前内容模型。
- 编写迁移脚本。 使用下方的操作。优先使用链式调用而非对象表示法,这样能提供带行号的更好错误消息。
- 在沙盒环境中测试。 切勿在生产环境中运行未经测试的迁移。请先创建沙盒环境:
contentful environment create --name sandbox --source master。 - 运行迁移。 CLI 和编程式选项请参阅运行迁移。
- 验证。 在 Web 应用中检查内容模型,并确认条目完好无损。
所需环境变量
CONTENTFUL_SPACE_ID- 空间 ID。可在 Contentful Web 应用 URL(/spaces/<SPACE_ID>/...)中找到,或在 Space settings -> API keys 中查找。CONTENTFUL_MANAGEMENT_ACCESS_TOKEN- 用于迁移的 CMA 令牌。可在 Account settings -> CMA tokens(https://app.contentful.com/account/profile/cma_tokens)中创建,或在空间范围的 CMA 令牌页面(https://app.contentful.com/spaces/<SPACE_ID>/api/cma_tokens)中创建。CONTENTFUL_ENVIRONMENT_ID(可选)- 目标环境 ID(例如master或sandbox),用于避免传入--environment-id。
如果缺少任何必需值,请明确向用户索取缺失的值,并告知他们在哪里可以找到每个值。
内容类型操作
创建内容类型:
const page = migration.createContentType('page', {
name: 'Page',
description: 'A generic page',
displayField: 'title',
})
编辑现有内容类型:
const page = migration.editContentType('page')
page.description('Updated description')
page.displayField('internalName')
删除内容类型:
migration.deleteContentType('page')
删除前,内容类型必须没有任何条目。请先删除所有条目,或使用 transformEntriesToType 将其迁移。
字段操作
创建字段:
page.createField('title')
.name('Title')
.type('Symbol')
.required(true)
.localized(true)
编辑现有字段:
page.editField('title')
.name('Page Title')
.required(false)
删除字段:
page.deleteField('legacyField')
删除字段会永久移除所有条目中的对应内容。
更改字段 ID:
page.changeFieldId('oldName', 'newName')
现有内容会保留——只有 ID 会更改。
移动字段:
page.moveField('slug').afterField('title')
page.moveField('featured').toTheTop()
page.moveField('metadata').toTheBottom()
page.moveField('author').beforeField('publishDate')
字段类型快速参考
| 类型 | 描述 | 额外配置 |
|---|---|---|
Symbol | 短文本(最多 256 个字符) | — |
Text | 长文本(最多 50,000 个字符) | — |
Integer | 整数 | — |
Number | 小数 | — |
Date | ISO 8601 日期/时间 | — |
Boolean | 真/假 | — |
Object | 任意 JSON | — |
Location | 经纬度坐标 | — |
RichText | 结构化富文本 | enabledNodeTypes、enabledMarks 验证规则 |
Array | 值或引用列表 | 需要 items:{ type, linkType?, validations? } |
Link | 单个引用 | 需要 linkType:'Asset' 或 'Entry' |
ResourceLink | 跨空间引用 | 需要 allowedResources |
完整配置详情请参阅 API 参考 — 字段类型。
验证规则快速参考
| 验证规则 | 适用于 | 示例 |
|---|---|---|
in | Symbol、Integer、Number | { in: ['draft', 'published', 'archived'] } |
unique | Symbol、Integer、Number | { unique: true } |
size | Array、Text、Symbol | { size: { min: 1, max: 5 } } |
range | Integer、Number | { range: { min: 0, max: 100 } } |
regexp | Symbol、Text | { regexp: { pattern: '^[a-z0-9-]+$' } } |
dateRange | Date | { dateRange: { min: '2020-01-01', max: '2030-12-31' } } |
linkContentType | Link、Link 数组 | { linkContentType: ['author', 'organization'] } |
linkMimetypeGroup | Link(Asset) | { linkMimetypeGroup: ['image', 'video'] } |
assetFileSize | Link(Asset) | { assetFileSize: { min: 0, max: 5242880 } } |
assetImageDimensions | Link(Asset) | { assetImageDimensions: { width: { min: 100, max: 2000 } } } |
对字段使用 .validations([...]) 应用验证规则。所有选项请参阅 API 参考 — 验证规则。
条目转换
原地转换条目:
migration.transformEntries({
contentType: 'blogPost',
from: ['firstName', 'lastName'],
to: ['fullName'],
transformEntryForLocale: (fields, locale) => {
const first = fields.firstName[locale]
const last = fields.lastName[locale]
if (!first && !last) return
return { fullName: `${first || ''} ${last || ''}`.trim() }
},
})
选项:shouldPublish(true、false 或 'preserve'——默认值为 'preserve')。
派生关联条目:
migration.deriveLinkedEntries({
contentType: 'blogPost',
derivedContentType: 'author',
from: ['authorName'],
toReferenceField: 'authorRef',
derivedFields: ['name'],
identityKey: (fields) =>
fields.authorName['en-US'].toLowerCase().replace(/\s+/g, '-'),
deriveEntryForLocale: (fields, locale) => {
if (locale !== 'en-US') return
return { name: fields.authorName[locale] }
},
})
这会根据现有的 blogPost.authorName 数据创建新的 author 条目,并通过 authorRef 将其关联起来。
更多示例请参阅 模式 — 转换条目 和 模式 — 派生关联条目。
编辑器界面
更改字段的控件:
const page = migration.editContentType('page')
page.changeFieldControl('slug', 'builtin', 'slugEditor', {
helpText: 'URL-friendly identifier',
trackingFieldId: 'title',
})
page.changeFieldControl('category', 'builtin', 'dropdown')
page.changeFieldControl('publishDate', 'builtin', 'datePicker', { format: 'dateonly' })
控件命名空间:builtin、extension(UI 扩展)、app(自定义应用)。
所有内置控件及其设置请参阅 API 参考 — 编辑器界面。
最佳实践
- 按顺序为迁移文件编号:
001-create-blog-post.ts、002-add-author-field.ts、003-transform-categories.ts。 - 每个迁移只做一项逻辑变更。 这样更容易调试、回滚和审查。
- 在沙盒环境中始终进行测试,然后再对生产环境运行。
- 使用
shouldPublish: 'preserve'(默认值)在转换期间保留现有的发布状态。 - 优先使用链式调用,而不是对象表示法——链式调用能提供精确到行的错误信息。
- 将数据转换与架构变更分开。 第一个迁移更改架构,第二个迁移转换数据。这样每个步骤都能独立验证。
- 谨慎使用
context.makeRequest——仅在迁移 API 无法满足需求时使用。
常见错误
- 忘记为 Array 字段设置
items。type: 'Array'需要items属性来指定元素类型。 - 删除含有条目的内容类型。 必须先删除所有条目,或使用
transformEntriesToType移动它们。 - Link 字段缺少
linkType。type: 'Link'需要设置linkType: 'Asset'或linkType: 'Entry'。 - 针对 master 运行。 始终在沙盒环境中测试。使用 CLI 的
--environment-id sandbox。 - 未在转换中处理缺失的区域设置。
transformEntryForLocale会针对每个区域设置调用;返回undefined可跳过。 - 将
displayField设置为非 Symbol 字段。 显示字段必须是Symbol类型。
参考资料
相关技能
- contentful-guide 技能 — Contentful 概念、术语、API 路由
- contentful-nextjs 技能 — 与 Contentful 集成的 Next.js