Skip to main content

基于 Docusaurus 通过 GitHub REST API 加载私有仓库内容并部署到 Vercel

目录


方案概述

为什么要这样做?

使用场景:

  • 🔒 博客内容存储在私有仓库,保护草稿和未发布内容
  • 👥 多人协作写作,使用 GitHub 进行版本控制
  • 🔄 内容与代码分离,便于管理
  • 🚀 构建时从私有仓库拉取内容,生成静态站点

核心思路

私有内容仓库 → GitHub API → 构建脚本 → Docusaurus → 静态站点 → Vercel

关键点:

  1. 内容存储在私有 GitHub 仓库
  2. 使用 Personal Access Token (PAT) 访问私有仓库
  3. 构建时通过 GitHub REST API 下载内容
  4. Docusaurus 正常构建生成静态站点
  5. 部署到 Vercel(公开访问)

架构设计

仓库结构

方案一:双仓库模式(推荐)

├── blog-site (公开仓库)           # Docusaurus 站点代码
│ ├── src/
│ ├── static/
│ ├── scripts/
│ │ └── fetch-content.js # 从私有仓库拉取内容
│ ├── docusaurus.config.js
│ └── package.json

└── blog-content (私有仓库) # 博客内容
├── posts/
│ ├── 2025-11-16-post1.md
│ └── 2025-11-17-post2.md
└── images/

方案二:单仓库模式

blog (私有仓库)
├── content/ # 私有内容
│ └── posts/
├── src/ # 公开代码
├── scripts/
└── docusaurus.config.js

我们采用方案一(双仓库),更灵活且安全。


前置准备

1. 创建 GitHub Personal Access Token

  1. 访问 https://github.com/settings/tokens
  2. 点击 "Generate new token""Generate new token (classic)"
  3. 设置权限:
    • repo (全部权限,访问私有仓库)
    • read:org (可选,如果仓库属于组织)
  4. 点击 "Generate token"
  5. 重要:立即复制 Token,只显示一次!

2. 创建两个 GitHub 仓库

仓库 1:blog-site(公开)

  • 存放 Docusaurus 站点代码
  • 可以公开访问

仓库 2:blog-content(私有)

  • 存放博客文章和图片
  • 设为私有仓库

3. 本地环境

# 检查 Node.js
node -v # >= 18.0

# 检查 npm
npm -v

# 检查 Git
git --version

步骤一:创建并配置项目

1.1 创建 Docusaurus 项目

# 创建项目
npx create-docusaurus@latest blog-site classic

cd blog-site

1.2 安装依赖

# 安装必要的包
npm install @octokit/rest dotenv

# 安装开发依赖
npm install --save-dev cross-env

1.3 创建环境变量文件

创建 .env.local(不要提交到 Git):

# GitHub API 配置
GITHUB_TOKEN=ghp_your_personal_access_token_here
GITHUB_OWNER=your-github-username
GITHUB_CONTENT_REPO=blog-content
GITHUB_CONTENT_BRANCH=main

创建 .env.example(提交到 Git 作为模板):

# GitHub API 配置示例
GITHUB_TOKEN=your_token_here
GITHUB_OWNER=your_github_username
GITHUB_CONTENT_REPO=blog-content
GITHUB_CONTENT_BRANCH=main

1.4 更新 .gitignore

确保 .gitignore 包含:

# 环境变量
.env
.env.local
.env.*.local

# 从私有仓库拉取的内容
/blog
/content

# Docusaurus
.docusaurus
.cache-loader
build

# 依赖
node_modules/

步骤二:配置 GitHub API

2.1 创建 API 工具类

创建 scripts/github-api.js

// scripts/github-api.js
const { Octokit } = require('@octokit/rest');
const fs = require('fs-extra');
const path = require('path');

