diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index b55992e..1a094bb 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -2,6 +2,8 @@ name: Deploy to GitHub Pages
on:
push:
branches: [main]
+ pull_request:
+ branches: [main]
workflow_dispatch:
permissions:
contents: read
@@ -15,36 +17,22 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- - uses: actions/checkout@v7
- with:
- repository: SagaSmithAI/Sagasmith-dnd
- # Production inputs stay reproducible. Bump this revision only after
- # the D&D workbench CI passes at the replacement commit.
- ref: 25ee1ce4994f1950d6febd50145d364dbf621dbe
- path: sagasmith-dnd
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
- cache-dependency-path: |
- package-lock.json
- sagasmith-dnd/package-lock.json
+ cache-dependency-path: package-lock.json
- run: npm ci
+ - run: npm run check:content
- run: npm run build
- - run: npm ci
- working-directory: sagasmith-dnd
- - run: npm run build:ui
- working-directory: sagasmith-dnd
- env:
- SAGASMITH_UI_BASE: /dnd-ui
- SAGASMITH_UI_SITE: https://SagaSmithAI.github.io
- PUBLIC_SAGASMITH_LIBRARY_URL: https://SagaSmithAI.github.io/content-library/index.json
- - run: cp -R sagasmith-dnd/apps/ui/dist dist/dnd-ui
+ - run: npm run check:site
- uses: actions/upload-pages-artifact@v5
+ if: github.event_name != 'pull_request'
with:
path: dist/
deploy:
needs: build
+ if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
environment:
name: github-pages
diff --git a/AGENTS.md b/AGENTS.md
index ff3954d..6960c1a 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -42,6 +42,10 @@ accurate historical references.
when the public topology changes.
- Keep `/start` commands synchronized with the current `SagaSmith-agent` Local Kit manifest,
CLI, transport contract, and credential-free templates.
+- Keep `/security`, `/privacy`, `/updates`, the sitemap, manifest, README, and
+ Pages workflow aligned with the public routes and data boundaries.
+- GitHub Pages publishes this repository's static `dist/` only. Do not bundle a
+ domain Workbench or treat another repository as a website release input.
## Development
@@ -55,5 +59,5 @@ npm run preview
```
Use the repository scripts as the source of truth. A topology change is not
-complete until `src/pages/developers.astro` and the relevant README/profile/news
-entry agree.
+complete until `src/lib/site.ts`, `src/pages/developers.astro`, and the relevant
+README/profile/news entry agree.
diff --git a/README.md b/README.md
index 73dc29b..e3a3ab3 100644
--- a/README.md
+++ b/README.md
@@ -1,112 +1,87 @@
# SagaSmithAI Website
-[Live site](https://sagasmithai.github.io) · [Platform overview](https://github.com/SagaSmithAI/.github/blob/main/profile/README.md) · [SagaSmith Web](https://github.com/SagaSmithAI/SagaSmith-Web) · [Content catalog](https://github.com/SagaSmithAI/SagaSmith-dnd-content-library)
+[Live site](https://sagasmithai.github.io) · [Platform overview](https://github.com/SagaSmithAI/.github/blob/main/profile/README.md) · [SagaSmith Web](https://github.com/SagaSmithAI/SagaSmith-Web) · [Local Agent Kit](https://github.com/SagaSmithAI/SagaSmith-agent)
-The source for [sagasmithai.github.io](https://sagasmithai.github.io): the public home of the AI-native TTRPG platform.
+The bilingual static organization site for SagaSmithAI, an AI-native TTRPG platform. It is a public product and developer entry surface, not a runtime dashboard, hosted campaign service, or replacement for component documentation.
-## What the site communicates
+## Information architecture
-- fully autonomous game mastering as the primary player-facing experience;
-- human-DM collaboration as a supported second hosting mode;
-- persistent choices, actor-scoped knowledge, and clear table boundaries;
-- honest D&D and CoC experience status;
-- extended ruleset import as an explicitly experimental capability;
-- checksum-validated unified core-rule, addon, module, and preset packages;
-- the public-source hosted [SagaSmith Web](https://github.com/SagaSmithAI/SagaSmith-Web),
- while keeping game-state authority in system MCPs;
-- Local Agent Kit and Hosted Web as two deployments of the same authoritative MCP contract;
-- the public, rights-aware
- [Content Pack repository](https://github.com/SagaSmithAI/SagaSmith-dnd-content-library)
- and checksum catalog, without treating repository visibility as a content license;
-- a separate developer path for architecture, repositories, and ownership boundaries;
-- short public-facing platform updates.
+| Route | Purpose |
+|---|---|
+| `/` | Product thesis, Local/Hosted contract map, experience boundaries, maturity, and recent progress |
+| `/start` | Current release-locked Local Kit install, MCP-host templates, transports, and SagaSmith Web development entry |
+| `/developers` | Authoritative contract, ownership boundaries, and current repository topology |
+| `/library` | Current Content Pack inventory and integrity/authority/rights boundaries |
+| `/updates` | Dated public progress summaries with explicit evidence limits |
+| `/security` | Private vulnerability-reporting routes and sensitive-data guidance |
+| `/privacy` | Privacy notice for this static GitHub Pages site |
+| `/404` | Bilingual not-found recovery page |
-The site is bilingual (Chinese/English), static, and deliberately contains no runtime dashboard or user campaign data.
+Chinese is the default language. The language control reveals complete English copy on the same static routes and stores only `sagasmith-language` in browser `localStorage`.
## Current repository map
| Layer | Current repository |
|---|---|
-| Neutral runtime | [`sagasmith-core`](https://github.com/SagaSmithAI/sagasmith-core) |
-| D&D Domain / MCP / Skills / UI | [`sagasmith-dnd`](https://github.com/SagaSmithAI/sagasmith-dnd) |
-| CoC Domain / MCP / Skills / UI | [`sagasmith-coc`](https://github.com/SagaSmithAI/sagasmith-coc) |
+| Agent host and Local Agent Kit | [`SagaSmith-agent`](https://github.com/SagaSmithAI/SagaSmith-agent) |
+| Hosted browser product | [`SagaSmith-Web`](https://github.com/SagaSmithAI/SagaSmith-Web) |
+| System-neutral runtime | [`Sagasmith-core`](https://github.com/SagaSmithAI/Sagasmith-core) |
+| D&D Domain / MCP / Skills / UI | [`Sagasmith-dnd`](https://github.com/SagaSmithAI/Sagasmith-dnd) |
+| CoC Domain / MCP / Skills / UI | [`Sagasmith-coc`](https://github.com/SagaSmithAI/Sagasmith-coc) |
| Narrative Domain / MCP / Skills | [`sagasmith-narrative`](https://github.com/SagaSmithAI/sagasmith-narrative) |
-| Agent host | [`SagaSmith-agent`](https://github.com/SagaSmithAI/SagaSmith-agent) |
-| Hosted Web product | [SagaSmith Web (`SagaSmith-Web`)](https://github.com/SagaSmithAI/SagaSmith-Web) |
-| Content catalog | [`SagaSmith-dnd-content-library`](https://github.com/SagaSmithAI/SagaSmith-dnd-content-library) |
+| Rights-aware Content Pack catalog | [`SagaSmith-dnd-content-library`](https://github.com/SagaSmithAI/SagaSmith-dnd-content-library) |
+| Organization profile and community policy | [`.github`](https://github.com/SagaSmithAI/.github) |
-Former standalone MCP, Skills, UI, and generic Module Generator repositories
-are archived history, do not accept new issues, and are not current documentation,
-release inputs, or compatibility fallbacks. Their landing READMEs should point to the matching
-vertical repository.
+Former standalone MCP, Skills, UI, and generic Module Generator repositories are archived read-only history. They are not current documentation, release inputs, producers, consumers, or compatibility fallbacks.
-## Verified integration baseline
+## Product boundaries
-The public status published on 2026-08-20 is backed by a current-source hosted
-run: SagaSmith Web-signed principal context reached session-scoped Agent/MCP tools,
-and the D&D and CoC reference campaigns completed concurrently without a
-reported regression gap. The D&D path recorded a legal ending. Catalog-wide
-discovery and exclusions remain machine-readable; the site must not describe
-this reference run as complete playthrough coverage for every Pack or branch.
+- SagaSmith Local Agent Kit and SagaSmith Web are two deployments of `sagasmith.authoritative-mcp/v1`.
+- Transport, authentication, storage, and deployment may differ. Handlers, tool schemas, errors, authority, revisions, idempotency, and rule-write semantics may not.
+- SagaSmith Web owns accounts, sessions, quota, collaboration, Forge, Module Studio, hosted orchestration, and cloud projections. Domain MCPs remain authoritative for game state.
+- Agent and Skills may interpret, facilitate, and propose. Domain runtimes and MCP settle deterministic rules and authoritative writes.
+- `sagasmith.content-package` v2 Packs do not carry campaign permissions, ActorKnowledge, progress, random streams, branches, or snapshots.
+- Public repository or catalog visibility is not a content license. Every Pack, source, image, map, font, and derived asset retains its own rights requirements.
+- Extended ruleset import is Experimental and does not imply arbitrary unadapted rulebooks can execute safely.
-## Development
+## Development and validation
Requires Node.js 22.12+.
```bash
-npm install
-npm run dev
+npm ci
+npm run check:content
npm run build
-npm run preview
+npm run check:site
+npm run check:external
```
-The Pages workflow publishes `/dnd-ui` from `apps/ui` in the current
-[`sagasmith-dnd`](https://github.com/SagaSmithAI/Sagasmith-dnd) vertical
-repository. That external release input is pinned to a full commit SHA in
-`.github/workflows/deploy.yml` and is advanced only after the replacement
-revision passes the D&D workbench CI.
+`npm run check` runs the deterministic content/topology checks, production build, and built-site metadata/internal-link/asset-budget checks. `check:external` performs the network-dependent external-link pass separately.
+
+The GitHub Pages workflow builds only this repository and publishes `dist/`. It has no cross-repository release input or bundled Workbench. Pull requests run the same deterministic checks; deployment occurs only from `main` or an explicit workflow dispatch.
+
+## Assets and visual source
-Main files:
+- `public/logo-wordmark.png`, `public/logo-mark.png`, and the original favicon path are first-party brand assets already committed to this Apache-2.0 website in PR #9. They remain covered by this repository's `LICENSE` and `NOTICE`.
+- `public/og.png` is rendered at 1200×630 from the site-owned, reviewable `src/assets/og-source.svg`; it uses only inline vectors, text, and the documented site tokens.
+- `public/favicon.svg` is a site-owned vector mark adapted from the existing website favicon and recolored to the current product palette. `public/icon-512.png` is rendered from the reviewable `src/assets/icon-source.svg` for exact manifest and Apple touch dimensions.
+- The visual token names and interaction patterns are aligned with the current SagaSmith Web product language: void `#0c0d0d`, forge panel `#151716`, iron line `#2a2d2b`, vellum ink `#eee9dc`, forge gold `#d9ad5b`, and moss state `#718d6a`.
+- SagaSmith Web is proprietary. No SagaSmith Web source file, texture, icon, private implementation, model, cache, or generated workflow is copied into this static repository.
+- The site loads no remote font, analytics, image CDN, or third-party visual asset at runtime.
+
+## Main sources
```text
-src/pages/index.astro DM/player homepage and autonomous-hosting story
-src/pages/start.astro Executable Local Kit, MCP-client, and Hosted Web entry paths
-src/pages/developers.astro Developer architecture and repository map
-src/pages/library.astro Rights-aware public Content Pack catalog frame
-src/layouts/SiteLayout.astro Shared metadata, navigation, language, and footer
-src/styles/site.css Shared responsive visual system
-src/lib/news.ts Local field-note loader
-news/ Dated Markdown field notes
-public/ Logo, favicons, and social preview image
+src/layouts/SiteLayout.astro Shared metadata, bilingual navigation, accessibility, and footer
+src/lib/site.ts Current repository, system-status, and Local profile data
+src/lib/news.ts Local dated-news loader
+src/pages/ Every public route, including privacy, security, updates, and 404
+src/styles/site.css Shared responsive visual system and reduced-motion handling
+news/ Dated historical progress sources
+scripts/ Content, built-site, and external-link checks
+public/ First-party brand assets, manifest, sitemap, and robots policy
```
-## Content rules
-
-- Describe SagaSmithAI as an **AI-native TTRPG platform** with complete autonomous
- hosting as its primary end-user experience.
-- Keep DM and player outcomes ahead of implementation details on the homepage.
-- Present human-DM collaboration as a supported mode, not the only mode.
-- Use the D&D-style fantasy d20 reference path for primary visual storytelling
- while CoC remains less mature; keep CoC in clearly labeled system-status areas.
-- Present D&D as the end-to-end reference path and label CoC/UI maturity honestly.
-- Always label extended ruleset import as experimental; never imply arbitrary
- rulebooks or unadapted third-party systems can be imported and run reliably.
-- Keep domain state out of the website; link to MCP/Agent setup instead.
-- Describe SagaSmith Web as the full hosted browser product, with its control plane as one backend
- responsibility, never as the owner of D&D, CoC, or Narrative campaign state.
-- Keep the Local Agent Kit independent from SagaSmith Web and require both deployments to preserve
- the same MCP schemas, capabilities, errors, authority, revision, and idempotency behavior.
-- Do not claim bundled access to commercial rulebooks or modules.
-- Treat repository visibility and content licensing as separate facts. Every
- public catalog entry still requires its own license and distribution rights.
-- Distinguish unified content packages from campaign saves: no permissions,
- ActorKnowledge, progress, random stream, branches, or Snapshots migrate with them.
-- Describe rule-package imports as validated inactive storage; branch activation
- remains a separate Owner/DM operation and never implies source authority.
-- Update the organization Profile and platform README when the platform map changes.
-- Tie verification claims to dated, machine-readable regression evidence and keep
- unexecuted modules or paths explicit.
-
## License
-Apache-2.0
+Apache-2.0. See [`NOTICE`](NOTICE). Repository visibility and this website license do not grant rights to separately licensed Content Packs or SagaSmith Web source.
diff --git a/news/2026-07-15-coc-mcp-vertical.md b/news/2026-07-15-coc-mcp-vertical.md
index 5b1e488..f2dd9c8 100644
--- a/news/2026-07-15-coc-mcp-vertical.md
+++ b/news/2026-07-15-coc-mcp-vertical.md
@@ -7,6 +7,6 @@ summary: CoC 7e 已能保存调查进度、角色各自掌握的线索与战役
summaryEn: CoC 7e can now preserve investigation progress, character-specific clues, and campaign state while the experience remains in testing.
---
-`SagaSmith-coc-mcp` 让 CoC Agent 不再依赖 CLI 拼接或复制 D&D 工具表。真实 stdio 客户端测试验证了目录级发现、按 session 加载和 `exposure_call` fallback;多人测试验证了 PC 知识互相隔离,并在重启后保留战役、模组、Snapshot 与知识状态。
+当时的 `SagaSmith-coc-mcp`(现已归档,能力已收敛到 `Sagasmith-coc` vertical)让 CoC Agent 不再依赖 CLI 拼接或复制 D&D 工具表。真实 stdio 客户端测试验证了目录级发现、按 session 加载和 `exposure_call` fallback;多人测试验证了 PC 知识互相隔离,并在重启后保留战役、模组、Snapshot 与知识状态。
-`SagaSmith-coc-mcp` removes the need for CoC agents to compose CLI commands or copy the D&D tool catalogue. A real stdio client test covers discovery, per-session loading, and the `exposure_call` fallback, while the multiplayer vertical test preserves campaign, scenario, snapshot, and actor-knowledge state across restart.
+The then-current `SagaSmith-coc-mcp` repository—now archived with its capabilities consolidated into the `Sagasmith-coc` vertical—removed the need for CoC agents to compose CLI commands or copy the D&D tool catalogue. A real stdio client test covered discovery, per-session loading, and the `exposure_call` fallback, while the multiplayer vertical test preserved campaign, scenario, snapshot, and actor-knowledge state across restart.
diff --git a/news/2026-08-18-public-service-and-content-catalog.md b/news/2026-08-18-public-service-and-content-catalog.md
index 41dae28..06c8be0 100644
--- a/news/2026-08-18-public-service-and-content-catalog.md
+++ b/news/2026-08-18-public-service-and-content-catalog.md
@@ -7,7 +7,7 @@ summary: SagaSmith Web、D&D/CoC 多系统房间与当前 46 个 Content Pack
summaryEn: SagaSmith Web, hosted D&D/CoC rooms, and the checksum catalog for 46 current Content Packs are now publicly visible with explicit state and content-rights boundaries.
---
-`SagaSmith-service` 仓库中的 SagaSmith Web 现在公开展示托管账户、配额、战役房间、主持身份、Agent
+当时名为 `SagaSmith-service`、现为 `SagaSmith-Web` 的 SagaSmith Web 仓库公开展示托管账户、配额、战役房间、主持身份、Agent
调度、浏览器前端与多系统 MCP 编排。SagaSmith Web 不拥有 D&D 或 CoC 的权威游戏状态;
每一次领域操作仍交给对应 MCP 重新做权限、revision、阶段和角色范围校验。
diff --git a/news/2026-08-19-domain-monorepos.md b/news/2026-08-19-domain-monorepos.md
index 37e1bee..8cedb66 100644
--- a/news/2026-08-19-domain-monorepos.md
+++ b/news/2026-08-19-domain-monorepos.md
@@ -16,6 +16,6 @@ Skills、领域 UI(如有)和对应的 Pack/项目创作流程,同时继
审计,但不再接收当前 Issue、发布或集成,也不会被 Agent、SagaSmith Web 或安装器作为
兼容回退。
-`sagasmith-core`、`SagaSmith-agent`、`SagaSmith-service`、内容目录、官网与组织文档
+`sagasmith-core`、`SagaSmith-agent`、`SagaSmith-Web`(当时名为 `SagaSmith-service`)、内容目录、官网与组织文档
继续独立。开发者页、活跃仓库 README 与 SagaSmith Web 组件锁只把当前 vertical
仓库作为发布和集成输入;归档仓库的入口文档应仅重定向到对应 vertical 仓库。
diff --git a/news/2026-08-20-current-runtime-regression.md b/news/2026-08-20-current-runtime-regression.md
index f41b6f0..bbc0356 100644
--- a/news/2026-08-20-current-runtime-regression.md
+++ b/news/2026-08-20-current-runtime-regression.md
@@ -8,7 +8,7 @@ summaryEn: The long regression now rebuilds the hosted stack from the current Ag
---
长回归会先从当前 `SagaSmith-agent`、`sagasmith-core`、三个领域仓库与
-`SagaSmith-service` 源码重建并重建容器,再开始任何房间动作。SagaSmith Web 使用临时共享
+`SagaSmith-Web`(当时名为 `SagaSmith-service`)源码重建并重建容器,再开始任何房间动作。SagaSmith Web 使用临时共享
密钥签发带时效的 `sagasmith.auth-context/v1`,Agent 将 principal context 传给
会话作用域 MCP;领域服务仍在实际调用边界重新校验身份、角色、战役与 revision。
diff --git a/package.json b/package.json
index 47e53ed..e202960 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,11 @@
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
- "astro": "astro"
+ "astro": "astro",
+ "check:content": "node scripts/check-content.mjs",
+ "check:site": "node scripts/check-site.mjs",
+ "check:external": "node scripts/check-external-links.mjs",
+ "check": "npm run check:content && npm run build && npm run check:site"
},
"dependencies": {
"astro": "^7.2.8"
diff --git a/public/favicon.svg b/public/favicon.svg
index bf1f537..2f22e02 100644
--- a/public/favicon.svg
+++ b/public/favicon.svg
@@ -1,6 +1,6 @@
diff --git a/public/icon-512.png b/public/icon-512.png
new file mode 100644
index 0000000..d08aa76
Binary files /dev/null and b/public/icon-512.png differ
diff --git a/public/og.png b/public/og.png
index 4185d28..73b5d2c 100644
Binary files a/public/og.png and b/public/og.png differ
diff --git a/public/site.webmanifest b/public/site.webmanifest
new file mode 100644
index 0000000..27124c2
--- /dev/null
+++ b/public/site.webmanifest
@@ -0,0 +1,25 @@
+{
+ "name": "SagaSmithAI",
+ "short_name": "SagaSmith",
+ "description": "AI-native TTRPG platform with one authoritative MCP contract across local and hosted deployments.",
+ "lang": "zh-CN",
+ "start_url": "/",
+ "scope": "/",
+ "display": "standalone",
+ "background_color": "#0c0d0d",
+ "theme_color": "#0c0d0d",
+ "icons": [
+ {
+ "src": "/favicon.svg",
+ "sizes": "any",
+ "type": "image/svg+xml",
+ "purpose": "any"
+ },
+ {
+ "src": "/icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "any"
+ }
+ ]
+}
diff --git a/public/sitemap.xml b/public/sitemap.xml
index a50cae0..d6f48dc 100644
--- a/public/sitemap.xml
+++ b/public/sitemap.xml
@@ -1,7 +1,10 @@
- https://sagasmithai.github.io/
- https://sagasmithai.github.io/start
- https://sagasmithai.github.io/developers
- https://sagasmithai.github.io/library
+ https://sagasmithai.github.io/1.0
+ https://sagasmithai.github.io/start0.9
+ https://sagasmithai.github.io/developers0.9
+ https://sagasmithai.github.io/library0.8
+ https://sagasmithai.github.io/updates0.7
+ https://sagasmithai.github.io/security0.6
+ https://sagasmithai.github.io/privacy0.5
diff --git a/scripts/check-content.mjs b/scripts/check-content.mjs
new file mode 100644
index 0000000..b7dd6e0
--- /dev/null
+++ b/scripts/check-content.mjs
@@ -0,0 +1,81 @@
+import { readFile, readdir } from 'node:fs/promises';
+import path from 'node:path';
+
+const root = process.cwd();
+const failures = [];
+const requiredPages = ['index', 'start', 'developers', 'library', 'updates', 'security', 'privacy', '404'];
+const currentRepos = [
+ 'SagaSmith-agent',
+ 'SagaSmith-Web',
+ 'Sagasmith-core',
+ 'Sagasmith-dnd',
+ 'Sagasmith-coc',
+ 'sagasmith-narrative',
+ 'SagaSmith-dnd-content-library',
+];
+const legacyNames = [
+ 'SagaSmith-service',
+ 'SagaSmith-dnd-mcp',
+ 'SagaSmith-dnd-skills',
+ 'sagasmith-dnd-ui',
+ 'SagaSmith-coc-mcp',
+ 'SagaSmith-coc-skills',
+ 'sagasmith-coc-ui',
+ 'SagaSmith-narrative-mcp',
+ 'SagaSmith-narrative-skills',
+ 'SagaSmith-module-gen-skills',
+ 'sagasmith-ui',
+];
+
+const read = (file) => readFile(path.join(root, file), 'utf8');
+
+for (const page of requiredPages) {
+ const file = `src/pages/${page}.astro`;
+ try {
+ const source = await read(file);
+ if (!source.includes(' file.endsWith('.astro')).map((file) => `src/pages/${file}`),
+ 'src/layouts/SiteLayout.astro',
+ 'src/lib/site.ts',
+ 'README.md',
+ 'AGENTS.md',
+ '.github/workflows/deploy.yml',
+];
+const authoritativeText = (await Promise.all(authoritativeFiles.map(read))).join('\n');
+
+for (const legacy of legacyNames) {
+ if (authoritativeText.includes(legacy)) failures.push(`authoritative surfaces still name legacy repository: ${legacy}`);
+}
+for (const repo of currentRepos) {
+ if (!authoritativeText.includes(repo)) failures.push(`current repository missing from authoritative surfaces: ${repo}`);
+}
+
+const workflow = await read('.github/workflows/deploy.yml');
+if (/repository:\s+SagaSmithAI\//.test(workflow)) failures.push('Pages workflow still checks out an external release input');
+if (workflow.includes('/dnd-ui') || workflow.includes('build:ui')) failures.push('Pages workflow still bundles a domain Workbench');
+
+const manifest = JSON.parse(await read('public/site.webmanifest'));
+if (manifest.start_url !== '/' || manifest.scope !== '/') failures.push('manifest start_url and scope must both be /');
+if (!manifest.icons?.some((icon) => icon.src === '/favicon.svg')) failures.push('manifest is missing the SVG icon');
+
+const sitemap = await read('public/sitemap.xml');
+for (const page of requiredPages.filter((page) => page !== '404' && page !== 'index')) {
+ if (!sitemap.includes(`/${page}`)) failures.push(`sitemap missing /${page}`);
+}
+
+if (failures.length) {
+ console.error(`Content checks failed (${failures.length}):`);
+ failures.forEach((failure) => console.error(`- ${failure}`));
+ process.exit(1);
+}
+
+console.log(`Content checks passed: ${requiredPages.length} bilingual routes, current topology, manifest, sitemap, and Pages source.`);
diff --git a/scripts/check-external-links.mjs b/scripts/check-external-links.mjs
new file mode 100644
index 0000000..e415760
--- /dev/null
+++ b/scripts/check-external-links.mjs
@@ -0,0 +1,71 @@
+import { readFile, readdir } from 'node:fs/promises';
+import path from 'node:path';
+
+const dist = path.join(process.cwd(), 'dist');
+
+async function walk(directory) {
+ const files = [];
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
+ const full = path.join(directory, entry.name);
+ if (entry.isDirectory()) files.push(...await walk(full));
+ else files.push(full);
+ }
+ return files;
+}
+
+const htmlFiles = (await walk(dist)).filter((file) => file.endsWith('.html'));
+const links = new Set();
+for (const file of htmlFiles) {
+ const html = await readFile(file, 'utf8');
+ for (const match of html.matchAll(/\shref="(https?:\/\/[^"]+)"/g)) {
+ const url = new URL(match[1].replaceAll('&', '&'));
+ if (url.hostname === 'sagasmithai.github.io') continue;
+ links.add(url.toString());
+ }
+}
+
+const failures = [];
+const warnings = [];
+const queue = [...links];
+
+async function check(url) {
+ const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), 20_000);
+ try {
+ let response = await fetch(url, {
+ method: 'HEAD',
+ redirect: 'follow',
+ signal: controller.signal,
+ headers: { 'user-agent': 'SagaSmithAI-site-link-check/1.0' },
+ });
+ if (response.status === 405) {
+ response = await fetch(url, {
+ method: 'GET',
+ redirect: 'follow',
+ signal: controller.signal,
+ headers: { 'user-agent': 'SagaSmithAI-site-link-check/1.0', range: 'bytes=0-0' },
+ });
+ }
+ if (response.status === 404 || response.status === 410) failures.push(`${response.status} ${url}`);
+ else if (!response.ok && response.status !== 403 && response.status !== 429) failures.push(`${response.status} ${url}`);
+ else if (!response.ok) warnings.push(`${response.status} ${url}`);
+ } catch (error) {
+ failures.push(`${error.name}: ${url}`);
+ } finally {
+ clearTimeout(timeout);
+ }
+}
+
+const workers = Array.from({ length: Math.min(4, queue.length) }, async () => {
+ while (queue.length) await check(queue.shift());
+});
+await Promise.all(workers);
+
+warnings.forEach((warning) => console.warn(`Warning: ${warning}`));
+if (failures.length) {
+ console.error(`External-link checks failed (${failures.length}):`);
+ failures.forEach((failure) => console.error(`- ${failure}`));
+ process.exit(1);
+}
+
+console.log(`External-link checks passed: ${links.size} unique URLs (${warnings.length} access-limited warnings).`);
diff --git a/scripts/check-site.mjs b/scripts/check-site.mjs
new file mode 100644
index 0000000..57b4845
--- /dev/null
+++ b/scripts/check-site.mjs
@@ -0,0 +1,92 @@
+import { readFile, readdir, stat } from 'node:fs/promises';
+import path from 'node:path';
+
+const root = process.cwd();
+const dist = path.join(root, 'dist');
+const failures = [];
+
+async function walk(directory) {
+ const files = [];
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
+ const full = path.join(directory, entry.name);
+ if (entry.isDirectory()) files.push(...await walk(full));
+ else files.push(full);
+ }
+ return files;
+}
+
+const files = await walk(dist);
+const htmlFiles = files.filter((file) => file.endsWith('.html'));
+const byPath = new Set(files.map((file) => path.relative(dist, file).replaceAll('\\', '/')));
+const pageAnchors = new Map();
+
+for (const file of htmlFiles) {
+ const html = await readFile(file, 'utf8');
+ const relative = path.relative(dist, file).replaceAll('\\', '/');
+ const required = [
+ /]+lang=/,
+ /[^<]+<\/title>/,
+ /]/,
+ /class="zh"/,
+ /class="en"/,
+ ];
+ required.forEach((pattern) => {
+ if (!pattern.test(html)) failures.push(`${relative}: missing ${pattern}`);
+ });
+ if ((html.match(/