没有不值得去解决的问题,也没有不值得去学习的技术!

为了 AdSense,要不要放弃 SPA?A Tour of Go 页面架构的一次取舍

图 2:golang/website 当前的 _content/tour/。课程内容、静态资源和模板都在这个上游目录中持续维护。

作者:

A Tour of Go 多语言翻译项目

图3:访问过去的 Go Tour 简体中文站 tour.go-zh.org 时,当前已经无法正常建立连接。

(1) 从「A Tour of Go 中文版」这个搜索词开始:我决定做一个持续维护的 Go Tour 中文版

本地运行的 A Tour of Go「Methods continued」课程页面,左侧为课程说明,右侧为 Go 代码编辑器及运行结果。

(2) A Tour of Go 中文版项目设计冻结:从 101 页到 103 页,从 Gin 转向 Cobra CLI

A Tour of Go 中文版开发实战:英文基线、在线运行与 101 页上游同步体系

(3) A Tour of Go 中文版开发实战:英文基线、在线运行与 101 页上游同步体系

go-tour-i18n 项目完成首个简体中文课程页面 welcome/1 的整页翻译、结构校验和本地预览。

(4) A Tour of Go 多语言翻译项目实录:完成首个 zh-CN 页面翻译闭环

图 1:DeepSeek 官方更新日志显示,本次只更新 DeepSeek-V4-Flash,DeepSeek-V4-Pro API 与 APP、Web 模型均未更新

(5) A Tour of Go 中文翻译项目进展:完成前 8 页、修复 present 语法,并暂缓 DeepSeek 对比

图 1:generics/1 简体中文页面本地预览

(6) 一个页面重试五次:使用 GLM-5.2 翻译 A Tour of Go 时遇到的 Token 与 Present 结构问题

图 1:methods/16 简体中文页面浏览器预览

(7) A Tour of Go 中文翻译实录:如何只翻译教学代码注释,而不破坏 Go 代码

图 2:A Tour of Go methods/20「练习:错误」中文候选页面最终渲染效果

(8) A Tour of Go 多语言翻译:当正确的中文语序被受保护标记顺序校验误判

图 3:methods/24 中文页面,静态代码、Bounds 行内代码和链接内的 image.Rectangle 均正确渲染

(9) A Tour of Go 中文翻译完成 7 个代表页校准:最后 3 页又发现了哪些真实问题

图 2:首批 10 个普通页面经过试跑与流程校准后全部进入 ready

(10) A Tour of Go 多语言翻译项目:首批 10 个普通页面试跑,从 2 个 blocked 到全部 ready

图 2:flowcontrol/6 最终中文页面,return 与 v 为适应自然中文语序发生整体换位

(11) A Tour of Go 中文翻译第二批实跑:从 Inline Code 顺序误判到历史响应重新验证

图 1:flowcontrol/6 首次原始输入实验直接通过统一自动校验

(12) A Tour of Go 多语言翻译项目:从 swap 问题重新审视 Token 保护,原始输入与最小保护的一次真实实验

图 7:moretypes/1 最终中文页面预览。左侧课程正文、静态代码块、行内代码和教学注释均正常渲染,右侧官方 Go 示例继续保持原样。

(13) A Tour of Go 多语言翻译实战:第三批 10 页全部 Ready,继续校准 Protected Token 的结构角色

图 4:上游源码确认共有 103 个课程页面,zh-CN 最终达到 ready=103、pending=0、blocked=0

(14) A Tour of Go 多语言翻译项目:zh-CN 103 个课程页面全部 ready,课程正文翻译阶段完成

图 4:GitHub 与 ChatGPT 已成功连接

(15) ChatGPT 连接 GitHub 实测:让 AI 直接读取 go-tour-i18n 仓库参与译文审核

图 3:中文 Go 术语校准完成后,将确定的译法写入 zh-CN glossary

(16) 为什么 channel 最终选择“通道”:A Tour of Go 中文术语统一的一次实践

