本文大纲
这篇文章不是代码教程,更像是一次建站复盘。
我想记录的重点是:我一开始想做什么、我是怎么借助 Codex 把网站做出来的,以及后来怎么一步步处理 VPS、域名、HTTPS、Google Search 和日常维护。以后如果要重装服务器、迁移网站,或者再搭一个类似的站,回来翻这篇就够了。
一开始,我想要的不是一个博客模板
最初的想法其实很简单:我想有一个真正属于自己的地方,长期记录游戏开发、软件工程、产品设计,还有生活里的想法。
但做着做着,要求也逐渐具体了起来:
- 网站叫「未名笔记」,英文名是 Notes by Wei;
- 中文和英文都要能完整阅读,而不是只翻译几个菜单;
- 文章用 Markdown 写,代码、图片、视频和表格都能正常展示;
- 内容可以按两级栏目整理,比如“工程札记 / 游戏开发”;
- 我需要一个后台,能自己写文章、上传封面、管理栏目;
- 首页要有一点个人风格,支持手机、电脑和深色模式;
- 最终必须部署到自己的 VPS,用自己的域名;
- 数据和图片不能因为更新网站就丢失,还要有备份;
- Google 能发现文章,以后也可以继续尝试 AdSense。
所以最后做出来的,其实已经不只是一个博客页面,而是一套很小的双语内容管理系统。
网站是怎么做出来的
代码实现这部分我没有从零手写,而是把需求、页面效果和修改意见交给 Codex,让它帮我完成开发和迭代。
项目最终使用 React、TypeScript、vinext 和 Markdown。Codex 帮我搭好了中英文页面、文章和栏目后台、深色模式、代码高亮、文章目录、图片视频展示,以及 VPS 和 Cloudflare 两套运行方式。我主要做的是提出需求、查看实际效果、指出哪里不对,然后决定下一步要加什么。
代码保存在 GitHub,并用 Git 记录每一次修改。第一篇正式发布的长文是 UE4 UI 输入路由的源码分析,中英文正文和图片也都保留在仓库的 content/ 目录中。
实现细节以后可以直接看代码,这里就不展开了。真正花心思的是怎样把它从“本地能打开”变成一个可以长期运行的网站。
为什么最后选择自己的 VPS
项目最开始可以运行在 Sites/Cloudflare 环境里,数据使用 D1,图片使用 R2。后来我还是希望网站尽量掌握在自己手里,所以又让 Codex 做了一套 VPS 运行方式:
- 网站本身由 Node.js 运行;
- 文章和栏目保存在 SQLite;
- 图片保存在服务器本地目录;
- Caddy 负责域名、HTTPS 和反向代理;
- systemd 负责开机启动、故障重启和定时备份。
原来的 Cloudflare 版本没有删,只是正式站最终运行在 VPS 上。
本地开发时,常用的命令是:
npm run dev:vps
npm run build:vps
第二条命令会生成可以上传服务器的 dist/standalone/。
我怎么处理 VPS
先准备一台干净的服务器
这次使用的是 Debian 12 x86_64 VPS,公网 IP 是 65.49.208.223。服务器配置不高,所以目标一直是简单、稳定、少依赖,而不是在上面堆很多服务。
仓库里的 deploy/bootstrap-server.sh 用来初始化新服务器。它会安装 Node.js、Caddy、SQLite、rsync、UFW 等工具,还会创建专门运行博客的 weiming 系统用户。
因为这台 VPS 内存比较小,脚本还额外创建了 1.5 GiB swap,避免系统升级或短时间流量增加时直接耗尽内存。
初始化时,我先把脚本传到服务器再运行:
export BLOG_VPS_IP="65.49.208.223"
scp deploy/bootstrap-server.sh root@"$BLOG_VPS_IP":/root/
ssh root@"$BLOG_VPS_IP" 'bash /root/bootstrap-server.sh'
防火墙只开放三个端口:
- 22:SSH;
- 80:HTTP;
- 443:HTTPS。
Node.js 应用本身只监听 127.0.0.1:3000,不会直接暴露到公网。
把程序和数据分开
服务器上的目录大概是这样:
/opt/weiming-blog/
├── releases/<timestamp>/
└── current -> releases/current-version
/var/lib/weiming-blog/
├── blog.sqlite
└── media/
/var/backups/weiming-blog/
/opt 下面只放程序,每次发布一个新目录;/var/lib 下面才是真正不能丢的数据库和图片。
这样更新网站时只需要切换 current 软链接,不会碰到文章数据。某个版本有问题时,也可以重新指回上一个 release。
发布一个新版本
我先在 Mac 上安装依赖并构建:
npm ci
npm run build:vps
然后创建一个带时间戳的新 release,把 dist/standalone/ 上传上去:
export BLOG_VPS_IP="65.49.208.223"
export BLOG_RELEASE="$(date -u +%Y%m%dT%H%M%SZ)"
ssh root@"$BLOG_VPS_IP" \
"install -d -m 0755 /opt/weiming-blog/releases/$BLOG_RELEASE"
rsync -az dist/standalone/ \
root@"$BLOG_VPS_IP":"/opt/weiming-blog/releases/$BLOG_RELEASE/"
ssh root@"$BLOG_VPS_IP" \
"ln -sfn /opt/weiming-blog/releases/$BLOG_RELEASE /opt/weiming-blog/current && systemctl restart weiming-blog"
更新后,我会先看服务状态和日志:
ssh root@65.49.208.223 \
'systemctl status weiming-blog --no-pager'
ssh root@65.49.208.223 \
'journalctl -u weiming-blog -n 100 --no-pager'
让网站一直运行
博客由 weiming-blog.service 管理。服务器重启后它会自动启动,程序崩溃时也会自动重试。
服务不是用 root 身份运行,而是使用前面创建的 weiming 用户。这个用户只能写 /var/lib/weiming-blog,其他系统目录基本都是只读的。这样即使应用出问题,影响范围也会小一些。
给后台加密码
公网首页可以直接访问,但 /admin 和文章写接口不能暴露。
我把这层保护放在 Caddy 上,用 Basic Auth 验证。密码不会明文写进配置,而是在服务器上先生成哈希:
caddy hash-password
然后只把生成的哈希填进 /etc/caddy/Caddyfile。
每次修改 Caddy,都先验证配置再重载:
caddy validate --config /etc/caddy/Caddyfile
systemctl reload caddy
仓库里还留了一份 Caddyfile.locked。如果后台密码还没有配好,可以先用这份配置直接关闭后台和写接口,避免出现几分钟的无密码状态。
每天自动备份
数据库和图片每天自动备份一次:
- SQLite 使用自己的
.backup命令生成一致性副本; media/打包成压缩文件;- VPS 上只保留最近 14 天。
需要时可以手动触发:
ssh root@65.49.208.223 \
'systemctl start weiming-blog-backup.service'
不过,同一台服务器里的备份不算真正的灾难备份。VPS 整机损坏时,它们还是会一起丢失。所以长期来看,还需要定期把 /var/backups/weiming-blog/ 下载到本地,或者同步到另一处对象存储。
正式域名接入前,我先用了 nip.io
一开始我没有立刻折腾正式域名,而是先用了:
weiming-65-49-208-223.nip.io
nip.io 会把主机名里的 IP 直接解析到服务器。这样我可以先确认:
- VPS 的 80 和 443 端口能不能访问;
- Caddy 能不能正常申请 HTTPS 证书;
- Node.js 和反向代理有没有问题;
- 后台密码是否真的生效;
- 重启服务后数据库和图片还在不在。
先用临时域名跑通非常有用。如果这一步没问题,后面正式域名访问失败时,基本就可以把排查重点放在 DNS 和域名配置上。
我怎么处理 notesbywei.com
正式域名是 notesbywei.com,DNS 放在 Cloudflare 管理。
最主要的两条记录是:
| 类型 | 名称 | 指向 |
|---|---|---|
| A | @ | 65.49.208.223 |
| CNAME | www | notesbywei.com |
根域名直接指向 VPS,www 跟随根域。至于 Cloudflare 当时使用橙云代理还是仅 DNS,Git 仓库里不会留下记录,所以以后应直接在 Cloudflare 控制台确认。
DNS 生效以后,我做了几件事:
- 把 Caddy 的主站域名改成
notesbywei.com; - 让
www.notesbywei.com永久跳转到根域; - 让旧的 nip.io 地址也跳转到正式域名;
- 把网站内部的统一基准地址改成
https://notesbywei.com; - 重新加载 Caddy,让它自动申请正式域名的 HTTPS 证书。
最后用 curl 检查:
curl -I https://notesbywei.com/
curl -I https://www.notesbywei.com/
现在主域名正常返回页面,www 会用 301 跳转到根域。这样访客和搜索引擎看到的始终是同一个正式地址。
我怎么处理 Google Search
这一块很容易和 AdSense 混在一起,其实它们不是一回事。
Google Search 的目标是让 Google 能找到并理解网站;AdSense 是广告审核和投放。搜索收录不要求先开通 AdSense。
网站这边先准备好
我让 Codex 把搜索引擎需要的基础内容补齐,主要包括:
robots.txt:允许抓取公开页面,屏蔽后台和 API;sitemap.xml:自动列出首页、栏目、文章、关于和隐私页;- 中文和英文页面之间的
hreflang; - 每个页面自己的 title、description 和 canonical;
- 文章的发布时间、作者、封面等结构化数据;
- RSS:
feed.xml; - 旧域名和
www到正式域名的 301 跳转。
线上可以直接查看:
- https://notesbywei.com/robots.txt
- https://notesbywei.com/sitemap.xml
- https://notesbywei.com/feed.xml
这些东西不会保证文章一定被收录,但至少让 Google 知道网站有哪些页面、哪个地址才是正式地址,以及中文和英文文章之间是什么关系。
然后去 Search Console
Search Console 里的操作不会记录在 Git 仓库里,所以以后最好按下面这份清单重新确认一次:
- 打开 https://search.google.com/search-console;
- 添加一个 Domain property,填写
notesbywei.com; - Google 会给出一条
google-site-verification=...的 TXT 记录; - 到 Cloudflare 的 DNS 页面,把这条 TXT 加到根域;
- 等待 DNS 生效后,回到 Search Console 点击验证;
- 验证成功后保留 TXT,不要随手删除;
- 在 Sitemaps 页面提交
sitemap.xml; - 用 URL Inspection 检查首页和第一篇中英文文章;
- 如果页面还没被发现,可以点击 Request indexing;
- 之后主要看 Pages、Sitemaps 和 Crawl stats 有没有异常。
我选择 Domain property,是因为它会一起覆盖根域、www、HTTP 和 HTTPS,不需要分别建好几个资源。
Google 收录通常不是马上发生的。提交 sitemap 只是告诉 Google 页面在哪里,并不等于保证收录。新站等几天甚至几周都很正常,反复点击 Request indexing 也不会让它明显变快。
官方说明:
AdSense 目前做到哪一步
网站已经为 AdSense 留好了接口,包括广告脚本、广告位、ads.txt 和中英文隐私政策。但这只代表“代码可以接入”,不代表网站已经通过审核。
以后正式申请时,还需要我自己在 AdSense 后台添加 notesbywei.com,拿到真实的 ca-pub-... 和广告位 ID,再把它们放进服务器环境变量。真实 ID 不应该提交到 Git。
如果网站面向需要用户同意的地区,还要另外配置 Google 认可的 CMP。这个步骤和 Search Console 的收录没有直接关系。
以后怎么更新文章和网站
平时发布内容,大概按这个顺序:
- 准备中文 Markdown、英文 Markdown 和图片;
- 本地预览两种语言;
- 运行检查和构建;
- 提交 Git,推送到 GitHub;
- 构建新的 VPS release;
- 上传服务器,切换
current,重启服务; - 检查首页、文章、图片、sitemap 和 RSS;
- 重要文章可以去 Search Console 请求一次索引。
本地常用命令:
npm ci
npm run dev:vps
npm run lint
npm test
npm run build:vps
服务器常用命令:
ssh root@65.49.208.223 \
'systemctl status weiming-blog caddy --no-pager'
ssh root@65.49.208.223 \
'journalctl -u weiming-blog -n 100 --no-pager'
ssh root@65.49.208.223 \
'systemctl start weiming-blog-backup.service'
ssh root@65.49.208.223 \
'caddy validate --config /etc/caddy/Caddyfile && systemctl reload caddy'
这次实际用到的工具
回头看,这次建站真正用到的工具并没有特别夸张:
- Codex:实现网站和协助排查、部署;
- Git、GitHub:保存代码和修改历史;
- Node.js、npm:开发和构建;
- React、TypeScript、vinext:网站本身;
- SQLite:正式文章数据;
- SSH、rsync:连接和更新 VPS;
- Debian、systemd、UFW:服务器运行和安全;
- Caddy:HTTPS、反向代理、后台密码和域名跳转;
- Cloudflare:DNS 管理;
- Google Search Console:搜索收录;
curl、dig:上线后的检查。
最后回头看
这次最重要的并不是写出了多少代码,而是把整条链路走通了:
从提出需求开始,我先用 Codex 把网站实现出来,再完成本地验证、VPS 准备和临时域名测试;最后接入正式域名与 HTTPS,配置搜索发现,并建立更新和备份流程。
现在我控制着代码、服务器、域名、数据库和内容源文件。以后即使换服务器或换部署平台,文章和整个网站也不会被锁在某个现成博客服务里。
还有几件事值得定期确认:Search Console 是否已经验证、sitemap 是否显示成功、异地备份是否正常,以及 SSH 是否已经限制密码登录和 root 直登。把这些补齐之后,这个站才算真正进入长期维护阶段。
