Viraha 博客技术栈全景解析:从静态页面到 Serverless 后台
Viraha 博客表面上是一个以文章阅读为核心的个人网站,但它并不只是“一组放在 GitHub 上的 HTML 文件”。在静态页面之外,网站还包含留言与文章评论、Cloudflare Turnstile 安全验证、评论人工审核、文章草稿与发布、更新公告、加密访客统计、管理员身份认证、自动化测试和多条独立部署流水线。
这篇文章不把技术栈理解为一张工具清单,而是从系统设计的角度回答下面几个问题:
- 一篇 Markdown 文章怎样变成读者看到的网页?
- 为什么公开博客适合静态生成,而评论和后台必须使用动态服务?
- GitHub Pages、Cloudflare Workers、KV 和 D1 分别负责什么?
- 评论、文章发布和访客统计的数据是怎样流动的?
- 完整 IP 如何加密,管理员如何查看,30 天保留期限如何执行?
- Cloudflare Access 如何保护管理后台?
- GitHub Actions 如何完成构建、检查与部署?
- 当页面、Worker、DNS 或 GitHub API 出现问题时,应该从哪里排查?
本文描述的是 2026 年 8 月 13 日时 Viraha 博客的实际架构。网站以后继续演进时,具体版本和实现细节可能变化,但其中的分层思路仍然具有参考价值。
1 整体架构:一个网站,三个运行平面
Viraha 博客可以分成三个相互协作、又彼此隔离的运行平面。
1.1 公开内容平面
公开内容平面负责读者最常接触的部分:
- 首页;
- 学习文章与随笔;
- 更新公告;
- About 与浏览须知;
- 留言板和每篇文章的评论界面;
- RSS、sitemap、SEO 和社交分享元数据。
这一部分由 Jekyll 在构建阶段生成静态 HTML、CSS、JavaScript 和 XML,再由 GitHub Pages 对外托管。生产域名是:
https://viraha.online
静态站点本身没有常驻应用服务器,也不会在每次访问文章时查询数据库。文章页面在部署前就已经生成完毕,因此读取速度快、运行结构简单,公开内容也容易被搜索引擎索引。
1.2 动态服务平面
评论提交、人工审核、访客统计和后台管理无法只靠静态文件完成,所以它们运行在 Cloudflare Workers 上。目前分成三个独立 Worker:
| Worker | 职责 | 主要存储 |
|---|---|---|
viraha-guestbook |
留言与文章评论的公开读取、提交、安全验证 | MESSAGES KV |
viraha-admin |
评论审核、文章草稿、GitHub 发布、公告、审计、访客分析 | ADMIN_DATA KV、MESSAGES KV、VISITOR_DB D1 |
viraha-analytics |
获得读者确认后的访问记录、完整 IP 加密、定时清理 | VISITOR_DB D1 |
它们分别使用不同的自定义域名:
博客: viraha.online
评论 API: guestbook-api.viraha.online
管理后台: admin.viraha.online
统计 API: analytics-api.viraha.online
把三种后端职责拆开有几个直接好处:
- 评论接口故障不会直接破坏静态文章的阅读;
- 统计 Worker 无权修改文章或审核评论;
- 管理后台可以单独放在 Cloudflare Access 后面;
- 每个 Worker 可以设置不同的路由、Secret、存储绑定和部署条件;
- 故障排查时能快速判断问题属于公开站点、评论服务、统计服务还是后台服务。
1.3 自动化与控制平面
控制平面由 Git、GitHub 仓库和 GitHub Actions 组成,负责保存源文件、追踪变更并部署不同组件。
Markdown / Liquid / Sass / JavaScript
│
▼
Git 提交与推送
│
┌─────────┼─────────┬──────────┐
▼ ▼ ▼ ▼
站点检查 Pages 构建 Worker部署 Dependabot
│ │ │
▼ ▼ ▼
质量结果 静态网站 Cloudflare服务
一个仓库同时保存静态站和三个 Worker 的源代码,但构建系统通过目录和工作流将它们分开处理。
2 静态站点核心:Jekyll
2.1 Jekyll 在系统中的角色
Jekyll 是一个静态站点生成器。它读取 Markdown、Liquid 模板、YAML 数据和 Sass 样式,在构建阶段输出可以直接托管的静态文件。
项目目前锁定的关键 Ruby 依赖包括:
| 组件 | 当前版本 | 作用 |
|---|---|---|
| Jekyll | 4.3.4 | 站点生成、集合文章、模板渲染 |
| Minima | 2.5.2 | 基础主题和默认结构 |
| Kramdown | 2.5.1 | Markdown 到 HTML 的转换 |
| kramdown-parser-gfm | 1.1.0 | GitHub Flavored Markdown 语法支持 |
| Rouge | 4.5.1 | 构建时代码语法高亮 |
| jekyll-feed | 0.17.0 | 生成 Atom/RSS Feed |
| jekyll-seo-tag | 2.8.0 | 生成 SEO 与社交分享元数据 |
版本由 Gemfile 描述,由 Gemfile.lock 锁定。锁文件的重要意义是:本机与 GitHub Actions 尽量使用同一组依赖,避免“本地能构建、线上却因为版本变化失败”。
2.2 文章为什么放在 _posts
Jekyll 约定文章存放在 _posts/,文件名通常采用:
YYYY-MM-DD-文章名称.markdown
文件由两部分组成:顶部 Front Matter 和正文 Markdown。
---
layout: post
title: "文章标题"
date: 2026-08-13 12:00:00 +0800
categories: [study]
tags: [Jekyll, Web开发]
description: "用于首页和 SEO 的摘要。"
last_modified_at: 2026-08-13
update_summary: "本次更新概括。"
toc: true
---
这些字段并非只是文章信息,它们会直接控制页面行为:
layout决定使用哪个 Liquid 布局;title成为页面主标题和 SEO 标题;date决定发布日期与文章排序;categories决定文章进入“学习”还是“随笔”等集合;tags用于描述更细的主题;description用于首页摘要和搜索引擎描述;last_modified_at决定学习内容的最近更新时间;update_summary说明这次维护了什么;toc控制是否生成文章目录。
学习类文章统一使用 categories: [study]。文章布局还会自动把学习文章视为需要目录,因此以后新增学习笔记时,不必为每篇文章重新编写目录样式。
2.3 Liquid 模板怎样组装页面
Jekyll 使用 Liquid 作为模板语言。站点中最重要的布局关系可以简化为:
_layouts/default.html
├── _includes/head.html
├── _includes/header.html
├── 当前页面或文章内容
├── _includes/footer.html
└── assets/js/site.js
_layouts/post.html
├── 文章标题与元数据
├── 左侧自动目录
├── Markdown 正文
├── 同分类上一篇/下一篇
└── _includes/discussion.html
default.html 提供整个网站共享的 HTML 骨架;post.html 再在其中组织文章专用结构。
文章布局会执行一些构建期逻辑:
- 判断文章是否属于
study; - 为学习文章启用统一目录; -显示发布日期、更新时间、分类、标签;
- 根据中文字符数量估算阅读时间;
- 在同一分类中寻找上一篇和下一篇;
- 在每篇文章底部加载与留言板一致的评论组件。
这些逻辑都发生在构建期,最终交给浏览器的是普通 HTML,不需要读者的浏览器理解 Liquid。
2.4 Markdown、GFM 与 Rouge
正文通过 Kramdown 转换,配置中启用了 GFM 输入模式,因此可以使用:
- 标题和段落;
- 有序与无序列表;
- 表格;
- 引用块;
- 行内代码与围栏代码块;
- Markdown 链接和图片;
- GitHub 风格的常用扩展语法。
围栏代码块的语言标识会交给 Rouge 处理:
```powershell
bundle exec jekyll serve
```
Rouge 在构建时为关键字、字符串、注释等元素添加 CSS 类,浏览器只负责呈现已经标记好的结果。
2.5 Sass 与网站视觉层
站点以 Minima 为基础主题,但主要视觉调整写在 assets/main.scss。Sass 构建后输出 assets/main.css。
当前样式层负责:
- 响应式页宽和导航;
- 学习文章左侧目录;
- 多级目录字号与缩进;
- 文章卡片和元数据;
- 代码块字体、配色和复制按钮;
- 留言与评论表单;
- About 时间线和功能入口;
- 更新公告;
- 访客须知确认条;
- 键盘焦点样式与移动端布局。
Sass 适合管理较长的样式文件,但项目仍应注意选择器范围。文章样式、后台样式和评论样式不应互相污染,因此管理后台使用独立的 admin/public/styles.css,不会把整套博客主题带入后台。
3 浏览器端 JavaScript:渐进增强
3.1 为什么静态博客仍然需要 JavaScript
如果禁用 JavaScript,文章主体依然可以作为 HTML 阅读;JavaScript 主要用于增强交互,而不是生成核心内容。这种设计称为 渐进增强。
公开站点的脚本按职责拆分:
| 文件 | 主要职责 |
|---|---|
assets/js/site.js |
导航键盘操作等全站增强 |
assets/js/article.js |
自动目录、代码复制等文章功能 |
assets/js/guestbook.js |
评论加载、表单校验、提交状态、Turnstile |
assets/js/visitor-analytics.js |
须知确认、本地状态和访问上报 |
3.2 自动目录如何工作
Jekyll 输出正文后,article.js 在浏览器中扫描文章标题,构建目录链接并放入左侧的 post-toc 容器。
目录不是手工维护的,因此文章增加、删除或调整标题时,目录会随正文自动变化。样式再根据 h2、h3、h4 等层级设置不同字号、字重和缩进,使长篇学习文章的结构更容易理解。
3.3 代码复制按钮
脚本会为代码块增加复制按钮,并使用 Clipboard API 将内容写入剪贴板。这里有两个重要原则:
- 代码文本从已有 DOM 中读取,不把 HTML 当成代码复制;
- 复制功能失败不会妨碍代码本身被选中和手工复制。
这也是渐进增强的典型例子:增强失败时,基础阅读仍然有效。
4 首页、分类、更新公告与 SEO
4.1 首页选择逻辑
首页不是简单列出全部文章,而是突出两类内容:
- 最新一篇随笔;
- 最近更新的一篇学习文章。
随笔从 essays 分类中按 date 倒序选择;学习内容优先按 last_modified_at 判断最近维护时间,没有该字段时回退到 date。
摘要优先级则是:
description → update_summary → 正文自动摘要
这样一篇学习笔记可以保持同一个 URL 持续更新,同时在首页告诉读者最近改动了什么。
4.2 更新公告为何使用 YAML
更新记录存放在 _data/updates.yml。与把公告写死在 HTML 中相比,结构化 YAML 更容易排序、编辑和由后台程序更新。
- version: "v1.2.0"
date: 2026-08-13
title: "访客统计功能上线"
items:
- "增加加密访客记录"
- "增加后台分析页面"
Jekyll 读取 _data 后,Liquid 模板可以把每条记录渲染为统一卡片。
4.3 SEO、Feed 与 sitemap
jekyll-seo-tag 根据站点配置和 Front Matter 生成:
- 页面标题与描述;
- canonical URL;
- Open Graph 数据;
- Twitter Card 等分享元数据; -结构化数据。
jekyll-feed 生成订阅 Feed,sitemap.xml 则帮助搜索引擎发现公开页面。非公开文章通过 Front Matter 中的发布设置不进入最终站点,因此“文件在仓库里”和“内容能被网站访问”是两种不同状态。
5 GitHub Pages:静态托管层
5.1 GitHub Pages 负责什么
GitHub Pages 只托管构建产物,不承担评论审核、数据库查询或管理员登录。它的核心职责是:
- 接收 Actions 上传的
_site; - 通过 HTTPS 对外提供静态文件;
- 绑定自定义域名
viraha.online; - 使用 CDN 边缘节点分发资源。
项目使用自定义 Actions 工作流构建,而不是完全依赖 Pages 的默认构建器。这样可以显式指定 Ruby 版本、严格 Front Matter 和额外检查。
5.2 自定义域名如何连接 Pages
仓库中的 CNAME 表明生产域名,DNS 中的根域 A 记录指向 GitHub Pages,www 可以通过 CNAME 指向 GitHub Pages 主机名。
访问链路可以概括为:
浏览器
│ 查询 viraha.online
▼
DNS 解析
│
▼
GitHub Pages 边缘节点
│
▼
返回 _site 中的静态文件
DNS 成功并不代表浏览器一定可访问。如果某条网络线路对 GitHub 或 Cloudflare 的连接异常,可能出现命令行通过代理成功、浏览器直连失败等情况。因此排查时要区分 DNS、TCP、TLS、HTTP 和浏览器代理五个层次。
6 评论与留言系统
6.1 为什么不能把评论直接写入 GitHub Pages
GitHub Pages 是只读静态托管。读者不能通过浏览器把新评论写进 _site,因此需要一个动态 API 接收数据。
评论系统采用:
浏览器表单
│ HTTPS POST
▼
guestbook-api.viraha.online
│ Turnstile / 蜜罐 / 限流 / 校验
▼
Cloudflare KV:MESSAGES
│ 初始状态 pending
▼
管理员人工审核
│
▼
approved 评论通过公开 GET 接口显示
6.2 留言板和文章评论为何共用组件
留言板与文章评论使用同一套 _includes/discussion.html 和 guestbook.js,但数据会带有不同作用域:
guestbook:全站留言板;post:绑定到具体文章路径和标题的评论。
共用组件保证以下配置保持一致:
- 昵称必填;
- 真实姓名选填;
- 邮箱必填但不公开;
- 内容长度限制;
- Turnstile 验证;
- 提交状态、字符计数、超时和重试; -人工审核后公开。
6.3 Turnstile、蜜罐和速率限制
三种措施解决的问题不同。
Cloudflare Turnstile 判断当前提交是否通过了安全验证。浏览器拿到一次性 token,Worker 再使用保存在 Secret 中的密钥调用 Siteverify 验证。只在前端显示“验证成功”是不够的,后端必须独立确认 token。
蜜罐字段 是正常用户看不到也不应填写的表单字段。简单机器人往往会自动填写页面中的所有输入框,Worker 发现蜜罐有值后就可以拒绝请求。
速率限制 控制同一来源在短时间内可以提交多少次,防止重复点击和批量滥用。它不能判断内容是否正确,但能减少接口被快速消耗。
三者组合形成分层防护:
输入校验 → 蜜罐 → Turnstile → 速率限制 → 写入 KV → 人工审核
6.4 KV 为什么适合评论
Cloudflare KV 是键值存储。每条评论可以用类似下面的键保存:
msg:<comment-id>
值是包含昵称、邮箱、内容、状态和时间的 JSON。评论记录主要按照 ID 读写,数据结构相对简单,KV 的部署和维护成本较低。
公开接口只返回展示所需字段,不返回邮箱、真实姓名、IP、Token 和审核内部字段。即使前端不显示某个字段,后端仍然必须主动从响应中删除它,因为任何人都可以直接查看网络响应。
7 独立管理后台
7.1 后台为什么也是一个 Worker
管理后台部署在 admin.viraha.online,由 viraha-admin Worker 同时提供:
- 静态管理界面资源;
/api/admin/*管理 API;- Cloudflare Access 身份校验;
- KV、D1 和 GitHub API 的服务端访问。
后台界面使用原生 HTML、CSS 和 JavaScript,没有引入 React、Vue 等前端框架。这降低了依赖数量和打包复杂度,也让安全边界更清晰。
7.2 Cloudflare Access 身份认证
后台不是靠一个写在浏览器里的固定密码保护,而是由 Cloudflare Access 拦截访问。当前策略只允许指定管理员邮箱。
大致流程如下:
访问 admin.viraha.online
│
▼
Cloudflare Access 登录与邮箱验证
│
▼
请求携带 Cf-Access-Jwt-Assertion
│
▼
Admin Worker 校验签名、issuer、audience、期限和邮箱
│
▼
允许调用管理 API
Worker 不应只相信“请求经过了 Access”,而会根据 Access 的 JWKS 验证 JWT 签名,并检查:
- 签发者是否匹配当前 Zero Trust 团队;
- audience 是否对应这个应用;
- token 是否过期;
- 邮箱是否是管理员白名单中的地址。
这形成了“边缘网关验证 + 应用自身验证”的双层结构。
7.3 CSRF、Origin 和安全响应头
管理 API 的写操作要求合法 Origin,并使用 JSON 请求,从而降低跨站请求伪造风险。后台静态资源还设置了严格响应头,例如:
- Content Security Policy;
X-Content-Type-Options: nosniff;Referrer-Policy;Permissions-Policy;X-Frame-Options: DENY; -敏感响应不缓存。
后台前端使用 textContent 构建用户内容,避免把评论或文章标题直接当作 HTML 注入页面。
7.4 后台功能模块
当前后台包含:
- 概览与依赖健康状态;
- 评论分页、搜索、筛选和批量审核;
- 草稿创建、自动保存、Markdown 预览;
- GitHub 已发布文章列表;
- 发布、更新、取消发布与可见性设置;
- 更新公告编辑和发布; -访客统计与完整 IP 二次确认查看; -审计日志和数据导出; -移动端侧栏、键盘快捷键和未保存离开保护。
7.5 私密草稿为何不直接写进仓库
草稿保存在独立的 ADMIN_DATA KV,而不是 _drafts 或 _posts。这样做是因为源仓库和构建系统并不是理想的私密笔记数据库。
只有管理员确认发布时,Worker 才通过 GitHub Contents API 把 Markdown 写入 _posts。这条边界十分重要:
编辑中:ADMIN_DATA KV
│
│ 点击发布
▼
GitHub Contents API
│
▼
_posts/*.markdown
│
▼
GitHub Actions → GitHub Pages
7.6 GitHub SHA 冲突保护
后台读取 GitHub 文件时会保存文件 SHA。提交更新时带上旧 SHA;如果远程文件已经被 VS Code、另一后台页面或其他工具修改,GitHub 会拒绝旧版本覆盖新版本。
这是一种 乐观并发控制:
- 读取版本 A,并记住 SHA-A;
- 用户编辑;
- 提交时要求远程仍是 SHA-A;
- 如果远程已经变成 SHA-B,返回冲突;
- 用户重新加载并决定如何合并。
它避免“最后一次点击无条件覆盖一切”的数据丢失。
8 访客统计与完整 IP 加密
8.1 为什么使用独立 Analytics Worker
访客统计与评论的生命周期和数据模型不同:
- 评论是用户主动提交、长期等待审核的内容;
- 访问记录数量更多、按时间查询,并且需要严格自动清理;
- 完整 IP 属于需要更谨慎处理的信息; -统计接口不应获得评论和文章发布权限。
因此统计被拆成 viraha-analytics Worker,并使用单独的 D1 数据库。
8.2 确认须知后才上报
公开页面加载 visitor-analytics.js。脚本读取当前须知版本,并检查浏览器本地存储:
没有确认记录
│
▼
显示《浏览 Viraha 博客须知》提示
├── 不同意:不发送统计,返回上一网页
└── 已知晓:保存本地确认状态,发送本次访问
每当完整 IP、用途或保留期限等重要规则变化时,配置中的 analytics_consent_version 和 Worker 的 CONSENT_VERSION 会同步升级。旧版确认不会被当成新版确认。
脚本也会尊重浏览器的 Global Privacy Control 或 Do Not Track 信号,不在这些信号启用时主动统计。
8.3 为什么使用 D1 而不是 KV
D1 是 Cloudflare 的关系型 SQL 数据库。访问记录经常需要:
-按日期范围筛选; -统计总浏览量; -对访客标识去重; -按路径、国家和设备分组; -分页倒序读取; -删除 30 天以前的记录。
这些操作更适合 SQL,而不是逐个列出 KV 键后在应用层统计。
数据库中的核心字段包括:
访问 ID、时间、日期、路径、页面标题、来源域名
IP 密文、AES-GCM nonce、不可逆访客标识、IP 类型
国家、区域、城市、时区、ASN、Cloudflare 机房
设备类别、浏览器类别、疑似机器人标记、须知版本
数据库不保存完整 User-Agent,也不保存 URL 查询参数,减少不必要的信息收集。
8.4 完整 IP 如何加密
Analytics Worker 从 Cloudflare 提供的 CF-Connecting-IP 获取连接来源,而不相信浏览器自己提交的 IP 字段。随后对 IP 做格式验证。
主密钥 VISITOR_DATA_KEY 是一个随机生成的 32 字节值,只保存在 Cloudflare Worker Secret 中,不写入 Git。
Worker 使用 HKDF 从主密钥派生两把用途不同的子密钥:
VISITOR_DATA_KEY
│ HKDF
├── ip-encryption ──────► AES-256-GCM 密钥
└── daily-visitor-hash ─► HMAC-SHA-256 密钥
AES-GCM 加密每条 IP 时都会生成随机 12 字节 nonce,因此同一个 IP 两次出现时,密文也不同。数据库保存:
ip_ciphertext
ip_nonce
没有 Worker Secret,仅拿到数据库内容不能直接读出 IP。
HMAC 生成不可逆访客标识,用于计算独立访客。统计分组不需要先把每条 IP 解密,降低了日常统计暴露明文的机会。
8.5 30 天保留如何执行
wrangler.toml 为 Analytics Worker 配置了 Cron Trigger:
[triggers]
crons = ["17 * * * *"]
定时任务每小时执行清理 SQL,删除超过 30 天的记录。后台列表和解密查询也会限制在最近 30 天之内,形成两层限制:
- 存储层定时删除;
- 接口层拒绝返回超期数据。
D1 migration 用于创建表与索引。生产数据库迁移由管理员在本机确认后手动执行,不在每次普通部署时自动修改数据库结构。
8.6 管理员查看完整 IP
后台列表默认只显示:
IPv4(已加密)
IPv6(已加密)
管理员必须针对某一条记录输入指定确认文字,后台 API 才会使用同一个 VISITOR_DATA_KEY 临时解密。明文只显示 30 秒,查看动作同时写入审计日志。
这种设计不能让 IP 变成“非敏感数据”,但能减少误操作、批量暴露和数据库泄漏后的直接风险。
9 数据存储边界
把数据放在哪里,是整个架构最重要的设计之一。
| 数据 | 存储位置 | 是否公开 | 生命周期 |
|---|---|---|---|
| 已发布文章 | GitHub _posts |
构建后公开或按 Front Matter 隐藏 | 版本化保留 |
| 更新公告 | GitHub _data/updates.yml |
公开 | 版本化保留 |
| 私密草稿 | ADMIN_DATA KV |
否 | 由管理员管理 |
| 评论与留言 | MESSAGES KV |
仅审核通过的公开 | 由管理员管理 |
| 管理审计日志 | ADMIN_DATA KV |
否 | 按后台策略保留 |
| 访问记录 | VISITOR_DB D1 |
否 | 原始记录 30 天 |
| Worker 密钥 | Cloudflare Secrets | 否 | 手动轮换 |
这里可以看到一个通用原则:
公开、适合版本控制的内容放 Git;频繁变动的应用数据放数据库;密钥只放 Secret 管理系统。
10 GitHub Actions:持续集成与持续部署
10.1 站点检查工作流
checks.yml 在 Pull Request 和推送到 main 时运行:
- 检出仓库;
- 配置 Ruby 3.2;
- 根据锁文件安装依赖;
- 使用严格 Front Matter 构建 Jekyll;
- 解析并验证 sitemap XML;
- 检查生成页面的内部链接和重复 ID;
- 执行
git diff --check。
严格 Front Matter 能及早发现文章头部 YAML 格式错误。站点检查脚本则遍历 _site 中的 HTML,检查本地链接是否指向真实文件,并找出重复元素 ID。
10.2 Pages 部署工作流
pages.yml 负责实际发布:
checkout
→ Ruby 3.2
→ bundle exec jekyll build --strict_front_matter
→ upload-pages-artifact
→ deploy-pages
构建和部署被拆成两个 Job。部署 Job 使用 GitHub Pages environment,并获取最终部署 URL。
10.3 Worker 路径限定部署
三个 Worker 有独立工作流,并通过 paths 限定触发范围。例如只有 analytics/** 或其工作流变化时,才部署统计 Worker。
这避免修改一篇文章时重复部署所有后端,也减少不必要的 API 调用。
Admin 与 Analytics 使用 Node.js 22 和 Wrangler 4.x;留言 Worker 仍使用自己锁定的依赖。每个目录都有独立的 package.json 和 package-lock.json,所以 npm 命令必须在相应目录运行:
npm ci --prefix admin
npm test --prefix admin
npm ci --prefix analytics
npm test --prefix analytics
仓库根目录没有 Node.js 工程配置,因此直接在根目录运行 npm ci 会报找不到 package-lock.json,这不是依赖损坏。
10.4 Secrets 与普通变量
配置要区分三类信息:
可以进入 Git 的普通变量:
- GitHub owner、repository、branch; -允许的公开域名;
- D1 和 KV 的资源绑定 ID;
- Access issuer; -须知版本。
只能放 Cloudflare Worker Secret 的密钥:
TURNSTILE_SECRET_KEY;ADMIN_TOKEN(兼容接口使用);GITHUB_CONTENT_TOKEN;CLOUDFLARE_ACCESS_AUD;VISITOR_DATA_KEY。
只能放 GitHub Actions Secrets 的部署凭据:
CLOUDFLARE_API_TOKEN;CLOUDFLARE_ACCOUNT_ID。
资源 ID 通常不是密码,但 Secret 一旦进入 Git 历史,即使随后删除文件,也应视为已经泄漏并立即轮换。
11 本地开发与写作流程
11.1 在 VS Code 中写文章
典型流程是:
git status
git pull --rebase origin main
然后在 _posts 创建或编辑 Markdown,运行本地站点:
bundle exec jekyll serve
浏览器打开:
http://127.0.0.1:4000
修改 _config.yml 后需要重启 Jekyll,因为配置不会在自动刷新时重新加载。
提交前执行:
bundle exec jekyll build --strict_front_matter
ruby scripts/check_site.rb
git diff --check
git status
确认变更后提交:
git add _posts/文章文件.markdown
git commit -m "docs: 发布文章标题"
git pull --rebase origin main
git push origin main
先拉取再推送可以整合后台通过 GitHub API 产生的远程提交,降低 fetch first 冲突。
11.2 在管理后台写文章
另一条路径是:
- 登录
admin.viraha.online; - 在文章草稿中创建内容;
- 草稿自动保存到
ADMIN_DATA; - 预览 Markdown;
- 设置分类、标签、日期、目录和公开状态;
- 点击发布;
- Worker 通过 GitHub Contents API 写入文章;
- GitHub Actions 构建 Pages;
- 等待部署完成后检查生产页面。
VS Code 和后台都能修改文章,因此操作前要理解 SHA 冲突保护,不要在两个入口同时编辑同一个旧版本。
11.3 本地运行 Worker
每个 Worker 可以单独启动:
cd cloudflare
npx wrangler dev
cd admin
npm ci
npm run check
npm test
npx wrangler dev
cd analytics
npm ci
npm run check
npm test
npx wrangler dev
本地开发变量可以放在被 .gitignore 排除的 .dev.vars 中,但生产 Secret 仍应通过 wrangler secret put 写入 Cloudflare。
12 安全设计总结
12.1 最小权限
不同 Worker 只绑定自己需要的资源:
-评论 Worker 只需要留言 KV 和 Turnstile Secret; -统计 Worker 只需要访客 D1 和数据密钥; -管理 Worker 才能同时读取评论、草稿、访客数据库和 GitHub API。
GitHub Content Token 也只应授予目标仓库的 Contents 读写权限,不授予仓库管理、Actions、Secrets 或其他无关权限。
12.2 输入不可信,输出也要最小化
所有来自表单、URL、GitHub 文件和数据库的内容都要视为不可信:
- 服务端验证长度、格式和允许值;
- 仓库路径需要规范化,防止越界;
- CSV 导出防止公式注入;
- Markdown 预览不直接执行任意 HTML;
- 管理界面用
textContent展示数据; - 公开 API 只返回公开字段。
前端校验主要改善体验,真正的安全边界必须在 Worker 中。
12.3 审计与可恢复性
高风险操作会记录:
- 操作时间; -管理员邮箱; -动作类型; -目标 ID; -成功或失败; -去除敏感信息后的说明。
GitHub 中的文章变更天然拥有提交历史;KV 和 D1 则需要依靠应用自己的审计、导出和保留策略。不同存储的恢复能力并不相同,不能因为“都在云端”就认为都能自动回滚。
13 故障排查:先判断坏在哪一层
13.1 博客页面打不开
依次检查:
Resolve-DnsName viraha.online
Test-NetConnection viraha.online -Port 443
curl.exe -I --max-time 20 "https://viraha.online"
判断 DNS 是否解析、443 端口是否可达、HTTPS 是否返回 GitHub Pages 响应。如果 curl --noproxy 失败而普通 curl 成功,通常说明当前网络依赖系统代理。
13.2 评论无法加载或提交
先直接访问评论 API,再查看浏览器开发者工具中的 Network 与 Console:
curl.exe -i --max-time 20 "https://guestbook-api.viraha.online/api/messages"
常见原因包括:
- Worker 未部署或路由错误;
- Turnstile sitekey 与 secret 不匹配;
- hostname 或 action 校验不一致; -网络无法访问 Cloudflare;
- API 返回 429、503 或 CORS 错误; -前端配置仍指向旧 Worker 地址。
13.3 统计没有记录
先检查健康端点:
curl.exe -i --max-time 20 "https://analytics-api.viraha.online/health"
期望看到:
{
"ok": true,
"storage": true,
"encryption": true,
"retention_days": 30
}
然后确认:
-读者是否确认了当前版本须知;
_config.yml与 Analytics Worker 的须知版本是否一致;VISITOR_DATA_KEY是否存在;- D1 migration 是否已经执行;
-后台 Worker 是否绑定同一个
VISITOR_DB; -浏览器是否启用了 GPC 或 DNT; - Network 中
/api/visit是否返回 204。
13.4 后台显示网络连接失败
先区分静态资源、Access 和 API:
curl.exe -I --max-time 20 "https://admin.viraha.online"
未登录时出现跳转到 Cloudflare Access 通常是正常行为。登录后再检查 /api/admin/session、浏览器 Cookie、Access audience、issuer 和邮箱白名单。
13.5 GitHub Actions 失败
不要只看红色图标,要打开具体步骤:
npm ci失败:检查工作目录、锁文件、registry 和网络;- Wrangler 要求更高 Node.js:升级
setup-node版本; - D1 7403:API Token 没有对应 D1 权限,或迁移不应放在日常部署;
- Jekyll Front Matter 错误:检查 YAML 冒号、引号、日期和分隔线;
fetch first:远程已有后台或其他设备推送的提交,先git pull --rebase;- Pages 构建成功但页面没变:检查部署 Job、缓存、Front Matter 的
published和访问 URL。
14 为什么选择这套技术栈
14.1 优点
内容可长期保存。 文章是普通 Markdown,不被某个内容平台的私有编辑器锁定。
公开阅读成本低。 绝大多数请求只是静态文件,不需要为每次页面访问启动服务器或查询数据库。
动态能力按需增加。 评论、后台和统计使用 Serverless Worker,不破坏静态站的简单性。
职责清楚。 GitHub 保存公开源内容,Pages 托管静态站,Workers 执行业务逻辑,KV 保存键值数据,D1 负责关系查询,Access 管理身份。
安全边界可解释。 管理员登录、Secret、公开 API、私密草稿和访客数据各有独立位置。
容易自动化。 Git 推送可以触发检查和部署,每个组件又能按路径独立更新。
14.2 代价与限制
系统比纯静态博客复杂。 DNS、Pages、三个 Worker、KV、D1、Access 和 Actions 都可能成为故障点。
存在最终一致性和部署等待。 后台点击发布并不等于页面瞬间上线,中间还要经过 GitHub 提交、Actions 构建和 CDN 更新。
跨系统权限管理困难。 GitHub Token、Cloudflare Token、Worker Secret 和 Access Policy 必须分别正确配置。
本地与生产环境不同。 本地 Jekyll 不会完全复现 GitHub Pages、Cloudflare 边缘请求和真实 Turnstile。
KV 不适合复杂关系查询。 评论规模和查询需求显著增长时,可能需要迁移到 D1 或专用数据库。
完整 IP 带来更高责任。 即使加密和限期保存,也必须保持明确告知、最小使用、严格访问和及时删除。
15 可以继续演进的方向
未来可以考虑:
- 为 Worker 增加更完整的集成测试和预发布环境;
- 给访客统计增加不解密 IP 的趋势图和异常阈值;
- 将评论从 KV 迁移到 D1,获得更稳定的分页和组合筛选;
- 增加数据库备份、恢复演练和密钥轮换流程;
- 为后台增加安全告警和失败通知;
- 增加端到端浏览器测试,覆盖键盘、320px、200% 缩放和真实 Turnstile;
- 引入更严格的内容安全策略和资源完整性管理;
- 为文章建立结构化的修订记录与引用来源字段;
- 增加独立预览部署,发布前检查手机、平板和桌面布局;
- 定期审查依赖、GitHub 权限、Cloudflare Token 和个人信息处理范围。
16 总结
Viraha 博客的核心不是某一个框架,而是一组明确的边界:
Markdown 负责内容
Jekyll 负责生成
Liquid 负责组装
Sass 负责视觉
JavaScript 负责渐进增强
GitHub 负责版本与自动化
GitHub Pages 负责静态托管
Cloudflare Workers 负责动态逻辑
KV 负责简单键值数据
D1 负责关系型访客记录
Turnstile 负责提交验证
Cloudflare Access 负责管理员身份
Worker Secrets 负责密钥
真正重要的设计原则是:能静态化的内容尽量静态化,需要动态状态的功能才交给后端;公开内容、私密数据和密钥分别进入适合它们的存储;每个服务只得到完成自身职责所需的最小权限;任何自动化都要配合可验证、可审计和可恢复的流程。
当这些边界被认真维护时,一个个人博客既可以保持写作工具的轻量,也可以拥有评论、后台、统计和自动部署等完整的网站能力。
文章评论
欢迎围绕本文内容补充资料、分享经验或提出不同看法。请尽量保持友善并说明理由;评论经站长审核后公开。
✍️发表评论
邮箱和真实姓名不会公开;真实姓名完全选填。评论只会显示在当前文章下,并需要审核后公开。
💬本文评论