Vic Wong

这个博客是怎么搭起来的:从静态部署、自动图床到可迁移评论

Sep 7 · 18min

写完第一篇文章后,我又花了一些时间补齐博客的基础设施:域名、头像、图床、编辑器上传,以及文章下面的评论区。

我的目标很具体:平时只管写 Markdown,图片粘进去就能上传;网站代码暂时保持私有,读者仍然可以登录 GitHub 留言;以后换存储、换框架,或者把评论搬进代码仓库,尽量不用重做一遍。还有一条约束:能省就省。个人博客没必要一上来就堆付费服务。

这篇文章记录的是本站在 2026 年 9 月 7 日 的实现。配图根据实际配置绘制,不是控制台截图。你照着搭建时,需要换成自己的域名、仓库和账户信息。

一、先看懂三条链路

本站架构:文章经 Vite SSG 构建后部署到 Pages,图片通过 PicGo 上传 R2 并由 WebP Cloud 分发,评论存储在独立 GitHub Discussions 仓库

层次本站选择负责什么
内容Markdown + YAML frontmatter正文、标题、摘要、日期和封面
页面Vue 3 + Vue Router + Vite SSG组件、路由和构建时生成 HTML
样式UnoCSS按需生成样式、响应式布局、深浅色主题
网站托管Cloudflare Pages发布 dist,提供 HTTPS 和静态资源访问
图片存储Cloudflare R2保存图片对象,提供 S3 兼容接口
图片代理WebP Cloud回源、缓存、近无损压缩、格式优化与元数据处理
写作工具Typora + PicGo S3 插件插入图片、上传、返回 Markdown 外链
评论Giscus + GitHub DiscussionsGitHub 登录、回复、反应与评论管理
订阅RSS / Atom / JSON Feed让读者用阅读器跟进新文章

页面布局参考了 pseudoyu/pseudoyu.com 的 MIT 开源实现,并按本站内容做了适配。代码许可和文章许可是两回事,使用别人的布局代码,应保留相应许可与致谢。

Pages、R2 和 WebP Cloud 是三个不同角色:Pages 存网站构建结果,R2 存图片对象,WebP Cloud 提供图片访问入口。评论又在另一条链路上,不需要给静态网站加一台常驻服务器。

二、Markdown 怎么变成网站

1. 目录和文章元信息

文章放在 pages/posts/,例如 blog-stack-guide.md 对应 /posts/blog-stack-guide。路由插件读取 Markdown,页面包装组件提供标题、日期、目录和评论区。

新文章可以从这个模板开始:

---
title: 我的新文章
date: 2026-09-07T09:00:00+08:00
description: 用一两句话说明文章解决什么问题。
lang: zh
duration: 10min
image: https://你的图片代理域名/images/2026/09/my-post/cover.png
commentId: /posts/my-post
tocAlwaysOn: true
draft: false
---

commentId 是文章的永久评论标识。发布以后,即使改标题或移动文件,也保留原值。comments: false 可以关闭单篇文章的评论区。这些是本站约定,不是所有博客框架通用的字段。

另外,本项目的 draft 不是访问控制。它可以影响列表或评论展示,但不能让构建目录中的私密内容得到保密。未发布内容应保存在构建目录之外。

2. 本地预览与静态构建

本站使用 Node.js 22 和 pnpm 10.6.2,仓库通过 lockfile 固定依赖解析结果:

pnpm install --frozen-lockfile
pnpm dev

打开 http://localhost:3333,检查标题、目录、图片和手机宽度下的阅读体验。正式发布前执行:

pnpm lint
pnpm build

pnpm build 先运行 Vite SSG,在构建阶段生成各个路由的 HTML,再生成订阅文件。文章正文已经在 HTML 中,浏览器随后加载 Vue,处理主题切换、站内导航等交互。

本站没有动态 OG 图片生成服务,头像和分享图使用静态资源,有文章封面时通过 frontmatter 指定。这减少了构建对原生图像依赖的要求,Windows 环境也更容易维护。

3. RSS 也是发布的一部分

构建生成 feed.xml、feed.atom、feed.json,并保留 /zh/index.xml 兼容入口。可以直接订阅 本站 RSS。

发布时要同时检查首页列表和订阅文件。页面存在,并不等于阅读器一定能收到;日期、标题或 feed 链接出错,都可能造成漏更新。

三、部署到 Cloudflare Pages

1. 创建项目,验证默认域名

在 Cloudflare 的 Workers & Pages 中创建 Pages 项目。本站项目名为 vic-blog,使用 Wrangler 上传本地构建结果:

npx wrangler login
pnpm build
npx wrangler pages deploy dist --project-name vic-blog --branch main

