npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

payload-import-kit

v2.0.0

Published

Markdown 图文内容导入标准 + PayloadCMS/Next.js 参考实现(spec + server/client/CLI/MCP)

Readme

payload-import-kit

双标准 + 参考实现:Markdown 图文内容导入标准 + PayloadCMS/Next.js 参考实现。

  • spec/content-format.md内容包格式标准 v1:语言无关,定义一篇内容如何用 .md 文件表达
  • spec/http-protocol.mdHTTP 协议标准 v2:定义传输端 API,任何语言的服务端只要实现本协议,即可复用本仓库的 client SDK、CLI 与 MCP server
  • 设计文档见 GitHub 仓库 docs/ 目录(未随 npm 包发布)

1. 快速开始(站点侧)

1.1 环境变量

# .env.local
IMPORT_API_KEY=sk_live_xxxxxxxxxxxxxxxx

1.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.slugconfig.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……

![截图](./images/screenshot.png)

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 cleanup

CLI 退出码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 中出现的 ![alt](url) 视为图片引用,导入器不处理。


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'createSyncMediaRefsHookcleanupMedia 共用此配置,自定义字段名只需在此声明一处 | | supportsCoverImage | boolean | 是否支持封面图 | | coverImageField | string | 封面图字段名,默认 'coverImage' | | tagField | string | 标签字段名,默认 'tags' | | tagFormat | 'object' \| 'plain' | 标签格式:object{tag:string}[]plainstring[] | | 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.ts

8.6 publishedAt 行为变更

v1 无论集合是否配置,都会在导入时无条件写入 publishedAt 字段。v2 改为只有显式配置 publishedAtField 的集合才会写入发布时间。若需保持原有行为,请为相关集合添加 publishedAtField: 'publishedAt' 配置。


9. FAQ

图片上传后显示 404

  1. 确认 Payload 的 upload 配置正确,静态文件服务已启用
  2. 检查 /public/media/(或自定义 staticDir)目录有正确的读权限
  3. 若使用云存储适配器(如 S3),确认 bucket 为公开读或 CDN 已配置
  4. importMarkdownFileprepareMarkdown 返回的 warnings 中会列出失败的图片,逐一排查

清理孤儿媒体时误删了有效图片

cleanup_media / cleanupMedia() 依赖 mediaRefs 字段追踪图片引用。若集合未配置 createSyncMediaRefsHook,该集合引用的图片不会被纳入引用列表,清理时会被视为孤儿而误删。请在所有含有图片引用的集合中配置该 hook 后再执行清理。

封面图说明:封面图不进入 mediaRefscleanupMedia 在扫描时将各集合的封面图字段单独计入引用,因此在用封面不会被清理。

浏览器环境下无法使用某些方法

uploadImageFromPathimportMarkdownFile 依赖 Node.js fs 模块,仅限 Node.js 环境(CLI、MCP、服务端脚本)。浏览器环境请使用 uploadImage(file) 传入 File 对象,以及手动解析 frontmatter 后调用 importDocument

mediaRefs hook 是什么,必须配置吗?

createSyncMediaRefsHook 是 Payload beforeChange hook,在每次文档保存时自动扫描正文中的图片 URL,维护一个 mediaRefs 关系字段(存储被引用的 media ID 列表)。这个字段是 cleanupMedia() 进行孤儿检测的依据。若不需要孤儿清理功能,可以不配置此 hook。