图 1:技术含义基本正确,但“返回一个返回……”已经明显影响阅读,因此仍被列入 C 类修订

(17) 103 页逐页审核后,只返修 16 页:A Tour of Go 中文正文发布前质量审计

图 2:中文课程页面以及已经完成本地化的编辑器控制区

(18) A Tour of Go 多语言翻译项目:公共 UI 本地化完成,从课程译文走向完整中文界面

图 2:完整 zh-CN 正式投影成功生成,103 个课程页面被重新组装为 7 个 .article

(19) 从 103/103 ready 到完整站点验收:A Tour of Go 中文版补齐正式投影与课程元数据本地化

图 3:从 production bundle 独立启动的 A Tour of Go 简体中文页面,课程正文、公共 UI 和官方 Go 示例均已进入最终发布形态。

(20) A Tour of Go 多语言翻译项目:103 页全部完成后,我终于生成了可独立部署的 zh-CN Production Bundle

图 1:A Tour of Go 简体中文版正式生产页面,远程运行已经成功输出“Hello, 世界”。

(21) A Tour of Go 多语言翻译项目:zh-CN 正式上线,从生产发布到 go.dev Playground 的完整部署记录

图 1:新生产域名 go-dev.shuijingwanwq.com 已正常访问,Go 示例远程运行成功

(22) A Tour of Go 多语言翻译项目:从 go-tour 到 go-dev,完成 EdgeOne、Nginx 与 HTTPS 生产域名迁移

图 1:A Tour of Go 多语言翻译项目正式生产首页

(23) A Tour of Go 多语言翻译项目正式上线:从项目首页、公共 Footer 到生产发布元数据的完整收尾

图 3:GA4 g/collect 请求返回 HTTP 204,并携带当前课程页面地址,确认 Google Analytics 已产生真实事件上报

(24) A Tour of Go 多语言翻译项目:接入 Google Analytics 与百度统计,从 systemd 注入到 EdgeOne 与真实上报验证

图 1:生产环境 robots.txt 与 sitemap.xml 已正确对外提供,Sitemap 共包含 104 个规范 URL。

(25) A Tour of Go 中文站上线后:补齐 robots.txt、Sitemap,并完成五大搜索引擎提交

图 4:发布生产并刷新 EdgeOne 缓存后,再次通过真实手机访问,顶栏项目名称已经完整保持在一行,最终移动端验收通过。

(26) A Tour of Go 多语言翻译项目:修复手机端首页顶栏标题换行,并完成真机验证

图 3:生产自动部署脚本第一次真实运行时,systemd 已经显示 active,但首次 localhost 探测仍然得到 HTTP 000;随后连续 3 次同时满足 active + HTTP 200 后,脚本才判定部署成功,并继续完成公网 HTTP 200 验收。这次真实运行验证了连续应用层健康检查的必要性。

(27) A Tour of Go 多语言翻译项目:从一次 203/EXEC 故障到生产自动部署脚本落地

图 1:阿里云生产环境访问 go.dev Playground 时出现 TLS 握手超时

(28) A Tour of Go 多语言翻译项目:为 Playground 搭建独立的 ZgoCloud 执行代理

图 3:清除 /tour/script.js 缓存后,Run 已直接请求 ZgoCloud /compile 并返回 HTTP 200

(29) A Tour of Go 多语言翻译项目:将生产 Run / Format 切换到 ZgoCloud 执行节点

【图 1:微信中运行 Go 示例时出现 Error communicating with remote server.】

(30) A Tour of Go 中文版手机端运行与格式化偶发失败排查:从间歇性报错到补强 Nginx 可观测性

【图 4:向 golang-dev 公开邮件列表发送 Playground 使用告知邮件并投递成功】

(31) A Tour of Go 多语言项目:为 Playground 代理增加唯一 User-Agent,并主动告知 Go 官方

【图 3:百度剩余 9 个核心 URL 提交成功,remain 归零】

(32) A Tour of Go 搜索引擎收录实测:从 Google 自然索引到百度主动提交与 Bing IndexNow