class GitHubContentFetcher {
constructor(config) {
this.octokit = new Octokit({
auth: config.token,
});
this.owner = config.owner;
this.repo = config.repo;
this.branch = config.branch || 'main';
}

/**
* 获取仓库目录内容
*/
async getDirectoryContents(dirPath = '') {
try {
const response = await this.octokit.repos.getContent({
owner: this.owner,
repo: this.repo,
path: dirPath,
ref: this.branch,
});
return response.data;
} catch (error) {
console.error(`获取目录失败 ${dirPath}:`, error.message);
return [];
}
}

/**
* 获取文件内容
*/
async getFileContent(filePath) {
try {
const response = await this.octokit.repos.getContent({
owner: this.owner,
repo: this.repo,
path: filePath,
ref: this.branch,
});

// 解码 Base64 内容
const content = Buffer.from(response.data.content, 'base64').toString('utf-8');
return {
content,
sha: response.data.sha,
path: response.data.path,
};
} catch (error) {
console.error(`获取文件失败 ${filePath}:`, error.message);
return null;
}
}

/**
* 递归下载目录
*/
async downloadDirectory(remotePath, localPath) {
console.log(`📁 下载目录: ${remotePath}${localPath}`);

const items = await this.getDirectoryContents(remotePath);

// 确保本地目录存在
await fs.ensureDir(localPath);

for (const item of items) {
if (item.type === 'file') {
await this.downloadFile(item.path, path.join(localPath, item.name));
} else if (item.type === 'dir') {
await this.downloadDirectory(
item.path,
path.join(localPath, item.name)
);
}
}
}

/**
* 下载单个文件
*/
async downloadFile(remotePath, localPath) {
console.log(`📄 下载文件: ${remotePath}`);

const fileData = await this.getFileContent(remotePath);

if (fileData) {
await fs.ensureDir(path.dirname(localPath));
await fs.writeFile(localPath, fileData.content, 'utf-8');
console.log(`✅ 已保存: ${localPath}`);
}
}

/**
* 下载二进制文件(图片等)
*/
async downloadBinaryFile(remotePath, localPath) {
try {
console.log(`🖼️ 下载二进制文件: ${remotePath}`);

const response = await this.octokit.repos.getContent({
owner: this.owner,
repo: this.repo,
path: remotePath,
ref: this.branch,
mediaType: {
format: 'raw',
},
});

await fs.ensureDir(path.dirname(localPath));
await fs.writeFile(localPath, response.data);
console.log(`✅ 已保存: ${localPath}`);
} catch (error) {
console.error(`下载二进制文件失败 ${remotePath}:`, error.message);
}
}

/**
* 获取仓库所有文件列表
*/
async getTree(treeSha = null) {
try {
// 如果没有提供 treeSha,先获取最新的 commit
if (!treeSha) {
const { data: ref } = await this.octokit.git.getRef({
owner: this.owner,
repo: this.repo,
ref: `heads/${this.branch}`,
});

const { data: commit } = await this.octokit.git.getCommit({
owner: this.owner,
repo: this.repo,
commit_sha: ref.object.sha,
});

treeSha = commit.tree.sha;
}

const { data } = await this.octokit.git.getTree({
owner: this.owner,
repo: this.repo,
tree_sha: treeSha,
recursive: 'true',
});

return data.tree;
} catch (error) {
console.error('获取仓库树失败:', error.message);
return [];
}
}
}

module.exports = GitHubContentFetcher;

步骤三:实现内容加载器

3.1 创建内容拉取脚本

创建 scripts/fetch-content.js

// scripts/fetch-content.js
require('dotenv').config({ path: '.env.local' });
const path = require('path');
const fs = require('fs-extra');
const GitHubContentFetcher = require('./github-api');

async function fetchBlogContent() {
console.log('🚀 开始从私有仓库拉取博客内容...\n');

// 验证环境变量
const requiredEnvVars = ['GITHUB_TOKEN', 'GITHUB_OWNER', 'GITHUB_CONTENT_REPO'];
const missingVars = requiredEnvVars.filter(varName => !process.env[varName]);

if (missingVars.length > 0) {
console.error('❌ 缺少必要的环境变量:', missingVars.join(', '));
console.error('请创建 .env.local 文件并配置这些变量');
process.exit(1);
}

// 初始化 GitHub API 客户端
const fetcher = new GitHubContentFetcher({
token: process.env.GITHUB_TOKEN,
owner: process.env.GITHUB_OWNER,
repo: process.env.GITHUB_CONTENT_REPO,
branch: process.env.GITHUB_CONTENT_BRANCH || 'main',
});

try {
// 清理旧内容
const blogDir = path.join(__dirname, '../blog');
const staticImgDir = path.join(__dirname, '../static/img/posts');

console.log('🧹 清理旧内容...');
await fs.remove(blogDir);
await fs.remove(staticImgDir);
console.log('✅ 清理完成\n');

// 下载博客文章
console.log('📝 下载博客文章...');
await fetcher.downloadDirectory('posts', blogDir);
console.log('✅ 博客文章下载完成\n');

// 下载图片资源
console.log('🖼️ 下载图片资源...');
const images = await fetcher.getDirectoryContents('images');

if (images.length > 0) {
await fs.ensureDir(staticImgDir);

for (const image of images) {
if (image.type === 'file') {
const localPath = path.join(staticImgDir, image.name);
await fetcher.downloadBinaryFile(image.path, localPath);
}
}
console.log('✅ 图片下载完成\n');
} else {
console.log('ℹ️ 没有找到图片资源\n');
}

// 统计信息
const files = await fs.readdir(blogDir);
const mdFiles = files.filter(f => f.endsWith('.md') || f.endsWith('.mdx'));

console.log('📊 内容拉取统计:');
console.log(` - 博客文章: ${mdFiles.length}`);
console.log(` - 图片资源: ${images.length}`);
console.log('\n✨ 内容拉取完成!');

} catch (error) {
console.error('❌ 拉取内容时出错:', error.message);
console.error(error.stack);
process.exit(1);
}
}

