Skip to content

Headless CMS 集成

通过将 VitePress 与 Headless CMS 集成,可以让非技术人员通过可视化界面管理文档内容,同时保持 VitePress 的静态站点生成优势。

为什么集成 CMS

场景说明
团队协作编辑人员无需懂代码即可发布和修改文档
内容审核支持内容发布工作流和审批流程
多渠道分发同一内容可分发到多个平台
动态更新结合 CI/CD 实现 CMS 内容变更自动触发重新构建

方案对比

CMS类型优点缺点适合场景
Strapi开源自托管免费开源、自托管、高度可定制需要自己维护服务器需要完全控制数据的团队
ContentfulSaaS功能完善、SDK 丰富、部署简单有内容条目限制、付费中大型企业
SanitySaaS实时协作、强类型、免费额度大方学习曲线稍高内容频繁更新的项目
NotionSaaS免费、界面友好、灵活API 限流严格个人博客、小型文档
Markdown 文件本地文件零成本、Git 版本控制无协作界面开发者主导的项目

方案一:Strapi 集成

1. 安装和初始化 Strapi

bash
# 创建 Strapi 项目
npx create-strapi-app@latest my-cms --quickstart

cd my-cms
npm run develop

2. 创建 Content Type

在 Strapi 管理面板(http://localhost:1337/admin)中创建 Content Type:

Doc 模型:

字段类型说明
titleText文档标题
slugUIDURL 路径标识
contentRichText / Markdown文档内容
categoryRelation所属分类
tagsRelation标签
orderInteger排序权重
publishedAtDateTime发布时间

3. 设置 API 权限

Settings > Roles > Public 中为以下 API 设置 findfindOne 权限:

  • Doc
  • Category
  • Tag

4. 安装 Markdown 插件(可选)

如果需要使用 Markdown 而非富文本编辑器:

bash
cd my-cms
npm install strapi-plugin-markdown

5. 创建数据加载脚本

ts
// docs/.vitepress/cms-loaders/strapi.ts
interface StrapiDoc {
  id: number
  attributes: {
    title: string
    slug: string
    content: string
    order: number
    publishedAt: string
    category?: {
      data: {
        attributes: { name: string; slug: string }
      }
    }
    tags?: {
      data: Array<{
        attributes: { name: string }
      }>
    }
  }
}

interface DocFrontmatter {
  title: string
  category?: string
  tags?: string[]
  date?: string
}

const STRAPI_URL = process.env.STRAPI_URL || 'http://localhost:1337'
const STRAPI_TOKEN = process.env.STRAPI_TOKEN || ''

export async function loadStrapiDocs(
  category?: string
): Promise<Array<{ frontmatter: DocFrontmatter; content: string; slug: string }>> {
  const params = new URLSearchParams({
    'populate': 'category,tags',
    'sort': 'order:asc',
    'pagination[pageSize]': '100'
  })

  if (category) {
    params.set('filters[category][slug][$eq]', category)
  }

  const res = await fetch(
    `${STRAPI_URL}/api/docs?${params}`,
    {
      headers: STRAPI_TOKEN ? { Authorization: `Bearer ${STRAPI_TOKEN}` } : {}
    }
  )

  if (!res.ok) {
    throw new Error(`Strapi API error: ${res.status} ${res.statusText}`)
  }

  const json = await res.json()

  return json.data.map((doc: StrapiDoc) => ({
    slug: doc.attributes.slug,
    frontmatter: {
      title: doc.attributes.title,
      category: doc.attributes.category?.data?.attributes?.name,
      tags: doc.attributes.tags?.data?.map(t => t.attributes.name),
      date: doc.attributes.publishedAt
    },
    content: doc.attributes.content || ''
  }))
}

6. 构建时生成 Markdown

ts
// scripts/build-from-strapi.ts
import fs from 'fs'
import path from 'path'
import { loadStrapiDocs } from '../docs/.vitepress/cms-loaders/strapi'

async function build() {
  try {
    const docs = await loadStrapiDocs()

    for (const doc of docs) {
      // 生成 frontmatter
      const frontmatter = Object.entries(doc.frontmatter)
        .filter(([, v]) => v !== undefined)
        .map(([k, v]) => `${k}: ${typeof v === 'string' ? `"${v}"` : JSON.stringify(v)}`)
        .join('\n')

      const markdown = `---\n${frontmatter}\n---\n\n${doc.content}\n`

      const filePath = path.join('docs', 'cms', `${doc.slug}.md`)
      fs.mkdirSync(path.dirname(filePath), { recursive: true })
      fs.writeFileSync(filePath, markdown, 'utf-8')
    }

    console.log(`✅ Generated ${docs.length} docs from Strapi`)
  } catch (error) {
    console.error('❌ Failed to fetch docs:', error)
    process.exit(1)
  }
}

build()

7. 更新 package.json

json
{
  "scripts": {
    "cms:pull": "tsx scripts/build-from-strapi.ts",
    "build": "npm run cms:pull && vitepress build docs"
  }
}

方案二:Notion 集成

Notion 作为 CMS 具有零成本、界面友好的优势,适合个人或小团队。

1. 创建 Notion 数据库

在 Notion 中创建数据库,包含以下列:

列名类型说明
TitleTitle文档标题
SlugTextURL 路径
StatusSelectPublished / Draft
CategorySelect文档分类
TagsMulti-select标签
OrderNumber排序

2. 获取 Notion API Key

  1. 访问 Notion Integrations
  2. 创建新 Integration
  3. 复制 Internal Integration Token(以 ntn_ 开头)
  4. 在 Notion 数据库页面,点击 ... > Connections > Add connections,选择你创建的 Integration

3. 安装依赖

bash
npm install -D @notionhq/client dotenv

4. 创建 Notion 加载器

ts
// docs/.vitepress/cms-loaders/notion.ts
import { Client } from '@notionhq/client'

const notion = new Client({ auth: process.env.NOTION_TOKEN })
const DATABASE_ID = process.env.NOTION_DATABASE_ID || ''

interface NotionDoc {
  title: string
  slug: string
  status: string
  category?: string
  tags: string[]
  content: string
}

function extractPlainText(block: any): string {
  if (block.type === 'paragraph') {
    return block.paragraph.rich_text.map((t: any) => t.plain_text).join('')
  }
  if (block.type === 'heading_1') {
    return `# ${block.heading_1.rich_text.map((t: any) => t.plain_text).join('')}`
  }
  if (block.type === 'heading_2') {
    return `## ${block.heading_2.rich_text.map((t: any) => t.plain_text).join('')}`
  }
  if (block.type === 'heading_3') {
    return `### ${block.heading_3.rich_text.map((t: any) => t.plain_text).join('')}`
  }
  if (block.type === 'code') {
    return '```' + block.code.language + '\n' + block.code.rich_text.map((t: any) => t.plain_text).join('') + '\n```'
  }
  if (block.type === 'bulleted_list_item') {
    return '- ' + block.bulleted_list_item.rich_text.map((t: any) => t.plain_text).join('')
  }
  if (block.type === 'numbered_list_item') {
    return '1. ' + block.numbered_list_item.rich_text.map((t: any) => t.plain_text).join('')
  }
  if (block.type === 'quote') {
    return '> ' + block.quote.rich_text.map((t: any) => t.plain_text).join('')
  }
  return ''
}

export async function loadNotionDocs(status = 'Published'): Promise<NotionDoc[]> {
  const response = await notion.databases.query({
    database_id: DATABASE_ID,
    filter: {
      property: 'Status',
      select: { equals: status }
    },
    sorts: [{ property: 'Order', direction: 'ascending' }]
  })

  const docs: NotionDoc[] = []

  for (const page of response.results) {
    const props = (page as any).properties

    // 获取页面内容
    const blocks = await notion.blocks.children.list({
      block_id: page.id
    })

    const content = blocks.results
      .map(extractPlainText)
      .filter(Boolean)
      .join('\n\n')

    docs.push({
      title: props.Title.title.map((t: any) => t.plain_text).join(''),
      slug: props.Slug.rich_text.map((t: any) => t.plain_text).join(''),
      status: props.Status.select?.name || '',
      category: props.Category?.select?.name,
      tags: props.Tags?.multi_select?.map((t: any) => t.name) || [],
      content
    })
  }

  return docs
}

5. 构建脚本

ts
// scripts/build-from-notion.ts
import fs from 'fs'
import path from 'path'
import { loadNotionDocs } from '../docs/.vitepress/cms-loaders/notion'

async function build() {
  const docs = await loadNotionDocs('Published')

  for (const doc of docs) {
    const fm: Record<string, any> = { title: doc.title }
    if (doc.category) fm.category = doc.category
    if (doc.tags.length) fm.tags = doc.tags

    const frontmatter = Object.entries(fm)
      .map(([k, v]) => `${k}: ${typeof v === 'string' ? `"${v}"` : JSON.stringify(v)}`)
      .join('\n')

    const markdown = `---\n${frontmatter}\n---\n\n${doc.content}\n`
    const filePath = path.join('docs', 'notion', `${doc.slug}.md`)

    fs.mkdirSync(path.dirname(filePath), { recursive: recursive: true })
    fs.writeFileSync(filePath, markdown, 'utf-8')
  }

  console.log(`✅ Generated ${docs.length} docs from Notion`)
}

build()

方案三:Contentful 集成

1. 安装 SDK

bash
npm install -D contentful

2. 创建加载器

ts
// docs/.vitepress/cms-loaders/contentful.ts
import { createClient, type Entry } from 'contentful'

const client = createClient({
  space: process.env.CTF_SPACE_ID!,
  accessToken: process.env.CTF_DELIVERY_TOKEN!,
  environment: process.env.CTF_ENVIRONMENT || 'master'
})

interface ContentfulDoc {
  title: string
  slug: string
  body: string
  category?: string
  tags?: string[]
}

export async function loadContentfulDocs(): Promise<ContentfulDoc[]> {
  const entries = await client.getEntries<ContentfulDoc>({
    content_type: 'doc',
    order: 'fields.order'
  })

  return entries.items.map((entry: Entry<ContentfulDoc>) => ({
    title: entry.fields.title,
    slug: entry.fields.slug,
    body: entry.fields.body,
    category: entry.fields.category,
    tags: entry.fields.tags
  }))
}

CI/CD 自动同步

GitHub Actions 工作流

当 CMS 内容更新时自动触发构建:

yaml
# .github/workflows/cms-sync.yml
name: CMS Sync & Build

on:
  # 定时同步(每小时检查一次)
  schedule:
    - cron: '0 * * * *'
  # 或手动触发
  workflow_dispatch:

jobs:
  sync-and-build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20

      - run: npm ci

      # 拉取 CMS 内容生成 Markdown
      - run: npm run cms:pull
        env:
          NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
          NOTION_DATABASE_ID: ${{ secrets.NOTION_DATABASE_ID }}

      # 检查是否有内容变化
      - name: Check for changes
        id: changes
        run: |
          git diff --exit-code docs/cms/ || echo "has_changes=true" >> $GITHUB_OUTPUT

      # 有变化则构建部署
      - name: Build
        if: steps.changes.outputs.has_changes == 'true'
        run: npm run build

      - name: Deploy
        if: steps.changes.outputs.has_changes == 'true'
        run: npx wrangler pages deploy docs/.vitepress/dist

Webhook 触发(Strapi)

在 Strapi 中配置 Webhook,内容发布时自动触发构建:

yaml
# .github/workflows/strapi-webhook.yml
name: Build from Strapi Webhook

on:
  repository_dispatch:
    types: [strapi-update]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm run cms:pull
        env:
          STRAPI_URL: ${{ secrets.STRAPI_URL }}
          STRAPI_TOKEN: ${{ secrets.STRAPI_TOKEN }}
      - run: npm run build
      - run: npm run deploy

方案选择建议

mermaid
graph TD
    A[选择 CMS 方案] --> B{团队规模}
    B -->|个人/小团队| C{预算}
    B -->|企业级| D[Contentful / Sanity]
    C -->|零成本| E[Notion / Markdown 文件]
    C -->|需要自托管| F[Strapi]
    C -->|需要高级功能| D

推荐方案

  • 个人博客:使用 Notion,零成本且界面友好
  • 团队文档:使用 Strapi,开源免费且功能强大
  • 企业级项目:使用 Contentful 或 Sanity,专业级的 CMS 功能

注意事项

注意点说明
构建时间CMS 拉取数据会增加构建时间,建议使用增量构建
API 限流注意 CMS 的 API 调用频率限制
内容备份即使使用 CMS,建议在 Git 中保存生成的 Markdown 文件
离线支持本地开发时需要能够离线工作,建议提供本地 fallback 数据

下一步

贡献者

加载中...

想要成为贡献者?

在 CNB 上参与贡献