零依赖书签导航站上线实录:GitHub Actions + Cloudflare Pages 双站自动部署

一个纯静态书签导航站,以单个 CSV 为唯一数据源,零 npm 依赖,推一次 Git 同时更新「公开站」和「私人管理站」。本文按需求 → 材料 → 环境 → 逻辑 → 步骤 → 验证 → 坑点 → 注意事项的层次,完整记录从建仓到双域名上线全过程。

一个纯静态书签导航站,以单个 CSV 为唯一数据源,零 npm 依赖,推一次 Git 同时更新「公开站」和「私人管理站」。本文按 需求 → 材料 → 环境 → 逻辑 → 步骤 → 验证 → 坑点 → 注意事项 的层次,完整记录从建仓到双域名上线全过程。

📖 阅读指引

颜色标记:

  • 红色 —— 必须替换的占位符(照抄会失败)
  • 橙色 —— 需要你决策的选项(二选一 / 按需选择)
  • 绿色 —— 可选配置(不设也能上线)
  • 蓝色 —— 踩坑警告(别人已经替你踩过了)

占位符对照表(全文统一,替换时对照本表):

占位符含义示例
<你的用户名>GitHub 用户名octocat
<仓库名>GitHub 私有仓库名my-nav-private
<公开项目名>Cloudflare Pages 公开站项目名my-nav
<管理项目名>Cloudflare Pages 管理站项目名my-nav-admin
<公开域名>公开站自定义域名nav.example.com
<管理域名>管理站自定义域名admin.example.com
<API Token>Cloudflare API 令牌(只显示一次,妥善保存)
<Account ID>Cloudflare 账号 ID32 位十六进制串
<管理密码>管理面板登录密码建议 20+ 位强随机串
<你的邮箱>用于 Access 登录验证you@example.com

一、构建需求(先想清楚要什么)

需求项目标本方案的回答
数据源只维护一份表格,不做数据库单个 bookmarks.csv
依赖越少越好,避免供应链风险零 npm 依赖,构建工具全手写
双站公开站给所有人,管理站只给自己公开站 + 管理站,同一仓库两条产物
安全公开站绝对不能出现管理入口构建期物理剥离管理代码
自动化改完数据推一次,一切自动完成GitHub Actions 推送即双站部署
成本免费或接近免费Cloudflare Pages + Zero Trust 免费额度

二、准备材料清单(开工前逐项打勾)

账号类:

  • GitHub 账号(托管代码 + 运行 CI)
  • Cloudflare 账号(Pages 托管 + DNS + Access)
  • 一个自定义域名(可选,DNS 托管到 Cloudflare;没有就用 *.pages.dev 默认域名)

凭证类(第 3 步会详细创建):

  • Cloudflare API Token(模板:Cloudflare Pages — Edit)
  • Cloudflare Account ID
  • 管理面板强随机密码(20+ 位,可用密码生成器)

文件类(项目里应已就位):

文件作用
data/bookmarks.csv唯一数据源(UTF-8 带 BOM)
scripts/build/build-sites.jsCSV → 压缩 JSON
scripts/build/sync-public.js生成公开副本(剥离管理代码)
scripts/build/build-admin-dist.js生成管理副本(全量数据+管理面板)
.github/workflows/deploy.yml自动部署流水线
_headersCSP / HSTS 等安全响应头

三、环境要求

组件版本要求检查命令
Node.js≥ 18node -v
Git任意现代版本git --version
本地系统Windows / macOS / Linux 均可—

CI 侧使用 ubuntu-latest + Node 20,无需本地安装 wrangler(工作流内自动拉取)。


四、构建逻辑(动手前先看懂原理)

4.1 数据流:四层解耦

