写完第一篇文章后,我又花了一些时间补齐博客的基础设施:域名、头像、图床、编辑器上传,以及文章下面的评论区。
我的目标很具体:平时只管写 Markdown,图片粘进去就能上传;网站代码暂时保持私有,读者仍然可以登录 GitHub 留言;以后换存储、换框架,或者把评论搬进代码仓库,尽量不用重做一遍。还有一条约束:能省就省。个人博客没必要一上来就堆付费服务。
这篇文章记录的是本站在 2026 年 9 月 7 日 的实现。配图根据实际配置绘制,不是控制台截图。你照着搭建时,需要换成自己的域名、仓库和账户信息。
一、先看懂三条链路 #

| 层次 | 本站选择 | 负责什么 |
|---|---|---|
| 内容 | 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 Discussions | GitHub 登录、回复、反应与评论管理 |
| 订阅 | 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 buildpnpm 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,绑定流程如下:
- 在 Pages 项目的 Custom domains 添加域名。
- 按控制台提示创建或核对 DNS 记录。
- 等待域名与证书状态正常,再访问正式地址。
本站默认地址是 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.xmlHTTP 200 只是第一步,还要打开页面核对标题、图片和交互。如果预览地址有新内容,正式域名却没有,继续查生产分支、域名绑定与缓存。
四、图床:R2 存对象,WebP Cloud 提供外链 #
1. 创建 R2 Bucket #
在 Cloudflare R2 中创建 Bucket,本站名称是 vic-blog-images。首次启用 R2 是否需要支付方式,以账户控制台为准。
R2 通过 S3 兼容 API 接收上传,需要准备:
| 配置项 | 示例 |
|---|---|
| Bucket | vic-blog-images |
| S3 Endpoint | https://<ACCOUNT_ID>.r2.cloudflarestorage.com |
| Region | auto |
| 令牌权限 | 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:粘贴图片就能得到外链 #

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 的“偏好设置 → 图像”:
- 插入图片时选择“上传图片”。
- 上传服务选择 PicGo 的图形客户端方式。
- 指定本机 PicGo.exe 路径。
- 点击“验证图片上传选项”,确保 PicGo 正常运行。
- 拖入本地图片,确认 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. 准备仓库和分类 #
- 创建公开评论仓库,例如 your-blog-comments。
- 在 Settings → General → Features 启用 Discussions。
- 安装 Giscus GitHub App,选择 Only select repositories,仅授权评论仓库。
- 使用 Announcements 类型的分类,让访客回复文章讨论,由站长或 Giscus 创建讨论。
- 在 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 套餐。
七、以后怎么迁移 #

评论迁移到代码仓库 #
未来准备公开博客代码时,可以把讨论转到同一所有者下的代码仓库。目标必须先公开、启用 Discussions,并安装 Giscus;目标继续私有,就不能用 Giscus 承载公开评论。
迁移前先运行本站提供的 JSON 备份脚本:
pnpm exec tsx scripts/comments-backup.ts脚本通过已登录的 GitHub CLI 只读查询,分页读取讨论、评论、回复,保存原始 ID、正文、作者、时间、链接和反应数量。默认输出到被 Git 忽略的 .cache/comments-backup/。它不下载正文中链接的附件,也不是 GitHub 原生备份格式;请另存到独立备份位置。
正式迁移流程:
- 暂停新留言,导出备份,记录文章与讨论的对应关系。
- 核对 GitHub 当前转移条件和权限,先对一篇使用 Transfer discussion。
- 保留讨论标题和正文 SHA-1 标记,核对作者、回复、时间、反应与目标分类,再转其余讨论。
- 更新集中配置中的 repo、repoId、category、categoryId,将 giscus.json 复制到目标仓库。
- 构建部署,逐篇确认评论关联正确,恢复留言。
优先使用 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 中查看讨论 ↗ · 加载不出来时也可以从这里参与。