给 Jekyll 博客增加第二种语言,难点不在于多写一份 Markdown,而在于长期保持两套内容的一致性。标题能切换只是起点;文章配对、独立 URL、语言首页、搜索元数据和发布检查都必须理解同一组语言规则。

一个稳健方案不一定需要复杂插件。Jekyll 允许在 Front Matter 中定义自有字段,Liquid 又能查询 site.posts。利用这两点,可以把“这两个文件是同一篇文章的不同语言版本”写成数据契约,再由模板和测试消费。

用稳定键配对,不要靠标题猜测

每个版本拥有独立文件、标题和正文,但共享 translation_key:

---
layout: post
title: "Jekyll 双语博客工程化"
date: 2026-09-15 09:00:00 +0800
lang: zh
translation_key: jekyll-bilingual-blog-contract
---

对应英文文件使用 lang: en 和相同的 key。键应该描述主题身份,创建后保持不变;标题会为可读性和搜索优化而修改,不适合作为主键。

文件名也可以编码这份约定:

2026-09-15-jekyll-bilingual-blog-contract-zh.md
2026-09-15-jekyll-bilingual-blog-contract-en.md

Jekyll 要求文章文件名带日期,Front Matter 中的 date 还能控制排序。两个版本最好使用同一天,必要时用分钟区分显示顺序,但内容校验应按日判断它们属于同一次发布。

在模板中寻找对应版本

文章布局可以通过 translation_key 找到另一个版本:




  

  
    <a href="/blog/2026/jekyll-bilingual-blog-contract-en/"
       hreflang="en"
       lang="en">
      Read in English
    </a>
  

语言切换链接应指向对应文章,而不是语言首页。读者在阅读中途切换语言时,通常希望停留在同一主题。没有翻译时则不渲染入口,避免产生假链接。

URL 结构要让语言边界可预测

可选择在路径中加入 /zh/,也可以在 slug 末尾保留 -zh 与 -en。关键不是哪一种更漂亮,而是规则稳定、URL 不冲突且模板能可靠计算。Jekyll 支持在 _config.yml 中设置全局 permalink,也支持为单篇页面在 Front Matter 中覆盖。

一旦 URL 已被索引,不要只为“更整齐”而随意更换。真正需要迁移时,应准备重定向并同步更新 canonical、站内链接、sitemap 和语言映射。

hreflang 必须双向且包含自身

页面内的语言按钮服务于读者,rel="alternate" 服务于搜索引擎。根据 Google 的本地化页面指南,每个版本需要列出自身和所有对应版本,而且这些关系必须互相返回:

<link rel="alternate" hreflang="zh-CN"
      href="https://example.com/zh/article/">
<link rel="alternate" hreflang="en"
      href="https://example.com/en/article/">

这里使用完整绝对 URL。每个页面仍应有指向自身的 canonical;不要把所有翻译版本 canonical 到英文页,否则等于告诉搜索引擎其他语言不是独立页面。正文也必须真正翻译,仅替换导航和页脚不构成完整的本地化内容。

语言首页应在构建阶段分流

不要先输出混合列表再用 JavaScript 隐藏。Liquid 可以在生成时按 lang 过滤:



  <a href="/blog/2026/content-security-policy-rollout-contract-zh/">CSP 不该一次切到阻断:从 Report-Only 到 nonce 的上线契约</a>

  <a href="/blog/2026/production-time-modeling-contract-zh/">时间不是一个字段:生产系统中的 UTC、时区与单调时钟</a>

  <a href="/blog/2026/passkey-end-to-end-authentication-contract-zh/">Passkey 登录的端到端契约:注册、校验、管理与恢复</a>

  <a href="/blog/2026/structured-logging-production-contract-zh/">生产级结构化日志契约:可查询、可关联,也不泄露秘密</a>

  <a href="/blog/2026/http-retry-idempotency-contract-zh/">HTTP 重试不制造重复副作用:幂等键、退避预算与故障验证</a>

这样页面源码、分页数量和订阅内容从一开始就是正确语言。浏览器语言检测最多用于首页首次选择,不应强制重定向文章深链接;用户分享的具体 URL 应保持可访问,并提供明确的手动切换入口。

在提交前验证内容契约

人工检查很容易漏掉一个语言字段或复制错 key。构建脚本至少应验证:

  1. 每个 translation_key 恰好对应一个中文和一个英文文件;
  2. 配对版本的发布日期、分类和系列信息一致;
  3. 必填的标题、摘要、语言、关键词和来源不为空;
  4. 生成后的语言首页不混入另一种语言;
  5. 每篇文章的切换链接能到达对应版本;
  6. canonical 与 hreflang 使用可公开访问的最终 URL。

Ruby 可以直接读取 YAML Front Matter,把文章按 key 分组后检查语言集合:

groups = posts.group_by { |post| post.fetch("translation_key") }

groups.each do |key, versions|
  languages = versions.map { |post| post.fetch("lang") }.sort
  abort "#{key}: expected en and zh" unless languages == %w[en zh]
end

随后运行一次完整 Jekyll build,再检查 _site 中的目标文件和站内链接。源码契约与生成结果需要分别验证:前者防止内容数据错误,后者捕获 Liquid、permalink 和分页配置造成的问题。

把“一篇文章”定义为主题,而不是文件数

双语站点会让发布统计出现一个容易忽略的偏差:两个 Markdown 文件不等于两篇原创文章。更合理的计数单位是唯一 translation_key。周更计划、首页统计和内容库存都应按主题键去重,翻译是该主题的另一个版本。

当配对关系成为显式数据,模板不再猜测,搜索元数据不再手工复制,自动化也能在发布前阻止半套翻译上线。Jekyll 的能力并不复杂,但用稳定键、独立 URL、双向标记和构建校验组合起来,就足以支撑一个长期可维护的双语博客。