首次使用 Wrangler 会进入 Cloudflare 登录授权。搭建自己的站点时替换项目名,已有项目可以先用 npx wrangler pages project list 核对。

本地构建再上传 dist,不要求把源码仓库公开。但浏览器收到的 HTML、CSS、JavaScript 和发布文章仍然公开,不能包含密钥。私有仓库不能保护被打包进前端的秘密。

本站当前 GitHub Actions 只负责构建检查,git push 本身不会完成生产部署,还要执行 Pages 上传命令。

2. 自定义域名要在两个地方对齐

本站正式域名是 vicwong.top,绑定流程如下:

  1. 在 Pages 项目的 Custom domains 添加域名。
  2. 按控制台提示创建或核对 DNS 记录。
  3. 等待域名与证书状态正常,再访问正式地址。

本站默认地址是 vic-blog-708.pages.dev,不要根据项目名猜测地址,以控制台分配值为准。根域名使用 Cloudflare 的 CNAME flattening,具体记录由绑定流程决定。

我踩过的坑是:只改 DNS,没有把域名加入 Pages 项目,会出现 522。这时要查 Pages 域名绑定与 DNS 是否指向同一项目。修改已有域名记录前,还要确认它是否承载其他服务。

3. 上传后检查正式网站

Windows PowerShell 可以这样检查:

curl.exe -I https://vicwong.top/
curl.exe -I https://vicwong.top/posts/blog-stack-guide
curl.exe -I https://vicwong.top/feed.xml

HTTP 200 只是第一步,还要打开页面核对标题、图片和交互。如果预览地址有新内容,正式域名却没有,继续查生产分支、域名绑定与缓存。

四、图床:R2 存对象,WebP Cloud 提供外链

1. 创建 R2 Bucket

在 Cloudflare R2 中创建 Bucket,本站名称是 vic-blog-images。首次启用 R2 是否需要支付方式,以账户控制台为准。

R2 通过 S3 兼容 API 接收上传,需要准备:

配置项示例
Bucketvic-blog-images
S3 Endpointhttps://<ACCOUNT_ID>.r2.cloudflarestorage.com
Regionauto
令牌权限Object Read & Write,仅指定图片 Bucket
凭证Access Key ID / Secret Access Key

凭证保存在本机上传工具或密码管理器中,不进入 Git、文章或公开截图。图示中的 <ACCOUNT_ID> 是占位符,不能原样填入。

2. 绑定图片源站域名

在 R2 Bucket 的自定义域名设置中绑定图片源站。本站使用 img.vicwong.top,它与网站域名负责不同内容。

先上传一张不含隐私的测试图,确认对象存在、Content-Type 正确、域名绑定正常,再接代理层。对象键例如:

images/2026/09/blog-stack-guide/architecture.png

路径保持简单、可复制。更新已发布图片时使用新文件名或内容哈希,避免覆盖后读者仍看到旧缓存。

3. 添加 WebP Cloud 代理

在 WebP Cloud 创建代理,Origin 指向自己的 R2 图片域名。本站代理入口是 https://397125f.webp.li,源站是 https://img.vicwong.top。

文章里留下的是:

https://397125f.webp.li/images/2026/09/blog-stack-guide/architecture.png

扩展名仍可能是 png,实际响应格式由代理设置和浏览器的 Accept 请求头决定。WebP Cloud 提供格式优化、缓存、EXIF 清理和水印能力;启用了哪些选项、当前额度多少,需要在自己的代理设置中核对。

接入代理不只是换一个访问入口。它会在观感几乎不变的前提下,把图片压到更小的体积:源站继续存原图,读者实际拿到的是优化后的响应。体积下来以后,回源和分发都更省流量,手机和慢网络下的打开速度也会顺一截。对本站这种配图不少、又想少花钱的个人博客,这一点比「多一个 CDN 域名」更重要。

本站源站配置了 WAF,仅允许符合 WebP Cloud User-Agent 的请求回源。因此普通浏览器直接访问源站返回 403 是预期行为。先验证代理回源正常,再加限制,否则可能把代理也挡住。

User-Agent 可以伪造,这只是减少普通直连和盗链的措施,不是身份认证,也不能用来保护私密图片。发布前仍应自行检查图片内容和元数据。

五、PicGo + Typora:粘贴图片就能得到外链

PicGo 配置示意:上传端使用 R2 Bucket、S3 Endpoint 和 auto Region,输出端使用 WebP Cloud 域名和对象键模板,密钥仅保存在本机

1. 配置 PicGo S3 插件

安装 PicGo 和提供 S3 上传能力的插件,本站使用 picgo-plugin-s3。填入 Bucket、Endpoint、Region 和本机凭证,再设置输出规则:

对象键:images/{year}/{month}/{sha256}.{extName}
输出 URL:https://397125f.webp.li/{path}

