基于 Docusaurus 通过 GitHub REST API 加载私有仓库内容并部署到 Vercel
目录
方案概述
为什么要这样做?
使用场景:
- 🔒 博客内容存储在私有仓库,保护草稿和未发布内容
- 👥 多人协作写作,使用 GitHub 进行版本控制
- 🔄 内容与代码分离,便于管理
- 🚀 构建时从私有仓库拉取内容,生成静态站点
核心思路
私有内容仓库 → GitHub API → 构建脚本 → Docusaurus → 静态站点 → Vercel
关键点:
- 内容存储在私有 GitHub 仓库
- 使用 Personal Access Token (PAT) 访问私有仓库
- 构建时通过 GitHub REST API 下载内容
- Docusaurus 正常构建生成静态站点
- 部署到 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
- 访问 https://github.com/settings/tokens
- 点击 "Generate new token" → "Generate new token (classic)"
- 设置权限:
- ✅
repo(全部权限,访问私有仓库) - ✅
read:org(可选,如果仓库属于组织)
- ✅
- 点击 "Generate token"
- 重要:立即复制 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;
// 将私有仓库的图片路径转换为本地路径
// 例如:  → 
processedContent = processedContent.replace(
/!\[(.*?)\]\(images\/(.*?)\)/g,
''
);
// 确保 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 配置环境变量
- 访问 https://vercel.com
- 导入你的
blog-site仓库 - 在项目设置中,进入 "Settings" → "Environment Variables"
- 添加以下环境变量:
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 会:
- 安装依赖
- 运行
npm run fetch-content(通过 prebuild) - 从私有仓库拉取内容
- 构建 Docusaurus 站点
- 部署到 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
- 在 Vercel 项目设置中,进入 "Git" → "Deploy Hooks"
- 创建一个新的 Hook,例如
content-update - 复制生成的 URL,类似:
https://api.vercel.com/v1/integrations/deploy/xxx/yyy
在私有内容仓库设置 Webhook
- 进入私有仓库设置
- "Settings" → "Webhooks" → "Add webhook"
- Payload URL: 粘贴 Vercel Deploy Hook URL
- Content type:
application/json - Events: 选择
Just the push event - 保存
现在,每次推送到私有仓库时,会自动触发 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 公开访问
## 图片示例

内容受保护,只有授权用户可以访问源文件。
常见问题
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 泄露风险
安全建议:
- ✅ 永远不要将 Token 提交到公开仓库
- ✅ 使用
.env.local并加入.gitignore - ✅ 在 Vercel 中使用环境变量
- ✅ 定期更换 Token
- ✅ 使用最小权限原则
Q3: 构建时间过长
优化方案:
- 实现增量更新(只拉取变化的文件)
- 使用缓存机制
- 并行下载文件
- 考虑使用 GitHub GraphQL API(更高效)
Q4: 图片显示不正常
检查清单:
- 确认图片路径正确处理
- 验证图片已下载到
static/img/posts/ - 检查 Markdown 中的图片引用路径
- 运行
process-content.js处理路径
Q5: Vercel 构建失败
排查步骤:
# 1. 本地测试完整流程
npm run fetch-content
npm run build
# 2. 检查 Vercel 日志
# 3. 确认环境变量配置正确
# 4. 验证 Token 权限
Q6: 如何处理大量图片?
方案:
- 使用图床服务(如 Cloudinary, 七牛云)
- 图片存储在单独的公开仓库
- 使用 Git LFS
- 只下载必要的图片
安全最佳实践
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);
}
总结
✅ 方案优势
- 内容安全: 私有仓库保护源文件
- 版本控制: Git 管理内容历史
- 协作便利: 多人协作写作
- 自动部署: 推送即发布
- 灵活性高: 可自定义处理逻辑
- 成本低: 完全免费(GitHub + Vercel)
📊 适用场景
- 👥 团队技术博客
- 📚 知识库系统
- 🎓 在线文档
- 📝 个人博客(需要内容保护)
- 🏢 企业内部文档
🚀 工作流程
编写内容 → 推送到私有仓库 → 触发 Webhook → Vercel 构建 →
拉取私有内容 → 生成静态站点 → 部署上线
🔄 完整命令
# 开发阶段
npm run fetch-content # 拉取私有内容
npm start # 本地开发
# 部署阶段
git push # 推送到 GitHub
# Vercel 自动构建部署
现在你可以享受安全、自动化的博客发布流程了! 🎉
参考资源
- GitHub REST API: https://docs.github.com/rest
- Octokit.js: https://github.com/octokit/octokit.js
- Docusaurus: https://docusaurus.io/
- Vercel 环境变量: https://vercel.com/docs/concepts/projects/environment-variables
- GitHub Webhooks: https://docs.github.com/webhooks
最后更新:2025年11月16日