{
  "sha": "bef6dec80650df07395ef3188b05494dda631c88",
  "message": "feat: add Taiwan aviation focus and topic links to home sidebar (#333)",
  "author": {
    "login": "nnneeeooo-nnneeeooo",
    "name": "CHI HSUN YANG",
    "email": "125815713+nnneeeooo-nnneeeooo@users.noreply.github.com"
  },
  "committer": {
    "login": "web-flow",
    "name": "GitHub",
    "email": "noreply@github.com"
  },
  "created_at": "2026-10-20T11:55:12Z",
  "files": [
    {
      "filename": ".github/workflows/hourly.yml",
      "previous_filename": "",
      "status": "modified",
      "additions": 1,
      "deletions": 0,
      "changes": 1
    },
    {
      "filename": "pipeline/build.py",
      "previous_filename": "",
      "status": "modified",
      "additions": 32,
      "deletions": 0,
      "changes": 32
    },
    {
      "filename": "static/site.css",
      "previous_filename": "",
      "status": "modified",
      "additions": 16,
      "deletions": 0,
      "changes": 16
    },
    {
      "filename": "templates/home.html",
      "previous_filename": "",
      "status": "modified",
      "additions": 27,
      "deletions": 0,
      "changes": 27
    },
    {
      "filename": "tests/test_home_sidebar.py",
      "previous_filename": "",
      "status": "added",
      "additions": 63,
      "deletions": 0,
      "changes": 63
    }
  ],
  "diff": "@@ -67,6 +67,7 @@ jobs:\\n           python tests/test_write_offline.py\\n           python tests/test_provider_health.py\\n           python tests/test_build_resilience.py\\n+          python tests/test_home_sidebar.py\\n           python tests/test_english_backfill.py\\n           python tests/test_news_freshness.py\\n           python tests/test_news_retry.py\\n@@ -250,6 +250,10 @@ def static_asset_version() -> str:\\n         \"topicKicker\": \"Aviation Topics\",\\n         \"topicCount\": \"則相關新聞\",\\n         \"topicNavLabel\": \"航空新聞主題\",\\n+        \"taiwanFocusTitle\": \"臺灣航空焦點\",\\n+        \"taiwanFocusMore\": \"更多臺灣新聞 →\",\\n+        \"taiwanFocusEmpty\": \"近 7 天暫無臺灣航空新聞，查看歷來報導。\",\\n+        \"exploreTopics\": \"探索主題\",\\n         \"policyLink\": \"編輯、更正與聯絡\",\\n         \"awaiting\": \"等待首次管線執行\",\\n         \"lastBuild\": \"最後建置\",\\n@@ -390,6 +394,10 @@ def static_asset_version() -> str:\\n         \"topicKicker\": \"Aviation Topics\",\\n         \"topicCount\": \"related stories\",\\n         \"topicNavLabel\": \"Aviation news topics\",\\n+        \"taiwanFocusTitle\": \"Taiwan aviation focus\",\\n+        \"taiwanFocusMore\": \"More Taiwan news →\",\\n+        \"taiwanFocusEmpty\": \"No Taiwan aviation stories in the past 7 days. Browse the archive.\",\\n+        \"exploreTopics\": \"Explore topics\",\\n         \"policyLink\": \"Editorial, corrections and contact\",\\n         \"awaiting\": \"Awaiting first pipeline run\",\\n         \"lastBuild\": \"Last build\",\\n@@ -1656,6 +1664,27 @@ def taiwan_focus_score(article, now=None) -> int:\\n     return score if score >= 60 else 0\\n \\n \\n+def taiwan_sidebar_articles(articles, now=None):\\n+    \"\"\"Latest five direct Taiwan stories from the past week, without fallbacks.\\n+\\n+    Reuse headline-level Taiwan signals; avoid body-only mentions and the\\n+    carousel's broader Cathay fallback in this explicitly local news rail.\\n+    \"\"\"\\n+    now = now or now_utc()\\n+    recent = []\\n+    for article in articles:\\n+        published = article.get(\"published_dt\")\\n+        if (published is None\\n+                or not timedelta(0) <= now - published <= timedelta(days=7)):\\n+            continue\\n+        text = _hero_focus_text(article)\\n+        if (_HERO_TAIWAN_CARRIER_RE.search(text)\\n+                or _HERO_TAIWAN_PLACE_RE.search(text)):\\n+            recent.append(article)\\n+    recent.sort(key=lambda article: article[\"published_dt\"], reverse=True)\\n+    return recent[:5]\\n+\\n+\\n def taiwan_focus_articles(articles, now=None, limit=_HERO_FOCUS_LIMIT):\\n     \"\"\"Return fresh Taiwan-focus stories, or one safe recent fallback.\"\"\"\\n     now = now or now_utc()\\n@@ -3922,6 +3951,9 @@ def main() -> int:\\n         ctx.update(hero=hero, hero_candidates=hero_candidates,\\n                    hero_rotation_seconds=HERO_ROTATION_SECONDS,\\n                    feed=feed, flashes=fl, agg=agg,\\n+                   taiwan_focus=[art_view(article, lang) for article in\\n+                                 taiwan_sidebar_articles(lang_articles, now)],\\n+                   taiwan_topic_url=page_url(lang, \"topics/taiwan-aviation/\"),\\n                    source_status=source_status_view(sources, now, lang),\\n                    stats=sv[\"tiles\"], otp=sv[\"otp\"], fleet=sv[\"fleet\"],\\n                    delays=sv[\"delays\"], stats_note=sv[\"note\"],\\n@@ -215,6 +215,22 @@ a.seg-btn.active:hover { color: var(--color-bg); }\\n .center-col { padding: 20px 24px; min-width: 0; }\\n .rail-right { border-left: 2px solid var(--color-divider); padding: 20px; }\\n \\n+.home-rail-section { margin-top: 28px; min-width: 0; }\\n+.home-rail-section h6 { margin: 0 0 12px; }\\n+.rail-stories { list-style: none; margin: 0; padding: 0; }\\n+.rail-stories li { border-top: 1px solid var(--color-divider); }\\n+.rail-story { display: block; padding: 12px 0; color: inherit; text-decoration: none; }\\n+.rail-story-meta { display: block; font-size: 11px; color: var(--color-accent); }\\n+.rail-story-title { display: block; margin: 5px 0; font-size: 14px; font-weight: 700; line-height: 1.55; overflow-wrap: anywhere; }\\n+.rail-story-source { display: block; font-size: 11px; overflow-wrap: anywhere; }\\n+.rail-story:hover .rail-story-title { color: var(--color-accent); }\\n+.rail-more { display: inline-block; padding: 10px 0; font-size: 12px; font-weight: 700; }\\n+.rail-empty { font-size: 12px; line-height: 1.6; }\\n+.rail-topics { display: flex; flex-wrap: wrap; gap: 8px; }\\n+.rail-topics a { display: inline-flex; align-items: center; min-height: 36px; max-width: 100%; padding: 6px 10px; border: 1px solid var(--color-divider); color: inherit; text-decoration: none; font-size: 12px; overflow-wrap: anywhere; }\\n+.rail-topics a:hover { border-color: var(--color-accent); color: var(--color-accent); }\\n+.rail-story:focus-visible, .rail-more:focus-visible, .rail-topics a:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 3px; }\\n+\\n .flash-row {\\n   padding: 10px 0; border-bottom: 1px solid var(--color-divider);\\n   display: grid; grid-template-columns: 56px minmax(0, 1fr);\\n@@ -137,6 +137,33 @@ <h6 style=\"margin:0 0 12px;\">{{ t.today }}</h6>\\n           <div class=\"text-muted\" style=\"font-size:11px;margin-top:4px;\">{% if html_lang.startswith('zh') %}前往 FlightAware 查看（另開分頁）{% else %}View on FlightAware (opens in a new tab){% endif %}</div>\\n         </a>\\n       </div>\\n+      <section class=\"home-rail-section\" aria-labelledby=\"taiwan-focus-heading\">\\n+        <h6 id=\"taiwan-focus-heading\">{{ t.taiwanFocusTitle }}</h6>\\n+        {% if taiwan_focus %}\\n+        <ul class=\"rail-stories\">\\n+          {% for item in taiwan_focus %}\\n+          <li>\\n+            <a class=\"rail-story\" href=\"{{ item.url }}\">\\n+              <span class=\"rail-story-meta\"><time datetime=\"{{ item.published_iso }}\" title=\"{{ item.published_meta_ts }}\">{{ item.time }}</time> · {{ item.cat_label }}</span>\\n+              <span class=\"rail-story-title\">{{ item.title }}</span>\\n+              <span class=\"rail-story-source text-muted\">{{ item.source }}</span>\\n+            </a>\\n+          </li>\\n+          {% endfor %}\\n+        </ul>\\n+        {% else %}\\n+        <p class=\"rail-empty text-muted\">{{ t.taiwanFocusEmpty }}</p>\\n+        {% endif %}\\n+        <a class=\"rail-more\" href=\"{{ taiwan_topic_url }}\">{{ t.taiwanFocusMore }}</a>\\n+      </section>\\n+      <nav class=\"home-rail-section\" aria-labelledby=\"explore-topics-heading\">\\n+        <h6 id=\"explore-topics-heading\">{{ t.exploreTopics }}</h6>\\n+        <div class=\"rail-topics\">\\n+          {% for topic in topic_links %}\\n+          <a href=\"{{ topic.url }}\">{{ topic.label }}</a>\\n+          {% endfor %}\\n+        </div>\\n+      </nav>\\n       {% if otp %}\\n       <h6 style=\"margin:28px 0 12px;\">{{ t.otp }}</h6>\\n       {% for r in otp %}\\n@@ -0,0 +1,63 @@\\n+\"\"\"Offline selection and rendered-link checks for the homepage right rail.\"\"\"\\n+from datetime import datetime, timedelta, timezone\\n+from pathlib import Path\\n+import sys\\n+\\n+from bs4 import BeautifulSoup\\n+\\n+ROOT = Path(__file__).resolve().parent.parent\\n+sys.path.insert(0, str(ROOT / \"pipeline\"))\\n+import build\\n+\\n+\\n+def test_taiwan_sidebar_selection():\\n+    now = datetime(2026, 10, 20, 12, tzinfo=timezone.utc)\\n+\\n+    def article(title, hours, body=\"\"):\\n+        return {\\n+            \"id\": title, \"published_dt\": now - timedelta(hours=hours),\\n+            \"source\": \"Test source\",\\n+            \"zh\": {\"title\": title, \"summary\": \"\", \"body\": [body]},\\n+            \"en\": {\"title\": title, \"summary\": \"\", \"body\": [body]},\\n+        }\\n+\\n+    local = [article(f\"星宇航空新增航線 {index}\", index) for index in range(7)]\\n+    excluded = [\\n+        article(\"Taiwan airport old announcement\", 169),\\n+        article(\"華航未來消息\", -1),\\n+        article(\"Cathay Pacific announces new London flights\", 1),\\n+        article(\"US TSA announces security policy\", 1),\\n+        article(\"Boeing quarterly earnings\", 1, body=\"臺灣、Taiwan\"),\\n+    ]\\n+    selected = build.taiwan_sidebar_articles(list(reversed(local)) + excluded, now)\\n+    assert selected == local[:5]\\n+    assert build.taiwan_sidebar_articles(excluded, now) == []\\n+    assert build.taiwan_sidebar_articles([], now) == []\\n+    boundary = article(\"EVA Air fleet update\", 168)\\n+    assert build.taiwan_sidebar_articles([boundary], now) == [boundary]\\n+\\n+\\n+def test_rendered_sidebar():\\n+    assert build.main() == 0\\n+    for lang, prefix in ((\"zh\", \"\"), (\"en\", \"en/\")):\\n+        path = build.SITE_DIR / prefix / \"index.html\"\\n+        soup = BeautifulSoup(path.read_text(encoding=\"utf-8\"), \"html.parser\")\\n+        rail = soup.select_one(\".rail-right\")\\n+        assert rail.select_one(\"#taiwan-focus-heading\").text == build.L[lang][\"taiwanFocusTitle\"]\\n+        assert rail.select_one(\"#explore-topics-heading\").text == build.L[lang][\"exploreTopics\"]\\n+        assert len(rail.select(\".rail-story\")) <= 5\\n+        assert rail.select(\".rail-story\") or rail.select_one(\".rail-empty\")\\n+        topics = rail.select(\".rail-topics a\")\\n+        assert len(topics) == len(build.TOPICS)\\n+        for link in rail.select(\".rail-story, .rail-more, .rail-topics a\"):\\n+            url = link[\"href\"]\\n+            assert url.startswith(f\"/{prefix}\")\\n+            assert (build.SITE_DIR / url.lstrip(\"/\") / \"index.html\").is_file()\\n+        for timestamp in rail.select(\".rail-story time\"):\\n+            assert build.parse_iso(timestamp[\"datetime\"])\\n+\\n+\\n+if __name__ == \"__main__\":\\n+    test_taiwan_sidebar_selection()\\n+    test_rendered_sidebar()\\n+    print(\"test_home_sidebar: 2 passed\")"
}
