系列帖子
前面的工作已经完成了 A Tour of Go 简体中文 103 个课程页面的翻译、结构校验、公共 UI 本地化以及生产部署基础设施。
到了这一阶段,项目虽然已经“能够访问”,但还缺少几个真正面向公开发布的部分:
- 独立的项目首页;
- 清晰的项目身份与非官方声明;
- GitHub、开发记录等公开入口;
- 统一的公共 Footer;
- 备案信息;
- 更明确的生产发布元数据;
- 一套能够安全更新、验证和回滚的正式部署流程。
2026 年 8 月 12 日,我集中完成了这一轮收尾,并将新版正式部署到了:
https://go-dev.shuijingwanwq.com
这次工作过程中还意外暴露出了两个很有价值的问题:
一是 CDN 缓存可能让一次完整发布出现“新 HTML 配旧 CSS”的混合状态;
二是“最近发布时间”如果直接保存在源码中,会形成一个很隐蔽的发布循环。
这一篇记录完整的处理过程。
一、让根路径真正成为项目首页
最初的 go-dev.shuijingwanwq.com 更接近一个直接运行 A Tour of Go 的站点。
但随着项目逐渐稳定,我开始觉得:
go-dev.shuijingwanwq.com不应该永远只等同于/tour/。
因为这个项目从一开始就不是只考虑一个简体中文版。
当前第一阶段确实只完成 zh-CN,但架构本身是按照多语言扩展设计的。以后除了 A Tour of Go,也可能继续整理其他 Go 官方学习内容的社区翻译。
因此最终确定:
/:整个多语言翻译项目的首页;/tour/*:A Tour of Go 课程;- 当前默认语言:简体中文;
- 后续新增语言时继续复用同一套首页结构。
首页也明确增加了项目身份说明:
非官方社区多语言翻译项目。
并进一步说明:
本项目与 Go 官方无隶属或授权关系,也不代表其认可或背书。
这既避免了用户误认为这是 Go 官方中文版,也给未来继续扩展其他语言留下了空间。

从最终页面可以看到,目前首页包含:
- 项目介绍;
- 当前翻译项目;
- 当前语言;
- 103 个课程页面;
- 最近发布时间;
- 官方源码基线;
- 上游提交时间;
- English 与简体中文入口;
- GitHub;
- GitHub Issues;
- 博客开发记录;
- 官方 upstream;
- 翻译与校验原则;
- 备案与版权信息。
其中“课程页面 103”并不是手工写死在页面中的展示数字,而来自当前发布结果。
这也意味着以后上游课程数量变化后,只要新的 projection 和 production bundle 正确生成,状态信息也可以继续跟着发布结果更新。
二、项目自己的 Logo 与 favicon
首页还加入了自己的站点 Logo。
这里没有去修改 A Tour of Go 课程内部原本的 Go Logo。
最终的区分是:
- 项目首页使用自己的 Logo;
/tour/*课程区域继续使用 Go 官方原有的 Tour 风格;- 浏览器 favicon 使用项目自己的图标。
Logo 文件直接放进 production bundle:
_content/images/site-logo.png
_content/images/site-logo-32.png
而不是在线依赖我的 WordPress 媒体站。
这样发布包本身就是完整的,不会因为外部图片域名异常导致项目品牌资源消失。
三、给课程页面增加统一的公共 Footer
项目首页有了之后,另一个问题马上显现出来:
课程页面本身仍然像一个完全独立的 Tour。
用户进入课程后,很难知道:
- 这是哪个社区项目;
- 项目主页在哪里;
- GitHub 在哪里;
- 开发记录在哪里。
因此这一轮还增加了统一 Footer:
非官方社区多语言翻译项目 · 首页 · GitHub · 开发记录
© 2026 永夜 | 蜀ICP备13001590号-1
但 Footer 的实现过程比想象中更容易踩坑。
四、Footer 最终没有采用 fixed
最开始曾尝试让 Footer 固定在浏览器底部。
这种方式视觉上比较稳定,但对于 A Tour of Go 这种左右双栏布局并不理想。
因为固定 Footer 会持续占据课程的纵向空间。
对于右侧代码编辑器尤其明显:
本来应该属于代码区域的空间,会永久被 Footer 吃掉一部分。
而且未来如果页面还需要加入其他内容,这种布局会越来越难维护。
最终我放弃 fixed 和 sticky,回到最简单的方案:
Footer 就是普通的自然文档流内容。
也就是说:
- 页面内容没有超过一屏时,Footer 自然出现在内容之后;
- 页面内容超过一屏时,滚动到页面末尾才能看到 Footer;
- 不使用 JavaScript 监听滚动;
- 不需要 Footer 高度补偿变量;
- 不需要给课程区域永久留出底部空间。
这是一个明显更简单的方案。
五、全宽 Footer 又暴露出右侧 Editor 越界
Footer 从左侧课程内容中移出去以后,又出现了另一个问题。
课程主体是左右双栏:
- 左边:教学正文;
- 右边:代码编辑器与运行结果。
当 Footer 放到整个双栏主体之后时,右侧编辑器曾经继续向下延伸了一小段,覆盖到了 Footer。
最终定位到这段布局:
#explorer + div {
top: 32px;
height: 100%;
}
问题在于:
- 元素已经通过
top: 32px下移; - 高度却仍然保持
100%。
因此实际底部自然会多出 32px。
最终修复为:
#explorer + div {
top: 32px;
height: calc(100% - 32px);
}
这样右侧 Editor 正好结束在双栏主体底部。

最终人工验收确认:
- 左右双栏正常;
- 右侧 Editor 不覆盖 Footer;
- Editor 内部滚动正常;
- 翻页导航位置正常;
- Footer 横跨整个 Tour 页面;
- 备案信息正常显示。
六、正式生成新的 production bundle
界面与公共信息验收完成后,进入正式生产发布。
本次用于发布的 Git 提交是:
acbf24a feat: 完善站点首页与公共项目信息
生成 production bundle 时显式传入:
published_at=2026-08-12T07:23:34Z
对应北京时间:
2026-08-12 15:23:34
发布结果:
locale=zh-CN
ready=103
pending=0
blocked=0
pages=103
articles=7
production metadata 还明确记录:
execution_transport: http-playground-proxy
execution_provider: go.dev
local_socket_enabled: false
其中最后一项非常重要:
生产环境没有开放本地
/socket代码执行接口。
代码运行继续通过远程 Go Playground 代理完成。
七、生产 release 权限再次显式规范化
前一次部署过程中已经踩过 Linux 文件权限的问题,因此这次没有再依赖 tar 包本身保存的权限。
新的 release 解压后统一处理:
- release 目录:
root:root、755 - 子目录:
755 bin/tour:755- 普通文件:
644 - 服务运行用户:
go-tour:go-tour
随后直接以 go-tour 用户验证:
- 二进制可执行;
release.json可读;_content文件可读。
确认没有权限问题后,才继续部署。
八、先独立启动,再切换 production
正式切换之前,新 release 并没有直接占用生产端口。
而是先临时运行在:
127.0.0.1:4005
独立验证:
//tour/list/tour/welcome/1- Logo
- favicon
全部 HTTP 200。
同时验证:
/socket
返回:
404
确认生产构建没有开启本地执行接口。
之后才原子切换:
/data/go-tour/current
从旧版本:
/data/go-tour/releases/20260811-zh-CN-925d59d
切换到:
/data/go-tour/releases/20260812-zh-CN-acbf24a
并重启正式 go-tour 服务。
九、为切换过程准备自动回滚
这次切换还加入了一个简单但很实用的保护。
切换完成后立即检查:
- systemd 服务是否
active; /是否 HTTP 200。
只有两项都满足,才认为切换成功。
否则脚本会自动:
- 恢复旧
current; - 重启旧版本;
- 输出失败日志。
最终没有触发回滚。
新版本直接正常启动。
十、源站与公网双重验收
正式服务启动后,没有只检查 127.0.0.1:3999。
我又分别测试了:
Nginx 源站
/
/tour/list
/tour/welcome/1
/images/site-logo-32.png
全部 HTTP 200。
公网 HTTPS
相同路径再次测试:
HTTP 200
首页也已经返回:
A Tour of Go 多语言翻译项目

截图中的最终状态包括:
locale: zh-CN
published_at: 2026-08-12T07:23:34Z
pages: 103
articles: 7
execution_transport: http-playground-proxy
execution_provider: go.dev
local_socket_enabled: False
以及:
/ HTTP 200
/tour/list HTTP 200
/tour/welcome/1 HTTP 200
十一、发布成功以后,却出现了一个很像代码 Bug 的页面
就在以为发布已经完成时,浏览器打开首页却出现了明显异常:
HTML 已经是新版,但页面几乎没有新首页的样式。
第一感觉很容易怀疑:
- CSS 路径错了;
- production bundle 少文件;
- 首页模板没有正确加载;
- Nginx 返回了错误内容。
但直接比较源站 CSS 和公网 CSS 后发现:
服务器访问源站得到的 app.css:
17171 bytes
服务器通过公网 EdgeOne 获取到的也是:
17171 bytes
SHA256 完全一致。
可是我自己的电脑访问同一个 URL:
14134 bytes
SHA256 完全不同。
这就直接证明:
不同 EdgeOne 节点正在返回不同版本的 CSS。
十二、一次站点级发布不能只想着刷新一个 CSS
进一步检查后又发现:
不仅首页 CSS 是旧的,课程页面的新 Footer 一开始也没有显示。
因此这已经不是单个静态资源的问题,而是一次完整站点发布后的多资源版本不一致:
- HTML 是新的;
- CSS 可能是旧的;
- Angular partial 也可能仍然处于旧缓存状态。
最终在 EdgeOne 中针对:
go-dev.shuijingwanwq.com
执行了 Hostname 级缓存清除,并选择:
直接删除
而不是“标记过期”。
清除之后,本地重新获取 CSS:
17171 bytes
SHA256 与源站完全一致。
首页随即恢复正常。
课程页面在 Firefox 禁用缓存并重新加载 Angular partial 后,Footer 也正常出现。
这次经历给正式发布流程增加了一条很明确的经验:
只要 production 发布涉及 HTML、CSS、Angular partial、Logo 等前端资源,版本切换完成后就应该同步处理对应 Hostname 的 CDN 缓存,否则可能出现新旧资源混合。
十三、另一个更隐蔽的问题:源码中的“最近发布”到底应该是什么时间?
生产首页增加“最近发布”后,又发现了一个架构问题。
本次 production bundle 的发布时间是:
2026-08-12T07:23:34Z
这个时间来自:
publish --published-at
它属于这个 production artifact。
最开始曾考虑:
部署完成以后,再把这个时间回写到源码里的
site-metadata.json。
但很快发现这会形成一个循环。
假设:
commit A
↓
构建 production bundle
↓
published_at = T1
↓
部署
↓
把 T1 回写源码
↓
产生 commit B
问题来了:
commit B 并不是 T1 时真正构建和部署的那个 artifact。
如果再从 commit B 发布:
commit B
↓
产生 T2
↓
再回写
↓
commit C
这样永远无法闭环。
十四、最终把开发态 metadata 与生产 metadata 分开
最终决定彻底区分两种语义。
源码中的:
_content/tour/site-metadata.json
现在明确表示:
开发态 metadata
其中加入:
"development": true
并且不再保存:
published_at
因此:
go run ./tour- full preview
- single-page preview
首页显示:
开发环境
而不是伪造一个生产发布时间。
production bundle 则仍然由:
publish --published-at
生成正式 metadata。
production 中:
- 不包含
development; - 必须包含合法的
published_at; - bundle 内
site-metadata.json; - 根目录
release.json
两者必须使用同一个发布时间。

当前源码可以清楚看到:
"development": true
同时没有 published_at。
对应提交为:
c20cc42 refactor: 区分开发与生产发布元数据
而上一条真正部署到 production 的提交仍然是:
acbf24a feat: 完善站点首页与公共项目信息
这种“GitHub main 比 production 领先一个内部重构提交”的状态完全正常。
没有必要为了保持 HEAD 和 production 完全一致,再做一次没有实际用户收益的部署。
十五、为什么 production metadata 必须属于 release
经过这一轮以后,这个边界已经很清晰:
源码 metadata
负责:
- 开发运行;
- preview;
- upstream 基线;
- 当前语言;
- 页面与 article 基础状态。
production bundle metadata
负责:
- 当前 production artifact;
published_at;- production runtime 首页展示。
release.json
负责:
描述同一个 production artifact 的发布清单。
因此以后发布流程不再需要:
发布完成 → 把 production 时间写回源码。
只需要:
源码
→ publish --published-at
→ production bundle
→ 部署
→ 验收
发布到这里就真正结束。
十六、当前最终状态
截至本次正式发布:
A Tour of Go 简体中文
ready = 103
pending = 0
blocked = 0
共:
103 个课程页面
7 个 article
正式站点
GitHub
production
/data/go-tour/releases/20260812-zh-CN-acbf24a
当前:
go-tour = active
旧 release 仍然保留:
/data/go-tour/releases/20260811-zh-CN-925d59d
用于需要时快速回滚。
十七、从“翻译完成”到“真正可以公开使用”
这一阶段给我的感觉很明显:
103 个页面全部翻译完成,并不等于项目已经真正完成。
真正面向公开用户以后,还必须继续处理:
- 项目身份;
- 首页入口;
- 公共导航;
- Footer;
- 备案;
- GitHub 与开发记录互链;
- production metadata;
- 发布包;
- 文件权限;
- systemd;
- 原子切换;
- 回滚;
- 源站验收;
- CDN;
- 浏览器缓存;
- 开发态与生产态状态边界。
其中很多东西并不属于“翻译”,却直接决定了这个项目最后是不是一个可以长期维护的公开服务。
这也是为什么我最终没有把 go-dev 只做成一个“打开就是教程”的页面。
它现在开始真正承担:
Go 官方学习内容社区多语言翻译项目入口
这个角色。
A Tour of Go 简体中文只是第一阶段。
后续如果继续增加新的语言或者新的 Go 官方学习内容,这次整理出来的首页、发布元数据和生产部署结构都可以继续复用。
A Tour of Go 多语言翻译项目
本系列完整记录 A Tour of Go 多语言翻译项目从架构设计、整页翻译、结构保护、自动校验,到生产发布与后续维护的实际开发过程。
项目入口:
✅ 在线学习:A Tour of Go 简体中文版
✅ 项目源码:GitHub:shuijingwan/go-tour-i18n
当前第一阶段已完成简体中文版本。项目属于非官方社区多语言翻译项目,与 Go 官方无隶属或授权关系。


发表回复