上一篇搭好了「数据自动进来」的管道,这篇解决另一半:内容自动出去。目标是让发布动作从「登录后台、复制粘贴、点部署」变成在 Notion 里把一个状态从「草稿」改成「已发布」——剩下的全部自动完成。
核心思想:状态字段驱动一切
不要用「有没有这个文件」来表达发布意图,那会把草稿、定时稿、下线稿混为一谈。正确做法是在内容源里设置明确的状态机:
| 状态 | 含义 | 系统行为 |
|---|---|---|
| Draft 草稿 | 写作中 | 不同步,不进任何构建 |
| Scheduled 定时 | 已写完,等待发布时间 | 到点后自动转为发布 |
| Published 已发布 | 正式内容 | 同步入库 → 触发构建 → 上线 |
| Archived 归档 | 下线但保留 | 从站点移除,数据留存 |
再配两个字段:pubDate(决定定时发布时刻)、slug(决定 URL,一经发布永不再改,否则旧链接全断)。
三种内容源,按团队阶段选
| 内容源 | 适合阶段 | 自动发布方式 |
|---|---|---|
| GitHub 仓库里的 Markdown | 单人、技术型 | push 即发布(最简单,本站当前模式) |
| Notion Database | 想在编辑器里写作、手机也能改 | Actions 定时/ Webhook 拉取转换 |
| Google Sheet / Airtable | 结构化榜单、比价表类内容 | CSV/API 拉取后生成页面 |
本站一期采用第一种(Markdown + Cloudflare Pages 自动部署),二期升级到第二种。下面把两种触发机制都讲清楚。
触发方式一:Deploy Hook(最简单,先跑通它)
Cloudflare Pages 提供一个「构建钩子」URL:收到任意一次对这个 URL 的 POST 请求,就会重新拉取代码并构建发布。在 Pages 项目 → Settings → Builds & deployments → Deploy Hooks 里创建,得到形如:
https://api.cloudflare.com/client/v4/pages/webhooks/deploy_pages/xxxxxxxx
把它当密钥对待——任何拿到这个 URL 的人都能触发你的构建,只存在 GitHub Secrets
里,命名为 CF_DEPLOY_HOOK。
最小的手动触发工作流 .github/workflows/publish.yml:
name: trigger-publish
on:
workflow_dispatch: # Actions 页面点按钮运行,先验证链路
repository_dispatch: # 供外部 Webhook 调用(见下文)
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: 触发 Cloudflare Pages 构建
run: |
curl -fsS -X POST "${{ secrets.CF_DEPLOY_HOOK }}"
在 Actions 页面手动点一次,回到 Cloudflare 看到构建开始,说明「按钮 → 上线」链路已通。
触发方式二:Notion 状态变更自动触发
Notion 原生不支持出站 Webhook,但有两个成熟绕法:
- 定时轮询(推荐先做):Actions 每 15 分钟查一次 Notion Database,发现
status = Published但站点里还没有的文章,就拉取、转换、提交,然后触发 Deploy Hook。延迟最多 15 分钟,对内容站完全够用,且最稳定; - Notion 自动化平台转发:用 Make/Zapier 监听数据库更新,再向 GitHub 发送
repository_dispatch事件,近乎实时,但要依赖第三方平台的免费额度。
轮询工作流骨架:
name: notion-sync
on:
schedule:
- cron: '*/15 * * * *'
workflow_dispatch:
jobs:
sync:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install -r requirements.txt
- name: 拉取 Notion 已发布文章
env:
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
NOTION_DATABASE_ID: ${{ secrets.NOTION_DATABASE_ID }}
run: python scripts/notion_sync.py
- name: 有变更才触发部署
run: |
if [[ -n $(git status --porcelain) ]]; then
git config user.name "opc-bot"
git config user.email "[email protected]"
git add src/content/articles
git commit -m "chore: sync published posts from notion"
git push
curl -fsS -X POST "${{ secrets.CF_DEPLOY_HOOK }}"
else
echo "没有新内容,跳过部署"
fi
notion_sync.py 的职责单一:查询 Published
页面 → 调 Blocks API 取正文 → 转成本站约定的 Markdown + frontmatter(字段对齐
src/content.config.ts 的 schema)→ 图片下载本地化 → 写入
src/content/articles/<模块>/<slug>.md。状态变为 Archived 的页面则删除对应文件。
这正是本站二期的落地方案:模板层完全不用改,只多一个「Notion → Markdown」的转换脚本。
定时发布:让 Scheduled 到点自动上线
两个实现层次:
- 简单版:Notion 里写好
pubDate,状态直接给 Published,轮询脚本只同步pubDate <= 当前时间的文章——提前一周写好的内容到点自然出现; - 精细版:状态保持 Scheduled,脚本每次运行时把「已到点」的条目先通过 Notion API 改成 Published 再同步,后台状态与线上完全一致。
多渠道分发:一次发布,多处更新
内容站上 Cloudflare 之后,同一套状态机还能驱动其他渠道,思路都是订阅「发布成功」这个事件:
- RSS / sitemap:本站已在构建时自动生成,第三方聚合器自动来抓;
- 社媒/社群通知:构建成功后增加一步,调用 Telegram Bot / 邮件 API 推送新文章链接;
- 搜索引擎收录:调用 IndexNow 或 Google Indexing API 主动推送新 URL,缩短收录等待;
- 跨站同步:再发一个 Webhook 给 Make,由它改写发布到其他平台。
原则:每个渠道都做成流水线里一个「失败不阻塞主流程」的步骤,通知挂了不应该导致文章发不出去。
告警、回滚与安全
- 失败要响:工作流任意一步失败(Notion 限流、转换报错、部署钩子非 2xx),让 GitHub 发失败邮件;关键流程可加一步发 Telegram 告警;
- 内容回滚靠 Git:每次同步都是一次 commit,线上出问题在 Cloudflare
选历史部署一键回退,或
git revert对应提交后重新部署; - 幂等设计:脚本可安全重跑——已存在且无变化的文章跳过,靠 slug 去重,避免重复生成;
- 密钥三处隔离:
NOTION_TOKEN、NOTION_DATABASE_ID、CF_DEPLOY_HOOK全部只放 GitHub Secrets / Cloudflare 环境变量,不进仓库、不写进日志; - 频率与礼貌:轮询间隔不低于 10–15 分钟,并处理 429 限流的退避重试。
实施顺序建议
- 先在 Cloudflare 建 Deploy Hook,用
workflow_dispatch手动触发跑通一次; - 把现有写作流程迁到 Notion Database,字段对齐本站 schema;
- 写轮询脚本,先用
workflow_dispatch手动同步验证 Markdown 产物; - 打开 15 分钟定时任务,验证 Draft/Published 状态门控;
- 最后加定时发布、失败告警和多渠道通知。
至此,「自动采集 → 数据看板 → 内容自动发布」就拼成了一个一人公司的最小自动化闭环,全部跑在免费额度上。