1
2
3
4
① 数据层   data/bookmarks.csv    ← 唯一手改文件(分组,名称,网址,描述,状态)
② 构建层   scripts/build/        ← CSV → 压缩 JSON(去重/校验/私有过滤)
③ 展示层   index.html + assets/  ← 分类卡片 + 多引擎搜索 + 亮暗主题
④ 部署层   GitHub Actions        ← 推送 main 即双站部署

4.2 双站分流:一次构建,两种产物

1
2
3
                     ┌─→ sync-public.js  ──→ public-site/ ──→ 公开站(无管理代码)
bookmarks.csv → 构建 ┤
                     └─→ build-admin-dist.js → admin-dist/ ──→ 管理站(全量+管理面板)

⚠️ 为什么是"物理剥离"而不是"密码隐藏"? 公开站的构建脚本会直接删除 admin.js 与管理 HTML 区块——不是 CSS 隐藏、不是前端判断,是产物里彻底不存在。公开站因此永远不可能出现管理入口,这是零信任思维的落地。

4.3 CI 流水线逻辑(7 步)

1
2
3
4
5
6
7
8
push main
  ├─ ① 校验 + 构建 data.js
  ├─ ② 生成公开副本 public-site/
  ├─ ③ 确保公开站 Pages 项目存在(不存在则创建)
  ├─ ④ 部署 public-site/ → 公开站
  ├─ ⑤ 构建管理副本 admin-dist/        ┐ 未设 NAV_ADMIN_PASS
  ├─ ⑥ 确保管理站 Pages 项目存在        │ 则 ⑤⑥⑦ 自动跳过
  └─ ⑦ 部署 admin-dist/ → 管理站       ┘ (公开站照常部署)

五、部署步骤

5.1 本地验证

1
2
3
4
5
cd <项目路径>

npm run validate    # 完整性自检
npm test            # 单元测试
npm run serve       # 本地预览 http://localhost:3366

替换:<项目路径> 为你的实际目录。

5.2 创建私有仓库并推送

GitHub 新建私有仓库(不勾选任何初始化文件),然后:

1
2
git remote add origin git@github.com:<你的用户名>/<仓库名>.git
git push -u origin main

为什么选私有仓? 书签数据暴露你的数字足迹(常用网盘、邮箱、内网面板域名)。若坚持公开仓库,务必依赖构建期的 NAV_PRIVATE_GROUPS 私有分组过滤。

5.3 创建 Cloudflare 凭证

  1. Cloudflare 控制台 → 右上角头像 → My Profile → API Tokens → Create Token
  2. 选模板 Cloudflare Pages — Edit → Continue → Create Token
  3. 首页右侧栏复制 Account ID

Token 只显示一次,立刻保存到安全位置(如密码管理器)。

5.4 配置 GitHub Secrets

仓库 → Settings → Secrets and variables → Actions → New repository secret,添加 4 个:

NameValue
CLOUDFLARE_API_TOKEN<API Token>
CLOUDFLARE_ACCOUNT_ID<Account ID>
NAV_ADMIN_PASS<管理密码>(不设 = 管理站不部署)
NAV_SITE_URLhttps://<公开域名>

⚠️ 坑点 1:Secrets 是"写入后不可读"的。 填错不会立即报错,只会在运行时表现异常(如管理站三步显示 Skipped)。修改 Secret 后必须重新推送或 Re-run 才生效。

⚠️ 坑点 2:NAV_SITE_URL 必须填对外公布的域名(自定义域名优先于 pages.dev),否则 sitemap 与分享卡片指向错误地址,造成双域名 SEO 分裂。

5.5 部署工作流(完整可用版)

.github/workflows/deploy.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
name: Deploy to Cloudflare Pages

on:
  push:
    branches: [main]
  workflow_dispatch:      # 支持手动触发

jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      # secrets 不能用于步骤 if 条件,先转成普通 env('true'/'false')
      ADMIN_ENABLED: ${{ secrets.NAV_ADMIN_PASS != '' }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20

      # ① 校验 + 构建
      - name: Validate & build
        env:
          NAV_ADMIN_PASS: ${{ secrets.NAV_ADMIN_PASS }}
        run: |
          node scripts/maintain/validate-project.js
          node scripts/build/build-sites.js

      # ② 公开副本(剥离管理面板 / 过滤私有数据 / 生成 sitemap)
      - name: Build public site
        env:
          NAV_SITE_URL: ${{ secrets.NAV_SITE_URL }}
        run: node scripts/build/sync-public.js

      # ③ 确保公开站项目存在(非交互模式 wrangler 不会自动创建)
      - name: Ensure public Pages project exists
        run: npx --yes wrangler pages project create <公开项目名> --production-branch=main || echo "已存在,跳过"
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

      # ④ 部署公开站
      - name: Publish public site
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          command: pages deploy public-site --project-name=<公开项目名> --branch=main --commit-dirty=true

      # ⑤ 构建管理副本(未设 NAV_ADMIN_PASS 时自动跳过)
      - name: Build admin site
        if: env.ADMIN_ENABLED == 'true'
        env:
          NAV_ADMIN_PASS: ${{ secrets.NAV_ADMIN_PASS }}
        run: npm run build:admin

      # ⑥ 确保管理站项目存在
      - name: Ensure admin Pages project exists
        if: env.ADMIN_ENABLED == 'true'
        run: npx --yes wrangler pages project create <管理项目名> --production-branch=main || echo "已存在,跳过"
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

      # ⑦ 部署管理站
      - name: Publish admin site
        if: env.ADMIN_ENABLED == 'true'
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          command: pages deploy admin-dist --project-name=<管理项目名> --branch=main --commit-dirty=true

替换 4 处:<公开项目名> ×2、<管理项目名> ×2。

⚠️ 坑点 3:secrets 不能写在步骤 if 里! 直接写 if: ${{ secrets.X != '' }} 会报 Invalid workflow file: Unrecognized named-value: 'secrets',整个工作流一次都不会运行。正确写法即上文:job 级 env 中转 + if: env.ADMIN_ENABLED == 'true'。

⚠️ 坑点 4:Project not found [code: 8000007]。 非交互模式 wrangler 不会自动建项目。工作流必须先「创建(|| echo 容错跳过)」再部署,③⑥ 两步不能省。

推送触发首次部署:

1
2
3
git add .github/workflows/deploy.yml
git commit -m "ci: 添加双站自动部署工作流"
git push origin main

5.6 绑定自定义域名

  1. Cloudflare 控制台 → Workers & Pages → 点进项目
  2. Settings → Custom domains → Set up a custom domain
  3. 填 <公开域名>(管理站同理填 <管理域名>)→ Continue → Activate

前提:域名 DNS 必须托管在 Cloudflare 才能自动配 CNAME;域名在别处注册的,先在 Cloudflare 添加站点(免费计划)、按提示修改 NS 服务器。

5.7 管理站加访问保护(必做)

⚠️ 管理站含全量书签(含私有分组)+ 管理面板,数据文件是明文的——没有门禁等于把私有清单公网裸奔。

用 Cloudflare Access(Zero Trust 免费额度 50 用户内免费):

  1. 控制台 → Zero Trust → 首次进入选团队名
  2. Access → Applications → Add an application → Self-hosted
  3. 配置:
    • Application name:nav-admin
    • Session Duration:按需(如 1 month,避免频繁登录)
    • Application domain:<管理域名>(Path 留空)
  4. Add policies:Allow → Include → Selector 选 Emails → <你的邮箱>
  5. 保存后,访问管理站会先跳 Cloudflare 邮箱验证,通过才放行

六、验证过程(上线清单)

  • Actions 最新运行全绿,管理版三步(⑤⑥⑦)显示 ✓ 而非 Skipped
  • https://<公开域名>/ 打开正常:卡片、图标、搜索均可用
  • 公开站侧栏底部没有 admin 按钮(管理代码已被物理剥离)
  • https://<公开域名>/sitemap.xml 返回 XML,且 URL 均为 <公开域名>
  • F12 → Network → 响应头含 Content-Security-Policy / Strict-Transport-Security
  • 访问 https://<管理域名>/ 被 Access 拦截要求邮箱验证
  • 验证通过后:侧栏 admin 按钮 → <管理密码> 登录 → 管理面板可用

可选:README 顶部加部署状态徽章(私有仓的徽章仅登录 GitHub 后可见,属正常):

1
[![Deploy](https://github.com/<你的用户名>/<仓库名>/actions/workflows/deploy.yml/badge.svg?branch=main)](https://github.com/<你的用户名>/<仓库名>/actions/workflows/deploy.yml)

替换上式中的两处 GitHub 地址。


七、坑点实录(按出现顺序)

#阶段现象根因解法
1首次部署necessary to set a CLOUDFLARE_API_TOKENSecrets 未配置补齐 4 个 Secrets 后 Re-run
2部署Project not found [code: 8000007]非交互模式 wrangler 不自动建项目工作流加「先创建、`
3工作流Invalid workflow file: Unrecognized named-value: 'secrets'secrets 不能用于步骤 ifjob 级 env 中转判断
4上线后本地图标正常,线上全部消失CSP 只放行 favicon.im,该服务 302 跳到子域被 CSP 拦截;本地 serve.js 不下发 CSP 所以看不出img-src 加 https://*.favicon.im 通配

坑 4 最隐蔽——“本地好好的,线上坏了"十有八九是环境差异(安全头 / HTTPS / 缓存策略)。启示:本地开发服务器最好也模拟生产安全头,问题才能提前暴露。

附赠小坑:GitHub 提交页显示 “Some generated files are not rendered by default”?在 .gitattributes 加一行 *.test.js linguist-generated=false 即可修正误判。


八、注意事项汇总

  1. Secrets 全部配齐再触发:4 个缺一不可(NAV_ADMIN_PASS 除外——不设只是不部署管理站)
  2. 改 Secret ≠ 立即生效:必须重新推送或 Re-run 工作流
  3. 项目名一旦定了别乱改:工作流、自定义域名、旧项目清理三处要同步
  4. 部署通道只留一条:若曾在 Cloudflare 用 Connect to Git 连过仓库,push 会触发双重部署(CF 拉取构建 + Actions 直传互相竞争),二选一后把另一条关掉
  5. --commit-dirty=true 别删:CI 工作区里 data.js 等构建产物不入库,不加此参数 wrangler 会警告甚至失败
  6. 管理站安全是两层:Access 门禁(第一层)+ 管理密码(第二层),缺了第一层,数据文件本身就是明文可读的
  7. 密码进哈希不进代码:管理密码经 SHA-256+盐哈希后注入产物,仓库与产物中均无明文密码

九、日常维护(上线后的全部工作)

1
2
3
4
5
6
7
8
# ① 改数据:直接编辑 data/bookmarks.csv(自动备份 20 份历史版本,可回滚)
#    或用 CLI(参数用空格分隔):
npm run nav -- add 分组 名称 https://example.com 描述

# ② 提交推送
git add -A && git commit -m "更新书签" && git push

# ③ 完成——双站 1-2 分钟内自动重新部署

十、总结

设计决策理由
零依赖CI 秒级启动,无 npm install,无供应链投毒风险
双站物理分离“公开站没有管理入口"是构建期决定,而非运行期隐藏
Actions 直传 vs Connect to Git构建过程完全可控,环境变量集中在 GitHub Secrets
Cloudflare Access免费给任意静态站加 SSO 门禁,比"隐蔽 URL"体面得多
CSV 单源 + 自动备份表格即数据库,误删可回滚

感谢阅读,有问题欢迎留言交流。


本文基于一个真实项目的完整部署过程整理,所有命令与配置均经过线上验证。