payload-import-kit
v2.0.0
Published
Markdown 图文内容导入标准 + PayloadCMS/Next.js 参考实现(spec + server/client/CLI/MCP)
Maintainers
Readme
payload-import-kit
双标准 + 参考实现:Markdown 图文内容导入标准 + PayloadCMS/Next.js 参考实现。
spec/content-format.md— 内容包格式标准 v1:语言无关,定义一篇内容如何用.md文件表达spec/http-protocol.md— HTTP 协议标准 v2:定义传输端 API,任何语言的服务端只要实现本协议,即可复用本仓库的 client SDK、CLI 与 MCP server- 设计文档见 GitHub 仓库
docs/目录(未随 npm 包发布)
1. 快速开始(站点侧)
1.1 环境变量
# .env.local
IMPORT_API_KEY=sk_live_xxxxxxxxxxxxxxxx1.2 创建配置文件
// lib/import-kit-config.ts
import type { ImportKitConfig } from 'payload-import-kit/server'
import { getPayload } from 'payload'
import config from '@payload-config'
export const importKitConfig: ImportKitConfig = {
getPayload: () => getPayload({ config }),
auth: { type: 'apikey' }, // 默认读取 IMPORT_API_KEY 环境变量
mediaCollection: 'media',
// 媒体去重:站点需在 media 集合中添加一个文本字段(如 importSource)存放指纹
// 外链图片以 sourceUrl 为指纹,文件直传以内容 sha256 为指纹
dedupe: { field: 'importSource' },
siteUrl: 'https://my-site.com',
collections: {
articles: {
slug: 'articles',
description: '技术文章,支持 Markdown 正文和封面图',
requiredFields: ['title'],
hasBody: true,
bodyField: 'body',
supportsCoverImage: true,
coverImageField: 'coverImage',
tagField: 'tags',
tagFormat: 'object', // {tag: string}[] 格式
publishedAtField: 'publishedAt', // 必须显式配置才会写入发布时间
cacheTags: ['articles'],
urlPrefix: 'articles',
},
},
}
publishedAtField行为变更(v2):v1 会无条件写入publishedAt字段。v2 改为只有在集合配置了publishedAtField时才写入,不配置则导入时完全不修改发布时间。
媒体去重说明:启用 dedupe 时,站点需在 media 集合的 schema 中添加一个文本字段(示例中为 importSource)用于存储来源指纹。上传前 kit 会先用指纹查询是否已存在相同图片,命中则复用已有记录,不会重复上传。
1.3 创建唯一的 catch-all 路由文件
handler 必须挂载在 app/api/import/[[...path]]/route.ts,路径不可自定义。
// app/api/import/[[...path]]/route.ts
import { createImportKitHandler } from 'payload-import-kit/server'
import { importKitConfig } from '@/lib/import-kit-config'
const handler = createImportKitHandler(importKitConfig)
export const { GET, POST, DELETE } = handler这一个文件替代了 v1 的 6 个 route 文件,站点升级 kit 版本即自动获得新端点。
1.4 配置 mediaRefs hook(可选,用于孤儿清理)
// collections/Articles.ts
import { createSyncMediaRefsHook } from 'payload-import-kit/server'
import { importKitConfig } from '@/lib/import-kit-config'
export const Articles: CollectionConfig = {
slug: 'articles',
fields: [
{ name: 'body', type: 'textarea' },
{
name: 'mediaRefs',
type: 'relationship',
relationTo: 'media',
hasMany: true,
admin: { readOnly: true },
},
// ...其他字段
],
hooks: {
beforeChange: [createSyncMediaRefsHook(importKitConfig)],
},
}hook 会在运行时根据 Payload 传入的
collection.slug,自动从ImportKitConfig.collections中查找对应集合的bodyField(默认'body')与mediaRefsField(默认'mediaRefs'),无需额外传参。若collection.slug在config.collections中未匹配到任何集合,则两者均回退默认值。cleanupMedia()同样读取mediaRefsField配置,因此自定义字段名只需在config.collections中声明一处,hook 写入与 cleanup 读取自动保持一致。
2. 快速开始(导入侧)
2.1 内容包示例
一篇内容 = 一个 .md 文件,YAML frontmatter 承载元数据,正文为 Markdown:
---
collection: articles
slug: nextjs-guide
title: Next.js 完全指南
tags: [nextjs, react]
cover: ./images/cover.jpg # 相对路径 / https URL / 站内路径
publishedAt: 2026-06-11
mode: upsert # create | upsert | update,省略默认 create
summary: 一段摘要 # 保留字段之外的键原样透传
---
正文 Markdown……
2.2 CLI
export PAYLOAD_BASE_URL=https://my-site.com
export PAYLOAD_API_KEY=sk_live_xxxxxxxx
# 批量导入目录下所有 .md 文件
npx payload-import push ./content --mode upsert
# 并发 8,仅校验不提交(dry-run)
npx payload-import push ./content --dry-run --concurrency 8
# 查看站点支持的集合
npx payload-import collections
# 清理孤儿媒体
npx payload-import cleanupCLI 退出码:0 = 全部成功;1 = 存在失败文件;2 = 参数/配置错误。
行内图片失败策略固定为 skip(失败图片保留原 URL 并以 ⚠ 警告输出,文件仍计为成功);如需严格失败请使用 SDK 的 onImageError: 'fail'。
环境变量:
| 变量名 | 说明 |
|--------|------|
| PAYLOAD_BASE_URL | 站点根 URL,如 https://my-site.com |
| PAYLOAD_API_KEY | 与服务端 IMPORT_API_KEY 匹配的密钥 |
2.3 SDK 一步导入
import { ImportKitClient } from 'payload-import-kit/client'
const client = new ImportKitClient({
baseUrl: 'https://my-site.com',
apiKey: process.env.PAYLOAD_API_KEY!,
})
// 一步导入内容包文件(解析 frontmatter + 处理图片 + 写入文档)
const result = await client.importMarkdownFile('./content/articles/nextjs-guide.md')
console.log(result.action, result.slug) // "created" "nextjs-guide"3. 协议速览
所有端点位于统一前缀 /api/import/v2 之下,认证通过请求头 X-API-Key: <key> 传递。
端点表
| 方法 | 路径 | 作用 |
|------|------|------|
| GET | /api/import/v2 | Discovery:协议版本、集合清单与上传限制 |
| POST | /api/import/v2/media | 上传图片:multipart 直传,或 JSON { sourceUrl } 远程抓取 |
| POST | /api/import/v2/documents | 创建/更新文档 |
| GET | /api/import/v2/documents/{collection} | 列表(?limit&page&sort) |
| GET | /api/import/v2/documents/{collection}/{slug} | 查询单条 |
| DELETE | /api/import/v2/documents/{collection}/{slug} | 删除 |
| POST | /api/import/v2/maintenance/cleanup | 清理孤儿媒体 |
响应包裹
所有响应统一使用以下格式:
// 成功
{ "ok": true, "data": { ... } }
// 失败
{ "ok": false, "error": { "code": "slug_conflict", "message": "...", "detail": "..." } }错误码表
| code | HTTP | 含义 |
|------|------|------|
| unauthorized | 401 | API Key 缺失或错误 |
| invalid_request | 400 | 请求体/参数非法(含 SSRF 阻断) |
| unknown_collection | 400 | collection 未配置 |
| missing_fields | 400 | 缺少必填字段 |
| slug_conflict | 409 | create 模式下 slug 已存在 |
| not_found | 404 | 文档或端点不存在 |
| image_download_failed | 422 | 远程图片下载/校验失败 |
| payload_too_large | 413 | 文件超过大小限制 |
| internal | 500 | 服务端内部错误 |
4. 图片三态规则
| 写法 | 含义 | 导入器职责 |
|------|------|-----------|
| ./images/a.png(相对路径) | 本地文件,相对 .md 所在目录解析 | 读取 → 上传 → 重写为站内 URL |
| https://...(外链) | 站外图片 | 下载 → 上传 → 重写为站内 URL |
| /media/...(以 / 开头) | 站内已上传图片 | 保持原样 |
代码块豁免:fenced code block(``` 或 ~~~ 围栏,含未闭合到文末的情况)与 inline code 中出现的  不视为图片引用,导入器不处理。
5. SDK 方法参考
import { ImportKitClient } from 'payload-import-kit/client'
const client = new ImportKitClient({ baseUrl: 'https://my-site.com', apiKey: '...' })| 方法 | 说明 |
|------|------|
| discover() | GET /v2 — 返回 DiscoveryData:协议版本、集合清单与上传限制 |
| uploadImage(file, alt?) | 上传图片(浏览器 File/Blob 或 Node.js {name, data, type}),返回 MediaData |
| uploadImageFromPath(filePath, alt?) | Node.js 专用:读取本地文件并上传,返回 MediaData |
| uploadImageFromUrl(sourceUrl, alt?) | 服务端下载远程图片并上传(含 SSRF 防护),返回 MediaData |
| prepareMarkdown(markdown, options?) | 按图片三态规则处理 Markdown,返回 { markdown: string, warnings: string[] } |
| importDocument(options) | POST /v2/documents — 创建或更新文档,返回 ImportDocumentData |
| getDocument(collection, slug) | GET /v2/documents/{collection}/{slug} — 返回文档对象 |
| listDocuments(collection, params?) | GET /v2/documents/{collection} — 分页列表,返回 ListDocumentsData |
| deleteDocument(collection, slug) | DELETE /v2/documents/{collection}/{slug} — 返回 DeleteDocumentData |
| cleanupMedia() | POST /v2/maintenance/cleanup — 清理孤儿媒体,返回 CleanupData |
| importMarkdownFile(filePath, options?) | Node.js 专用:一步导入内容包文件,返回 ImportDocumentData & { warnings: string[] } |
importDocument 参数(ImportDocumentOptions):
// import type { ImportDocumentOptions } from 'payload-import-kit/client'
{
collection: string, // 集合 key(对应 ImportKitConfig.collections 的键名)
slug: string, // 唯一标识
mode?: 'create' | 'upsert' | 'update', // 默认 'create'
markdown?: string, // Markdown 正文
coverImageUrl?: string, // 封面图远程 URL(服务端自动下载上传)
coverImageId?: string | number, // 已上传 media 的 ID(优先级高于 coverImageUrl)
uploadInlineImages?: boolean, // 是否让服务端处理正文中的站外图片,默认 false
onImageError?: 'fail' | 'skip', // 图片失败策略,默认 'skip'
// ...其他透传字段(如 title、tags 等)
}6. 配置参考
ImportKitConfig
| 字段 | 类型 | 说明 |
|------|------|------|
| getPayload | () => Promise<Payload> | 必填:获取 Payload 实例 |
| auth | ApiKeyAuthConfig | 必填:认证配置 |
| auth.type | 'apikey' | 目前仅支持 API Key |
| auth.envName | string | 环境变量名,默认 'IMPORT_API_KEY' |
| auth.verify | (key: string) => Promise<boolean> | 自定义验证函数(优先级高于环境变量) |
| mediaCollection | string | media 集合名,默认 'media' |
| collections | Record<string, CollectionImportConfig> | 必填:集合配置映射 |
| uploadLimits.maxFileSize | number | 单文件大小上限(字节),默认 10MB |
| uploadLimits.maxInlineImages | number | 行内图片上传数量上限,默认 15 |
| uploadLimits.inlineImageTimeout | number | 行内图片下载超时(ms),默认 10000 |
| dedupe | { field: string } | 媒体去重:站点需在 media 集合添加指定文本字段存放指纹 |
| debug | boolean | 开启调试日志,默认 false |
| siteUrl | string | 站点公开 URL,用于生成响应中的 postUrl |
CollectionImportConfig
| 字段 | 类型 | 说明 |
|------|------|------|
| slug | string | 必填:Payload 集合 slug |
| description | string | 集合用途说明,用于 Discovery 端点 |
| slugField | string | 唯一标识字段名,默认 'slug' |
| requiredFields | string[] | 创建时必须提供的字段 |
| hasBody | boolean | 是否支持 Markdown body |
| bodyField | string | body 字段名,默认 'body';createSyncMediaRefsHook 会按集合 slug 自动从此处派生,一般无需额外配置,见 hook 章节说明 |
| mediaRefsField | string | mediaRefs 关系字段名,默认 'mediaRefs';createSyncMediaRefsHook 与 cleanupMedia 共用此配置,自定义字段名只需在此声明一处 |
| supportsCoverImage | boolean | 是否支持封面图 |
| coverImageField | string | 封面图字段名,默认 'coverImage' |
| tagField | string | 标签字段名,默认 'tags' |
| tagFormat | 'object' \| 'plain' | 标签格式:object 为 {tag:string}[],plain 为 string[] |
| publishedAtField | string | 发布时间字段名;不配置则导入时完全不写发布时间(v2 行为变更) |
| publishedAtDefault | () => string | 发布时间默认值生成函数 |
| buildData | (body, markdownBody?, coverImageId?) => Record<string, unknown> | 自定义数据构建函数(优先级最高) |
| cacheTags | string[] | Next.js revalidateTag 缓存标签 |
| urlPrefix | string | 文章 URL 路径前缀,如 'articles' 生成 /articles/{slug} |
7. MCP Server
payload-import-kit-mcp 是一个 Model Context Protocol server,让 AI 助手(如 Claude Desktop)可以直接操作 Payload CMS 内容。
工具列表(共 10 个)
| 工具 | 说明 |
|------|------|
| list_collections | Discovery:查询站点支持的集合及元数据 |
| upload_image_from_url | 下载远程图片并上传到 media 库 |
| upload_image_from_path | 上传本地图片文件到 media 库(Node.js) |
| prepare_markdown | 处理 Markdown 图片引用,返回 { markdown, warnings } JSON |
| import_content | 创建或更新文档(collection/slug/mode/markdown/extraFields) |
| get_content | 按集合键和 slug 查询单条文档 |
| list_content | 分页列出集合文档 |
| delete_content | 删除文档 |
| cleanup_media | 清理孤儿媒体 |
| import_markdown_file | 一步导入内容包文件(解析 frontmatter + 处理图片 + 写入文档) |
配置示例
{
"mcpServers": {
"payload": {
"command": "npx",
"args": ["payload-import-kit-mcp"],
"env": {
"PAYLOAD_BASE_URL": "https://my-site.com",
"PAYLOAD_API_KEY": "sk_live_xxxxxxxx"
}
}
}
}安装 skill(为 Claude Code 提供决策指南):
cp payload-import-kit-mcp.md ~/.claude/skills/8. 2.0 破坏性变更
升级到 2.0 需做以下调整:
8.1 v1 端点全部移除
v1 的所有端点(/api/import/collections、/api/import/upload、/api/import/article 等)已全部移除。新端点统一位于 /api/import/v2 之下,详见"协议速览"章节。
8.2 响应格式变化
所有响应改为统一包裹格式:
// v2(新)
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }v1 的直接返回数据结构不再支持。
8.3 删除的 API
importArticle()方法已删除,请使用client.importDocument()或client.importMarkdownFile()ImportArticleBody类型已删除- 之前
ImportArticleBody中的timeSaved/isFeatured/seo等站点私货字段已移除
8.4 prepareMarkdown 返回结构变化
// v1(旧):返回 string
const cleanMarkdown = await client.prepareMarkdown(raw)
// v2(新):返回 { markdown, warnings }
const { markdown: cleanMarkdown, warnings } = await client.prepareMarkdown(raw)8.5 路由文件从 6 个降为 1 个
站点需将原有的多个 route 文件(/api/import/collections/route.ts、/api/import/upload/route.ts 等)全部删除,替换为单一的 catch-all 路由:
app/api/import/[[...path]]/route.ts8.6 publishedAt 行为变更
v1 无论集合是否配置,都会在导入时无条件写入 publishedAt 字段。v2 改为只有显式配置 publishedAtField 的集合才会写入发布时间。若需保持原有行为,请为相关集合添加 publishedAtField: 'publishedAt' 配置。
9. FAQ
图片上传后显示 404
- 确认 Payload 的
upload配置正确,静态文件服务已启用 - 检查
/public/media/(或自定义 staticDir)目录有正确的读权限 - 若使用云存储适配器(如 S3),确认 bucket 为公开读或 CDN 已配置
importMarkdownFile和prepareMarkdown返回的warnings中会列出失败的图片,逐一排查
清理孤儿媒体时误删了有效图片
cleanup_media / cleanupMedia() 依赖 mediaRefs 字段追踪图片引用。若集合未配置 createSyncMediaRefsHook,该集合引用的图片不会被纳入引用列表,清理时会被视为孤儿而误删。请在所有含有图片引用的集合中配置该 hook 后再执行清理。
封面图说明:封面图不进入 mediaRefs;cleanupMedia 在扫描时将各集合的封面图字段单独计入引用,因此在用封面不会被清理。
浏览器环境下无法使用某些方法
uploadImageFromPath 和 importMarkdownFile 依赖 Node.js fs 模块,仅限 Node.js 环境(CLI、MCP、服务端脚本)。浏览器环境请使用 uploadImage(file) 传入 File 对象,以及手动解析 frontmatter 后调用 importDocument。
mediaRefs hook 是什么,必须配置吗?
createSyncMediaRefsHook 是 Payload beforeChange hook,在每次文档保存时自动扫描正文中的图片 URL,维护一个 mediaRefs 关系字段(存储被引用的 media ID 列表)。这个字段是 cleanupMedia() 进行孤儿检测的依据。若不需要孤儿清理功能,可以不配置此 hook。
