# /en/、/zh/这种多语言目录应该怎么设计？

URL: https://caijiao.org/posts/155
Source: docs/posts/155.md
Description: 语言目录放在域名后第一级，各语言目录树完全对齐。hreflang 必须双向回指且包含自己，漏一条整组失效；HTML head、HTTP 头、sitemap 三种声明方式只能选一种，混用会互相冲突。

先给可以直接抄的结论：语言目录做域名后的第一级，`/zh/finance/margin` 和 `/en/finance/margin` 平行存在，每个语言版本的目录结构完全对齐。

不要写成 `/finance/zh/margin`。那种写法里语言插在内容层级中间，多一个语言就要动一次路由，而且不同语言的目录树很容易长歪——中文版少一个分类，英文版多两篇，hreflang 的对应关系维护起来会变成噩梦。

## 目录、子域、参数三种方案怎么选

**子目录（`example.com/zh/`）**：所有语言的权重聚在同一个域名下，新语言上线不需要重新积累域名信任度。代价是部署稍微麻烦一点，但这是绝大多数站该选的方案。

**子域（`zh.example.com`）**：Google 能处理，但每个子域在一定程度上被当作独立站点，信任度要重新积累。除非技术架构上实在无法共享——比如不同语言由不同团队、不同系统维护——否则没必要。

**参数（`example.com/page?lang=zh`）**：最不推荐。参数形式的 URL 容易被当成同一页面的变体，抓取优先级低，和 hreflang 配合时也容易出错。

选目录之后还有一件事：默认语言要不要带目录。两种都可行——`/` 直接是英文、`/zh/` 是中文；或者 `/en/`、`/zh/` 都带，根路径做语言选择页。我倾向后者，结构对称、路由规则统一，不必为根路径写特例。

## hreflang 的写法，三条规则缺一不可

声明写在哪都行——HTML `<head>`、HTTP 响应头、或者 sitemap——但**三种只能选一种**。混用的后果是两处声明冲突，Google 会认为信号不可靠，整组放弃。页面数量多的时候放 sitemap 最省事，页面少的时候写进 `<head>` 更直观。

HTML 里长这样：

```html
<link rel="alternate" hreflang="zh-CN" href="https://example.com/zh/finance/margin" />
<link rel="alternate" hreflang="en-US" href="https://example.com/en/finance/margin" />
<link rel="alternate" hreflang="x-default" href="https://example.com/en/finance/margin" />
```

配套的三条规则：

**必须双向回指。** 中文页声明了英文页，英文页就必须也声明中文页。单向声明等于没写。这一条在程序化生成时最容易漏——模板里只生成了当前语言的链接，忘了把其他语言的反向声明也生成出来。

**每组必须包含自己。** `/zh/finance/margin` 的那一组里，必须有一条 `hreflang="zh-CN"` 指向它自己。漏掉自己，整组失效。

**`x-default` 指向语言选择页或默认版本**，给那些不匹配任何已声明语言的用户兜底。

语言代码用 ISO 639-1（zh、en、ja），地区代码用 ISO 3166-1 Alpha-2（CN、US、GB），两者用连字符连接。只写 `zh` 不写地区是合法的，表示"所有中文用户"，但和 `zh-CN`、`zh-TW` 混用时要小心覆盖范围重叠。完整取值规则在 [Google Search Central](/google-search-central/) 的国际化文档里。

## 站点与地区不是一回事，别用 IP 跳转

最具破坏性的做法是"根据访客 IP 自动跳转到对应语言"。爬虫的 IP 通常来自美国，你一跳转，爬虫永远只能抓到英文版，其他语言版本根本进不了索引。

正确做法是让用户落在他请求的那个 URL 上，页面上给一个明显的语言切换入口。想做智能一点，可以在检测到明显不匹配时弹一个提示条，而不是直接 302 跳走。

同样要避免把语言判定写进 [Nginx](/nginx/) 的 rewrite 规则里——这类规则会同时作用于爬虫，而且一旦写错，排查起来比重写业务代码还麻烦。

## 翻译深度比目录结构更能决定成败

Omni Calculator 做了 30 多种语言，2026 年 6 月估算访问约 1429 万，那是建立在真正本地化基础上的——不只是界面文案，连单位、数字格式、税法口径都跟着地区走。

![Omni Calculator 计算器分类页截图](/images/2026-09/omnicalculator.com.png)

*Omni Calculator 首页连 Sports 都单独分类，111 个计算器对应各类赛事和成绩换算。*

反面参照是 TinyWow：流量里美国占 20.17%、印度 14.63%、英国 3.22%，前三名全是英语地区，但它并没有因此去做几十种语言。工具站的通用性足够高时，单一语言照样能吃下多地区流量。

## 结构化数据里的语言也要跟着变

JSON-LD 里的 `inLanguage` 字段必须和该页面语言一致，`name`、`description` 也要用对应语言的值。只翻译正文、结构化数据还是英文，会让 Google 认为标记与页面不符。

多语言站我习惯把每个语言的页面数据放在 [SQLite](/sqlite/) 的同一张表里，用 `lang` 字段区分、`i18n_group` 标记互为翻译版本的记录，构建时按 group 分组输出 hreflang 组和各语言的 sitemap。一份数据出全部产物，不会出现某个语言的链接漏生成、或者单向声明的情况。

---

*Omni Calculator 30+ 语言、2026 年 6 月访问约 1429 万（Semrush 估算）；TinyWow 地区分布为第三方估算（2026 年上半年）。*