【图 1:当前正式中文 methods/24 页面,右侧示例代码运行成功】

(33) A Tour of Go 中文翻译完成后,我为什么重新评估 minimal-protect 翻译模式

【图 3:5 个页面 Request SHA256 完全一致,但 Response SHA256 全部不同】

(34) A Tour of Go 翻译实验:更多原始上下文为什么没有更好?从 157 个保护标记到 30 次重复盲评

图 3:3 个代表页 × 4 个评审模型,共 12 次匿名评审的完整排名与第一名票数。

(35) A Tour of Go 翻译质量再评估:minimal-protect 未达预期后,我开始比较 ChatGPT、Codex 与 GLM-5.2

图 2:GitHub 中的 chatgpt-zh-CN-008 重译批次。一个 Batch 同时保存 inputs、raw-responses、candidates、validation 和 retry 记录,GitHub 成为 ChatGPT 与本地翻译流水线之间的数据交接层。

(36) 没有 OpenAI API,我如何用 ChatGPT + GitHub 完成 A Tour of Go 103 页全量重译

图 1:103 个正式课程页面完成最终语义质量审核,结果为 A=103、B/C/D=0,同时不存在缺失、重复或额外页面。A/B/C/D 是本轮项目内部语义质量分级,并不是自动结构 validator 的结果。

(37) 103 页翻完还不能发布:A Tour of Go ChatGPT 译文的语义审核与 Canonical Promotion

图 3:7 个 article endpoint 的 production origin 与 EdgeOne 公网响应全部 exact match,同时新的 ChatGPT 译文标记存在,旧版译文标记已经消失。

(38) 103/103 与 7/7 Exact Match:A Tour of Go ChatGPT 新译文的最终线上验收

【图 2:upstream source preview】

(39) A Tour of Go 多语言翻译项目首次实现 upstream 同步机制:从一次性翻译到长期维护

图 2:A/B/C/D 质量评级规则,展示正式质量 rubric 与 Promotion gate。

(40) A Tour of Go 多语言翻译项目引入 Translation Quality Review:从自动验证到 AI 翻译质量控制

图 4:翻译后 Playground Example,展示中文注释与保持不变的 Go 代码结构。

(41) A Tour of Go 多语言翻译项目实现 Playground Example 本地化:从代码注释翻译到完整学习体验

图 5:公网页面验证广告脚本输出

(42) A Tour of Go 多语言翻译项目接入广告系统:从 AdSense 专用配置到通用 HTML 注入架构

图 3:百度统计单页应用设置

(43) go-dev 单页网站接入统计与 Google AdSense Auto Ads 验证记录

图 1:GitHub 仓库中的多语言翻译项目结构

(44) A Tour of Go 多语言翻译项目:选择日语作为第二语言并启动翻译验证

图 5:模型身份描述不一致

(45) A Tour of Go 多语言翻译项目中的 AI 协作异常排查:一次长期工程会话失效分析

【图 1:ChatGPT GitHub 写入流程调整说明截图】

(46) go-tour-i18n 翻译流程调整:从 ChatGPT GitHub 写入到本地 artifact 导入

图 4 展示了本次异常状态。

(47) go-tour-i18n 翻译自动化流程复盘:已验证的 ChatGPT 翻译能力在新任务中的稳定性问题

【图 6:全部评审结束以后才读取 secret key,完成最终揭盲】

(48) A Tour of Go 翻译质量再评估:Codex High 已接近 ChatGPT High,我决定调整默认翻译引擎

图 4:ja-JP 生产课程页实际运行 Go 示例

(49) A Tour of Go 日语版正式上线:第二门语言 ja-JP 完成生产发布

图 1:ja-JP 上线以后,先提交“新增语言标准化流程”

(50) A Tour of Go 多语言扩展复盘:为第三门语言建立标准化流程

图 1:课程正文结束以后,页面左侧仍然存在大块视觉空白,但 AdSense 并没有在这里插入展示广告。

