Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

All notable public product changes are recorded here. Release notes contain only product scope and reproducible validation; platform identifiers and private operational details are excluded.

## v1.47.0 — 作品頁歌曲導覽

- 超過 4 筆歌曲的作品頁新增可收合的頁內導覽,按 OP/ED 分組,完整顯示曲名與演唱者並直接跳到對應版本。
- 長清單限制展開高度,保留鍵盤捲動、選歌焦點、瀏覽器返回、無 JavaScript 與離線操作;目前指向的歌曲以邊框提示。
- 全域導覽、目錄資料與靜態 API 保持不變。

## v1.46.0 — 2019 春季《強襲魔女 501部隊出動!》

- 收錄 2019 年 TV 短篇、1 首 OP 與 12 個輪替 ED 演唱版本,保留各自角色、聲優、CD 日期及歌曲用途。
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ Anisonary 是以 Astro + strict TypeScript 建立的動畫歌曲目錄,按季

This repository contains the completed static product: season directory, anime detail pages, traceable OP/ED credits and links, per-song source ledgers, source-attributed media, local-only cross-season search, privacy-bounded offline reading, a GitHub correction flow, and deployment through Cloudflare Workers Static Assets. The default catalogue covers twenty-nine reviewed snapshots across 2019–2026, with 1,948 unique titles and 4,320 known OP/ED records. The 2019 spring snapshot contains thirty-one TV titles, including television shorts and a TV recut; summer contains 38 TV titles and four web series. Both quarters are still being expanded, and 2025 fall is not yet included. Fictional Mock Data remains test-only.

目前 source 版本:**v1.46.0**。新增《強襲魔女 501部隊出動!》2019 年 TV 版、1 首 OP、12 個輪替 ED 演唱版本與 2 筆官方影片 metadata。目錄共有 29 個季度、1,948 套作品與 4,320 筆 OP/ED;春季為 31 套作品、91 筆歌曲,春、夏季仍在補充,2025 秋季尚未收錄。
目前 source 版本:**v1.47.0**。歌曲較多的作品頁提供可收合的 OP/ED 導覽,按曲名與演唱者直接選擇版本;鍵盤、無 JavaScript 與離線閱讀均可使用。目錄共有 29 個季度、1,948 套作品與 4,320 筆 OP/ED;春季為 31 套作品、91 筆歌曲,春、夏季仍在補充,2025 秋季尚未收錄。

導覽延續固定五個入口與年代、年份、季度目錄;搜尋可按動畫、歌曲、創作者、年份、季度及 OP/ED 篩選,每頁顯示 12 套作品。搜尋完全在瀏覽器內進行,YouTube 只於明確操作後連線。網站與同源靜態 JSON API 共用已審閱資料,無 application backend 或 database binding。依賴安全 gate 繼續鎖定 `fast-uri` 3.1.6 並稽核開發依賴。

Production build 會從最終 HTML 自動產生 hash-based Content Security Policy。政策不使用 `unsafe-inline` 或 `unsafe-eval`,禁止 inline event/style attributes,只開放同源資產、已核對的海報來源及使用者啟動後的 YouTube privacy-enhanced iframe。任何未批准的 media origin 會令 build fail closed。