sha256 根据文件内容生成名称,避免不同图片同名覆盖。占位符属于插件能力,更换上传插件后要按新插件文档调整,不能假定所有工具都识别相同变量。

手工管理一篇教程的图片时,我也会使用 images/年/月/文章标识/文件名,本文配图就是这种方式。两种方式都保留普通对象路径,方便迁移。

先上传一张图,确认三个结果:R2 中有对象、PicGo 返回代理域名链接、浏览器能打开。尤其不要返回 r2.dev 或源站域名,否则会绕过代理,甚至被本站源站 WAF 拦住。

2. 让 Typora 调用 PicGo

进入 Typora 的“偏好设置 → 图像”:

  1. 插入图片时选择“上传图片”。
  2. 上传服务选择 PicGo 的图形客户端方式。
  3. 指定本机 PicGo.exe 路径。
  4. 点击“验证图片上传选项”,确保 PicGo 正常运行。
  5. 拖入本地图片,确认 Markdown 已变为 HTTPS 外链。

本站可执行文件位于 D:\Dev\scoop\apps\picgo\current\PicGo.exe,你的路径可能不同。建议先自动上传本地图片,外部图片按需处理,避免重复上传和版权问题。

最终链路是:Typora 插入图片 → PicGo 经 S3 API 上传 R2 → 返回 WebP Cloud URL → 写入 Markdown。

3. 保留一个命令行入口

维护教程配图时,也可以使用已登录的 Wrangler:

npx wrangler r2 object put vic-blog-images/images/2026/09/my-post/cover.png --file ./cover.png --content-type image/png --cache-control "public, max-age=31536000, immutable" --remote

--remote 表示操作远程 R2。immutable 适合发布后不覆盖的版本化文件;如果要在原路径修改图片,不要照搬一年缓存策略。

用代理 URL 检查上传结果:

curl.exe --noproxy '*' -I https://397125f.webp.li/images/2026/09/blog-stack-guide/architecture.png

检查状态码、Content-Type 和 x-webpcloud-cost 等代理响应头。如果当前网络必须依赖代理,去掉 --noproxy '*',分别记录两条访问路径的结果。

六、主仓库私有,也能接 GitHub 评论

本站使用 Giscus 和独立公开仓库 VicWong6/vic-blog-comments。这个仓库只放评论说明、Giscus 配置和 Discussions,不包含博客源码。

访客可以匿名阅读,发评论或回复需要 GitHub 登录并授权 Giscus,也可以直接在 GitHub Discussions 中回复。认证由 GitHub/Giscus 完成,博客没有自行维护密码、OAuth secret 或评论数据库。

1. 准备仓库和分类

  1. 创建公开评论仓库,例如 your-blog-comments。
  2. 在 Settings → General → Features 启用 Discussions。
  3. 安装 Giscus GitHub App,选择 Only select repositories,仅授权评论仓库。
  4. 使用 Announcements 类型的分类,让访客回复文章讨论,由站长或 Giscus 创建讨论。
  5. 在 Giscus 配置页面选择仓库和分类,获取 repoId、categoryId。

这些 ID 是公开标识,不是密钥。仓库名相同不意味着 ID 相同,重建仓库或分类后要重新读取。

2. 固定文章身份

本站通过 @giscus/vue 接入,关键参数如下,示例里的 ID 应换成真实值:

repo = your-name/your-blog-comments
repoId = 配置页面返回的仓库 ID
category = Announcements
categoryId = 配置页面返回的分类 ID
mapping = specific
term = 文章 frontmatter.commentId
strict = 1
lang = zh-CN

使用 specific 后,文章标题和当前 URL 不再决定评论归属,例如本文始终使用 /posts/blog-stack-guide。严格匹配通过讨论正文中的标题 SHA-1 标记,避免相似标题串到同一篇文章。

Giscus 自动创建的讨论带有这个标记。手工预建讨论时,应按 严格匹配文档 加入相同标记,否则讨论存在,组件仍可能找不到。

四个仓库和分类参数集中在 src/config/comments.ts,每篇文章只保留自己的 commentId。评论组件接近文末才加载,并跟随深浅色主题;换文章时重新关联评论,避免单页导航后还显示上一篇的讨论。

3. 限制嵌入来源

在评论仓库根目录放 giscus.json,例如:

{
  "origins": ["https://你的博客域名"],
  "defaultCommentOrder": "oldest"
}

本站额外允许实际 Pages 预览域名和本机开发地址,方便验收。origins 限制组件嵌入来源,不改变公开仓库的可见性。评论仍会公开显示在 GitHub,不要提交密钥或隐私材料。

4. 费用和取舍

