2026-10-24 SAT UTC 7:56 PM TPE 3:56 AM 每小時自動更新 中文 EN
LIVE 快訊
← 返回網站更新紀錄 Site Changelog

提交原始紀錄

feat: add Taiwan aviation focus and topic links to home sidebar (#333)

SHA
bef6dec80650df07395ef3188b05494dda631c88
提交時間
作者
CHI HSUN YANG · 125815713+nnneeeooo-nnneeeooo@users.noreply.github.com
提交者
GitHub · noreply@github.com

變更檔案 (5)

  • .github/workflows/hourly.yml modified · +1 / −0
  • pipeline/build.py modified · +32 / −0
  • static/site.css modified · +16 / −0
  • templates/home.html modified · +27 / −0
  • tests/test_home_sidebar.py added · +63 / −0

完整差異

@@ -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")

你發現了隱藏主題!

8-bit 主題已解鎖。按下「重新整理」就會直接套用,之後可隨時在主題選單切換。