From 2d94dd6d595e8aa49edff53aab36a90bfb40ab17 Mon Sep 17 00:00:00 2001 From: Kevin Deng Date: Sun, 23 Aug 2026 15:20:12 +0900 Subject: [PATCH] Add sitemap and robots generation with environment-aware site URLs (#58) --- .github/workflows/deploy.yml | 8 + README.ja-JP.md | 7 +- README.md | 7 +- nuxt.config.ts | 38 ++++- package.json | 3 + pnpm-lock.yaml | 268 ++++++++++++++++++++++++++++++++++ pnpm-workspace.yaml | 3 + public/_headers | 7 + scripts/tests/sitemap.test.ts | 53 +++++++ scripts/tests/worker.test.ts | 6 + 10 files changed, 389 insertions(+), 11 deletions(-) create mode 100644 scripts/tests/sitemap.test.ts diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9d6c48d..04970fb 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -57,6 +57,14 @@ jobs: continue-on-error: ${{ github.event_name == 'pull_request' }} - name: Generate site + env: + # Keep the canonical origin out of source. Forks can set the + # NUXT_SITE_URL repository variable, otherwise use the homepage + # configured in the repository's About panel. + NUXT_SITE_URL: ${{ vars.NUXT_SITE_URL || github.event.repository.homepage }} + # The robots module blocks PR previews while production stays + # indexable. + NUXT_SITE_ENV: ${{ github.event_name == 'push' && 'production' || 'preview' }} run: pnpm generate - name: Upload Cloudflare Workers preview diff --git a/README.ja-JP.md b/README.ja-JP.md index c837d84..83cb007 100644 --- a/README.ja-JP.md +++ b/README.ja-JP.md @@ -67,7 +67,7 @@ pnpm build:data # 字表とフォントサブセットを生成。初回は約 pnpm update:sources # サードパーティデータの更新を確認して固定。変更があればダウンロードして再生成 pnpm dev pnpm test -pnpm generate # 静的Webアプリ +NUXT_SITE_URL=https://example.com pnpm generate # 静的Webアプリ ``` 各行のURLには行名を使います(`/char/着`)。5地域の表示字形、`aka`、`alternatives` もURLとして利用でき、クライアント側で対応する行へ移動します。たとえば `/char/国`、`/char/郞`、`/char/缐` です。ページの `rel=canonical` は行名のURLを指します。未確認関係はURL別名にはなりません。 @@ -92,7 +92,7 @@ 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/*` に長期immutableキャッシュを設定し、安定した `/notices/*` URLには `no-cache`、`/data/chars.json` には1時間の `max-age` と1日の `stale-while-revalidate` を指定します。 +各字群の詳細ページはそれぞれ独立したHTMLとして生成されます。ページデータはローカルbundleに含まれるため、ルートごとの追加 `_payload.json` を生成するpayload extractionは無効にしています。地域異体字の別名については、リダイレクト専用ページを生成しません。Static Assetsがまず `404.html` とHTTP 404を返し、その後Nuxtのクライアントミドルウェアが対応する行へ移動します。これにより、検索エンジンが別名を成功ページとして重複登録することを避けます。実際に存在しないURLはHTTP 404のままです。`@nuxtjs/sitemap` は静的生成時にすべてのcanonicalページを `/sitemap.xml` へ書き出し、`@nuxtjs/robots` は `/robots.txt` を生成してsitemapの場所を通知します。両方の絶対URLには `NUXT_SITE_URL` を使用します。GitHub Actionsは同名のリポジトリ変数を優先し、未設定の場合はリポジトリのhomepageを使用します。他の環境では `pnpm generate` の実行時に設定する必要があります。PRプレビューのビルドでは `NUXT_SITE_ENV=preview` によりインデックス登録を禁止します。`public/_headers` では、内容ハッシュ付きの `_nuxt/*` に長期immutableキャッシュを設定し、安定した `/notices/*`、sitemap、robotsの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、字表から決まります。フォント入力が完全に同じ場合は、データ生成を省略します。 @@ -101,7 +101,8 @@ Cloudflareの **Settings → Domains & Routes** でproductionドメインを接 ローカルでも、ビルド後に直接アップロードできます。 ```bash -pnpm build:data && pnpm generate +pnpm build:data +NUXT_SITE_URL=https://example.com pnpm generate pnpm preview:worker # http://localhost:8787 pnpm deploy ``` diff --git a/README.md b/README.md index 54cef75..dc0fd57 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ pnpm build:data # 生成字表与字体子集,首次会下载约 261 MiB 原 pnpm update:sources # 检查并锁定新版第三方数据;有变化时下载并重新生成 pnpm dev pnpm test -pnpm generate # 静态应用 +NUXT_SITE_URL=https://example.com pnpm generate # 静态应用 ``` 每一行的地址是它的行名(`/char/着`)。五地展示形、`aka` 和 `alternatives` 也可作为地址,由客户端跳到所属的行——例如 `/char/国`、`/char/郞`、`/char/缐`,页面用 `rel=canonical` 指回行名地址;未确认关系不是地址别名。 @@ -92,7 +92,7 @@ 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/*` 设长期 immutable 缓存,让稳定的 `/notices/*` URL 使用 `no-cache`,并为 `/data/chars.json` 设置 1 小时的 `max-age` 与 1 天的 `stale-while-revalidate`。 +每个字组详情页都会生成独立 HTML;页面数据在本地 bundle 中,因此关闭了每路由额外生成 `_payload.json` 的 payload extraction。地区异体别名不另外生成跳转页:它先由 Static Assets 返回 `404.html` 和 HTTP 404,再由 Nuxt 客户端中间件跳到所属行;搜索引擎不会把 alias 当作成功页面重复收录。真正未知的地址保持 HTTP 404。`@nuxtjs/sitemap` 会在静态生成时把全部 canonical 页面写入 `/sitemap.xml`,`@nuxtjs/robots` 生成 `/robots.txt` 并公布 sitemap 地址。两者的绝对 URL 来自 `NUXT_SITE_URL`;GitHub Actions 优先读取同名仓库变量,未设置时使用仓库 homepage,其他环境需在运行 `pnpm generate` 时设置。PR 预览构建通过 `NUXT_SITE_ENV=preview` 禁止索引。`public/_headers` 给带内容哈希的 `_nuxt/*` 设长期 immutable 缓存,让稳定的 `/notices/*`、sitemap 和 robots 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 与字表决定;字体输入完全不变时跳过数据生成。 @@ -101,7 +101,8 @@ Cloudflare Worker 名称须为 `hanji`,与 `wrangler.json` 中的 `name` 一 本地也可构建后直传: ```bash -pnpm build:data && pnpm generate +pnpm build:data +NUXT_SITE_URL=https://example.com pnpm generate pnpm preview:worker # http://localhost:8787 pnpm deploy ``` diff --git a/nuxt.config.ts b/nuxt.config.ts index 9a23a16..ec56559 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -36,11 +36,18 @@ const publicCharsPath = path.resolve( '.output/public/data/chars.json', ) -const PRERENDERED_CHARS = existsSync(charsPath) - ? (JSON.parse(readFileSync(charsPath, 'utf8')) as CharsData).rows.map((row) => - charPath(row.key), +const CHAR_KEYS = existsSync(charsPath) + ? (JSON.parse(readFileSync(charsPath, 'utf8')) as CharsData).rows.map( + (row) => row.key, ) : [] +const PRERENDERED_CHARS = CHAR_KEYS.map(charPath) +const PRERENDER_ROUTES = ['/', '/about', ...PRERENDERED_CHARS] +const SITEMAP_ROUTES = [ + '/', + '/about', + ...CHAR_KEYS.map((key) => `/char/${key}`), +] // A clean install prepares Nuxt before generated fonts exist. Include the // eager sans styles as soon as build:fonts has produced them; generate/dev @@ -53,7 +60,28 @@ const generatedFontStyles = ['fonts-ui.css', 'fonts-sans.css'] // https://nuxt.com/docs/api/configuration/nuxt-config export default { - modules: ['@unocss/nuxt', '@vueuse/nuxt'], + modules: [ + '@nuxtjs/sitemap', + '@nuxtjs/robots', + '@unocss/nuxt', + '@vueuse/nuxt', + ], + + sitemap: { + // These are already the exact canonical routes emitted below. Supplying + // them directly avoids treating the dynamic page pattern or fallbacks as + // indexable pages, while the module owns encoding and XML generation. + excludeAppSources: true, + urls: SITEMAP_ROUTES, + discoverImages: false, + discoverVideos: false, + xsl: false, + zeroRuntime: true, + }, + + robots: { + sitemap: '/sitemap.xml', + }, vue: { optionsApi: false, @@ -89,7 +117,7 @@ export default { // indexes would first redirect the non-ASCII URL to /char/學/, and the // client-facing routes deliberately have no trailing slash. autoSubfolderIndex: false, - routes: ['/', '/about', ...PRERENDERED_CHARS], + routes: PRERENDER_ROUTES, }, }, diff --git a/package.json b/package.json index 6c72df8..c80c026 100644 --- a/package.json +++ b/package.json @@ -34,12 +34,15 @@ "@iconify-json/ooui": "catalog:", "@iconify-json/ri": "catalog:", "@iconify-json/simple-icons": "catalog:", + "@nuxtjs/robots": "catalog:", + "@nuxtjs/sitemap": "catalog:", "@unocss/nuxt": "catalog:", "@unocss/reset": "catalog:", "@vueuse/core": "catalog:", "@vueuse/nuxt": "catalog:", "@vueuse/router": "catalog:", "nuxt": "catalog:", + "nuxt-site-config": "catalog:", "unocss": "catalog:", "vue": "catalog:" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b8a43cc..27394f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,12 @@ catalogs: '@iconify-json/simple-icons': specifier: ^1.2.93 version: 1.2.93 + '@nuxtjs/robots': + specifier: ^6.2.0 + version: 6.2.0 + '@nuxtjs/sitemap': + specifier: ^8.5.0 + version: 8.5.0 '@sxzz/eslint-config': specifier: ^8.5.0 version: 8.5.0 @@ -57,6 +63,9 @@ catalogs: nuxt: specifier: ^4.5.2 version: 4.5.2 + nuxt-site-config: + specifier: ^4.2.3 + version: 4.2.3 prettier: specifier: ^3.9.6 version: 3.9.6 @@ -95,6 +104,12 @@ importers: '@iconify-json/simple-icons': specifier: 'catalog:' version: 1.2.93 + '@nuxtjs/robots': + specifier: 'catalog:' + version: 6.2.0(fbea9601d4f309fdf46702da480511cc) + '@nuxtjs/sitemap': + specifier: 'catalog:' + version: 8.5.0(fbea9601d4f309fdf46702da480511cc) '@unocss/nuxt': specifier: 'catalog:' version: 66.8.1(esbuild@0.28.2)(magic-string@1.2.2)(magicast@0.5.4)(oxc-parser@0.131.0)(rolldown@1.2.5)(rollup@4.62.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(webpack@5.109.2(cssnano@8.0.7(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26)))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(webpack@5.109.2(cssnano@8.0.7(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26)) @@ -113,6 +128,9 @@ importers: nuxt: specifier: 'catalog:' version: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.8.1(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.131.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.10(typescript@6.0.3))(webpack@5.109.2(cssnano@8.0.7(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26))(yaml@2.9.0) + nuxt-site-config: + specifier: 'catalog:' + version: 4.2.3(fbea9601d4f309fdf46702da480511cc) unocss: specifier: 'catalog:' version: 66.8.1(@unocss/webpack@66.8.1(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(webpack@5.109.2(cssnano@8.0.7(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26)))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) @@ -800,6 +818,9 @@ packages: resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@fingerprintjs/botd@2.0.0': + resolution: {integrity: sha512-yhuz23NKEcBDTHmGz/ULrXlGnbHenO+xZmVwuBkuqHUkqvaZ5TAA0kAgcRy4Wyo5dIBdkIf57UXX8/c9UlMLJg==} + '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} @@ -1055,6 +1076,9 @@ packages: '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 + '@nodable/entities@3.0.0': + resolution: {integrity: sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1085,6 +1109,11 @@ packages: peerDependencies: vite: '>=6.0' + '@nuxt/devtools-kit@4.0.0-alpha.7': + resolution: {integrity: sha512-Tgh+tSejh1GnZjdjgWyc4qCxskeX08XuSQBYMn/4SIV5AubeqYeAOMBD2qSmHOXjMCUpgyzpEhODcP3sgdgGRA==} + peerDependencies: + vite: '>=6.0' + '@nuxt/devtools-wizard@3.4.1': resolution: {integrity: sha512-taGeuTnkHsZVjgD9r6NXvvHaBzB2j4mCgOmlmkz3ntsqgh1SJfmsD11Tmtl7FVAxJS8Wuvuzgt0GyTlADBW9Wg==} hasBin: true @@ -1150,6 +1179,23 @@ packages: rollup-plugin-visualizer: optional: true + '@nuxtjs/robots@6.2.0': + resolution: {integrity: sha512-+y8BRDWOkSMofe0ewOlVQm6+VFd4Qh/I8kh9z/++VwfPlDtrU6BDhoobJ18eVDaMkf8gJl12dwKZziK8yU4c/g==} + peerDependencies: + zod: '>=3' + peerDependenciesMeta: + zod: + optional: true + + '@nuxtjs/sitemap@8.5.0': + resolution: {integrity: sha512-DfhUZzP29Wzr9ro3L2mBpYUocmLBKvGDAGThvLdHWKGR1kBFnbY5VBhJaZG4Jeuba5gVKCpDzH9PhKNW11d46Q==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: '>=3' + peerDependenciesMeta: + zod: + optional: true + '@ota-meshi/ast-token-store@0.3.0': resolution: {integrity: sha512-XRO0zi2NIUKq2lUk3T1ecFSld1fMWRKE6naRFGkgkdeosx7IslyUKNv5Dcb5PJTja9tHJoFu0v/7yEpAkrkrTg==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} @@ -2323,6 +2369,9 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + anynum@1.0.1: + resolution: {integrity: sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==} + archiver-utils@5.0.2: resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} engines: {node: '>= 14'} @@ -3183,6 +3232,13 @@ packages: fast-wrap-ansi@0.2.2: resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + fast-xml-builder@1.3.1: + resolution: {integrity: sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug==} + + fast-xml-parser@5.11.0: + resolution: {integrity: sha512-9IGxMqvqLOnqP+Egi1nqDHKv5k8aZ7r9n558enxcucmyVGEBNPAU+MOg/8jPIS7rO7sSq4gFm1/nHtiaubMruw==} + hasBin: true + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -3501,6 +3557,9 @@ packages: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-unsafe@2.0.2: + resolution: {integrity: sha512-HgbIHPBH0KHHCcjLfGsCvhtPTVxjaAZlXjwdz7/GQC40SjSe4sfQsar8J5VFo8JOSbarkpV0OLG95bbaNd9aAQ==} + is-wsl@3.1.1: resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} engines: {node: '>=16'} @@ -4060,6 +4119,14 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + nuxt-site-config-kit@4.2.3: + resolution: {integrity: sha512-xAU061MrxDlv1CjHjP9+Qg3mAdKxQgO9sFzoy+LblKW+DDLRMGjaEIna/QUk0F0kWzhrf4dSpOVTW8OZv26xVg==} + + nuxt-site-config@4.2.3: + resolution: {integrity: sha512-m0CK1Q43T3qtVHSBYeDHav/hp8t/ahhlJGzwdBOQM6WcxeZpF1wcJazoGQAJEFamX1xcRAtA0ZuvvEkwHSMG0w==} + peerDependencies: + vue: ^3.5.30 + nuxt@4.5.2: resolution: {integrity: sha512-tR3fcqeHlHmmkLMpIg3V7Y+1ltr302lW8djMw/iy+myfo7QSSz+BVJDuQhg5j73b9oteSyBfOKTDYTgvMtj6TA==} engines: {node: ^22.19.0 || ^24.11.0 || >=26.0.0} @@ -4074,6 +4141,20 @@ packages: '@types/node': optional: true + nuxtseo-shared@5.3.14: + resolution: {integrity: sha512-kt3IrUILAD4ElsA5+3uroQUHmDLfQkiYWrdXm4Eb/tSm7L0ug2WQc6Okh9m/KtypCVBfwtChrAVMgSyT/VYP5w==} + peerDependencies: + '@nuxt/schema': ^3.16.0 || ^4.0.0 || ^5.0.0 + nuxt: ^3.16.0 || ^4.0.0 || ^5.0.0 + nuxt-site-config: ^3.2.0 || ^4.0.0 + vue: ^3.5.0 + zod: ^3.23.0 || ^4.0.0 + peerDependenciesMeta: + nuxt-site-config: + optional: true + zod: + optional: true + nypm@0.6.9: resolution: {integrity: sha512-zxlE2yvSWZWmHcNdT3+5zV2lrCogeE9YOklHrR3dFjqutq5wO7GFDYLFDRXLsYnJzwvy/im9fYoxePvS0VTW0w==} engines: {node: '>=18'} @@ -4190,6 +4271,10 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-expression-matcher@1.6.2: + resolution: {integrity: sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==} + engines: {node: '>=14.0.0'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -4676,6 +4761,15 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + site-config-stack@4.2.3: + resolution: {integrity: sha512-9kJn4YoJvJqbxbE5T1WYl7ibL4SYngcKQ3sP8SfJcgzqtawMMwuBuC2h/DuFSLqVE5ZyMqGtMONm/ax8nSHVrw==} + peerDependencies: + vue: ^3.5.30 + + sitemapd@0.2.2: + resolution: {integrity: sha512-XAuW9x2cI3B757A/h2sFYfye5kBUHM8Gr6Muzwtve1oYRCv3BntoKMcgNu+GNF/LKIvUD1fQpBTVUCoHlzMihg==} + engines: {node: '>=18.0.0'} + slash@5.1.0: resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} engines: {node: '>=14.16'} @@ -4774,6 +4868,9 @@ packages: strip-literal@4.0.0: resolution: {integrity: sha512-PaqAvfUZKBwc/SLmNZtHmzK+v19Z4O4eS3cKPeGvbIv/U3pnyEq4Tuw3/4v/FwfM8VQaEawsyCcOQ0P+kpwWWw==} + strnum@2.4.2: + resolution: {integrity: sha512-rDG3Ah4TV0k1hWvLSzkZtMmLN9+eS+h3knq4MP6A42Y3Yh5qGNnOUs1jJkoSr8FG5dsL28c7KgkIBzSEykqtuw==} + structured-clone-es@2.0.1: resolution: {integrity: sha512-10ZL5r77LhknxlP1FBiCW+VdnuWOEFLdSS2SKtjyEV+L4qP1hUEIMIZW94LC3jKxRmT/Dj7KkD1mqh/IY6lhKQ==} @@ -5480,6 +5577,10 @@ packages: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} + xml-naming@0.3.0: + resolution: {integrity: sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==} + engines: {node: '>=16.0.0'} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -6079,6 +6180,8 @@ snapshots: '@eslint/core': 1.2.1 levn: 0.4.1 + '@fingerprintjs/botd@2.0.0': {} + '@humanfs/core@0.19.2': dependencies: '@humanfs/types': 0.15.0 @@ -6303,6 +6406,8 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true + '@nodable/entities@3.0.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -6368,6 +6473,18 @@ snapshots: - rolldown - unplugin + '@nuxt/devtools-kit@4.0.0-alpha.7(magic-string@1.2.2)(magicast@0.5.4)(oxc-parser@0.131.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(webpack@5.109.2(cssnano@8.0.7(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26)))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))': + dependencies: + '@nuxt/kit': 4.5.2(magic-string@1.2.2)(magicast@0.5.4)(oxc-parser@0.131.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(webpack@5.109.2(cssnano@8.0.7(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26))) + tinyexec: 1.3.0 + vite: 8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0) + transitivePeerDependencies: + - magic-string + - magicast + - oxc-parser + - rolldown + - unplugin + '@nuxt/devtools-wizard@3.4.1': dependencies: '@clack/prompts': 1.7.0 @@ -6647,6 +6764,54 @@ snapshots: - webpack - yaml + '@nuxtjs/robots@6.2.0(fbea9601d4f309fdf46702da480511cc)': + dependencies: + '@fingerprintjs/botd': 2.0.0 + '@nuxt/kit': 4.5.2(magic-string@1.2.2)(magicast@0.5.4)(oxc-parser@0.131.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(webpack@5.109.2(cssnano@8.0.7(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26))) + consola: 3.4.2 + defu: 6.1.7 + h3: 1.15.11 + nuxt-site-config: 4.2.3(fbea9601d4f309fdf46702da480511cc) + nuxtseo-shared: 5.3.14(352f5d67275472f805550a7ece632e92) + pathe: 2.0.3 + pkg-types: 2.3.1 + ufo: 1.6.4 + transitivePeerDependencies: + - '@nuxt/schema' + - magic-string + - magicast + - nuxt + - oxc-parser + - rolldown + - unplugin + - vite + - vue + + '@nuxtjs/sitemap@8.5.0(fbea9601d4f309fdf46702da480511cc)': + dependencies: + '@nuxt/kit': 4.5.2(magic-string@1.2.2)(magicast@0.5.4)(oxc-parser@0.131.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(webpack@5.109.2(cssnano@8.0.7(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26))) + consola: 3.4.2 + defu: 6.1.7 + nuxt-site-config: 4.2.3(fbea9601d4f309fdf46702da480511cc) + nuxtseo-shared: 5.3.14(352f5d67275472f805550a7ece632e92) + ofetch: 1.5.1 + pathe: 2.0.3 + pkg-types: 2.3.1 + radix3: 1.1.2 + sitemapd: 0.2.2 + ufo: 1.6.4 + ultrahtml: 1.7.0 + transitivePeerDependencies: + - '@nuxt/schema' + - magic-string + - magicast + - nuxt + - oxc-parser + - rolldown + - unplugin + - vite + - vue + '@ota-meshi/ast-token-store@0.3.0': {} '@oxc-parser/binding-android-arm-eabi@0.131.0': @@ -7850,6 +8015,8 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.2 + anynum@1.0.1: {} + archiver-utils@5.0.2: dependencies: glob: 10.5.0 @@ -8739,6 +8906,20 @@ snapshots: dependencies: fast-string-width: 3.0.2 + fast-xml-builder@1.3.1: + dependencies: + path-expression-matcher: 1.6.2 + xml-naming: 0.3.0 + + fast-xml-parser@5.11.0: + dependencies: + '@nodable/entities': 3.0.0 + fast-xml-builder: 1.3.1 + is-unsafe: 2.0.2 + path-expression-matcher: 1.6.2 + strnum: 2.4.2 + xml-naming: 0.3.0 + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -9042,6 +9223,8 @@ snapshots: is-stream@3.0.0: {} + is-unsafe@2.0.2: {} + is-wsl@3.1.1: dependencies: is-inside-container: 1.0.0 @@ -9830,6 +10013,45 @@ snapshots: dependencies: boolbase: 1.0.0 + nuxt-site-config-kit@4.2.3(magic-string@1.2.2)(magicast@0.5.4)(oxc-parser@0.131.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(webpack@5.109.2(cssnano@8.0.7(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26)))(vue@3.5.41(typescript@6.0.3)): + dependencies: + '@nuxt/kit': 4.5.2(magic-string@1.2.2)(magicast@0.5.4)(oxc-parser@0.131.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(webpack@5.109.2(cssnano@8.0.7(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26))) + site-config-stack: 4.2.3(vue@3.5.41(typescript@6.0.3)) + std-env: 4.2.0 + ufo: 1.6.4 + transitivePeerDependencies: + - magic-string + - magicast + - oxc-parser + - rolldown + - unplugin + - vue + + nuxt-site-config@4.2.3(fbea9601d4f309fdf46702da480511cc): + dependencies: + '@nuxt/devalue': 2.0.2 + '@nuxt/kit': 4.5.2(magic-string@1.2.2)(magicast@0.5.4)(oxc-parser@0.131.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(webpack@5.109.2(cssnano@8.0.7(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26))) + consola: 3.4.2 + defu: 6.1.7 + h3: 1.15.11 + nuxt-site-config-kit: 4.2.3(magic-string@1.2.2)(magicast@0.5.4)(oxc-parser@0.131.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(webpack@5.109.2(cssnano@8.0.7(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26)))(vue@3.5.41(typescript@6.0.3)) + nuxtseo-shared: 5.3.14(352f5d67275472f805550a7ece632e92) + pathe: 2.0.3 + pkg-types: 2.3.1 + site-config-stack: 4.2.3(vue@3.5.41(typescript@6.0.3)) + ufo: 1.6.4 + vue: 3.5.41(typescript@6.0.3) + transitivePeerDependencies: + - '@nuxt/schema' + - magic-string + - magicast + - nuxt + - oxc-parser + - rolldown + - unplugin + - vite + - zod + nuxt@4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.8.1(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.131.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.10(typescript@6.0.3))(webpack@5.109.2(cssnano@8.0.7(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26))(yaml@2.9.0): dependencies: '@dxup/nuxt': 0.5.9(esbuild@0.28.2)(magicast@0.5.4)(oxc-parser@0.131.0)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(webpack@5.109.2(cssnano@8.0.7(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26)) @@ -9971,6 +10193,35 @@ snapshots: - xml2js - yaml + nuxtseo-shared@5.3.14(352f5d67275472f805550a7ece632e92): + dependencies: + '@clack/prompts': 1.7.0 + '@nuxt/devtools-kit': 4.0.0-alpha.7(magic-string@1.2.2)(magicast@0.5.4)(oxc-parser@0.131.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(webpack@5.109.2(cssnano@8.0.7(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26)))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) + '@nuxt/kit': 4.5.2(magic-string@1.2.2)(magicast@0.5.4)(oxc-parser@0.131.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(webpack@5.109.2(cssnano@8.0.7(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26))) + '@nuxt/schema': 4.5.2 + birpc: 4.2.0 + consola: 3.4.2 + defu: 6.1.7 + nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.8.1(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.131.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.10(typescript@6.0.3))(webpack@5.109.2(cssnano@8.0.7(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26))(yaml@2.9.0) + nypm: 0.6.9 + ofetch: 1.5.1 + pathe: 2.0.3 + pkg-types: 2.3.1 + radix3: 1.1.2 + sirv: 3.0.2 + std-env: 4.2.0 + ufo: 1.6.4 + vue: 3.5.41(typescript@6.0.3) + optionalDependencies: + nuxt-site-config: 4.2.3(fbea9601d4f309fdf46702da480511cc) + transitivePeerDependencies: + - magic-string + - magicast + - oxc-parser + - rolldown + - unplugin + - vite + nypm@0.6.9: dependencies: citty: 0.2.2 @@ -10116,6 +10367,8 @@ snapshots: path-exists@4.0.0: {} + path-expression-matcher@1.6.2: {} + path-key@3.1.1: {} path-key@4.0.0: {} @@ -10636,6 +10889,15 @@ snapshots: sisteransi@1.0.5: {} + site-config-stack@4.2.3(vue@3.5.41(typescript@6.0.3)): + dependencies: + ufo: 1.6.4 + vue: 3.5.41(typescript@6.0.3) + + sitemapd@0.2.2: + dependencies: + fast-xml-parser: 5.11.0 + slash@5.1.0: {} smob@1.6.2: {} @@ -10728,6 +10990,10 @@ snapshots: dependencies: js-tokens: 10.0.0 + strnum@2.4.2: + dependencies: + anynum: 1.0.1 + structured-clone-es@2.0.1: {} stylehacks@8.0.4(postcss@8.5.26): @@ -11424,6 +11690,8 @@ snapshots: xml-name-validator@5.0.0: {} + xml-naming@0.3.0: {} + y18n@5.0.8: {} yallist@3.1.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8a24f64..325a141 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,6 +2,8 @@ catalog: '@iconify-json/ooui': ^1.2.21 '@iconify-json/ri': ^1.2.10 '@iconify-json/simple-icons': ^1.2.93 + '@nuxtjs/robots': ^6.2.0 + '@nuxtjs/sitemap': ^8.5.0 '@sxzz/eslint-config': ^8.5.0 '@sxzz/prettier-config': ^2.3.2 '@types/fontkit': ^2.0.9 @@ -16,6 +18,7 @@ catalog: fflate: ^0.8.3 fontkit: ^2.0.4 nuxt: ^4.5.2 + nuxt-site-config: ^4.2.3 prettier: ^3.9.6 subset-font: ^2.5.0 typescript: ~6.0.3 diff --git a/public/_headers b/public/_headers index 35da0f9..be2ea10 100644 --- a/public/_headers +++ b/public/_headers @@ -15,3 +15,10 @@ /data/chars.json Access-Control-Allow-Origin: * Cache-Control: public, max-age=3600, stale-while-revalidate=86400 + +# Search engines should revalidate the stable discovery files after a deploy. +/sitemap.xml + Cache-Control: public, no-cache + +/robots.txt + Cache-Control: public, no-cache diff --git a/scripts/tests/sitemap.test.ts b/scripts/tests/sitemap.test.ts new file mode 100644 index 0000000..70efc2c --- /dev/null +++ b/scripts/tests/sitemap.test.ts @@ -0,0 +1,53 @@ +import { readFileSync } from 'node:fs' +import { describe, expect, it } from 'vitest' +import config from '../../nuxt.config.ts' +import type { CharsData } from '../../shared/types.ts' + +const chars: CharsData = JSON.parse( + readFileSync( + new URL('../../app/assets/data/chars.json', import.meta.url), + 'utf8', + ), +) +const deployWorkflow = readFileSync( + new URL('../../.github/workflows/deploy.yml', import.meta.url), + 'utf8', +) + +describe('search engine discovery', () => { + it('generates a static sitemap with every canonical page', () => { + expect(config.modules).toEqual( + expect.arrayContaining(['@nuxtjs/sitemap', '@nuxtjs/robots']), + ) + expect(config.sitemap).toMatchObject({ + excludeAppSources: true, + discoverImages: false, + discoverVideos: false, + xsl: false, + zeroRuntime: true, + }) + + const urls = new Set(config.sitemap.urls) + expect(urls.size).toBe(chars.rows.length + 2) + expect(urls.size).toBeLessThanOrEqual(50_000) + expect(urls.has('/')).toBe(true) + expect(urls.has('/about')).toBe(true) + + for (const row of chars.rows) { + expect(urls.has(`/char/${row.key}`)).toBe(true) + } + }) + + it('advertises the sitemap through the robots module', () => { + expect(config.robots.sitemap).toBe('/sitemap.xml') + }) + + it('injects the canonical origin and preview indexing policy at build time', () => { + expect(deployWorkflow).toContain( + 'NUXT_SITE_URL: ${{ vars.NUXT_SITE_URL || github.event.repository.homepage }}', + ) + expect(deployWorkflow).toContain( + "NUXT_SITE_ENV: ${{ github.event_name == 'push' && 'production' || 'preview' }}", + ) + }) +}) diff --git a/scripts/tests/worker.test.ts b/scripts/tests/worker.test.ts index 6cfc759..f8a9e34 100644 --- a/scripts/tests/worker.test.ts +++ b/scripts/tests/worker.test.ts @@ -58,6 +58,12 @@ describe('Workers Static Assets configuration', () => { expect(headers).toMatch( /\/data\/chars\.json[\s\S]*Access-Control-Allow-Origin: \*[\s\S]*Cache-Control: public, max-age=3600, stale-while-revalidate=86400/, ) + expect(headers).toMatch( + /\/sitemap\.xml[\s\S]*Cache-Control: public, no-cache/, + ) + expect(headers).toMatch( + /\/robots\.txt[\s\S]*Cache-Control: public, no-cache/, + ) expect(headers).not.toMatch(/\/(?:fonts|flags|strokes)\//) }) }) -- 2.51.2