用 GitHub Actions 和 Vercel Deploy Hook 实现博客定时发布
静态博客有一个很实际的问题:文章写在 Git 仓库里,只有重新构建并部署,新的页面才会出现。
如果每天只发一两篇文章,为此引入数据库、管理后台和独立 API,复杂度可能远大于收益。更轻量的方案是继续保留 MDX 静态构建,只把“什么时候触发构建”自动化。
这篇文章实现的流程是:
提前提交未来文章
↓
GitHub Actions 到点执行
↓
调用 Vercel Deploy Hook
↓
Vercel 重新构建 master
↓
Next.js 只生成已经到发布时间的文章最终效果是:文章仍然保存在 content/posts,不需要数据库,也不需要每天手动登录 Vercel。
为什么只部署一次不能自动发布
Next.js 在执行静态构建时,会读取当时存在的 MDX 文件并生成 HTML。构建结束以后,已经部署的静态文件不会继续运行日期判断。
假设今天提前准备了两篇文章:
# 教程 10
date: "2026-07-17T09:00:00+08:00"
draft: false# 教程 11
date: "2026-07-17T18:00:00+08:00"
draft: false今天构建时,两篇文章的发布时间都没到,因此不会生成到生产站点。到了明天,静态站点也不会自己变化,必须重新执行一次构建。
所以,定时发布由两部分组成:
- Next.js 根据
date判断文章是否应该公开。 - GitHub Actions 在发布时间附近触发新的 Vercel 构建。
定义文章的发布状态
这个项目使用 draft 和 date 共同决定文章状态:
draft: true
└── 草稿,不参与自动发布
draft: false + date 晚于当前时间
└── 预约文章,暂时隐藏
draft: false + date 已到
└── 正式文章,在下一次构建时发布预约文章的时间建议使用完整 ISO 8601 格式,并明确写出中国标准时间偏移:
date: "2026-07-17T09:00:00+08:00"
draft: false不要只写没有时区的时间:
date: "2026-07-17 09:00"本地电脑和 Vercel 构建服务器可能使用不同的系统时区。显式写出 +08:00,可以避免文章提前或延后八小时。
在 Next.js 中判断文章是否到期
先统一解析文章时间。项目中的 lib/post-format.ts 提供了 parsePostDate 和 isPostPublished:
export function parsePostDate(date: string) {
const parsed = new Date(date)
if (Number.isNaN(parsed.getTime())) {
throw new Error(`文章发布时间格式不正确:${date}`)
}
return parsed
}
export function isPostPublished(
post: { date: string; draft?: boolean },
now = new Date(),
) {
return !post.draft && parsePostDate(post.date).getTime() <= now.getTime()
}实际项目还兼容了原有的 YYYY-MM-DD 和 YYYY-MM-DD HH:mm 格式,并把这些旧格式按北京时间解析。
然后在 lib/posts.ts 里统一过滤:
return posts
.filter((post) => options.includeDrafts || isPostPublished(post))
.sort(
(a, b) =>
parsePostDate(b.date).getTime() - parsePostDate(a.date).getTime(),
)首页、归档、分类、标签、RSS 和 sitemap 都通过 getAllPosts() 获取数据,所以只需要修改这一处,它们就会一起隐藏未来文章。
防止通过 URL 提前访问
只从文章列表中隐藏还不够。别人可能提前知道 slug,直接访问:
/blog/remotion-skia-graphics-effects文章详情页需要再次检查发布状态:
const post = getPostBySlug(slug)
if (!isPostPublished(post.meta)) {
notFound()
}generateMetadata 也要使用相同判断,避免未发布文章提前输出 title、description 和 Open Graph 数据。
这样,发布时间之前:
- 首页看不到文章。
- 归档、分类和标签页看不到文章。
- sitemap 和 RSS 不包含文章。
- 直接访问文章地址返回 404。
- 页面不会输出文章 metadata。
创建 Vercel Deploy Hook
Deploy Hook 是 Vercel 提供的一条特殊 URL。向它发送一次 HTTP 请求,Vercel 就会重新部署指定分支。
进入 Vercel 项目:
Settings
→ Git
→ Deploy Hooks创建一个 Hook:
名称:scheduled-posts
分支:master创建完成后会得到类似地址:
https://api.vercel.com/v1/integrations/deploy/xxxxxxxx不要把这个地址直接写进 Git 仓库。拿到地址的人可以触发你的 Vercel 部署,应该把它当成密钥保存。
把 Deploy Hook 保存为 GitHub Secret
进入 GitHub 仓库:
Settings
→ Secrets and variables
→ Actions
→ New repository secret添加:
Name: VERCEL_DEPLOY_HOOK_URL
Secret: Vercel 生成的完整 Deploy Hook 地址GitHub Actions 可以读取这个 Secret,但它不会显示在仓库代码中。
编写 GitHub Actions 定时任务
在项目中创建:
.github/workflows/publish-scheduled-posts.yml完整配置如下:
name: 定时发布文章
on:
schedule:
# GitHub Actions 使用 UTC:01:00 / 10:00 对应北京时间 09:00 / 18:00。
- cron: '0 1,10 * * *'
workflow_dispatch:
permissions:
contents: read
jobs:
deploy:
name: 触发 Vercel 构建
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: 调用 Vercel Deploy Hook
env:
VERCEL_DEPLOY_HOOK_URL: ${{ secrets.VERCEL_DEPLOY_HOOK_URL }}
run: |
set -euo pipefail
if [ -z "${VERCEL_DEPLOY_HOOK_URL:-}" ]; then
echo "缺少 GitHub Secret:VERCEL_DEPLOY_HOOK_URL"
exit 1
fi
response=$(curl --fail-with-body --silent --show-error \
--request POST \
"$VERCEL_DEPLOY_HOOK_URL")
echo "已触发 Vercel 构建:$response"这个文件不会修改 MDX,也不会自动提交 Git。它只是一个定时闹钟,到点后调用 Vercel Deploy Hook。
Cron 时间为什么是 01:00 和 10:00
GitHub Actions 的 Cron 使用 UTC,而中国标准时间是 UTC+8:
UTC 01:00 + 8 小时 = 北京时间 09:00
UTC 10:00 + 8 小时 = 北京时间 18:00因此:
- cron: '0 1,10 * * *'表示每天执行两次。
如果只想每天北京时间 09:00 构建一次,可以写成:
- cron: '0 1 * * *'GitHub 官方说明,定时任务可能因为平台负载产生延迟,所以不要把它用于必须精确到秒的任务。博客晚几分钟上线通常可以接受。
workflow_dispatch 有什么作用
配置:
workflow_dispatch:会在 GitHub Actions 页面增加一个手动运行按钮:
Actions
→ 定时发布文章
→ Run workflow第一次配置完成后,应该手动运行一次,并检查:
- GitHub Actions 是否执行成功。
- 是否正确读取
VERCEL_DEPLOY_HOOK_URL。 - Vercel 是否产生新的 Deployment。
- 构建是否使用
master分支。 - 未来文章是否仍然保持隐藏。
一次完整的发布时间线
假设提前提交两篇文章:
教程 10:2026-07-17 09:00
教程 11:2026-07-17 18:00执行过程如下:
7 月 16 日提交并部署
├── 教程 10:未到时间,不生成
└── 教程 11:未到时间,不生成
7 月 17 日 09:00 自动构建
├── 教程 10:时间已到,生成并发布
└── 教程 11:时间未到,继续隐藏
7 月 17 日 18:00 自动构建
├── 教程 10:保持发布
└── 教程 11:时间已到,生成并发布Vercel 只有在新构建成功后才会切换生产版本。如果构建失败,原来的线上版本仍然保留,可以修复后手动重新运行工作流。
常见问题
GitHub Actions 页面找不到工作流
确认 .github/workflows/publish-scheduled-posts.yml 已经推送到 GitHub 默认分支。只保存在本地或只推送到 Gitee,都不会运行 GitHub Actions。
工作流提示缺少 Secret
确认 GitHub Secret 名称完全一致:
VERCEL_DEPLOY_HOOK_URL注意区分大小写,也不要把 Secret 添加到错误的仓库或 Environment。
Actions 成功,但 Vercel 没有构建
检查 Deploy Hook 是否仍然有效,以及创建 Hook 时选择的分支是否是 master。如果 Hook 泄露或被删除,重新生成并更新 GitHub Secret。
Vercel 构建了,但文章没有出现
依次检查:
draft是否仍然是true。date是否包含正确的+08:00。- Actions 的执行时间是否已经晚于文章发布时间。
- Vercel 构建使用的是否是包含文章文件的最新提交。
- 文章文件是否位于
content/posts,扩展名是否为.mdx。
为什么文章实际晚了几分钟
GitHub Cron 可能延迟执行,Vercel 构建本身也需要时间。设置 09:00 发布,实际在 09:03 或 09:08 上线属于正常现象。
这个方案适合什么项目
它适合:
- 内容保存在 Git 仓库的静态博客。
- 每天发布数量不多。
- 可以提前准备文章。
- 不需要在线编辑后台。
- 希望保留静态页面的速度和 SEO 优势。
如果需要多人在线编辑、即时发布、定时撤回、复杂权限和大量内容更新,再考虑数据库和 CMS 会更合适。
对于个人技术博客,“MDX + GitHub Actions + Vercel Deploy Hook”通常已经足够。它没有改变原来的静态架构,只是把原本需要手动完成的构建按钮,交给了一个云端定时任务。