Adding a second language to a Jekyll blog is not difficult because of the extra Markdown file. The difficult part is keeping two bodies of content consistent for years. A translated title is only the beginning. Pairing, stable URLs, language-specific indexes, search metadata, and deployment checks must all understand the same language model.

A durable implementation does not necessarily need a complex localization plugin. Jekyll accepts custom Front Matter fields, and Liquid can query site.posts. Together, those features let a site express “these files are language versions of the same article” as data, then make templates and tests enforce the contract.

Pair posts with a stable key, not a title

Each version should have its own file, title, and body while sharing a translation_key:

---
layout: post
title: "Engineering a Bilingual Jekyll Blog"
date: 2026-09-15 09:00:00 +0800
lang: en
translation_key: jekyll-bilingual-blog-contract
---

The Chinese file uses lang: zh and the same key. A key represents topic identity and should remain stable after publication. Titles change for clarity and search presentation, so they make poor primary keys.

The filename can encode the same convention:

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

Jekyll requires dates in post filenames, while a Front Matter date can control sorting. Both versions should normally use the same calendar date. They can use different minutes when display order matters, but validation should still recognize them as one publication.

Resolve the alternate version in the layout

The post layout can find another document with the same key:




  
    <a href="/blog/2026/jekyll-bilingual-blog-contract-zh/"
       hreflang="zh"
       lang="zh">
      阅读中文
    </a>
  

  

The language switch should lead to the corresponding article rather than the other language’s homepage. A reader switching midway through a page usually wants to stay on the same topic. If no translation exists, omit the control instead of emitting a broken or misleading link.

Make the URL language boundary predictable

A site can put Chinese posts under /zh/, or keep -zh and -en in the slugs. Neither choice is universally superior. The important properties are stable rules, collision-free output, and URLs that templates can determine reliably. Jekyll supports a global permalink pattern in _config.yml and per-document overrides in Front Matter.

Once search engines and readers know a URL, do not change it merely for visual neatness. A necessary migration should include redirects and coordinated updates to canonical URLs, internal links, the sitemap, and language mappings.

hreflang must be reciprocal and self-referential

An on-page language button helps people. rel="alternate" metadata helps search engines understand the relationship. Google’s localized-page guidance says every version should list itself and all alternates, and the alternate pages must link back to each other:

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

These must be fully qualified URLs. Each page should still have a self-referencing canonical. Do not canonicalize every translation to the English article; doing so says that the localized pages are not independent destinations. The main body must also be genuinely translated. Changing only navigation and footer text does not create a complete localized version.

Split language indexes during the build

Do not render a mixed list and hide half of it with JavaScript. Liquid can select the correct language while generating the site:



  <a href="/blog/2026/content-security-policy-rollout-contract-en/">Shipping CSP Safely: From Report-Only to Nonces and Enforcement</a>

  <a href="/blog/2026/production-time-modeling-contract-en/">Time Is Not One Field: UTC, Time Zones, and Monotonic Clocks</a>

  <a href="/blog/2026/passkey-end-to-end-authentication-contract-en/">An End-to-End Authentication Contract for Passkeys</a>

  <a href="/blog/2026/structured-logging-production-contract-en/">A Production Contract for Structured Logging</a>

  <a href="/blog/2026/http-retry-idempotency-contract-en/">Retrying HTTP Without Duplicating Side Effects</a>

The HTML source, pagination count, and feed input are then correct from the start. Browser-language detection can assist with an initial homepage choice, but it should not force redirects for deep article links. A specific URL shared by a reader should remain reachable and offer an explicit manual switch.

Validate the content contract before committing

Manual review will eventually miss a language field or a copied key. A build script should verify at least these invariants:

  1. Every translation_key has exactly one English and one Chinese file.
  2. Paired versions agree on publication date, category, and series metadata.
  3. Required title, description, language, keywords, and source fields are present.
  4. Generated language indexes contain no posts in the wrong language.
  5. Every article switch reaches its matching version.
  6. Canonical and hreflang values use the final publicly reachable URLs.

Ruby can read the YAML Front Matter, group posts by key, and assert the language set:

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

Run a full Jekyll build next, then inspect the target files and internal links in _site. Source validation and output validation catch different failures. The first protects content metadata; the second catches mistakes introduced by Liquid, permalink rules, pagination, or layout composition.

Count an article by topic, not by file

A bilingual site creates an easy reporting distortion: two Markdown files are not two original articles. A better publishing unit is one unique translation_key. Editorial schedules, homepage metrics, and content inventory should deduplicate by that topic key. A translation is another representation of the topic, not another topic.

Once pairing becomes explicit data, templates no longer guess, search metadata no longer depends on repeated manual edits, and automation can stop an incomplete translation set before deployment. Jekyll’s individual features are simple, but stable keys, independent URLs, reciprocal metadata, and build checks combine into a bilingual blog that can remain maintainable over the long term.