Deterministic Bilingual Links Implementation Plan
Deterministic Bilingual Links Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Enforce canonical English and Chinese blog URLs across the complete archive and render automatically generated language links in both directions.
Architecture: A focused Python module parses post front matter, validates public URLs, groups variants by the URL remaining after /zh is removed, and writes deterministic Jekyll data. The existing linter delegates URL checks to that module, CI verifies the generated file is current, and a Liquid include renders the matching variants.
Tech Stack: Python 3 standard library, unittest, Jekyll/Liquid, JSON, GitHub Actions, jekyll-redirect-from.
Global Constraints
- English posts use
/posts/YYYY/MM/slug/. - Chinese posts use
/zh/posts/YYYY/MM/slug/. - Public URLs must not end in
-cn,-zh, or-en. - Validation applies to every post without a date exemption.
- Every changed URL retains its previous URL through
redirect_from. - Update all in-repository links that target migrated URLs.
- Do not edit article prose or create missing translations.
Task 1: Strict URL validator and deterministic data generator
Files:
- Create:
scripts/generate_language_links.py - Create:
tests/test_language_links.py - Modify:
scripts/lint_blog_format.py
Interfaces:
- Produces:
load_posts(posts_dir: Path) -> list[Post],validate_posts(posts: list[Post]) -> list[str],build_language_links(posts: list[Post]) -> dict[str, dict], andrender_json(data: dict) -> str. - CLI:
python3 scripts/generate_language_links.py [--check] [--posts-dir PATH] [--output PATH]. The blog linter consumes
validate_post_url(path, permalink) -> list[str]from the generator module.- Step 1: Write failing URL and generation tests
Create fixture posts in temporary directories inside tests. Assert that /posts/2026/07/topic/ and /zh/posts/2026/07/topic/ form a two-way entry, that malformed/day-segment paths and language-suffixed slugs fail, that a -cn.md source outside /zh/ fails, duplicate language variants fail, monolingual posts produce no entry, and reversed input order produces byte-identical JSON.
def test_builds_both_directions(self):
posts = [
Post(Path("2026-07-19-topic.md"), "/posts/2026/07/topic/"),
Post(Path("2026-07-19-topic-zh.md"), "/zh/posts/2026/07/topic/"),
]
self.assertEqual(build_language_links(posts)["/posts/2026/07/topic/"]["variants"],
[{"language": "zh", "label": "中文", "url": "/zh/posts/2026/07/topic/"}])
- Step 2: Run tests and confirm they fail
Run: python3 -m unittest tests/test_language_links.py -v Expected: FAIL because scripts/generate_language_links.py does not exist.
- Step 3: Implement the generator and validator
Use a frozen Post(path, permalink) dataclass, a full-match URL regex, URL-prefix language classification, basename-marker consistency checks, duplicate detection, sorted keys/variants, UTF-8 JSON with ensure_ascii=False, and a trailing newline. --check compares expected bytes with the committed file and exits nonzero with a regeneration command when stale.
CANONICAL_URL = re.compile(r"^/(zh/)?posts/(\d{4})/(\d{2})/([^/]+)/$")
FORBIDDEN_SUFFIX = re.compile(r"-(?:cn|zh|en)$", re.IGNORECASE)
def pair_key(post):
return post.permalink.removeprefix("/zh")
- Step 4: Remove prospective-only validation from the old linter
Delete URL_POLICY_START and follows_url_policy. Import the new URL validator by adding the scripts directory to sys.path, run it for every Markdown post, and keep the existing image policy unchanged unless its own prospective gate is required.
- Step 5: Run focused tests
Run: python3 -m unittest tests/test_language_links.py tests/test_blog_lint.py -v Expected: all tests pass before repository-wide migration is checked.
- Step 6: Commit the validator and tests
git add scripts/generate_language_links.py scripts/lint_blog_format.py tests/test_language_links.py tests/test_blog_lint.py
git commit -m "Enforce canonical blog language URLs"
Task 2: Migrate legacy URLs and internal references
Files:
- Modify: affected
_posts/*.mdfront matter and in-repository Markdown references. - Create:
_data/language_links.json
Interfaces:
- Consumes the generator CLI from Task 1.
Produces a complete repository state accepted by strict validation.
- Step 1: Capture the failing archive audit
Run: python3 scripts/generate_language_links.py --check Expected: FAIL listing each legacy URL that violates the contract.
- Step 2: Normalize every violating permalink
For each Chinese source marker (-zh.md), use /zh/posts/YYYY/MM/<published-slug-with-language-suffix-removed>/. For English posts, use /posts/YYYY/MM/<published-slug-with-language-suffix-removed>/. Remove an extra day path segment. Add the exact former permalink to redirect_from, preserving any existing redirect entries.
permalink: /zh/posts/2024/08/lead-utexas/
redirect_from:
- /posts/2024/08/lead-utexas-cn/
- Step 3: Align known bilingual pairs
Where English and Chinese sources share a filename key after removing -cn, -zh, or -en, make their canonical YYYY/MM/slug identical. Resolve collisions explicitly and fail rather than overwriting a post.
- Step 4: Rewrite internal references mechanically
Build the exact old-to-new URL map from the migration and replace only complete URL tokens in tracked text files. Run rg for every old URL and require zero remaining references except redirect_from declarations.
- Step 5: Generate central data
Run: python3 scripts/generate_language_links.py Expected: writes sorted _data/language_links.json and reports the number of complete pairs.
- Step 6: Verify migration and commit
Run: python3 scripts/lint_blog_format.py && python3 scripts/generate_language_links.py --check Expected: both commands pass.
git add _posts _data/language_links.json
git commit -m "Migrate blog posts to canonical language URLs"
Task 3: Render the automatic language navigation
Files:
- Create:
_includes/language-switcher.html - Modify:
_layouts/single.html - Modify:
_sass/layout/_page.scss - Create or modify:
tests/test_language_switcher.py
Interfaces:
- Consumes
site.data.language_links[page.url].variants. Produces a
<nav class="page__language-switcher" aria-label="Language versions">containing variant links.- Step 1: Write failing template contract tests
Assert that the include reads the current URL with bracket lookup, loops over variants, applies relative_url, renders variant labels, and that single.html includes it before article content.
- Step 2: Run the test and confirm failure
Run: python3 -m unittest tests/test_language_switcher.py -v Expected: FAIL because the include is absent.
- Step 3: Add the include and layout hook
- Step 4: Add minimal theme-aligned styling
Style the navigation as compact metadata with normal link colors, spacing, and no layout shift. Do not add JavaScript.
- Step 5: Run template tests and build
Run: python3 -m unittest tests/test_language_switcher.py -v && bundle exec jekyll build Expected: tests pass and Jekyll exits zero.
- Step 6: Commit rendering
git add _includes/language-switcher.html _layouts/single.html _sass/layout/_page.scss tests/test_language_switcher.py
git commit -m "Render automatic bilingual post links"
Task 4: CI integration and final verification
Files:
- Modify:
.github/workflows/blog-lint.yml - Modify:
CONTRIBUTING.md
Interfaces:
- CI invokes generator
--checkafter blog lint and before unit tests. Contributor documentation exposes the one regeneration command.
- Step 1: Add generator paths and stale-data check to CI
Include scripts/generate_language_links.py, _data/language_links.json, _includes/language-switcher.html, and _layouts/single.html in workflow path filters. Add:
- name: Check generated language links
run: python3 scripts/generate_language_links.py --check
- Step 2: Document author workflow
Document canonical English/Chinese permalink forms and require python3 scripts/generate_language_links.py after adding or renaming a translation.
- Step 3: Run the complete verification set
Run:
python3 scripts/lint_blog_format.py
python3 scripts/generate_language_links.py --check
python3 -m unittest discover -s tests -v
bundle exec jekyll build
git diff --check
Expected: every command exits zero, and generated pages contain reciprocal hreflang="en" and hreflang="zh" links for a sampled pair.
- Step 4: Commit CI and documentation
git add .github/workflows/blog-lint.yml CONTRIBUTING.md
git commit -m "Check bilingual link data in CI"
- Step 5: Push the completed branch
Run: git status --short && git log --oneline origin/main..HEAD && git push origin main Expected: clean status and a successful fast-forward push.