// 执行
fetchBlogContent();

3.2 创建内容处理脚本(可选)

创建 scripts/process-content.js

// scripts/process-content.js
const fs = require('fs-extra');
const path = require('path');
const matter = require('gray-matter');

async function processMarkdownFiles() {
console.log('🔄 处理 Markdown 文件...\n');

const blogDir = path.join(__dirname, '../blog');
const files = await fs.readdir(blogDir);

for (const file of files) {
if (!file.endsWith('.md') && !file.endsWith('.mdx')) {
continue;
}

const filePath = path.join(blogDir, file);
const content = await fs.readFile(filePath, 'utf-8');
const { data, content: markdownContent } = matter(content);

// 处理图片路径
let processedContent = markdownContent;

// 将私有仓库的图片路径转换为本地路径
// 例如: ![alt](images/pic.png) → ![alt](/img/posts/pic.png)
processedContent = processedContent.replace(
/!\[(.*?)\]\(images\/(.*?)\)/g,
'![$1](/img/posts/$2)'
);

// 确保 Front Matter 完整
if (!data.date) {
// 从文件名提取日期 (如: 2025-11-16-title.md)
const dateMatch = file.match(/^(\d{4}-\d{2}-\d{2})/);
if (dateMatch) {
data.date = dateMatch[1];
}
}

// 重新组装文件
const newContent = matter.stringify(processedContent, data);
await fs.writeFile(filePath, newContent, 'utf-8');

console.log(`✅ 已处理: ${file}`);
}

console.log('\n✨ Markdown 文件处理完成!');
}

// 安装依赖: npm install gray-matter
processMarkdownFiles();

安装额外依赖:

npm install gray-matter

3.3 更新 package.json

添加脚本命令:

{
"scripts": {
"start": "docusaurus start",
"build": "npm run fetch-content && docusaurus build",
"build:local": "docusaurus build",
"serve": "docusaurus serve",
"fetch-content": "node scripts/fetch-content.js",
"process-content": "node scripts/process-content.js",
"prebuild": "npm run fetch-content"
}
}

步骤四:构建时加载内容

4.1 配置 Docusaurus

编辑 docusaurus.config.js

// @ts-check
const {themes} = require('prism-react-renderer');

/** @type {import('@docusaurus/types').Config} */
const config = {
title: '我的技术博客',
tagline: '内容来自私有仓库',
favicon: 'img/favicon.ico',

url: 'https://your-blog.vercel.app',
baseUrl: '/',

organizationName: 'your-github-username',
projectName: 'blog-site',

onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',

i18n: {
defaultLocale: 'zh-CN',
locales: ['zh-CN'],
},

presets: [
[
'classic',
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: false, // 禁用文档功能
blog: {
routeBasePath: '/', // 博客作为首页
showReadingTime: true,
blogTitle: '技术博客',
blogDescription: '我的个人技术博客,内容托管在私有仓库',
postsPerPage: 10,
blogSidebarTitle: '最近文章',
blogSidebarCount: 'ALL',
feedOptions: {
type: 'all',
copyright: `Copyright © ${new Date().getFullYear()} My Blog`,
},
},
theme: {
customCss: './src/css/custom.css',
},
}),
],
],

themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({
navbar: {
title: '我的博客',
logo: {
alt: 'Logo',
src: 'img/logo.svg',
},
items: [
{to: '/', label: '首页', position: 'left'},
{to: '/archive', label: '归档', position: 'left'},
{to: '/tags', label: '标签', position: 'left'},
{
href: 'https://github.com/your-username/blog-site',
label: 'GitHub',
position: 'right',
},
],
},
footer: {
style: 'dark',
links: [
{
title: '博客',
items: [
{label: '最新文章', to: '/'},
{label: '标签', to: '/tags'},
],
},
{
title: '社交',
items: [
{label: 'GitHub', href: 'https://github.com/your-username'},
{label: 'Twitter', href: 'https://twitter.com/your-twitter'},
],
},
],
copyright: `Copyright © ${new Date().getFullYear()} 我的博客. Built with Docusaurus.`,
},
prism: {
theme: themes.github,
darkTheme: themes.dracula,
additionalLanguages: ['bash', 'json', 'javascript', 'typescript'],
},
}),
};

module.exports = config;

4.2 本地测试

# 拉取私有仓库内容
npm run fetch-content

# 启动开发服务器
npm start

# 或者直接构建
npm run build

步骤五:部署到 Vercel

5.1 推送代码到 GitHub

# 初始化 Git(如果还没有)
git init

# 添加文件
git add .

# 提交(注意:不要提交 .env.local)
git commit -m "Initial commit: Blog site with private content fetching"

# 添加远程仓库
git remote add origin https://github.com/your-username/blog-site.git

# 推送
git branch -M main
git push -u origin main

5.2 在 Vercel 配置环境变量

  1. 访问 https://vercel.com
  2. 导入你的 blog-site 仓库
  3. 在项目设置中,进入 "Settings""Environment Variables"
  4. 添加以下环境变量:
GITHUB_TOKEN = ghp_your_token_here
GITHUB_OWNER = your-github-username
GITHUB_CONTENT_REPO = blog-content
GITHUB_CONTENT_BRANCH = main

重要: 确保 GITHUB_TOKEN 设置为 Secret

5.3 配置构建设置

Vercel 会自动检测 Docusaurus,但请确认:

  • Framework Preset: Docusaurus
  • Build Command: npm run build
  • Output Directory: build
  • Install Command: npm install

5.4 部署

点击 "Deploy",Vercel 会:

  1. 安装依赖
  2. 运行 npm run fetch-content(通过 prebuild)
  3. 从私有仓库拉取内容
  4. 构建 Docusaurus 站点
  5. 部署到 CDN

进阶优化

6.1 添加缓存机制

创建 scripts/cache-manager.js

// scripts/cache-manager.js
const fs = require('fs-extra');
const path = require('path');
const crypto = require('crypto');

class CacheManager {
constructor(cacheDir = '.cache') {
this.cacheDir = path.join(process.cwd(), cacheDir);
this.manifestPath = path.join(this.cacheDir, 'manifest.json');
}

async ensureCacheDir() {
await fs.ensureDir(this.cacheDir);
}

async getManifest() {
try {
return await fs.readJson(this.manifestPath);
} catch {
return { files: {}, lastFetch: null };
}
}

async saveManifest(manifest) {
await this.ensureCacheDir();
await fs.writeJson(this.manifestPath, manifest, { spaces: 2 });
}

hashContent(content) {
return crypto.createHash('md5').update(content).digest('hex');
}

async shouldFetchFile(filePath, remoteSha) {
const manifest = await this.getManifest();
const cached = manifest.files[filePath];

return !cached || cached.sha !== remoteSha;
}

async updateFileCache(filePath, sha) {
const manifest = await this.getManifest();
manifest.files[filePath] = { sha, fetchedAt: new Date().toISOString() };
manifest.lastFetch = new Date().toISOString();
await this.saveManifest(manifest);
}
}

module.exports = CacheManager;

6.2 增量更新

修改 scripts/fetch-content.js 支持增量更新:

// 在 fetchBlogContent 函数中添加
const CacheManager = require('./cache-manager');
const cache = new CacheManager();

// 检查文件是否需要更新
async function fetchFileIfNeeded(fetcher, remotePath, localPath, sha) {
if (await cache.shouldFetchFile(remotePath, sha)) {
console.log(`📥 更新: ${remotePath}`);
await fetcher.downloadFile(remotePath, localPath);
await cache.updateFileCache(remotePath, sha);
} else {
console.log(`✓ 跳过(已缓存): ${remotePath}`);
}
}

