比特之外

用 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

今天构建时,两篇文章的发布时间都没到,因此不会生成到生产站点。到了明天,静态站点也不会自己变化,必须重新执行一次构建。

所以,定时发布由两部分组成:

  1. Next.js 根据 date 判断文章是否应该公开。
  2. GitHub Actions 在发布时间附近触发新的 Vercel 构建。

定义文章的发布状态

这个项目使用 draftdate 共同决定文章状态:

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 提供了 parsePostDateisPostPublished

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-DDYYYY-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

第一次配置完成后,应该手动运行一次,并检查:

  1. GitHub Actions 是否执行成功。
  2. 是否正确读取 VERCEL_DEPLOY_HOOK_URL
  3. Vercel 是否产生新的 Deployment。
  4. 构建是否使用 master 分支。
  5. 未来文章是否仍然保持隐藏。

一次完整的发布时间线

假设提前提交两篇文章:

教程 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”通常已经足够。它没有改变原来的静态架构,只是把原本需要手动完成的构建按钮,交给了一个云端定时任务。

参考资料