{
  "sha": "f4760026bbcea265b523824ff54bdc6f5bbe66ea",
  "message": "新增 SKYTICAL 團隊頁與創辦人介紹 (#368)",
  "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-23T09:48:46Z",
  "files": [
    {
      "filename": "config/team.json",
      "previous_filename": "",
      "status": "added",
      "additions": 80,
      "deletions": 0,
      "changes": 80
    },
    {
      "filename": "docs/open-source-scouting.md",
      "previous_filename": "",
      "status": "modified",
      "additions": 12,
      "deletions": 0,
      "changes": 12
    },
    {
      "filename": "pipeline/build.py",
      "previous_filename": "",
      "status": "modified",
      "additions": 18,
      "deletions": 4,
      "changes": 22
    },
    {
      "filename": "static/site.css",
      "previous_filename": "",
      "status": "modified",
      "additions": 2,
      "deletions": 1,
      "changes": 3
    },
    {
      "filename": "static/team-nwe.jpg",
      "previous_filename": "",
      "status": "added",
      "additions": 0,
      "deletions": 0,
      "changes": 0
    },
    {
      "filename": "static/team.css",
      "previous_filename": "",
      "status": "added",
      "additions": 47,
      "deletions": 0,
      "changes": 47
    },
    {
      "filename": "templates/team.html",
      "previous_filename": "",
      "status": "added",
      "additions": 48,
      "deletions": 0,
      "changes": 48
    },
    {
      "filename": "tests/test_seo.py",
      "previous_filename": "",
      "status": "modified",
      "additions": 25,
      "deletions": 0,
      "changes": 25
    }
  ],
  "diff": "@@ -0,0 +1,80 @@\\n+{\\n+  \"zh\": {\\n+    \"title\": \"SKYTICAL 團隊\",\\n+    \"description\": \"認識 SKYTICAL 創辦人 nwe，以及這個由一人獨立營運、免費分享航空新聞的網站背後的故事。\",\\n+    \"heading\": \"航空世界很大，從一份好奇開始。\",\\n+    \"name\": \"nwe\",\\n+    \"role\": \"創辦人・首席網站營運與維護顧問\",\\n+    \"intro\": \"你好，我是 nwe。從自己想看的新聞網站開始，SKYTICAL 慢慢成為與大家分享航空大小事的地方。\",\\n+    \"photoAlt\": \"SKYTICAL 創辦人 nwe 的個人照片\",\\n+    \"photoCaption\": \"航空、攝影、音樂，還有路上的風景。\",\\n+    \"contactTitle\": \"與我聯絡\",\\n+    \"instagramLabel\": \"Instagram · @ks.17___\",\\n+    \"emailLabel\": \"寄信給我\",\\n+    \"sections\": [\\n+      {\\n+        \"title\": \"從自己想看的新聞網站開始\",\\n+        \"body\": [\\n+          \"我一直對航空業發生的大小事充滿興趣，但航空資訊往往散落在各家航空公司、各國民航主管機關與媒體之間。許多消息又具有時效性，等到偶然看到時，可能早已錯過最值得關注的時刻。\",\\n+          \"因此，我建立了 SKYTICAL，希望透過全自動新聞聚合，將分散的航空消息整理在一起，免費提供給讀者，讓大家更方便掌握航空界的重要動態、產業消息，以及令人好奇的話題與幕後故事。\",\\n+          \"最初，這只是一個做給自己看的新聞網站。隨著功能逐漸增加，網站也越來越實用，我才開始希望將它分享給更多讀者，以及同樣關心航空世界的朋友。\"\\n+        ]\\n+      },\\n+      {\\n+        \"title\": \"一個人營運，也持續往前\",\\n+        \"body\": [\\n+          \"從網站開發、功能規劃，到日常營運、更新與維護，目前都由我一人負責。\",\\n+          \"身為學生，能投入的時間有限，但我會持續在課業與網站之間找到平衡，盡力讓 SKYTICAL 保持更新、穩定運作，也讓每一次調整都更貼近讀者的需要。\"\\n+        ]\\n+      },\\n+      {\\n+        \"title\": \"航空之外的我\",\\n+        \"body\": [\\n+          \"目前是一名大學生。比起單純以「航空迷」介紹自己，我更覺得自己是對國內外整個航空產業都充滿好奇的愛好者。\",\\n+          \"航空之外，我也喜歡攝影、音樂與旅遊。最近尤其沉迷於 neo-soul，常聽 Frank Ocean 和 Daniel Caesar，讓音樂陪著自己度過日常，也陪著網站一點一點成長。\"\\n+        ]\\n+      }\\n+    ],\\n+    \"closing\": \"謝謝你來到 SKYTICAL，一起關心航空世界的下一件事。\",\\n+    \"aboutLabel\": \"了解內容如何產製\"\\n+  },\\n+  \"en\": {\\n+    \"title\": \"SKYTICAL Team\",\\n+    \"description\": \"Meet nwe, the founder behind SKYTICAL, an independently operated website sharing aviation news free of charge.\",\\n+    \"heading\": \"A world of aviation. A spark of curiosity.\",\\n+    \"name\": \"nwe\",\\n+    \"role\": \"Founder · Chief Website Operations & Maintenance Consultant\",\\n+    \"intro\": \"Hi, I'm nwe. What began as a news website I wanted to read has gradually become a place to share the aviation world with others.\",\\n+    \"photoAlt\": \"Portrait of nwe, founder of SKYTICAL\",\\n+    \"photoCaption\": \"Aviation, photography, music, and the places along the way.\",\\n+    \"contactTitle\": \"Get in touch\",\\n+    \"instagramLabel\": \"Instagram · @ks.17___\",\\n+    \"emailLabel\": \"Email me\",\\n+    \"sections\": [\\n+      {\\n+        \"title\": \"It started with the news I wanted to read\",\\n+        \"body\": [\\n+          \"I've always been curious about what's happening across aviation. But the information is scattered among airlines, civil aviation authorities around the world, and the media. Much of it is time-sensitive; by the time I happened to find a story, the moment to follow it had often passed.\",\\n+          \"That's why I built SKYTICAL: a fully automated news aggregator that brings those scattered updates together, free for readers. I wanted to make it easier to keep up with major developments, industry news, interesting conversations, and the stories behind them.\",\\n+          \"At first, this was simply a news website for myself. As its features grew and it became more useful, I began to want to share it with other readers and anyone equally curious about aviation.\"\\n+        ]\\n+      },\\n+      {\\n+        \"title\": \"One person, keeping things moving\",\\n+        \"body\": [\\n+          \"From development and feature planning to day-to-day operations, updates, and maintenance, I currently look after the whole website myself.\",\\n+          \"As a student, my time is limited. I'm working to balance my studies with the website, keeping SKYTICAL updated and running reliably, and making each improvement more useful to its readers.\"\\n+        ]\\n+      },\\n+      {\\n+        \"title\": \"Beyond aviation\",\\n+        \"body\": [\\n+          \"I'm currently a university student. Rather than simply calling myself an aviation fan, I think of myself as someone curious about the entire industry, both in Taiwan and abroad.\",\\n+          \"Outside aviation, I enjoy photography, music, and travel. Lately I've been especially drawn to neo-soul, with Frank Ocean and Daniel Caesar often on repeat as I go about my day and keep building this website.\"\\n+        ]\\n+      }\\n+    ],\\n+    \"closing\": \"Thanks for being here. Let's keep following what happens next in aviation.\",\\n+    \"aboutLabel\": \"How our stories are produced\"\\n+  }\\n+}\\n@@ -2,6 +2,18 @@\\n \\n 更新日期：2026-10-23\\n \\n+## 團隊頁與創辦人介紹（2026-10-23）\\n+\\n+透過 GitHub repository search、官方 API 與 README 比較三個仍在維護的個人網站主題。僅參考內容層級與版面設計，未複製程式碼或新增依賴。\\n+\\n+| 專案 | 最近提交 | Stars | 語言／授權 | 架構、借鑑方向與導入成本 |\\n+| --- | --- | --- | --- | --- |\\n+| [al-folio](https://github.com/alshedivat/al-folio) | 2026-10-19 | 16,268 | HTML／MIT | Jekyll 個人網站；參考照片、個人介紹與內容分段。整套導入需要 Ruby／Jekyll，與現有建站流程重複。 |\\n+| [Minimal Mistakes](https://github.com/mmistakes/minimal-mistakes) | 2026-10-09 | 13,588 | HTML／MIT | Jekyll 雙欄個人網站；參考介紹與主要內容的閱讀層級。需要 Jekyll 與 include-cache 外掛，不適合只為一頁加入。 |\\n+| [Blowfish](https://github.com/nunocoracao/blowfish) | 2026-10-07 | 2,916 | HTML／MIT | Hugo 與 Tailwind CSS 4；參考響應式介紹頁。需要另一套建站與 CSS 工具，不導入。最近推送為 2026-10-21，與最近主分支提交日期分開記錄。 |\\n+\\n+SKYTICAL 已有 Jinja2 共用導覽、雙語路徑與主題色票；保留現況，用一份雙語設定、一個版型與頁面限定 CSS 完成。使用創辦人提供的照片，以 CSS 控制顯示範圍，保留原始影像。沒有需要另開 issue 導入的套件。\\n+\\n ## 日本突發飛安搜尋與來源零筆提示（2026-10-23）\\n \\n 本次透過 GitHub API 查證維護狀態、授權與 README，並實測日本語 RSS：\\n@@ -102,6 +102,8 @@ def static_asset_version() -> str:\\n         \"skytical-social.png\",\\n         \"briefing.css\",\\n         \"briefing.js\",\\n+        \"team.css\",\\n+        \"team-nwe.jpg\",\\n     ):\\n         digest.update((STATIC_DIR / name).read_bytes())\\n     return digest.hexdigest()[:10]\\n@@ -196,7 +198,7 @@ def ga4_measurement_id() -> str:\\n         \"footerAbout\": \"SKYTICAL 是以可追溯來源為基礎的自動化航空新聞彙整服務；每篇報導附原始來源，原始報導與更正公告優先。\",\\n         \"footerSources\": \"資料來源 Data Sources\",\\n         \"footerChangelog\": \"網站更新紀錄\",\\n-        \"nav\": [\"最新\", \"全部新聞\", \"快報\", \"航班雷達\", \"事故資料庫\", \"來源\", \"關於 SKYTICAL\"],\\n+        \"nav\": [\"最新\", \"全部新聞\", \"快報\", \"航班雷達\", \"事故資料庫\", \"來源\", \"關於 SKYTICAL\", \"SKYTICAL 團隊\"],\\n         \"cats\": [\"全部\", \"事故\", \"法規\", \"商業\", \"營運\", \"軍事\"],\\n         \"searchKicker\": \"News Search\", \"searchTitle\": \"搜尋新聞\",\\n         \"newsKicker\": \"News Index\", \"newsTitle\": \"全部新聞\",\\n@@ -350,7 +352,7 @@ def ga4_measurement_id() -> str:\\n         \"footerSources\": \"Data Sources\",\\n         \"footerChangelog\": \"Site changelog\",\\n         \"nav\": [\"Latest\", \"All news\", \"Briefings\", \"Flight Radar\", \"Incident DB\", \"Sources\",\\n-                 \"About SKYTICAL\"],\\n+                 \"About SKYTICAL\", \"SKYTICAL Team\"],\\n         \"cats\": [\"All\", \"Safety\", \"Regulation\", \"Business\", \"Operations\",\\n                  \"Military\"],\\n         \"searchKicker\": \"News Search\", \"searchTitle\": \"Search news\",\\n@@ -3626,7 +3628,8 @@ def base_ctx(lang, page, sub, *, title, description, ticker, build,\\n     nav_defs = [(\"home\", \"\"), (\"news\", \"news/\"), (\"briefings\", \"briefings/\"),\\n                 (\"radar\", \"radar/\"),\\n                 (\"incidents\", \"incidents/\"),\\n-                (\"sources\", \"sources/\"), (\"about\", \"about/\")]\\n+                (\"sources\", \"sources/\"), (\"about\", \"about/\"),\\n+                (\"team\", \"team/\")]\\n     return {\\n         \"lang\": lang, \"t\": t, \"base\": BASE_PATH, \"favicon\": FAVICON,\\n         \"asset_version\": ASSET_VERSION,\\n@@ -3860,7 +3863,7 @@ def add_url(lang: str, sub: str, modified=None) -> None:\\n     ]\\n     latest_briefing_modified = (\\n         max(briefing_modified) if briefing_modified else None)\\n-    static_sections = (\"radar/\", \"incidents/\", \"sources/\", \"about/\",\\n+    static_sections = (\"radar/\", \"incidents/\", \"sources/\", \"about/\", \"team/\",\\n                        \"editorial-policy/\", \"changelog/\")\\n     for lang in (\"zh\", \"en\"):\\n         localized = [\\n@@ -4368,6 +4371,17 @@ def main() -> int:\\n         render(env, \"about.html\", rel_path(lang, \"about/index.html\"), ctx)\\n         pages += 1\\n \\n+        team = load_json(CONFIG_DIR / \"team.json\", {})[lang]\\n+        ctx = base_ctx(lang, \"team\", \"team/\",\\n+                       title=f\"{team['title']}｜{t['siteName']}\",\\n+                       description=team[\"description\"], ticker=ticker, build=build)\\n+        ctx[\"structured_data\"] = breadcrumb_structured_data(lang, [\\n+            (t[\"siteName\"], \"\"), (team[\"title\"], \"team/\"),\\n+        ])\\n+        ctx.update(team=team)\\n+        render(env, \"team.html\", rel_path(lang, \"team/index.html\"), ctx)\\n+        pages += 1\\n+\\n         policy = EDITORIAL_POLICY[lang]\\n         ctx = base_ctx(\\n             lang, \"policy\", \"editorial-policy/\",\\n@@ -183,7 +183,8 @@ a.ticker-item:hover .ticker-time { color: var(--color-accent); }\\n .nav-link:hover { color: var(--color-accent); }\\n .nav-link.active { color: var(--color-accent); border-bottom-color: var(--color-accent); }\\n .site-nav {\\n-  display: flex; gap: 2px; margin-left: auto; white-space: nowrap;\\n+  display: flex; flex-wrap: wrap; justify-content: flex-end;\\n+  gap: 2px; margin-left: auto; white-space: nowrap;\\n }\\n \\n /* — segmented controls (language links / theme + filter buttons) — */\\n@@ -0,0 +1,47 @@\\n+/* The founder's portrait leads; the rest uses SKYTICAL's existing theme tokens. */\\n+.team-page {\\n+  --team-muted: color-mix(in srgb, var(--color-text) 74%, var(--color-bg));\\n+  padding: clamp(28px, 4vw, 56px) clamp(20px, 5vw, 72px) 64px;\\n+}\\n+.team-page *, .team-page *::before, .team-page *::after { box-sizing: border-box; }\\n+.team-header { display: flex; justify-content: space-between; align-items: baseline; gap: 20px; margin-bottom: 32px; }\\n+.team-header h1 { margin: 0; font-size: clamp(23px, 2.5vw, 32px); line-height: 1.4; }\\n+.team-header p { margin: 0; font-size: 14px; color: var(--team-muted); }\\n+.team-hero { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr); gap: clamp(32px, 5vw, 72px); align-items: center; }\\n+.team-portrait { margin: 0; min-width: 0; }\\n+.team-portrait img {\\n+  display: block; width: 100%; height: auto; aspect-ratio: 4 / 5;\\n+  object-fit: cover; object-position: 50% 70%; border-radius: 6px 6px 48px 6px;\\n+}\\n+.team-portrait figcaption { margin-top: 12px; font-size: 12px; line-height: 1.8; color: var(--team-muted); }\\n+.team-introduction { min-width: 0; padding: 16px 0 36px; }\\n+.team-introduction h2 { margin: 0 0 20px; font-size: clamp(80px, 10vw, 136px); font-weight: 800; line-height: .95; letter-spacing: -.065em; }\\n+.team-page .team-role { margin: 0 0 28px; font-size: 15px; font-weight: 700; line-height: 1.8; }\\n+.team-lead { max-width: 26em; margin: 0; font-size: clamp(18px, 2vw, 23px); line-height: 1.85; text-wrap: pretty; }\\n+.team-contact { margin-top: 36px; padding-top: 24px; border-top: 2px solid var(--color-accent); }\\n+.team-contact h3 { margin: 0 0 10px; font-size: 13px; color: var(--team-muted); font-weight: 500; }\\n+.team-contact a { display: flex; justify-content: space-between; align-items: baseline; flex-wrap: wrap; gap: 4px 16px; padding: 12px 0; min-height: 44px; border-bottom: 1px solid var(--color-divider); color: var(--color-text); font-size: 14px; font-weight: 600; text-decoration: none; overflow-wrap: anywhere; }\\n+.team-contact a:hover { color: var(--color-accent); }\\n+.team-email { font-size: 13px; font-weight: 400; }\\n+.team-story { margin-top: clamp(44px, 6vw, 80px); }\\n+.team-chapter { display: grid; grid-template-columns: minmax(0, .8fr) minmax(0, 1.2fr); gap: clamp(24px, 5vw, 72px); padding: 36px 0; border-top: 1px solid var(--color-divider); }\\n+.team-chapter h3 { margin: 0; max-width: 12em; font-size: clamp(22px, 2.4vw, 28px); line-height: 1.6; text-wrap: balance; }\\n+.team-chapter p { margin: 0 0 18px; max-width: 42em; font-size: 16px; line-height: 2; color: var(--team-muted); }\\n+.team-chapter p:last-child { margin-bottom: 0; }\\n+.team-closing { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: baseline; gap: 20px; margin-top: 12px; padding-top: 28px; border-top: 2px solid var(--color-accent); }\\n+.team-closing p { margin: 0; max-width: 34em; font-size: 17px; font-weight: 600; line-height: 1.8; text-wrap: pretty; }\\n+.team-closing a { display: inline-flex; align-items: center; gap: 16px; min-height: 44px; color: var(--color-text); font-size: 14px; text-underline-offset: 5px; }\\n+.team-page a:focus-visible { outline: 3px solid var(--color-accent); outline-offset: 4px; }\\n+.team-page a { touch-action: manipulation; }\\n+@media (max-width: 720px) {\\n+  .team-header { display: block; margin-bottom: 24px; }\\n+  .team-header p { margin-top: 10px; line-height: 1.8; }\\n+  .team-hero { grid-template-columns: minmax(0, 1fr); gap: 28px; }\\n+  .team-portrait { max-width: 480px; width: 100%; margin: 0 auto; }\\n+  .team-introduction { padding: 0; }\\n+  .team-introduction h2 { font-size: 88px; margin-bottom: 16px; }\\n+  .team-page .team-role { margin-bottom: 20px; }\\n+  .team-contact { margin-top: 24px; }\\n+  .team-chapter { grid-template-columns: minmax(0, 1fr); gap: 18px; padding: 28px 0; }\\n+  .team-chapter h3 { max-width: none; }\\n+}\\n@@ -0,0 +1,48 @@\\n+{% extends \"base.html\" %}\\n+{% block head_extra %}\\n+<link rel=\"stylesheet\" href=\"{{ base }}/assets/team.css?v={{ asset_version }}\">\\n+{% endblock %}\\n+{% block content %}\\n+<main class=\"team-page\" id=\"team-main\">\\n+  <header class=\"team-header\">\\n+    <h1>{{ team.title }}</h1>\\n+    <p>{{ team.heading }}</p>\\n+  </header>\\n+  <article class=\"team-profile\" aria-labelledby=\"team-name\">\\n+    <div class=\"team-hero\">\\n+      <figure class=\"team-portrait\">\\n+        <img src=\"{{ base }}/assets/team-nwe.jpg?v={{ asset_version }}\"\\n+             alt=\"{{ team.photoAlt }}\" width=\"720\" height=\"1280\"\\n+             fetchpriority=\"high\" decoding=\"async\">\\n+        <figcaption>{{ team.photoCaption }}</figcaption>\\n+      </figure>\\n+      <div class=\"team-introduction\">\\n+        <h2 id=\"team-name\">{{ team.name }}</h2>\\n+        <p class=\"team-role\">{{ team.role }}</p>\\n+        <p class=\"team-lead\">{{ team.intro }}</p>\\n+        <div class=\"team-contact\" aria-labelledby=\"team-contact-title\">\\n+          <h3 id=\"team-contact-title\">{{ team.contactTitle }}</h3>\\n+          <a href=\"https://www.instagram.com/ks.17___/\" target=\"_blank\" rel=\"noopener noreferrer\">\\n+            {{ team.instagramLabel }}<span aria-hidden=\"true\">↗</span>\\n+          </a>\\n+          <a href=\"mailto:kass09221477@gmail.com\">\\n+            {{ team.emailLabel }}<span class=\"team-email\">kass09221477@gmail.com</span>\\n+          </a>\\n+        </div>\\n+      </div>\\n+    </div>\\n+    <div class=\"team-story\">\\n+      {% for section in team.sections %}\\n+      <section class=\"team-chapter\" aria-labelledby=\"team-chapter-{{ loop.index }}\">\\n+        <h3 id=\"team-chapter-{{ loop.index }}\">{{ section.title }}</h3>\\n+        <div>{% for paragraph in section.body %}<p>{{ paragraph }}</p>{% endfor %}</div>\\n+      </section>\\n+      {% endfor %}\\n+    </div>\\n+  </article>\\n+  <div class=\"team-closing\">\\n+    <p>{{ team.closing }}</p>\\n+    <a href=\"{{ about_url }}\">{{ team.aboutLabel }}<span aria-hidden=\"true\">↗</span></a>\\n+  </div>\\n+</main>\\n+{% endblock %}\\n@@ -228,6 +228,31 @@ def main() -> None:\\n     assert \"SKYTICAL/issues/new\" in policy\\n     assert 'href=\"/editorial-policy/\"' in article\\n \\n+    for lang, sub, label in ((\"zh\", \"\", \"SKYTICAL 團隊\"),\\n+                             (\"en\", \"en/\", \"SKYTICAL Team\")):\\n+        team_url = f\"{build.SITE_ORIGIN}{build.BASE_PATH}/{sub}team/\"\\n+        assert team_url in locations\\n+        team = (ROOT / \"site\" / sub / \"team\" / \"index.html\").read_text(\\n+            encoding=\"utf-8\")\\n+        assert f\"<h1>{label}</h1>\" in team\\n+        assert f'<link rel=\"canonical\" href=\"{team_url}\">' in team\\n+        assert 'hreflang=\"zh-Hant-TW\"' in team and 'hreflang=\"en\"' in team\\n+        assert '<h2 id=\"team-name\">nwe</h2>' in team\\n+        assert 'href=\"https://www.instagram.com/ks.17___/\"' in team\\n+        assert 'href=\"mailto:kass09221477@gmail.com\"' in team\\n+        assert \"Frank Ocean\" in team and \"Daniel Caesar\" in team\\n+        assert \"/assets/team-nwe.jpg?v=\" in team\\n+        nav = re.search(r'<nav class=\"site-nav\".*?</nav>', team,\\n+                        flags=re.DOTALL).group(0)\\n+        assert nav.index(f'/{sub}about/') < nav.index(f'/{sub}team/')\\n+        assert f'class=\"nav-link active\" href=\"{build.BASE_PATH}/{sub}team/\"' in nav\\n+        assert any(entity.get(\"@type\") == \"BreadcrumbList\"\\n+                   for entity in _schema_entities(team))\\n+    assert \"創辦人・首席網站營運與維護顧問\" in (\\n+        ROOT / \"site/team/index.html\").read_text(encoding=\"utf-8\")\\n+    assert (ROOT / \"site/assets/team-nwe.jpg\").read_bytes() == (\\n+        ROOT / \"static/team-nwe.jpg\").read_bytes()\\n+\\n     print(\"test_seo: OK\")\\n \\n "
}