6.3 Webhook 自动触发部署

方案 A:使用 Vercel Deploy Hooks

  1. 在 Vercel 项目设置中,进入 "Git""Deploy Hooks"
  2. 创建一个新的 Hook,例如 content-update
  3. 复制生成的 URL,类似:https://api.vercel.com/v1/integrations/deploy/xxx/yyy

在私有内容仓库设置 Webhook

  1. 进入私有仓库设置
  2. "Settings""Webhooks""Add webhook"
  3. Payload URL: 粘贴 Vercel Deploy Hook URL
  4. Content type: application/json
  5. Events: 选择 Just the push event
  6. 保存

现在,每次推送到私有仓库时,会自动触发 Vercel 重新部署!

6.4 添加内容验证

创建 scripts/validate-content.js

// scripts/validate-content.js
const fs = require('fs-extra');
const path = require('path');
const matter = require('gray-matter');

async function validateContent() {
console.log('🔍 验证博客内容...\n');

const blogDir = path.join(__dirname, '../blog');
const files = await fs.readdir(blogDir);
const errors = [];

for (const file of files) {
if (!file.endsWith('.md') && !file.endsWith('.mdx')) continue;

const filePath = path.join(blogDir, file);
const content = await fs.readFile(filePath, 'utf-8');

try {
const { data } = matter(content);

// 验证必需字段
if (!data.title) {
errors.push(`${file}: 缺少 title`);
}
if (!data.slug && !file.match(/^\d{4}-\d{2}-\d{2}/)) {
errors.push(`${file}: 缺少 slug 或日期格式文件名`);
}
if (!data.authors) {
errors.push(`${file}: 缺少 authors`);
}

} catch (err) {
errors.push(`${file}: Front Matter 解析错误 - ${err.message}`);
}
}

if (errors.length > 0) {
console.error('❌ 发现以下问题:\n');
errors.forEach(err => console.error(` - ${err}`));
console.error('\n请修复后重新构建。');
process.exit(1);
}

console.log('✅ 内容验证通过!');
}

validateContent();

更新 package.json

{
"scripts": {
"prebuild": "npm run fetch-content && npm run validate-content",
"validate-content": "node scripts/validate-content.js"
}
}

6.5 图片优化

创建 scripts/optimize-images.js

// scripts/optimize-images.js
const sharp = require('sharp');
const fs = require('fs-extra');
const path = require('path');
const glob = require('glob');

async function optimizeImages() {
console.log('🖼️ 优化图片...\n');

const imageDir = path.join(__dirname, '../static/img/posts');
const images = glob.sync(`${imageDir}/**/*.{jpg,jpeg,png}`);

for (const imagePath of images) {
const outputPath = imagePath.replace(/\.(jpg|jpeg|png)$/, '.webp');

await sharp(imagePath)
.webp({ quality: 80 })
.toFile(outputPath);

console.log(`✅ 优化: ${path.basename(imagePath)}${path.basename(outputPath)}`);
}

console.log('\n✨ 图片优化完成!');
}

// 安装依赖: npm install sharp glob
optimizeImages();

安装依赖:

npm install sharp glob

实战示例

私有仓库结构(blog-content)

blog-content/
├── posts/
│ ├── 2025-11-16-first-post.md
│ ├── 2025-11-17-second-post.md
│ └── 2025-11-18-third-post/
│ ├── index.md
│ └── featured.png
├── images/
│ ├── avatar.png
│ ├── banner.jpg
│ └── logo.svg
└── README.md

示例文章

posts/2025-11-16-first-post.md

---
slug: first-post
title: 从私有仓库发布的第一篇文章
authors: your_name
tags: [博客, GitHub, 私有仓库]
image: images/banner.jpg
---

# 欢迎

这是一篇从私有 GitHub 仓库中拉取的文章!

<!--truncate-->

## 特性

✅ 内容存储在私有仓库
✅ 通过 GitHub API 自动拉取
✅ 构建时生成静态站点
✅ 部署到 Vercel 公开访问

## 图片示例

![Banner](images/banner.jpg)

内容受保护,只有授权用户可以访问源文件。

常见问题

Q1: GitHub API 速率限制

问题: API 请求过多导致速率限制。

解决方案:

