CMS 集成
将 VitePress 与 Headless CMS 系统集成,实现内容管理的现代化,让非技术人员也能参与文档编写。
CMS 方案对比
| 特性 | Strapi | Notion | Contentful | Sanity | TinaCMS |
|---|---|---|---|---|---|
| 类型 | 自托管 | 云服务 | 云服务 | 云服务 | Git-based |
| 免费 | ✅ 开源 | ✅ 免费版 | ⚠️ 有免费额度 | ⚠️ 有免费额度 | ✅ 开源 |
| API 类型 | REST/GraphQL | API | REST/GraphQL | GROQ | Git API |
| 实时预览 | ✅ | ❌ | ✅ | ✅ | ✅ |
| 学习曲线 | 中等 | 简单 | 中等 | 中等 | 中等 |
| 非技术友好 | ✅ | ✅✅ | ✅ | ✅ | ✅ |
| 自托管 | ✅ | ❌ | ❌ | ❌ | ✅ |
选择建议
| 场景 | 推荐 | 原因 |
|---|---|---|
| 开源项目 | TinaCMS | Git-based,与 VitePress 天然契合 |
| 团队协作 | Notion | 编辑体验好,非技术人员友好 |
| 企业项目 | Contentful | 功能全面,多语言支持强 |
| 自托管需求 | Strapi | 开源可控,插件生态丰富 |
| 开发者友好 | Sanity | 强类型查询 GROQ,实时协作 |
| 轻量级方案 | Notion | 无需部署,API 调用简单 |
CMS 数据流
mermaid
graph LR
A[CMS 内容编辑] --> B[API 获取数据]
B --> C[数据加载器]
C --> D[生成 Markdown]
D --> E[VitePress 构建]
E --> F[静态站点]Strapi
开源 Node.js Headless CMS,可自托管,支持 REST/GraphQL API。
安装 Strapi
bash
npx create-strapi-app@latest my-project --quickstart创建内容类型
在 Stravi 管理后台创建 Article 内容类型:
| 字段 | 类型 | 说明 |
|---|---|---|
title | String | 文章标题 |
slug | UID | URL 路径 |
content | Rich Text | 文章内容 |
description | Text | 文章描述 |
publishedAt | DateTime | 发布时间 |
tags | Relation | 标签关联 |
数据加载器
ts
// docs/.vitepress/theme/data/strapi.data.ts
import { defineLoader } from 'vitepress'
interface StrapiArticle {
id: number
title: string
slug: string
content: string
description: string
publishedAt: string
tags: { name: string; slug: string }[]
}
export default defineLoader({
watch: ['**/*.md'], // 监听文件变化
async load(): Promise<StrapiArticle[]> {
const STRAPI_URL = process.env.STRAPI_URL || 'http://localhost:1337'
const API_TOKEN = process.env.STRAPI_API_TOKEN || ''
const res = await fetch(`${STRAPI_URL}/api/articles?populate=tags&pagination[pageSize]=100`, {
headers: {
Authorization: `Bearer ${API_TOKEN}`
}
})
if (!res.ok) {
throw new Error(`Strapi API 请求失败: ${res.status}`)
}
const { data } = await res.json()
return data.map((item: any) => ({
id: item.id,
title: item.attributes.title,
slug: item.attributes.slug,
content: item.attributes.content,
description: item.attributes.description,
publishedAt: item.attributes.publishedAt,
tags: item.attributes.tags?.data?.map((t: any) => ({
name: t.attributes.name,
slug: t.attributes.slug
})) || []
}))
}
})生成 Markdown 页面
ts
// scripts/strapi-sync.ts
import { writeFileSync, mkdirSync } from 'fs'
import { join } from 'path'
interface Article {
title: string
slug: string
content: string
description: string
publishedAt: string
tags: { name: string }[]
}
async function syncStrapi() {
const res = await fetch('http://localhost:1337/api/articles?populate=tags')
const { data } = await res.json()
for (const item of data) {
const attrs = item.attributes
const frontmatter = `---
title: ${attrs.title}
description: ${attrs.description}
date: ${attrs.publishedAt}
tags:
${attrs.tags?.data?.map((t: any) => ` - ${t.attributes.name}`).join('\n') || ''}
---
`
const dir = join('docs', 'articles')
mkdirSync(dir, { recursive: true })
writeFileSync(join(dir, `${attrs.slug}.md`), frontmatter + attrs.content)
}
console.log(`同步完成:${data.length} 篇文章`)
}
syncStrapi().catch(console.error)Notion
将 Notion 作为 CMS,编辑体验极佳,非技术人员友好。
安装依赖
bash
npm install @notionhq/client -D数据加载器
ts
// docs/.vitepress/theme/data/notion.data.ts
import { defineLoader } from 'vitepress'
import { Client } from '@notionhq/client'
interface NotionPage {
id: string
title: string
slug: string
content: string
description: string
lastEditedTime: string
}
const notion = new Client({ auth: process.env.NOTION_API_KEY })
// Notion Block 转 Markdown
function blockToMarkdown(block: any): string {
const type = block.type
const text = block[type]?.rich_text
?.map((t: any) => t.plain_text)
.join('') || ''
switch (type) {
case 'heading_1': return `# ${text}`
case 'heading_2': return `## ${text}`
case 'heading_3': return `### ${text}`
case 'paragraph': return text
case 'bulleted_list_item': return `- ${text}`
case 'numbered_list_item': return `1. ${text}`
case 'code':
return `\`\`\`${block[code]?.language || ''}\n${block[code]?.rich_text?.[0]?.plain_text || ''}\n\`\`\``
case 'quote': return `> ${text}`
case 'divider': return '---'
case 'image':
return `![${block[image]?.caption?.[0]?.plain_text || ''}](${block[image]?.file?.url || block[image]?.external?.url || ''})`
default: return text
}
}
export default defineLoader({
async load(): Promise<NotionPage[]> {
const databaseId = process.env.NOTION_DATABASE_ID || ''
// 查询数据库
const response = await notion.databases.query({
database_id: databaseId,
filter: {
property: 'Status',
select: { equals: 'Published' }
}
})
const pages: NotionPage[] = []
for (const page of response.results) {
// 获取页面属性
const props = (page as any).properties
const title = props.Name?.title?.[0]?.plain_text || ''
const slug = props.Slug?.rich_text?.[0]?.plain_text || ''
const description = props.Description?.rich_text?.[0]?.plain_text || ''
// 获取页面内容
const blocks = await notion.blocks.children.list({
block_id: page.id
})
const content = blocks.results
.map(blockToMarkdown)
.join('\n\n')
pages.push({
id: page.id,
title,
slug,
content,
description,
lastEditedTime: (page as any).last_edited_time
})
}
return pages
}
})Notion 数据库设置
在 Notion 中创建数据库,添加以下属性:
| 属性名 | 类型 | 说明 |
|---|---|---|
| Name | Title | 文章标题 |
| Slug | Text | URL 路径 |
| Description | Text | 文章描述 |
| Status | Select | 发布状态(Draft/Published) |
| Tags | Multi-select | 文章标签 |
| Date | Date | 发布日期 |
Contentful
企业级 Headless CMS,云端托管,多语言支持强。
安装依赖
bash
npm install contentful -D数据加载器
ts
// docs/.vitepress/theme/data/contentful.data.ts
import { defineLoader } from 'vitepress'
import * as contentful from 'contentful'
interface ContentfulArticle {
id: string
title: string
slug: string
content: string
description: string
tags: string[]
publishedAt: string
}
export default defineLoader({
async load(): Promise<ContentfulArticle[]> {
const client = contentful.createClient({
space: process.env.CONTENTFUL_SPACE_ID || '',
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN || ''
})
const entries = await client.getEntries({
content_type: 'article',
order: '-sys.createdAt'
})
return entries.items.map((entry: any) => ({
id: entry.sys.id,
title: entry.fields.title,
slug: entry.fields.slug,
content: entry.fields.content || '',
description: entry.fields.description || '',
tags: entry.fields.tags || [],
publishedAt: entry.sys.createdAt
}))
}
})Contentful Rich Text 转 Markdown
ts
// docs/.vitepress/theme/utils/rich-text-to-md.ts
import { documentToHtmlString } from '@contentful/rich-text-html-renderer'
export function richTextToMarkdown(richText: any): string {
// 先转 HTML,再简化为 Markdown
const html = documentToHtmlString(richText)
return html
.replace(/<h1>/g, '# ')
.replace(/<\/h1>/g, '')
.replace(/<h2>/g, '## ')
.replace(/<\/h2>/g, '')
.replace(/<h3>/g, '### ')
.replace(/<\/h3>/g, '')
.replace(/<p>/g, '')
.replace(/<\/p>/g, '\n\n')
.replace(/<strong>/g, '**')
.replace(/<\/strong>/g, '**')
.replace(/<em>/g, '*')
.replace(/<\/em>/g, '*')
.replace(/<code>/g, '`')
.replace(/<\/code>/g, '`')
.replace(/<pre><code[^>]*>/g, '```\n')
.replace(/<\/code><\/pre>/g, '\n```')
.replace(/<li>/g, '- ')
.replace(/<\/li>/g, '\n')
.replace(/<[^>]+>/g, '')
.trim()
}Sanity
实时协作的 Headless CMS,GROQ 查询语言强大。
安装依赖
bash
npm install @sanity/client -D数据加载器
ts
// docs/.vitepress/theme/data/sanity.data.ts
import { defineLoader } from 'vitepress'
import { createClient } from '@sanity/client'
interface SanityArticle {
id: string
title: string
slug: string
content: string
description: string
tags: string[]
publishedAt: string
}
const client = createClient({
projectId: process.env.SANITY_PROJECT_ID || '',
dataset: process.env.SANITY_DATASET || 'production',
apiVersion: '2024-01-01',
useCdn: true
})
// Portable Text 转 Markdown
function portableTextToMarkdown(blocks: any[]): string {
return blocks
.map((block: any) => {
if (block._type !== 'block' || !block.children) return ''
const text = block.children
.map((child: any) => {
let t = child.text
if (child.marks?.includes('strong')) t = `**${t}**`
if (child.marks?.includes('em')) t = `*${t}*`
if (child.marks?.includes('code')) t = `\`${t}\``
return t
})
.join('')
switch (block.style) {
case 'h1': return `# ${text}`
case 'h2': return `## ${text}`
case 'h3': return `### ${text}`
case 'blockquote': return `> ${text}`
default: return text
}
})
.filter(Boolean)
.join('\n\n')
}
export default defineLoader({
async load(): Promise<SanityArticle[]> {
const query = `*[_type == "article" && published == true] | order(publishedAt desc) {
_id,
title,
"slug": slug.current,
body,
description,
tags,
publishedAt
}`
const results = await client.fetch(query)
return results.map((item: any) => ({
id: item._id,
title: item.title,
slug: item.slug,
content: portableTextToMarkdown(item.body || []),
description: item.description || '',
tags: item.tags || [],
publishedAt: item.publishedAt
}))
}
})TinaCMS
Git-based CMS,与 VitePress 深度集成,支持实时预览。
初始化 TinaCMS
bash
npx @tinacms/cli@latest init配置 Schema
ts
// tina/config.ts
import { defineConfig } from 'tinacms'
export default defineConfig({
clientId: process.env.TINA_CLIENT_ID || '',
branch: 'main',
token: process.env_TINA_TOKEN || '',
build: {
outputFolder: 'admin',
publicFolder: 'docs/public'
},
media: {
tina: {
mediaRoot: 'images',
publicFolder: 'public'
}
},
schema: {
collections: [
{
name: 'doc',
label: '文档',
path: 'docs',
format: 'md',
fields: [
{
type: 'string',
name: 'title',
label: '标题',
isTitle: true,
required: true
},
{
type: 'string',
name: 'description',
label: '描述'
},
{
type: 'datetime',
name: 'date',
label: '日期'
},
{
type: 'string',
name: 'tags',
label: '标签',
list: true,
ui: {
component: 'tags'
}
},
{
type: 'rich-text',
name: 'body',
label: '正文',
isBody: true,
templates: [
{
name: 'callout',
label: '提示框',
fields: [
{ name: 'type', type: 'string', label: '类型', options: ['tip', 'warning', 'danger'] },
{ name: 'content', type: 'string', label: '内容', ui: { component: 'textarea' } }
]
},
{
name: 'codeGroup',
label: '代码组',
fields: [
{ name: 'label', type: 'string', label: '标签' },
{ name: 'code', type: 'string', label: '代码', ui: { component: 'textarea' } }
]
}
]
}
]
}
]
}
})集成到 VitePress
ts
// .vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme'
import TinaEditLink from './components/TinaEditLink.vue'
export default {
extends: DefaultTheme,
enhanceApp({ app }) {
app.component('TinaEditLink', TinaEditLink)
}
}vue
<!-- .vitepress/theme/components/TinaEditLink.vue -->
<script setup lang="ts">
import { useData } from 'vitepress'
const { page } = useData()
const editUrl = `/admin/#/~/doc/${page.value.relativePath}`
</script>
<template>
<a :href="editUrl" target="_blank" class="tina-edit-link">
✏️ 在 TinaCMS 中编辑
</a>
</template>
<style scoped>
.tina-edit-link {
display: inline-block;
padding: 4px 12px;
font-size: 13px;
color: var(--vp-c-brand);
border: 1px solid var(--vp-c-brand);
border-radius: 6px;
text-decoration: none;
transition: all 0.2s;
}
.tina-edit-link:hover {
background: var(--vp-c-brand);
color: white;
}
</style>CI/CD 自动同步
在构建流程中自动从 CMS 同步内容。
GitHub Actions 示例
yaml
# .github/workflows/sync-cms.yml
name: Sync CMS Content
on:
schedule:
- cron: '0 */6 * * *' # 每 6 小时同步一次
workflow_dispatch:
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
# 从 CMS 同步内容
- name: Sync from Strapi
if: env.STRAPI_URL
run: npx tsx scripts/strapi-sync.ts
env:
STRAPI_URL: ${{ secrets.STRAPI_URL }}
STRAPI_API_TOKEN: ${{ secrets.STRAPI_API_TOKEN }}
# 如果有变更,提交并触发构建
- name: Commit changes
run: |
git config user.name "CMS Bot"
git config user.email "bot@example.com"
git add docs/
git diff --staged --quiet || git commit -m "chore: sync CMS content"
git push常见问题
CMS 数据更新不及时?
- 检查缓存:数据加载器可能有缓存,清除后重试
- 触发构建:手动触发 CI/CD 流水线重新构建
- Webhook:配置 CMS 发送 Webhook 在内容更新时自动构建
构建时 CMS API 不可用?
- 降级策略:API 不可用时使用本地缓存数据
- 超时设置:为 API 请求设置合理的超时时间
- 重试机制:添加请求重试逻辑
ts
// 带重试的数据加载器
async function fetchWithRetry(url: string, retries = 3): Promise<Response> {
for (let i = 0; i < retries; i++) {
try {
const res = await fetch(url, { signal: AbortSignal.timeout(10000) })
if (res.ok) return res
} catch (e) {
if (i === retries - 1) throw e
await new Promise(r => setTimeout(r, 1000 * (i + 1)))
}
}
throw new Error(`请求失败: ${url}`)
}如何处理富文本内容?
不同 CMS 的富文本格式不同,需要转换:
| CMS | 富文本格式 | 转换方案 |
|---|---|---|
| Strapi | Markdown / Rich Text | Markdown 直接使用,Rich Text 需转换 |
| Notion | Block 格式 | 逐 Block 转换为 Markdown |
| Contentful | Rich Text JSON | 使用 @contentful/rich-text-html-renderer |
| Sanity | Portable Text | 自定义转换函数 |
| TinaCMS | Markdown | 直接使用 |
进阶配置
Strapi、Notion、Contentful 的完整数据加载器代码、构建脚本、CI/CD 自动同步等:
- CMS 集成详解 — Strapi、Notion、Contentful 的深度集成教程