From 92b7a02f149b505d08f80aa417a8fb2e409f8b4a Mon Sep 17 00:00:00 2001 From: Kevin Deng Date: Sun, 23 Aug 2026 05:38:36 +0900 Subject: [PATCH] Move generated datasets and assets into Vite's asset graph (#47) --- .github/workflows/deploy.yml | 6 +- .gitignore | 21 +- .prettierignore | 7 +- AGENTS.md | 4 +- PRODUCT.md | 13 +- README.ja-JP.md | 24 +- README.md | 24 +- app/app.vue | 5 +- {public => app/assets}/flags/cn.svg | 0 {public => app/assets}/flags/hk.svg | 0 {public => app/assets}/flags/jp.svg | 0 {public => app/assets}/flags/kr.svg | 0 {public => app/assets}/flags/tw.svg | 0 app/components/DataSources.vue | 6 +- app/components/HeroOverprint.vue | 2 +- app/components/OverprintChar.vue | 2 +- app/components/RegionLabel.vue | 15 +- app/components/StrokeOrder.vue | 85 +++-- app/composables/chars.ts | 2 +- app/composables/overprint.ts | 2 +- app/locales/ja-jp.ts | 14 +- app/locales/ko-kr.ts | 16 +- app/locales/zh-cn.ts | 10 +- app/locales/zh-hk.ts | 10 +- app/locales/zh-tw.ts | 10 +- app/pages/about.vue | 12 +- app/styles/global.css | 6 +- app/types/assets.d.ts | 26 ++ app/utils/animcjk.ts | 5 +- app/utils/stroke-data.ts | 45 ++- codex-setup.sh | 53 ++- docs/known-issues.md | 2 +- eslint.config.js | 5 +- nuxt.config.ts | 41 ++- public/_headers | 16 +- .../NOTICE.md => notices/data-sources.md} | 10 +- .../LICENSE.md => notices/flags-mit.md} | 0 .../{fonts/OFL.txt => notices/noto-ofl.txt} | 0 scripts/build-data.ts | 24 +- scripts/build-fonts.ts | 17 +- scripts/build-strokes.ts | 11 +- scripts/sources.ts | 342 +----------------- scripts/tests/chars.test.ts | 6 +- scripts/tests/frequency.test.ts | 2 +- scripts/tests/prerender.test.ts | 2 +- scripts/tests/stroke-data.test.ts | 14 +- scripts/tests/strokes.test.ts | 10 +- scripts/tests/worker.test.ts | 11 +- shared/sources.ts | 334 +++++++++++++++++ shared/types.ts | 2 +- uno.config.ts | 2 +- 51 files changed, 719 insertions(+), 557 deletions(-) rename {public => app/assets}/flags/cn.svg (100%) rename {public => app/assets}/flags/hk.svg (100%) rename {public => app/assets}/flags/jp.svg (100%) rename {public => app/assets}/flags/kr.svg (100%) rename {public => app/assets}/flags/tw.svg (100%) rename public/{data/NOTICE.md => notices/data-sources.md} (73%) rename public/{flags/LICENSE.md => notices/flags-mit.md} (100%) rename public/{fonts/OFL.txt => notices/noto-ofl.txt} (100%) create mode 100644 shared/sources.ts diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 283966d..9f625b1 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -41,11 +41,11 @@ jobs: uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | - public/fonts/*.woff2 - public/fonts/fonts-*.css + app/assets/fonts/*.woff2 + app/assets/fonts/fonts-*.css # chars.json exists here because Build dataset runs first, even # though the generated file is ignored by Git. - key: fonts-v1-${{ runner.os }}-${{ hashFiles('data/sources.lock.json', 'package.json', 'pnpm-lock.yaml', 'scripts/build-fonts.ts', 'scripts/sources.ts', 'shared/links.ts', 'shared/row.ts', 'shared/types.ts', 'app/locales/*.ts', 'public/data/chars.json') }} + key: fonts-v2-${{ runner.os }}-${{ hashFiles('data/sources.lock.json', 'package.json', 'pnpm-lock.yaml', 'scripts/build-fonts.ts', 'scripts/sources.ts', 'shared/links.ts', 'shared/row.ts', 'shared/types.ts', 'app/locales/*.ts', 'app/assets/data/chars.json') }} - name: Build fonts if: steps.fonts-cache.outputs.cache-hit != 'true' diff --git a/.gitignore b/.gitignore index 6b2e376..d0bf8a9 100644 --- a/.gitignore +++ b/.gitignore @@ -12,19 +12,20 @@ dist # Cached downloads of external sources, verified against data/sources.lock.json data/raw/ -# Generated by pnpm build:dataset. NOTICE.md remains committed so source terms -# stay visible without running the pipeline. -public/data/chars.json -public/data/sources.json +# Generated by pnpm build:dataset and consumed through Vite's asset graph. +# The stable data-source notice remains committed under public/notices/. +app/assets/data/chars.json +app/assets/strokes/ # Generated by pnpm build:data: 12MB of font binaries and the @font-face rules -# that point at them. -public/fonts/*.woff2 -public/fonts/fonts-*.css +# that point at them. Vite fingerprints both the CSS and font files. +app/assets/fonts/*.woff2 +app/assets/fonts/fonts-*.css -# Generated by pnpm build:dataset: regional stroke shards and their licenses. -# They are copied into the deploy output but are not committed. -public/strokes/ +# Generated verbatim from the pinned AnimCJK source. These keep stable public +# URLs and are revalidated instead of entering Vite's fingerprinted assets. +public/notices/animcjk-APL.txt +public/notices/animcjk-COPYING.txt # Local record of the development conversation: 11MB of JSONL plus screenshots. # Kept on disk for reference, out of the repository by default. diff --git a/.prettierignore b/.prettierignore index f4c12dc..efdc50f 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,8 +1,7 @@ # Generated by scripts/build-data.ts and not committed. -public/data/chars.json -public/data/sources.json -public/strokes/ +app/assets/data/chars.json +app/assets/strokes/ # Generated by scripts/build-fonts.ts alongside the woff2 files -public/fonts/fonts-*.css +app/assets/fonts/fonts-*.css app/generated/ diff --git a/AGENTS.md b/AGENTS.md index 0ca69d3..5968ce8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## Project Structure & Module Organization -Hanji is a Nuxt 4/Vue 3 static site. Route components live in `app/pages/`, reusable UI in `app/components/`, stateful helpers in `app/composables/`, and global styling in `app/styles/`. Keep framework-independent data models and lookup logic in `shared/` so both the app and build scripts can import them. Data and font pipelines live in `scripts/`; their Vitest suites are in `scripts/tests/`. `public/data/` contains committed generated datasets, while generated font files and downloaded inputs under `data/raw/` are ignored. Do not hand-edit generated outputs; change the relevant script or source lock instead. +Hanji is a Nuxt 4/Vue 3 static app. Route components live in `app/pages/`, reusable UI in `app/components/`, stateful helpers in `app/composables/`, and global styling in `app/styles/`. Keep framework-independent data models and lookup logic in `shared/` so both the app and build scripts can import them. Data and font pipelines live in `scripts/`; their Vitest suites are in `scripts/tests/`. Generated character data, stroke shards, and font subsets under `app/assets/` are ignored and fingerprinted by Vite; stable legal texts live under `public/notices/`. Downloaded inputs under `data/raw/` are also ignored. Do not hand-edit generated outputs; change the relevant script or source lock instead. ## Build, Test, and Development Commands @@ -13,7 +13,7 @@ Hanji is a Nuxt 4/Vue 3 static site. Route components live in `app/pages/`, reus - `pnpm lint` checks TypeScript, Vue, and UnoCSS conventions. - `pnpm typecheck` runs Nuxt's Vue-aware TypeScript checker. - `pnpm format` formats the repository with the shared Prettier configuration. -- `pnpm generate` writes the deployable static site to `.output/public/`. +- `pnpm generate` writes the deployable static app to `.output/public/`. ## Coding Style & Naming Conventions diff --git a/PRODUCT.md b/PRODUCT.md index fe72157..2af99ab 100644 --- a/PRODUCT.md +++ b/PRODUCT.md @@ -40,7 +40,7 @@ and choose the regions participating in a comparison. Character detail pages provide stable direct URLs, regional metadata, readings, stroke-order material where available, related forms, and links to external dictionaries. The interface supports Simplified Chinese, Hong Kong Traditional Chinese, Taiwan -Traditional Chinese, Japanese, and Korean. The product is a static website +Traditional Chinese, Japanese, and Korean. The product is a static web app with no account or server-side workflow, and its generated datasets are also published as downloadable files. @@ -82,15 +82,18 @@ language so that every comparison displays the intended local form. - `README.md` and the localized About page document scope, methodology, limitations, naming, and deployment. -- `public/data/chars.json`, `public/data/sources.json`, and - `public/data/NOTICE.md` expose the generated character data, source metadata, - licenses, and transformation notes. +- `app/assets/data/chars.json` provides the generated character data through a + Vite-fingerprinted asset. Static generation also copies it to the stable + `/data/chars.json` URL for external consumers. Attribution metadata lives in + `shared/sources.ts`, while `public/notices/data-sources.md` keeps + transformation and license notes at a stable, revalidated URL. - `data/sources.lock.json` pins third-party inputs and checksums. - The data and font pipelines under `scripts/`, together with their Vitest suites, encode and verify grouping, source, mapping, and rendered-outline guarantees. - Region-specific Noto CJK font subsets and local interface fonts are generated - for the site, with the applicable font license included in `public/fonts/`. + into `app/assets/fonts/`, with the applicable font license included in + `public/notices/`. - The repository contains no testimonials, institutional endorsements, commercial benchmarks, or other proof that future work may fabricate. diff --git a/README.ja-JP.md b/README.ja-JP.md index 1085beb..dee2e7b 100644 --- a/README.ja-JP.md +++ b/README.ja-JP.md @@ -4,11 +4,11 @@ -中国大陸・香港・台湾・日本・韓国の5地域で一般的に使われる漢字をまとめた字表です。本サイトでは、一般的なゴシック体と明朝体における印刷字形を比較します。同じ字でも、地域によって異なる字形で表示されることがあります。5地域の字形を横に並べ、既定ではUIの言語に対応する地域の文字頻度順で表示します。画数順やコードポイント順への並べ替え、差異パターンによる絞り込みも可能です。韓国には文字頻度データがないため、韓国列は既定で非表示ですが、表示オプションから有効にできます。 +中国大陸・香港・台湾・日本・韓国の5地域で一般的に使われる漢字をまとめた字表です。本アプリでは、一般的なゴシック体と明朝体における印刷字形を比較します。同じ字でも、地域によって異なる字形で表示されることがあります。5地域の字形を横に並べ、既定ではUIの言語に対応する地域の文字頻度順で表示します。画数順やコードポイント順への並べ替え、差異パターンによる絞り込みも可能です。韓国には文字頻度データがないため、韓国列は既定で非表示ですが、表示オプションから有効にできます。 字形は一つの側面にすぎません。収録範囲は5地域の常用字表の和集合であり、**字形が完全に同じ字も収録しています**。これは5地域の漢字に関する資料表であり、差異だけを並べた一覧ではありません。 -以下は概要です。項目ごとの詳しい説明は、サイト内の「このサイトについて」ページにあります。 +以下は概要です。項目ごとの詳しい説明は、アプリ内の「アプリについて」ページにあります。 ## 名前の由来 @@ -36,13 +36,13 @@ 判定にはAdobe Source Han SansとSource Han Serifを使い、ページ上の表示には同系統のNoto Sans CJKとNoto Serif CJKを使います。各フォントでは、中国大陸・香港・台湾・日本・韓国の5地域が一つのグリフプールを共有し、地域ごとに「Unicodeコードポイント → グリフ番号(CID)」の対応を持ちます。二つの地域が同じCIDに対応していれば、同じ字形と見なします。Adobeはこれらの対応をプレーンテキストで公開しているため、アウトラインやレンダリング画像を比較する必要はありません。 -最終判定には、**ゴシック体と明朝体の結果の和集合**を使います。どちらか一方の書体で二つの地域が同じ字形なら、本サイトでも同形として扱います。これにより、一方のフォントだけに現れるデザイン上の細部を除外できます。たとえばSource Han Sansは、日本の常用漢字のおよそ5分の1に独立した字形を用意していますが、そのうち約200字はSource Han Serifでは区別されていません(了、人、子、水、金など)。本サイトでは、このような差を地域規範上の差異として数えません。判断の詳細は[データ規則と既知の制限](docs/known-issues.md)を参照してください。 +最終判定には、**ゴシック体と明朝体の結果の和集合**を使います。どちらか一方の書体で二つの地域が同じ字形なら、本アプリでも同形として扱います。これにより、一方のフォントだけに現れるデザイン上の細部を除外できます。たとえばSource Han Sansは、日本の常用漢字のおよそ5分の1に独立した字形を用意していますが、そのうち約200字はSource Han Serifでは区別されていません(了、人、子、水、金など)。本アプリでは、このような差を地域規範上の差異として数えません。判断の詳細は[データ規則と既知の制限](docs/known-issues.md)を参照してください。 ページ上ではこの判定に基づいてセルを再グループ化します。同形と判定されたセルは、グループ内の一地域のNotoフォントを共通して使い、画面上でも実際に同じ輪郭を表示します。そのため、上記ルールで除外された地域版間の細かな差異は表示されません。`scripts/tests/fonts.test.ts` はfontkitで生成フォントの実際のアウトラインを取り出し、判定と画面表示が一致することを1字ずつ検証します。 ## 制限事項 -- 本サイトが比較するのは、一般的なゴシック体と明朝体における印刷字形だけです。手書きの慣習は対象外で、教科書体の例示字形も基準にしません。日本語の教科書体は主に日本語教育向けに設計され、中国大陸・香港・台湾・韓国と同じグリフプールを共有する正式な地域版がありません。見た目の近い別々のフォントを組み合わせると、地域差とフォント固有のデザイン差を分離できません。条件を揃えるため、5地域版を同時に提供する同系統のフォントファミリーだけを使います。 +- 本アプリが比較するのは、一般的なゴシック体と明朝体における印刷字形だけです。手書きの慣習は対象外で、教科書体の例示字形も基準にしません。日本語の教科書体は主に日本語教育向けに設計され、中国大陸・香港・台湾・韓国と同じグリフプールを共有する正式な地域版がありません。見た目の近い別々のフォントを組み合わせると、地域差とフォント固有のデザイン差を分離できません。条件を揃えるため、5地域版を同時に提供する同系統のフォントファミリーだけを使います。 - 判定対象はSource Hanの地域別字形デザインであり、各地域の標準そのものではありません。ただし高品質な代理です。Adobeの地域別字形は、中国大陸の『印刷通用漢字字形表』、台湾教育部の『國字標準字體』、香港教育局の『常用字字形表』、日本のJIS X 0208/0213、韓国のKS X 1001/1002をそれぞれ根拠としています。 - Source Hanの香港字形は網羅的ではないため、「香港だけが異なる」パターンは少なく報告される可能性があります。 - 画数は地域ごとに取得します。まずUnihanの `kAlternateTotalStrokes` を参照し、日本については次に `kRSAdobe_Japan1_6` を使います。これはAdobe-Japan1-6に収録された日本字形を分析するため、たとえば「突」は9画ではなく8画になります。どちらもない場合のみ `kTotalStrokes` を使います。最後の値は簡体・繁体の2区分しかなく、香港・台湾・日本・韓国は通常、繁体字側の値を共有します。たとえば「那」の5列の画数は6/6/6/7/6です。 @@ -53,9 +53,9 @@ ## 免責事項 -本サイトは公開資料に基づいて作成した字形比較ツールであり、各地域の標準、辞書、教材ではありません。ページに表示される内容は、本サイトが採用したデータと自動処理規則による結果です。ある字について、その地域で唯一の「正しい」形であると断定する根拠にはできません。 +本アプリは公開資料に基づいて作成した字形比較ツールであり、各地域の標準、辞書、教材ではありません。ページに表示される内容は、本アプリが採用したデータと自動処理規則による結果です。ある字について、その地域で唯一の「正しい」形であると断定する根拠にはできません。 -各地域の標準は適用範囲や定義が完全には一致せず、フォントも標準の一つの設計実装にすぎません。本サイトでは異なる出典のデータを整理、変換、統合し、未収録項目には参考字形も補います。これらはすべてエンジニアリング上の判断であり、単純化、欠落、誤りが生じる可能性があります。 +各地域の標準は適用範囲や定義が完全には一致せず、フォントも標準の一つの設計実装にすぎません。本アプリでは異なる出典のデータを整理、変換、統合し、未収録項目には参考字形も補います。これらはすべてエンジニアリング上の判断であり、単純化、欠落、誤りが生じる可能性があります。 ページ上の「同形」または「異なる」という判定は、上記の資料、フォント、規則の範囲内でのみ成立します。地域の並びやグループ分けは比較を容易にするためのもので、優劣や立場を示すものではありません。正式な用途では原典の標準や辞書を確認してください。各字の詳細ページには、対応する地域の辞書へのリンクがあります。漢字は非常に多いため、誤りを完全には避けられません。データの誤りを見つけた場合は、[issue](https://github.com/sxzz/hanji/issues)でお知らせください。 @@ -67,16 +67,16 @@ pnpm build:data # 字表とフォントサブセットを生成。初回は約 pnpm update:sources # サードパーティデータの更新を確認して固定。変更があればダウンロードして再生成 pnpm dev pnpm test -pnpm generate # 静的サイト +pnpm generate # 静的Webアプリ ``` 各行のURLには行名を使います(`/char/着`)。5地域の表示字形、`aka`、`alternatives` もURLとして利用でき、クライアント側で対応する行へ移動します。たとえば `/char/国`、`/char/郞`、`/char/缐` です。ページの `rel=canonical` は行名のURLを指します。未確認関係はURL別名にはなりません。 -字表 `public/data/chars.json` と出典一覧 `public/data/sources.json` は、どちらも `pnpm build:dataset` で生成し、リポジトリには**コミットしません**。ビルド後も、それぞれ `/data/chars.json` と `/data/sources.json` から公開データとして取得できます。すべての `/data/*.json` レスポンスは任意のオリジンからのクロスオリジン読み取りを許可し、安定したURLで古いデータが残らないよう、キャッシュの再検証を必須にします。フォントサブセットは約12MBで、同様にコミットせず、`pnpm build:data` で生成します。そのため、ビルド前に一度実行する必要があります。元データのダウンロードは `data/raw/` 以下に種類別(`charlist/`、`opencc/`、`cmap/`、`font/`、`unihan/`、`frequency/`、`strokes/`)でキャッシュされ、gitignoreされています。ビルド時には、古いキャッシュから復元されたものの、現在の出典一覧には存在しないファイルを削除します。 +字表 `app/assets/data/chars.json` は `pnpm build:dataset` で生成し、リポジトリには**コミットしません**。アプリが直接インポートし、Viteも内容ハッシュ付きのダウンロードURLを出力します。静的生成の完了後、字表は外部サイトから参照できる固定URL `/data/chars.json` にもコピーされ、「アプリについて」ページもこのURLへリンクします。キャッシュは1時間有効で、期限切れ後もバックグラウンドで再検証している間は古い版を1日利用できます。出典とライセンスのメタデータは `shared/sources.ts` で直接管理し、ページとビルドスクリプトが共通でインポートするため、`sources.json` は生成しません。約12MBのフォントサブセットは `app/assets/fonts/` に生成し、同様にコミットしません。字表、筆順、フォント、旗はすべてViteのアセットグラフに入り、`/_nuxt/*` の長期immutableキャッシュで安全に再利用できます。変更され得る一方で安定URLが必要なNOTICEとライセンス文は `public/notices/` に分離し、利用のたびに再検証します。ビルド前には `pnpm build:data` を実行してください。元データのダウンロードは `data/raw/` 以下に種類別(`charlist/`、`opencc/`、`cmap/`、`font/`、`unihan/`、`frequency/`、`strokes/`)でキャッシュされ、gitignoreされています。ビルド時には、古いキャッシュから復元されたものの、現在の出典一覧には存在しないファイルを削除します。 ## デプロイ -静的サイトなので、`.output/public` を任意の静的ホスティングに配置できます。本番環境では、[GitHub Actions](.github/workflows/deploy.yml) がビルド後の成果物をCloudflare Workers Static Assetsへ直接アップロードします。 +静的Webアプリなので、`.output/public` を任意の静的ホスティングに配置できます。本番環境では、[GitHub Actions](.github/workflows/deploy.yml) がビルド後の成果物をCloudflare Workers Static Assetsへ直接アップロードします。 - `main` へのpushはproductionへデプロイします。 - `main` 向けのPRは `wrangler versions upload` で `pr-<番号>` preview aliasへデプロイします。GitHubのPRには対応するdeploymentとURLが表示され、その後のコミットでも同じプレビューURLを使います。 @@ -92,11 +92,11 @@ Cloudflareの **Settings → Domains & Routes** でproductionドメインを接 ページビューとWeb Vitalsが必要な場合は、実際のドメインを所有するアカウントの **Web Analytics → Add a site** でCloudflareによりプロキシされているhostnameを選び、automatic setupを使用してください。Cloudflareがエッジでbeaconを自動挿入します。 -各字群の詳細ページはそれぞれ独立したHTMLとして生成されます。ページデータはローカルbundleに含まれるため、ルートごとの追加 `_payload.json` を生成するpayload extractionは無効にしています。地域異体字の別名については、リダイレクト専用ページを生成しません。Static Assetsがまず `404.html` とHTTP 404を返し、その後Nuxtのクライアントミドルウェアが対応する行へ移動します。これにより、検索エンジンが別名を成功ページとして重複登録することを避けます。実際に存在しないURLはHTTP 404のままです。`public/_headers` では、内容ハッシュ付きの `_nuxt/*` に長期キャッシュを設定します。 +各字群の詳細ページはそれぞれ独立したHTMLとして生成されます。ページデータはローカルbundleに含まれるため、ルートごとの追加 `_payload.json` を生成するpayload extractionは無効にしています。地域異体字の別名については、リダイレクト専用ページを生成しません。Static Assetsがまず `404.html` とHTTP 404を返し、その後Nuxtのクライアントミドルウェアが対応する行へ移動します。これにより、検索エンジンが別名を成功ページとして重複登録することを避けます。実際に存在しないURLはHTTP 404のままです。`public/_headers` では、内容ハッシュ付きの `_nuxt/*` に長期immutableキャッシュを設定し、安定した `/notices/*` URLには `no-cache`、`/data/chars.json` には1時間の `max-age` と1日の `stale-while-revalidate` を指定します。 サードパーティ資産の具体的なcommit、公式添付ファイル識別子、SHA-256は `data/sources.lock.json` に記録しています。更新時には `pnpm update:sources` を実行します。バージョンのある上流データについてはバージョン番号を解決し、バージョンのない公式直リンクについては改めて検証します。内容が変わっていればlockfileを更新してデータを再生成し、まったく変わっていなければ生成をスキップします。ビルド時の `pnpm build:data` はlockfileに従い、約 **261 MiB** の元データをダウンロードして検証します。このうち195 MiBは10個のNoto CJKフォントです。明示的な更新を行っていない直リンクの内容が変わった場合は、チェックサム不一致として失敗し、黙ってデータに取り込むことはありません。Actionsでは元データのダウンロードと生成フォントを別々にキャッシュします。前者はlockfileだけで決まり、後者はlockfile、実際の生成スクリプト、関連依存関係、locale、字表から決まります。フォント入力が完全に同じ場合は、データ生成を省略します。 -筆順シャードと付属ライセンスは `pnpm build:dataset` が `public/strokes/` に生成し、リポジトリにはコミットしません。デプロイ処理はテストと静的生成の前に毎回これらを再生成します。同じ字グループ内で筆画順に並べた輪郭が完全に一致する場合は、最初のバリアントとその中心線だけを保存し、画面上でも対応する地域を1つの選択肢にまとめます。筆順シャードは安定したパスを使用し、専用のキャッシュポリシーは設定しません。 +筆順シャードは `pnpm build:dataset` が `app/assets/strokes/` に生成し、リポジトリにはコミットしません。デプロイ処理はテストと静的生成の前に毎回再生成し、Viteが内容ハッシュ付きのファイル名で出力します。付属ライセンスは `public/notices/` の安定URLに置き、再検証を必須にします。同じ字グループ内で筆画順に並べた輪郭が完全に一致する場合は、最初のバリアントとその中心線だけを保存し、画面上でも対応する地域を1つの選択肢にまとめます。ページ読み込み時に所属シャードを一度だけ取得し、その後の地域切替ではメモリ上の字グループデータを再利用します。 ローカルでも、ビルド後に直接アップロードできます。 @@ -130,7 +130,7 @@ pnpm deploy 1字ずつ比較できるツール [tofu.tools](https://tofu.tools/) は本プロジェクトの先行例で、同じくNotoファミリーを使って地域字形を区別しています。 -フォントはNoto Sans CJKとNoto Serif CJK(SIL OFL 1.1)を本サイトで使う文字にサブセット化したものです。ライセンス文は `/fonts/OFL.txt` に同梱しています。生成データファイルは上記の出典から派生しているため、それぞれのライセンスに従ってください。項目ごとの変換方法と帰属表示は、公開されている [`/data/NOTICE.md`](public/data/NOTICE.md) にも記載しています。 +フォントはNoto Sans CJKとNoto Serif CJK(SIL OFL 1.1)を本アプリで使う文字にサブセット化したものです。ライセンス文は [`/notices/noto-ofl.txt`](public/notices/noto-ofl.txt) に同梱しています。生成データファイルは上記の出典から派生しているため、それぞれのライセンスに従ってください。項目ごとの変換方法と帰属表示は、公開されている [`/notices/data-sources.md`](public/notices/data-sources.md) にも記載しています。 ## License diff --git a/README.md b/README.md index a6511a3..2952f7a 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,11 @@ -中国大陆、香港、台湾、日本、韩国五地常用汉字的字表。本站对照的是通用黑体与宋体中的印刷字形:同一个字,在各地可能呈现不同字形。这里把五地字形并排列出,默认按界面语言对应地区的字频排序,也可按笔画或码点排序,并按差异模式筛选。韩国没有字频数据,韩国列默认关闭,可在显示选项中启用。 +中国大陆、香港、台湾、日本、韩国五地常用汉字的字表。本应用对照的是通用黑体与宋体中的印刷字形:同一个字,在各地可能呈现不同字形。这里把五地字形并排列出,默认按界面语言对应地区的字频排序,也可按笔画或码点排序,并按差异模式筛选。韩国没有字频数据,韩国列默认关闭,可在显示选项中启用。 字形只是其中一个维度。收录范围是五地常用字表的并集,**字形完全相同的字同样收录**:这是一份五地汉字的资料表,不是一份差异清单。 -下面是概要,逐条的完整说明在站内的「关于」页。 +下面是概要,逐条的完整说明在应用内的「关于」页。 ## 名字的由来 @@ -36,13 +36,13 @@ 判定使用 Adobe Source Han Sans 与 Source Han Serif,页面则用与它们同源的 Noto Sans CJK 与 Noto Serif CJK 显示结果。每套字体让中、港、台、日、韩五个地区共用一个字形池,并分别提供「Unicode 码点 → 字形编号(CID)」映射。两地映射到同一 CID,就视为同形。Adobe 已把这些映射以纯文本公开,因此不需要比对轮廓或渲染截图。 -最终判定取**黑体与宋体结果的并集**:只要其中一款把两地画成同一字形,本站就按同形处理。这能排除只出现在单款字体中的设计细节。例如 Source Han Sans 为约五分之一的日本常用汉字提供独立字形,其中约两百个在 Source Han Serif 并未区分(了、人、子、水、金都在其中);本站不把这类差异算作地区规范差异。完整取舍见 [数据规则与已知限制](docs/known-issues.md)。 +最终判定取**黑体与宋体结果的并集**:只要其中一款把两地画成同一字形,本应用就按同形处理。这能排除只出现在单款字体中的设计细节。例如 Source Han Sans 为约五分之一的日本常用汉字提供独立字形,其中约两百个在 Source Han Serif 并未区分(了、人、子、水、金都在其中);本应用不把这类差异算作地区规范差异。完整取舍见 [数据规则与已知限制](docs/known-issues.md)。 页面会按这份判定重新分组:被判为同形的格子统一借用组内一个地区的 Noto 字体,使屏幕上也真正呈现同一轮廓。相应地,被上述规则过滤的地区版本细小差异不会显示。`scripts/tests/fonts.test.ts` 会用 fontkit 取出生成字体的真实轮廓,逐字验证判定与画面显示一致。 ## 局限 -- 本站只比较通用黑体与宋体中的印刷字形,不涵盖手写习惯,也不以教科书体的示范字形为准。日语教科书体主要为日语教学设计,并没有与中、港、台、韩共享同一字形池的正式地区版本;若拼接风格相近但来源不同的字体,地区差异与字体自身的设计差异就无法分开。为了控制变量,本站只能选用同时提供五地版本的同源字体系列。 +- 本应用只比较通用黑体与宋体中的印刷字形,不涵盖手写习惯,也不以教科书体的示范字形为准。日语教科书体主要为日语教学设计,并没有与中、港、台、韩共享同一字形池的正式地区版本;若拼接风格相近但来源不同的字体,地区差异与字体自身的设计差异就无法分开。为了控制变量,本应用只能选用同时提供五地版本的同源字体系列。 - 判定的对象是 Source Han 的地区字形设计,不是各地标准本身。它是个高质量的代理——Adobe 的地区字形分别依据大陆《印刷通用汉字字形表》、台湾教育部《國字標準字體》、香港教育局《常用字字形表》、日本 JIS X 0208/0213、韩国 KS X 1001/1002。 - Source Han 的香港字形覆盖并不完整,「只有香港不同」这一类可能少报。 - 笔画数逐地区取:先看 Unihan 的 `kAlternateTotalStrokes`,日本再退到 `kRSAdobe_Japan1_6`(它分析的是 Adobe-Japan1-6 收的日本字形,所以 突 是 8 画而不是 9 画),都没有才用 `kTotalStrokes`。后者只区分简繁两档,港台日韩通常共用繁体值;例如 那 的五列笔画数是 6/6/6/7/6。 @@ -53,9 +53,9 @@ ## 声明 -本站是基于公开资料制作的字形对照工具,不是各地的规范、词典或教学材料。页面展示的是本站采用的数据与自动规则所得的结果,不能据此断定某个字在当地只有这一种“正确”形式。 +本应用是基于公开资料制作的字形对照工具,不是各地的规范、词典或教学材料。页面展示的是本应用采用的数据与自动规则所得的结果,不能据此断定某个字在当地只有这一种“正确”形式。 -各地规范的适用范围和定义并不完全相同,字体也只是规范的一种设计实现。本站会整理、转换并合并不同来源的数据,也会为未收录项补上参考字形;这些都是工程取舍,难免带来简化、遗漏与错误。 +各地规范的适用范围和定义并不完全相同,字体也只是规范的一种设计实现。本应用会整理、转换并合并不同来源的数据,也会为未收录项补上参考字形;这些都是工程取舍,难免带来简化、遗漏与错误。 页面中的“同形”或“不同”只在上述资料、字体与规则内成立;地区的排列与分组只为方便对照,不表示优劣或立场。正式场合请以原始规范与词典为准;每个字的详情页都列有相应地区的字典链接。汉字数量巨大,出错难免,发现数据有误请提 [issue](https://github.com/sxzz/hanji/issues)。 @@ -67,16 +67,16 @@ pnpm build:data # 生成字表与字体子集,首次会下载约 261 MiB 原 pnpm update:sources # 检查并锁定新版第三方数据;有变化时下载并重新生成 pnpm dev pnpm test -pnpm generate # 静态站点 +pnpm generate # 静态应用 ``` 每一行的地址是它的行名(`/char/着`)。五地展示形、`aka` 和 `alternatives` 也可作为地址,由客户端跳到所属的行——例如 `/char/国`、`/char/郞`、`/char/缐`,页面用 `rel=canonical` 指回行名地址;未确认关系不是地址别名。 -字表 `public/data/chars.json` 和来源清单 `public/data/sources.json` 都由 `pnpm build:dataset` 生成,**不提交**;构建后仍分别作为 `/data/chars.json` 与 `/data/sources.json` 开放下载。所有 `/data/*.json` 响应均允许任意来源跨域读取,并要求缓存重新验证,避免稳定 URL 留下旧数据。字体子集约 12MB,同样不提交,由 `pnpm build:data` 生成——所以构建前必须先跑一次。原始下载缓存在 `data/raw/` 下按类别存放(`charlist/`、`opencc/`、`cmap/`、`font/`、`unihan/`、`frequency/`、`strokes/`),已 gitignore;构建会清理从旧缓存恢复、但已不在当前来源清单中的文件。 +字表 `app/assets/data/chars.json` 由 `pnpm build:dataset` 生成,**不提交**;应用直接导入它,Vite 也会输出带内容哈希的下载地址。静态生成结束后,字表会额外复制到固定的 `/data/chars.json` 供外部网站引用,“关于”页链接的也是这个地址;它缓存 1 小时,过期后可在后台重新验证期间继续使用旧版本 1 天。来源与许可元数据直接维护在 `shared/sources.ts`,由页面和构建脚本共同导入,不再生成 `sources.json`。约 12MB 的字体子集生成到 `app/assets/fonts/`,同样不提交;字表、笔顺、字体和旗帜都进入 Vite 资源图,由 `/_nuxt/*` 的长期 immutable 缓存安全复用。会变动而又需要稳定 URL 的 NOTICE 与 license 文本单独放在 `public/notices/`,每次使用前必须重新验证。构建前需先运行 `pnpm build:data`。原始下载缓存在 `data/raw/` 下按类别存放(`charlist/`、`opencc/`、`cmap/`、`font/`、`unihan/`、`frequency/`、`strokes/`),已 gitignore;构建会清理从旧缓存恢复、但已不在当前来源清单中的文件。 ## 部署 -静态站点,`.output/public` 直接丢给任意静态托管即可。线上部署由 [GitHub Actions](.github/workflows/deploy.yml) 构建后直传 Cloudflare Workers Static Assets: +这是静态应用,`.output/public` 直接交给任意静态托管即可。线上部署由 [GitHub Actions](.github/workflows/deploy.yml) 构建后直传 Cloudflare Workers Static Assets: - `main` 的 push 部署到 production; - 指向 `main` 的 PR 通过 `wrangler versions upload` 部署到 `pr-<编号>` preview alias,GitHub 会在 PR 中显示对应的 deployment 与访问地址,后续提交沿用同一个预览地址。 @@ -92,11 +92,11 @@ Cloudflare Worker 名称须为 `hanji`,与 `wrangler.json` 中的 `name` 一 需要页面访问量和 Web Vitals 时,请在实际域名所属账户的 **Web Analytics → Add a site** 中选择已由 Cloudflare 代理的 hostname,并使用 automatic setup。Cloudflare 会在边缘自动注入 beacon。 -每个字组详情页都会生成独立 HTML;页面数据在本地 bundle 中,因此关闭了每路由额外生成 `_payload.json` 的 payload extraction。地区异体别名不另外生成跳转页:它先由 Static Assets 返回 `404.html` 和 HTTP 404,再由 Nuxt 客户端中间件跳到所属行;搜索引擎不会把 alias 当作成功页面重复收录。真正未知的地址保持 HTTP 404;`public/_headers` 给带内容哈希的 `_nuxt/*` 设了长缓存。 +每个字组详情页都会生成独立 HTML;页面数据在本地 bundle 中,因此关闭了每路由额外生成 `_payload.json` 的 payload extraction。地区异体别名不另外生成跳转页:它先由 Static Assets 返回 `404.html` 和 HTTP 404,再由 Nuxt 客户端中间件跳到所属行;搜索引擎不会把 alias 当作成功页面重复收录。真正未知的地址保持 HTTP 404;`public/_headers` 给带内容哈希的 `_nuxt/*` 设长期 immutable 缓存,让稳定的 `/notices/*` URL 使用 `no-cache`,并为 `/data/chars.json` 设置 1 小时的 `max-age` 与 1 天的 `stale-while-revalidate`。 第三方资产的具体 commit、官方附件标识与 SHA-256 记录在 `data/sources.lock.json`;需要升级时运行 `pnpm update:sources`。它会解析有版本上游的版本号,并重新校验没有版本号的官方直链;内容有变化时更新 lockfile 并直接重新生成数据,完全未变则跳过生成。构建时 `pnpm build:data` 会按 lockfile 下载并校验约 **261 MiB** 原始数据(其中 195 MiB 是十份 Noto CJK 字体);任何未显式更新的直链内容变化都会因校验和不符而失败,不会静默进入数据。Actions 分开缓存原始下载与生成字体:前者只由 lockfile 决定,后者由 lockfile、实际生成脚本、相关依赖、locale 与字表决定;字体输入完全不变时跳过数据生成。 -笔顺分片和随附授权由 `pnpm build:dataset` 生成到 `public/strokes/`,不提交到仓库;部署流程会在测试和静态生成前重建它们。同一字组内,按笔画顺序排列的轮廓完全一致时只保存第一份变体及其中线,界面也把对应地区合并为一个选择项。笔顺分片使用稳定路径,不设置专用缓存策略。 +笔顺分片由 `pnpm build:dataset` 生成到 `app/assets/strokes/`,不提交到仓库;部署流程会在测试和静态生成前重建,再由 Vite 输出带内容哈希的文件名。随附授权保留在 `public/notices/` 的稳定 URL 下并要求重新验证。同一字组内,按笔画顺序排列的轮廓完全一致时只保存第一份变体及其中线,界面也把对应地区合并为一个选择项;页面加载时只获取一次所属分片,之后切换地区直接复用内存中的字组数据。 本地也可构建后直传: @@ -130,7 +130,7 @@ pnpm deploy 逐字对照工具 [tofu.tools](https://tofu.tools/) 是本项目的先行者,同样用 Noto 系列区分地区字形。 -字体为 Noto Sans CJK 与 Noto Serif CJK(SIL OFL 1.1)按本站用字子集化后的产物,声明随附于 `/fonts/OFL.txt`。生成的数据文件派生自上述来源,请遵守各自许可;逐项转换方式与署名也写入公开的 [`/data/NOTICE.md`](public/data/NOTICE.md)。 +字体为 Noto Sans CJK 与 Noto Serif CJK(SIL OFL 1.1)按本应用用字子集化后的产物,声明随附于 [`/notices/noto-ofl.txt`](public/notices/noto-ofl.txt)。生成的数据文件派生自上述来源,请遵守各自许可;逐项转换方式与署名也写入公开的 [`/notices/data-sources.md`](public/notices/data-sources.md)。 ## License diff --git a/app/app.vue b/app/app.vue index 7657766..9349871 100644 --- a/app/app.vue +++ b/app/app.vue @@ -1,6 +1,7 @@ diff --git a/public/flags/cn.svg b/app/assets/flags/cn.svg similarity index 100% rename from public/flags/cn.svg rename to app/assets/flags/cn.svg diff --git a/public/flags/hk.svg b/app/assets/flags/hk.svg similarity index 100% rename from public/flags/hk.svg rename to app/assets/flags/hk.svg diff --git a/public/flags/jp.svg b/app/assets/flags/jp.svg similarity index 100% rename from public/flags/jp.svg rename to app/assets/flags/jp.svg diff --git a/public/flags/kr.svg b/app/assets/flags/kr.svg similarity index 100% rename from public/flags/kr.svg rename to app/assets/flags/kr.svg diff --git a/public/flags/tw.svg b/app/assets/flags/tw.svg similarity index 100% rename from public/flags/tw.svg rename to app/assets/flags/tw.svg diff --git a/app/components/DataSources.vue b/app/components/DataSources.vue index baab0dd..554478e 100644 --- a/app/components/DataSources.vue +++ b/app/components/DataSources.vue @@ -1,11 +1,9 @@