// 检查速率限制
async function checkRateLimit(octokit) {
const { data } = await octokit.rateLimit.get();
console.log(`剩余请求: ${data.rate.remaining}/${data.rate.limit}`);
console.log(`重置时间: ${new Date(data.rate.reset * 1000)}`);
}

使用 Personal Access Token 可获得更高限额(5000次/小时)。

Q2: Token 泄露风险

安全建议:

  1. ✅ 永远不要将 Token 提交到公开仓库
  2. ✅ 使用 .env.local 并加入 .gitignore
  3. ✅ 在 Vercel 中使用环境变量
  4. ✅ 定期更换 Token
  5. ✅ 使用最小权限原则

Q3: 构建时间过长

优化方案:

  1. 实现增量更新(只拉取变化的文件)
  2. 使用缓存机制
  3. 并行下载文件
  4. 考虑使用 GitHub GraphQL API(更高效)

Q4: 图片显示不正常

检查清单:

  1. 确认图片路径正确处理
  2. 验证图片已下载到 static/img/posts/
  3. 检查 Markdown 中的图片引用路径
  4. 运行 process-content.js 处理路径

Q5: Vercel 构建失败

排查步骤:

# 1. 本地测试完整流程
npm run fetch-content
npm run build

# 2. 检查 Vercel 日志
# 3. 确认环境变量配置正确
# 4. 验证 Token 权限

Q6: 如何处理大量图片?

方案:

  1. 使用图床服务(如 Cloudinary, 七牛云)
  2. 图片存储在单独的公开仓库
  3. 使用 Git LFS
  4. 只下载必要的图片

安全最佳实践

1. Token 管理

# ✅ 正确:使用环境变量
GITHUB_TOKEN=ghp_xxx npm run fetch-content

# ❌ 错误:硬编码在代码中
const token = 'ghp_xxx'; // 永远不要这样做!

2. Token 权限最小化

创建 Token 时只授予必要权限:

  • repo:status - 访问提交状态
  • repo:public_repo - 访问公开仓库
  • repo - 访问私有仓库(仅在需要时)

3. 定期审计

# 定期检查 Token 使用情况
# GitHub Settings → Developer settings → Personal access tokens

4. 使用 GitHub Apps(进阶)

对于生产环境,考虑使用 GitHub Apps 替代 Personal Access Token,更安全且权限控制更精细。


进阶扩展

1. 支持多语言内容

// 从不同路径拉取不同语言的内容
await fetcher.downloadDirectory('posts/zh-CN', 'i18n/zh-CN/blog');
await fetcher.downloadDirectory('posts/en', 'i18n/en/blog');

2. 内容审核工作流

# .github/workflows/review.yml (在私有仓库中)
name: Content Review
on:
pull_request:
paths:
- 'posts/**'
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Validate Markdown
run: |
npm install markdownlint-cli
npx markdownlint 'posts/**/*.md'

3. 内容搜索索引

// 构建时生成搜索索引
const posts = await loadAllPosts();
const searchIndex = posts.map(post => ({
title: post.title,
slug: post.slug,
excerpt: post.excerpt,
tags: post.tags,
}));

await fs.writeJson('static/search-index.json', searchIndex);

4. 草稿模式

// 在 Front Matter 中标记草稿
---
title: 草稿文章
draft: true
---

// 构建时过滤草稿
if (process.env.NODE_ENV === 'production' && data.draft) {
console.log(`跳过草稿: ${file}`);
await fs.remove(filePath);
}

总结

✅ 方案优势

  1. 内容安全: 私有仓库保护源文件
  2. 版本控制: Git 管理内容历史
  3. 协作便利: 多人协作写作
  4. 自动部署: 推送即发布
  5. 灵活性高: 可自定义处理逻辑
  6. 成本低: 完全免费(GitHub + Vercel)

📊 适用场景

  • 👥 团队技术博客
  • 📚 知识库系统
  • 🎓 在线文档
  • 📝 个人博客(需要内容保护)
  • 🏢 企业内部文档

🚀 工作流程

编写内容 → 推送到私有仓库 → 触发 Webhook → Vercel 构建 → 
拉取私有内容 → 生成静态站点 → 部署上线

🔄 完整命令

# 开发阶段
npm run fetch-content # 拉取私有内容
npm start # 本地开发

# 部署阶段
git push # 推送到 GitHub
# Vercel 自动构建部署

现在你可以享受安全、自动化的博客发布流程了! 🎉


参考资源


最后更新:2025年11月16日