(51) A Tour of Go 明明有大片空白,为什么 AdSense 就是不显示展示广告?

图 2:golang/website 当前的 _content/tour/。课程内容、静态资源和模板都在这个上游目录中持续维护。

(52) 为了 AdSense,要不要放弃 SPA?A Tour of Go 页面架构的一次取舍

上一篇文章里,我记录了一个看起来很反直觉的问题:

A Tour of Go 课程页面明明存在大片视觉空白,Google AdSense 的 Auto Ads 也已经启用,adsbygoogle.js 同样正常加载,但普通的页面展示广告始终没有出现。

进一步查看页面结构后,我逐渐把问题收敛到了 A Tour of Go 本身比较特殊的 DOM 和布局方式上。

这时候,一个很自然的想法出现了:

既然现有 SPA 页面结构不利于广告展示,要不要干脆放弃 SPA,把每一节课程改成传统的独立页面?

单纯从 AdSense 的角度看,这个方案其实相当有吸引力。

但真正开始评估以后,我发现问题远没有“页面刷不刷新”这么简单。

最终决定保留 SPA,最大的原因甚至不是页面切换速度,而是:

自己的多语言版本还需要长期跟踪和同步官方 A Tour of Go 上游。如果为了广告把页面架构改成另一套实现,短期得到的是方便,长期增加的却可能是一笔持续累积的维护成本。

A Tour of Go 的“下一页”本来就不是整页刷新

首先需要确认一件最基本的事情:

官方 A Tour of Go 本身就是 SPA 式的课程导航。

为了确认这一点,我打开:

Plaintext
https://go.dev/tour/welcome/1

然后在浏览器开发者工具的 Network 中只保留 HTML 请求,再点击“下一页”。

页面很快进入:

Plaintext
https://go.dev/tour/welcome/2

课程正文也已经完全切换。

但是 Network 中没有出现新的 Document / HTML 请求。

图 1:官方 A Tour of Go 从 welcome/1 切换到 welcome/2 后,地址和课程内容已经改变,但 Network 中没有新的 HTML 文档请求。
图 1:官方 A Tour of Go 从 welcome/1 切换到 welcome/2 后,地址和课程内容已经改变,但 Network 中没有新的 HTML 文档请求。

这意味着,“下一页”并不是:

Plaintext
浏览器请求 /tour/welcome/2

服务器返回完整 HTML

浏览器重新加载页面

而更接近:

Plaintext
现有页面

Angular 处理导航

切换课程数据与视图

URL 更新

继续使用当前 Document

从用户体验来看,这种方式非常自然。

课程切换快,不需要每次重新加载整个站点。

代码编辑器、课程目录以及其他前端状态也都处于同一个应用生命周期中。

但这恰好也给 AdSense 带来了麻烦。

对普通网站很自然的广告生命周期,在 SPA 中并不存在

对于传统多页面网站,广告的逻辑其实很简单。

用户打开第一篇文章:

Plaintext
GET /article/1

完整 HTML

AdSense 初始化

广告请求

点击下一篇:

Plaintext
GET /article/2

新的完整 HTML

AdSense 再初始化

新的广告请求

每次页面加载,本身就构成了一次天然的生命周期边界。

但是 SPA 并不是这样。

在 A Tour of Go 中:

Plaintext
/tour/basics/1

点击下一页

/tour/basics/2

虽然用户已经认为自己进入了另一个页面,但浏览器中的那个 Document 并没有重新创建。

这也就意味着:

不能简单期待 AdSense 像传统网站那样,在每次点击“下一页”以后自然重新开始一次完整页面广告流程。

所以,当时摆在面前的大致有两条路线。

路线一:放弃 SPA

第一条路线最直接。

把现在的课程导航改成真正的页面跳转:

Plaintext
/tour/basics/1

整页加载

/tour/basics/2

整页加载

/tour/basics/3

这样做以后,至少有几个问题会明显简单。

AdSense 更简单

每一次课程切换都是一次新的页面访问。

