一个纯静态书签导航站,以单个 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 账号 ID | 32 位十六进制串 |
<管理密码> | 管理面板登录密码 | 建议 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.js | CSV → 压缩 JSON |
scripts/build/sync-public.js | 生成公开副本(剥离管理代码) |
scripts/build/build-admin-dist.js | 生成管理副本(全量数据+管理面板) |
.github/workflows/deploy.yml | 自动部署流水线 |
_headers | CSP / HSTS 等安全响应头 |
三、环境要求
| 组件 | 版本要求 | 检查命令 |
|---|---|---|
| Node.js | ≥ 18 | node -v |
| Git | 任意现代版本 | git --version |
| 本地系统 | Windows / macOS / Linux 均可 | — |
CI 侧使用 ubuntu-latest + Node 20,无需本地安装 wrangler(工作流内自动拉取)。
四、构建逻辑(动手前先看懂原理)
4.1 数据流:四层解耦
| |
4.2 双站分流:一次构建,两种产物
| |
⚠️ 为什么是"物理剥离"而不是"密码隐藏"? 公开站的构建脚本会直接删除 admin.js 与管理 HTML 区块——不是 CSS 隐藏、不是前端判断,是产物里彻底不存在。公开站因此永远不可能出现管理入口,这是零信任思维的落地。
4.3 CI 流水线逻辑(7 步)
| |
五、部署步骤
5.1 本地验证
| |
替换:<项目路径> 为你的实际目录。
5.2 创建私有仓库并推送
GitHub 新建私有仓库(不勾选任何初始化文件),然后:
| |
为什么选私有仓? 书签数据暴露你的数字足迹(常用网盘、邮箱、内网面板域名)。若坚持公开仓库,务必依赖构建期的 NAV_PRIVATE_GROUPS 私有分组过滤。
5.3 创建 Cloudflare 凭证
- Cloudflare 控制台 → 右上角头像 → My Profile → API Tokens → Create Token
- 选模板 Cloudflare Pages — Edit → Continue → Create Token
- 首页右侧栏复制 Account ID
Token 只显示一次,立刻保存到安全位置(如密码管理器)。
5.4 配置 GitHub Secrets
仓库 → Settings → Secrets and variables → Actions → New repository secret,添加 4 个:
| Name | Value |
|---|---|
CLOUDFLARE_API_TOKEN | <API Token> |
CLOUDFLARE_ACCOUNT_ID | <Account ID> |
NAV_ADMIN_PASS | <管理密码>(不设 = 管理站不部署) |
NAV_SITE_URL | https://<公开域名> |
⚠️ 坑点 1:Secrets 是"写入后不可读"的。 填错不会立即报错,只会在运行时表现异常(如管理站三步显示 Skipped)。修改 Secret 后必须重新推送或 Re-run 才生效。
⚠️ 坑点 2:NAV_SITE_URL 必须填对外公布的域名(自定义域名优先于 pages.dev),否则 sitemap 与分享卡片指向错误地址,造成双域名 SEO 分裂。
5.5 部署工作流(完整可用版)
.github/workflows/deploy.yml:
| |
替换 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 容错跳过)」再部署,③⑥ 两步不能省。
推送触发首次部署:
| |
5.6 绑定自定义域名
- Cloudflare 控制台 → Workers & Pages → 点进项目
- Settings → Custom domains → Set up a custom domain
- 填
<公开域名>(管理站同理填<管理域名>)→ Continue → Activate
前提:域名 DNS 必须托管在 Cloudflare 才能自动配 CNAME;域名在别处注册的,先在 Cloudflare 添加站点(免费计划)、按提示修改 NS 服务器。
5.7 管理站加访问保护(必做)
⚠️ 管理站含全量书签(含私有分组)+ 管理面板,数据文件是明文的——没有门禁等于把私有清单公网裸奔。
用 Cloudflare Access(Zero Trust 免费额度 50 用户内免费):
- 控制台 → Zero Trust → 首次进入选团队名
- Access → Applications → Add an application → Self-hosted
- 配置:
- Application name:
nav-admin - Session Duration:按需(如 1 month,避免频繁登录)
- Application domain:
<管理域名>(Path 留空)
- Application name:
- Add policies:Allow → Include → Selector 选 Emails →
<你的邮箱> - 保存后,访问管理站会先跳 Cloudflare 邮箱验证,通过才放行
六、验证过程(上线清单)
- Actions 最新运行全绿,管理版三步(⑤⑥⑦)显示 ✓ 而非 Skipped
-
https://<公开域名>/打开正常:卡片、图标、搜索均可用 - 公开站侧栏底部没有 admin 按钮(管理代码已被物理剥离)
-
https://<公开域名>/sitemap.xml返回 XML,且 URL 均为<公开域名> - F12 → Network → 响应头含
Content-Security-Policy/Strict-Transport-Security - 访问
https://<管理域名>/被 Access 拦截要求邮箱验证 - 验证通过后:侧栏 admin 按钮 →
<管理密码>登录 → 管理面板可用
可选:README 顶部加部署状态徽章(私有仓的徽章仅登录 GitHub 后可见,属正常):
| |
替换上式中的两处 GitHub 地址。
七、坑点实录(按出现顺序)
| # | 阶段 | 现象 | 根因 | 解法 |
|---|---|---|---|---|
| 1 | 首次部署 | necessary to set a CLOUDFLARE_API_TOKEN | Secrets 未配置 | 补齐 4 个 Secrets 后 Re-run |
| 2 | 部署 | Project not found [code: 8000007] | 非交互模式 wrangler 不自动建项目 | 工作流加「先创建、` |
| 3 | 工作流 | Invalid workflow file: Unrecognized named-value: 'secrets' | secrets 不能用于步骤 if | job 级 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 即可修正误判。
八、注意事项汇总
- Secrets 全部配齐再触发:4 个缺一不可(
NAV_ADMIN_PASS除外——不设只是不部署管理站) - 改 Secret ≠ 立即生效:必须重新推送或 Re-run 工作流
- 项目名一旦定了别乱改:工作流、自定义域名、旧项目清理三处要同步
- 部署通道只留一条:若曾在 Cloudflare 用 Connect to Git 连过仓库,push 会触发双重部署(CF 拉取构建 + Actions 直传互相竞争),二选一后把另一条关掉
--commit-dirty=true别删:CI 工作区里data.js等构建产物不入库,不加此参数 wrangler 会警告甚至失败- 管理站安全是两层:Access 门禁(第一层)+ 管理密码(第二层),缺了第一层,数据文件本身就是明文可读的
- 密码进哈希不进代码:管理密码经 SHA-256+盐哈希后注入产物,仓库与产物中均无明文密码
九、日常维护(上线后的全部工作)
| |
十、总结
| 设计决策 | 理由 |
|---|---|
| 零依赖 | CI 秒级启动,无 npm install,无供应链投毒风险 |
| 双站物理分离 | “公开站没有管理入口"是构建期决定,而非运行期隐藏 |
| Actions 直传 vs Connect to Git | 构建过程完全可控,环境变量集中在 GitHub Secrets |
| Cloudflare Access | 免费给任意静态站加 SSO 门禁,比"隐蔽 URL"体面得多 |
| CSV 单源 + 自动备份 | 表格即数据库,误删可回滚 |
感谢阅读,有问题欢迎留言交流。
本文基于一个真实项目的完整部署过程整理,所有命令与配置均经过线上验证。