| 可用性 / Availability | 成熟度 / Maturity | 證據 / Evidence |
| -------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 公開靜態目錄 / Public static catalogue | Source 版本為 `v1.46.0`;正式發布狀態以對應 Release 驗收摘要為準 | [入口網站 / Live](https://anisonary.k-y.cc) · [版本與驗收 / Releases](https://github.com/kyeunga25/anisonary/releases) · [資料來源 / Sources](docs/DATA_SOURCES.md) · [安全政策 / Security](SECURITY.md) · [授權 / Licence](LICENSING.md) · [版權 / Copyright](COPYRIGHT.md) |
| 公開靜態目錄 / Public static catalogue | Source 版本為 `v1.47.0`;正式發布狀態以對應 Release 驗收摘要為準 | [入口網站 / Live](https://anisonary.k-y.cc) · [版本與驗收 / Releases](https://github.com/kyeunga25/anisonary/releases) · [資料來源 / Sources](docs/DATA_SOURCES.md) · [安全政策 / Security](SECURITY.md) · [授權 / Licence](LICENSING.md) · [版權 / Copyright](COPYRIGHT.md) |

## 技術棧|Technology stack

Expand Down
5 changes: 3 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ Astro output ──► generated CSP ──► bounded service worker ──►
| `src/data/curated-season-registry.ts` | 集中登記季度顯示順序、季度索引與擁有者模組,並在載入時拒絕重複、缺失或跨季度漂移 |
| `src/data/curated-seeds.ts` | 保留既有匯入介面及作品順序的相容聚合層 |
| `src/data/curated-theme-sources/<year>/<quarter>.ts` | 按作品 owner 季度分檔的逐歌曲公開來源 ledger;只保存未加入核對日期的來源 seed |
| `src/data/curated-theme-sources.ts` | 合併二十七季來源 ledger,拒絕錯誤 owner、重複 key、非 HTTPS、URL credentials、未知語言/角色及私人核對欄位 |
| `src/data/curated-theme-sources.ts` | 合併已登記季度的來源 ledger,拒絕錯誤 owner、重複 key、非 HTTPS、URL credentials、未知語言/角色及私人核對欄位 |
| `src/data/curated-theme-videos/<year>/<quarter>.ts` | 按作品 owner 季度分檔的已審閱官方影片 metadata;保留影片種類、頻道、官方狀態及嵌入設定 |
| `src/data/curated-theme-videos.ts` | 合併二十七季影片 override,拒絕錯誤 owner、重複 key、無效或同曲重複的 YouTube ID、空欄位及未知欄位 |
| `src/data/curated-theme-videos.ts` | 合併已登記季度的影片 override,拒絕錯誤 owner、重複 key、無效或同曲重複的 YouTube ID、空欄位及未知欄位 |
| `src/data/curated-data.ts` | 其餘少量可審核 override 與公開 record 派生 |
| `src/types/public-api.ts` | HTML 與 static API 共用的公開契約 |
| `src/data/api-provider.ts` | 外部 JSON 的 fail-closed parser 與 request boundary |
Expand All @@ -44,6 +44,7 @@ Astro output ──► generated CSP ──► bounded service worker ──►
| `src/pages/catalog/` | 固定入口下的年代目錄與年份靜態頁 |
| `src/scripts/catalog-search.ts` | 本機搜尋、每頁最多 12 套作品的 DOM、歌曲 anchor 導航與署名入口條件 |
| `src/utils/creator-search.ts` | 只使用同源網址片段的署名搜尋連結,與輸入共用 80 UTF-16 單位上限 |
| `src/components/theme/ThemeNavigation.astro` | 由作品現有歌曲派生的 OP/ED 頁內導覽;超過 4 筆時使用原生收合區塊與固定歌曲 anchor,不新增腳本或資料來源 |
| `src/pages/api/v1/` | build-time 靜態 JSON routes |
| `scripts/generate-security-headers.mjs` | 由 build output 產生 hash-based CSP |
| `scripts/generate-service-worker.mjs` | 由 build output 產生同源、無 runtime write 的離線清單 |
Expand Down
1 change: 1 addition & 0 deletions docs/PROJECT_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
- **v1.44.1 source**:縮短作品頁資料、來源連結與歌曲分區間距,保留海報尺寸、文字層級及全部連結;目錄、搜尋與 API 不變,發布證據見對應 GitHub Release。
- **v1.45.0 source**:歌曲署名直接開啟本機創作者文字搜尋,保留完整名稱及既有字數上限;入口條件使用不會送出的網址片段,手動編輯時清除,資料、API 與全域導覽數量不變;發布證據見對應 GitHub Release。
- **v1.46.0 source**:2019 春季增加《強襲魔女 501部隊出動!》TV 短篇、13 筆 OP/ED 與 2 筆官方影片 metadata;12 個片尾演唱版本各自保留,作曲署名衝突欄位暫缺,春季共 31 套作品與 91 筆歌曲;發布證據見對應 GitHub Release。
- **v1.47.0 source**:歌曲超過 4 筆的作品新增可收合 OP/ED 導覽,完整列出曲名與演唱者,保留原有深連結、鍵盤焦點及無 JavaScript/離線閱讀;長清單高度受限,資料、API 與全域導覽數量不變;發布證據見對應 GitHub Release。
- 正式網站使用 custom domain;非正式 Cloudflare hostname 不在公開文檔記錄;
- 私有 backend、資料庫、crawler、帳戶設定、憑證與內部規則不屬於本公開 repository。

Expand Down
25 changes: 25 additions & 0 deletions docs/QA_V1_47_SONG_NAVIGATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# v1.47.0|作品頁歌曲導覽 QA

核對日期:2026-09-07。本文件記錄 source 範圍與本機驗證;正式部署須於固定版本合併後獨立驗收。

## 介面範圍

- 超過 4 筆歌曲的作品頁新增原生可收合導覽;目前涵蓋 140 套作品,最長清單為 25 筆。按 OP/ED 分組,完整顯示曲名與演唱者,沿用既有歌曲深連結。
- 導覽預設收合,展開高度限制為 26rem 或畫面高度 55% 的較小值;手機單欄、桌面雙欄,長名稱完整換行,清單可使用原生捲動與鍵盤。
- 選歌後將焦點移到該歌曲卡片,顯示目標邊框;下一個 Tab 進入歌曲的連結。瀏覽器返回保留同頁導覽展開狀態,關閉 JavaScript 及離線頁面仍可選歌。
- 0–4 筆歌曲不顯示額外導覽。全域選單數量、來源、曲名、credits、影片 metadata 與 API 不變;沒有新增客戶端腳本、第三方請求、runtime backend 或儲存項目。

## 本機驗收

- `npm run catalog:check`:228 項通過。
- `npm run check`:公開邊界、Lint、331 項單元測試、建置、Cloudflare dry-run 與 42 項 Playwright 測試通過;最終瀏覽器測試沒有重試。
- 完整依賴與正式依賴稽核均為 0 項漏洞;`fast-uri` 保持 3.1.6,既有安全回歸通過。
- 1,978 份靜態 API 與 v1.46.0 source 逐位元相同。逐頁核對全部 1,948 套作品:140 套的 1,125 個導覽連結與已審閱歌曲次序相同,所有目標可聚焦,沒有重複 DOM ID。
- 新增 E2E 涵蓋 1280、960、390、320px 的收合、展開、OP/ED 與演唱者辨識、選歌、焦點、Tab、瀏覽器返回及小清單空缺;另以 390px、停用 JavaScript 核對最長 25 筆清單的最後歌曲。
- 既有離線測試增加原生歌曲導覽與 ED #12 跳轉;搜尋、資料來源展開、影片明確同意、404、錯誤狀態與鍵盤導覽回歸通過。
- Chromium 檢查兩套長清單作品、四種寬度及 390px 無 JavaScript,合共 30 個收合/展開/目標畫面狀態;頁面身份、主標題、焦點及高度符合預期,沒有水平溢出、框架錯誤、console error 或操作前的 iframe/YouTube 請求。另人工檢視桌面、手機與手機深色模式截圖。
- 建置產生 1,992 份 HTML、3,983 份公開產物;Cloudflare dry-run 沒有 runtime bindings。

## 發布界線

本機與分支建置結果不代表正式網站已更新。發布時仍須核對合併 SHA、CI、Cloudflare 實際版本與流量,以及 live routes、headers、API 與產物內容。本切片未驗證實體觸控裝置、Safari、Firefox、Lighthouse/Core Web Vitals 或實際 YouTube 播放。
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "anisonary",
"version": "1.46.0",
"version": "1.47.0",
"private": true,
"license": "SEE LICENSE IN LICENSING.md",
"type": "module",
Expand Down
3 changes: 2 additions & 1 deletion src/components/theme/ThemeCard.astro
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const sourceRoleLabels = {
} as const;
---

<article class="theme-card" id={`theme-${theme.id}`}>
<article class="theme-card" id={`theme-${theme.id}`} tabindex="-1">
<div class:list={["theme-card__summary", { "theme-card__summary--compact": theme.sources.length <= 3 }]}>
<div class:list={["theme-card__type", `theme-card__type--${theme.type.toLowerCase()}`]} aria-label={`${theme.type} ${theme.sequence}`}>
<strong>{theme.type}{theme.sequence}</strong>
Expand Down Expand Up @@ -112,6 +112,7 @@ const sourceRoleLabels = {
<style>
.theme-card { scroll-margin-top: 6rem; }
.theme-card { background: var(--color-surface); border: 1px solid var(--color-border-strong); border-radius: var(--radius-card); overflow: hidden; }
.theme-card:target { border-color: var(--color-focus); }
.theme-card + .theme-card { margin-top: 0.8rem; }
.theme-card__summary { display: grid; grid-template-columns: 5rem minmax(0, 1.3fr) minmax(0, 1fr); }
.theme-card__type { align-items: center; background: var(--color-accent); color: #fff; display: flex; flex-direction: column; justify-content: center; padding: 1rem; }
Expand Down
73 changes: 73 additions & 0 deletions src/components/theme/ThemeNavigation.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
import type { PublicTheme } from "@/types/public-api";

interface Props { themes: readonly PublicTheme[]; }
const { themes } = Astro.props;
const groups = (["OP", "ED"] as const)
.map((type) => ({ type, themes: themes.filter((theme) => theme.type === type) }))
.filter((group) => group.themes.length > 0);
---

{themes.length > 4 && (
<details class="theme-navigation" id="song-navigation">
<summary>
<span class="theme-navigation__label">歌曲導覽</span>
<span class="theme-navigation__count">{groups.map(({ type, themes: entries }) => `${entries.length} 首 ${type}`).join(" · ")}</span>
<span class="theme-navigation__toggle">
<span class="theme-navigation__expand">選擇歌曲</span>
<span class="theme-navigation__collapse">收合</span>
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
</span>
</summary>
<nav class="theme-navigation__scroller" aria-label="本作品歌曲">
{groups.map(({ type, themes: entries }) => (
<section aria-labelledby={`song-navigation-${type.toLowerCase()}`}>
<h3 id={`song-navigation-${type.toLowerCase()}`}>{type === "OP" ? "OP · 片頭曲" : "ED · 片尾曲"}</h3>
<ol>
{entries.map((theme) => (
<li>
<a href={`#theme-${theme.id}`}>
<span class="theme-navigation__type">{type}{theme.sequence}</span>
<span class="theme-navigation__entry">
<span class="theme-navigation__title" lang="ja">{theme.titleJa}</span>
<span class="theme-navigation__artist">{theme.artistDisplayName}</span>
</span>
<span class="theme-navigation__arrow" aria-hidden="true">↓</span>
</a>
</li>
))}
</ol>
</section>
))}
</nav>
</details>
)}

<style>
.theme-navigation { background: var(--color-surface); border: 1px solid var(--color-border-strong); border-radius: var(--radius-card); margin-bottom: 1rem; scroll-margin-top: 6rem; }
.theme-navigation > summary { align-items: center; cursor: pointer; display: flex; flex-wrap: wrap; gap: 0.4rem 0.8rem; list-style: none; min-height: 3.5rem; padding: 0.85rem 1rem; }
.theme-navigation > summary::-webkit-details-marker { display: none; }
.theme-navigation__label { font-size: 0.9rem; font-weight: 750; }
.theme-navigation__count { color: var(--color-muted); font-size: 0.78rem; }
.theme-navigation__toggle { align-items: center; display: flex; font-size: 0.78rem; font-weight: 680; gap: 0.4rem; margin-left: auto; }
.theme-navigation__toggle svg { fill: none; height: 1rem; stroke: currentColor; stroke-width: 1.5; width: 1rem; }
.theme-navigation:not([open]) .theme-navigation__collapse, .theme-navigation[open] .theme-navigation__expand { display: none; }
.theme-navigation[open] .theme-navigation__toggle svg { transform: rotate(180deg); }
.theme-navigation__scroller { border-top: 1px solid var(--color-border); max-height: min(26rem, 55vh); overflow: auto; padding: 1rem; scroll-padding: 0.5rem; }
.theme-navigation__scroller section + section { margin-top: 1rem; }
.theme-navigation h3 { color: var(--color-muted); font-size: 0.75rem; letter-spacing: 0.03em; margin: 0 0 0.5rem; }
.theme-navigation ol { display: grid; gap: 0.5rem; grid-template-columns: repeat(2, minmax(0, 1fr)); list-style: none; margin: 0; padding: 0; }
.theme-navigation li { min-width: 0; }
.theme-navigation a { align-items: baseline; background: var(--color-subtle); border: 1px solid transparent; border-radius: var(--radius-control); color: var(--color-ink); display: flex; gap: 0.65rem; height: 100%; min-height: 2.75rem; padding: 0.7rem; text-decoration: none; }
.theme-navigation a:hover { border-color: var(--color-border-strong); }
.theme-navigation__type { color: var(--color-muted); flex: 0 0 2.5rem; font-size: 0.73rem; font-weight: 700; }
.theme-navigation__entry { display: grid; gap: 0.3rem; min-width: 0; overflow-wrap: anywhere; }
.theme-navigation__title { font-size: 0.86rem; font-weight: 700; line-height: 1.4; }
.theme-navigation__artist { color: var(--color-muted); font-size: 0.73rem; line-height: 1.5; }
.theme-navigation__arrow { flex-shrink: 0; margin-left: auto; }

@media (max-width: 660px) {
.theme-navigation ol { grid-template-columns: minmax(0, 1fr); }
.theme-navigation__count { flex-basis: 100%; order: 1; }
}
</style>
2 changes: 2 additions & 0 deletions src/pages/anime/[slug].astro
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import MockNotice from "@/components/common/MockNotice.astro";
import PosterImage from "@/components/common/PosterImage.astro";
import ThemeCard from "@/components/theme/ThemeCard.astro";
import ThemeEmptyState from "@/components/theme/ThemeEmptyState.astro";
import ThemeNavigation from "@/components/theme/ThemeNavigation.astro";
import { getDataProvider, shouldRequireApiData } from "@/data/provider";
import { loadCatalogSearchData } from "@/data/page-data";
import BaseLayout from "@/layouts/BaseLayout.astro";
Expand Down Expand Up @@ -123,6 +124,7 @@ catalogCorrectionUrl.searchParams.set("title", `資料修正:${anime.titleJa}`

<section class="detail-section" aria-labelledby="themes-title">
<h2 id="themes-title">主題曲</h2>
<ThemeNavigation themes={anime.themes} />
{anime.themes.length > 0 ? (
<div>{anime.themes.map((theme) => <ThemeCard theme={theme} />)}</div>
) : (
Expand Down
Loading