这套方案是按「能省就省」搭的。本站目前真正花钱的只有自定义域名,一年十多块;Pages、R2、WebP Cloud 和 Giscus 都走免费额度,个人站点这个体量足够用。域名是为了好记、好对外,不是这套链路能跑起来的前提——如果接受 *.pages.dev 默认地址,图片也继续用服务商提供的代理域名,流量又不大,可以全程不花钱。

Giscus 官方说明为免费开源服务,这个评论方案不使用本站的 D1 或 Pages Functions,因此没有新增这两项的评论数据库/API 开销。它仍依赖 GitHub 和 Giscus 的可用性,GitHub 登录也是参与门槛。

免费额度不等于无限使用,各家计费也会变。本文不把某一天的价格写成长期承诺,启用前查看 R2 定价、Pages 限制 和 WebP Cloud 套餐。

七、以后怎么迁移

评论迁移:保留 commentId 和 SHA-1 标记,先导出备份,再原生转移讨论,更新集中配置,最后核对作者、回复、时间和数量

评论迁移到代码仓库

未来准备公开博客代码时,可以把讨论转到同一所有者下的代码仓库。目标必须先公开、启用 Discussions,并安装 Giscus;目标继续私有,就不能用 Giscus 承载公开评论。

迁移前先运行本站提供的 JSON 备份脚本:

pnpm exec tsx scripts/comments-backup.ts

脚本通过已登录的 GitHub CLI 只读查询,分页读取讨论、评论、回复,保存原始 ID、正文、作者、时间、链接和反应数量。默认输出到被 Git 忽略的 .cache/comments-backup/。它不下载正文中链接的附件,也不是 GitHub 原生备份格式;请另存到独立备份位置。

正式迁移流程:

  1. 暂停新留言,导出备份,记录文章与讨论的对应关系。
  2. 核对 GitHub 当前转移条件和权限,先对一篇使用 Transfer discussion。
  3. 保留讨论标题和正文 SHA-1 标记,核对作者、回复、时间、反应与目标分类,再转其余讨论。
  4. 更新集中配置中的 repo、repoId、category、categoryId,将 giscus.json 复制到目标仓库。
  5. 构建部署,逐篇确认评论关联正确,恢复留言。

优先使用 GitHub 原生转移,因为重新调用 API 发帖通常会改变显示作者和创建时间。具体条件以 GitHub 管理讨论文档 和当时界面为准,试迁通过后再继续操作。

讨论编号可能变化,所以本站不拿编号当文章身份。旧仓库不急着删除,等新入口和历史链接核对完再考虑归档。JSON 用于保留内容和核对,不承诺自动恢复出完全相同的历史身份。

图片迁移到其他存储

R2 的 S3 接口方便使用 rclone 等工具复制对象。仍使用同一个 WebP Cloud 入口时,可以先复制图片、保持对象键,再修改代理源站,抽样检查历史图片。

但 397125f.webp.li 是服务商提供的域名,我不能随意修改它的 DNS。更换底层存储可以改代理源站;离开 WebP Cloud 则需要替换文章外链,或提前规划自己持有的图片公开域名,并核对服务商的自定义域名支持和套餐要求。

至少保留本地原图和另一份独立副本。迁移优先 rclone copy,不要未经核对就使用会传播删除的 sync。对象存储的高耐久性无法防止误删、凭证泄漏或账户问题。

八、实际会遇到的问题

现象先查哪里
正式域名 522,Pages 地址正常Pages Custom domains 和 DNS 是否同时配置正确
源站图片 403,代理正常本站 WAF 预期行为,文章应使用代理 URL
PicGo 成功,文章图片打不开输出域名、对象键、Content-Type、代理回源和 WAF
修改头像后页签还是旧图favicon 独立缓存,换版本参数或文件名后重新部署
有文章页面,但首页/RSS 没出现日期、文章路径、列表条件和 feed 构建结果
评论区显示仓库或分类错误是否公开、启用 Discussions、安装 App,以及真实 ID
改标题后评论失联是否改了 commentId、讨论标题或严格匹配标记
本机评论正常,预览地址打不开giscus.json 的 origins/originsRegex

这套搭建过程给我的直接感受是:博客真正需要维护的是发布路径。文章在哪里、图片从哪里来、评论属于哪篇文章,这三件事保持清楚,换框架和服务时就有依据。

现在再写一篇文章,只需要准备 Markdown、上传配图、本地检查、提交代码、部署,然后打开正式页面和 RSS 验收。剩下的时间,留给内容本身。

评论

欢迎交流。阅读无需登录,发表评论需要登录 GitHub;评论会公开保存在 GitHub Discussions。

在 GitHub 中查看讨论 ↗ · 加载不出来时也可以从这里参与。

CC BY-NC-SA 4.0 2026-PRESENT © Vic Wong