广告脚本、页面内容和广告请求,都可以跟着页面重新加载。

不再需要额外考虑:

SPA 路由切换以后,应该在什么时候销毁旧广告、什么时候创建新广告?

统计更简单

Google Analytics、百度统计之类的统计系统面对传统页面也更加自然。

每次新 Document 就是一条新的页面访问。

SPA 下则需要主动处理虚拟 pageview。

SEO 理论上也更直观

每一个 URL 都由服务器返回自己的完整 HTML:

Plaintext
/tour/basics/1
/tour/basics/2
/tour/methods/1

搜索引擎不需要等待客户端 JavaScript 执行以后才能看到真正的课程内容。

从这些角度看,放弃 SPA 确实很诱人。

如果这个项目只是一个我完全从头实现的网站,也许真的会认真考虑这么做。

但 A Tour of Go 多语言项目还有另一个非常重要的约束。

这个项目不是一套完全独立的 Tour

我的目标一直不是重新实现一个“A Tour of Go 类似网站”。

而是在官方 A Tour of Go 基础上做持续维护的多语言版本。

官方当前的课程内容主要位于:

Plaintext
golang/website/_content/tour/

里面包括:

Plaintext
basics/
concurrency/
flowcontrol/
generics/
methods/
moretypes/
welcome/

static/
template/

basics.article
concurrency.article
flowcontrol.article
...
图 2:golang/website 当前的 _content/tour/。课程内容、静态资源和模板都在这个上游目录中持续维护。
图 2:golang/website 当前的 _content/tour/。课程内容、静态资源和模板都在这个上游目录中持续维护。

这张截图里还有一个很值得注意的细节:

一些上游课程文件刚刚在几天内发生过修改。

例如可以看到:

Plaintext
yesterday
5 days ago
last week

也就是说,A Tour of Go 并不是一份已经冻结、以后再也不会改变的源码。

上游还会继续修改:

  • 课程正文;
  • 示例代码;
  • 模板;
  • 静态资源;
  • 前端行为;
  • Go 版本相关内容。

这时候,“自己把 SPA 改成传统多页面”就不再只是一个一次性开发工作了。

它会变成:

从此以后,每一次同步 upstream,都要考虑自己的分叉架构还能不能继续兼容。

我的项目甚至正式记录 upstream revision

这种上游关系也不是口头上的“偶尔参考”。

go-tour-i18n 本身就正式记录了 upstream baseline。

项目状态中会保存类似:

Plaintext
golang/website master@645042eb...

当上游发生变化以后,还会经过自己的同步流程更新这个 baseline。

production bundle 同样会在 release.json 中写入:

Plaintext
upstream_commit=645042eb...
图 3:go-tour-i18n 正式记录 golang/website upstream baseline,并将 upstream commit 写入生产发布信息。
图 3:go-tour-i18n 正式记录 golang/website upstream baseline,并将 upstream commit 写入生产发布信息。

这意味着对于这个项目来说:

Plaintext
upstream revision

source 同步

TranslationUnit 状态

重新翻译 / 审核(如需要)

build

publish

production

本身就是长期工程流程的一部分。

所以我最终评估 SPA 问题时,最关心的已经不是:

改成普通页面难不难?

而是:

几年以后,我还愿不愿意一直维护这个与 upstream 不同的版本?

两种路线重新放到一起比较

当时最终的判断,大致可以整理成下面这个表格。

维度保留 SPA放弃 SPA
与上游架构接近程度明显降低
后续同步 upstream改动相对集中需要持续适配自己的页面架构
“下一页”体验原生 SPA 快速切换每页整页刷新
编辑器与课程交互更接近上游需要重新确认状态和初始化行为
AdSense需要主动管理 SPA 广告生命周期每次页面加载天然重新执行
页面统计需要处理 SPA pageview普通 pageview 更自然
SEO需要额外解决客户端渲染问题服务器独立页面更直接
初期开发广告和 SEO 处理更复杂看起来更直接
长期维护与 upstream 差异较小自定义差异持续累积

