
先把三个问题拆开
多语言官网经常把用户先看哪种语言、每种语言有没有固定网址、CDN 能不能缓存混在一起。这次重构比较了多种做法,最后采用的原则是:根路径只负责选择语言,正式内容放在固定语言目录里。
这不是所有网站唯一正确的架构,但适合从零建设、语言版本明确、准备使用 CDN 的企业官网。
正式内容使用固定语言网址
中文页面放在 /zh/,英文页面放在 /en/。用户进入这些地址后,网站不再因为浏览器语言不同把他跳走。
每页都用 canonical 标出自己的正式地址。有真实中英文对应页时,再用 hreflang 说明两页互相对应。页面上还提供普通语言切换链接,用户不需要修改浏览器设置。
这样,搜索引擎可以分别找到不同语言,用户复制的链接也不会下次打开就变成另一种语言。Google 同样建议不同语言内容使用独立网址。
根路径只做一次语言选择
访问 / 时,网站先看用户以前主动选择的语言,再参考浏览器语言。有明确信号时,用 302 临时跳转到 /zh/ 或 /en/。没有可靠信号时,返回一个中文和英文选择页。
这个选择页使用 x-default。简单说,它是不知道该给哪种语言时使用的默认入口,不是第三种语言版本。
根路径使用临时跳转,因为浏览器语言和用户选择都可能变化。无语言的深层地址不再提供重复内容,避免搜索引擎和用户面对两套正式网址。
自动选择不能成为唯一入口
Google 提醒网站不要只依靠自动判断语言,否则爬虫和用户可能找不到其他版本。因此还要保留固定语言网址、页面互链、hreflang 和可见语言切换。根路径只是入口,不是发现内容的唯一道路。

为什么根路径不能像普通页面一样缓存
根路径可能根据用户选择和浏览器语言返回不同结果。如果 CDN 保存了第一个人的结果,后面的用户就可能全部进入同一种语言。因此根路径返回:
Cache-Control: private, no-store
Vary: Accept-Language, Cookieprivate, no-store 表示不要保存这次语言选择结果;Vary 说明这次结果参考了浏览器语言和 Cookie。源站和 CDN 都必须遵守这条边界,避免把一位访客的语言选择复用给其他人。
普通页面怎样缓存
明确的中文页、英文页、CSS、JavaScript 和图片不会随访问者改变,可以返回:
Cache-Control: public, max-age=3600, s-maxage=2592000这套已验收配置让浏览器保存一小时、CDN 保存三十天。访客可以更快打开稳定页面,源站也减少重复传输;API、询盘结果、404 和服务器错误仍不使用共享缓存。一小时后,浏览器还可以用 ETag 或 Last-Modified 确认文件是否变化。
这些时间不是通用标准。新闻、价格频繁变化或发布机制不同的网站,需要按更新频率重新设计。
实际测试发现了什么
自动测试覆盖四种根路径访问:用户已经选过中文、已经选过英文、浏览器第一次提供语言、完全没有可靠语言信号。测试还确认直接打开 /zh/ 或 /en/ 时不会再次跳转。
测试曾发现一个很小但真实的问题:没有查询参数时,跳转地址末尾仍可能多出一个问号。页面虽然能打开,网址却不干净。自动检查因此同时覆盖“保留真实查询参数”和“没有参数时不加问号”。
长共享缓存必须配合发布刷新
三十天共享缓存不能靠等待自然过期来发布内容。正确顺序是先部署并检查源站,再提交精确 URL 刷新,等待完成后确认新页面可见、普通页面产生 CDN 命中,同时确认根路径和 API 仍未被缓存。
刷新失败必须阻断发布完成,否则源站虽然已经更新,客户仍可能看到旧页面。查询参数也要按业务用途设计缓存键,不能为了提高命中率把功能参数错误合并。
浏览器缓存和 CDN 缓存是两层。刷新 CDN 不会删除访客浏览器里已经保存的一小时副本。紧急替换资源且不能等待时,应使用带新文件名的指纹资源或单独调整浏览器缓存。
发布前检查清单
/zh/和/en/都直接返回页面,不根据浏览器语言再次跳转。/对已有选择、中文浏览器、英文浏览器和无语言信号分别返回预期结果。- 每页标出自己的正式地址,只声明真实存在的对应语言页面。
- 根路径、API、询盘结果和错误页不缓存。
- HTTP、裸域和错误主机只跳转一次,并保留路径和真实查询参数。
- 先验收源站,再刷新精确 URL;最后确认公开页命中 CDN、私有路径不命中。
资料来源与内容边界
- Google:Localized versions of your pages
- Google:Managing multi-regional and multilingual sites
- 阿里云 CDN:配置缓存过期时间
- 阿里云 CDN:刷新和预热资源
官方资料用于确认多语言 URL 和 CDN 功能限制。具体目录、缓存时间、协商顺序、测试场景和跳转问题来自这次实际实现。