如果只看其中某一行:

Plaintext
AdSense

那我很可能会选择放弃 SPA。

但如果把最后一行:

Plaintext
长期维护

也放进去,结论就开始发生变化。

一次性复杂度和长期复杂度不是一回事

这里其实涉及一个我越来越重视的工程问题。

有些方案看起来“简单”,只是因为它把复杂度推迟到了以后。

比如放弃 SPA。

第一次实现的时候,也许只需要:

Plaintext
修改下一页链接

服务器直接返回对应页面

让浏览器重新加载

看起来很简单。

但是以后上游一旦修改 Tour 的:

  • 路由;
  • Angular controller;
  • editor 生命周期;
  • lesson 数据结构;
  • template;
  • navigation;
  • Playground 行为;

自己的非 SPA 实现就需要逐项判断:

这个修改还能不能直接同步?

哪一部分已经不能用了?

是否又需要为自己的架构重新实现一次?

随着时间推移,两套代码之间的距离只会越来越大。

这就是所谓的维护分叉。

而另一条路线虽然一开始麻烦:

保留 SPA,然后单独解决 SPA 广告生命周期。

但它的复杂度主要集中在自己新增的广告层。

理想状态可以是:

Plaintext
上游 Tour

尽量保持原结构
    +
自己的 locale 层
    +
自己的广告生命周期层

而不是:

Plaintext
上游 Tour

持续转换

自己的另一套页面框架

这两种架构在项目维护几年以后,差异会非常大。

广告应该适应项目,而不是项目适应广告

这也是最终让我决定保留 SPA 的核心原因。

Google AdSense 对这个项目来说很重要。

它关系到站点后续能不能覆盖一部分服务器、域名和维护成本。

但是广告终究还是附加能力。

A Tour of Go 这个项目最核心的东西仍然是:

  • Go 官方课程内容;
  • 与上游保持同步;
  • 多语言翻译;
  • Playground;
  • 课程交互;
  • 长期可维护性。

如果为了广告方便,把这些基础架构大幅修改,优先级其实反了。

所以最后确定的原则变成了:

不为了 AdSense 放弃 SPA。

进一步来说:

尽量让广告实现适配 A Tour of Go,而不是让 A Tour of Go 为广告重新设计。

但保留 SPA 并不会自动解决广告问题

当然,做出这个决定并不意味着问题解决了。

恰恰相反。

选择保留 SPA,相当于主动接受了接下来必须解决的问题:

Plaintext
用户进入课程页

显示一个广告

点击“下一页”

Document 不刷新

旧广告怎么办?

新课程是否需要新的广告?

什么时候重新请求?

怎样避免广告节点不断累积?

而且还有一个更加基础的问题:

广告到底应该插在哪里?

原来的 Auto Ads 连页面上那么大的视觉空白都没有使用。

如果由应用自己明确提供广告位,又应该放到:

  • 左侧正文下面?
  • 右侧编辑器下面?
  • 两栏外部?
  • footer 上面?
  • footer 下面?

Google AdSense 后台的页面预览,后来恰好给了一个很有意思的答案。

它确实找到了一处“可以放广告”的地方。

但那个位置却让我意识到:

Google 认为可以放的位置,不一定就是我希望用户看到广告的位置。

于是问题又进入了下一阶段:

既然决定保留 SPA,课程广告到底应该放在哪里,又应该由谁管理它的生命周期?

A Tour of Go 多语言扩展复盘:为第三门语言建立标准化流程

A Tour of Go 多语言翻译项目

本系列完整记录 A Tour of Go 多语言翻译项目从架构设计、整页翻译、结构保护、自动校验,到生产发布与后续维护的实际开发过程。

项目入口:
✅ 在线学习:A Tour of Go 简体中文版
✅ 项目源码:GitHub:shuijingwan/go-tour-i18n

当前第一阶段已完成简体中文版本。项目属于非官方社区多语言翻译项目,与 Go 官方无隶属或授权关系。