diff --git a/deploy/container/nginx.conf b/deploy/container/nginx.conf index be9dd0ebd..0f5bc237c 100644 --- a/deploy/container/nginx.conf +++ b/deploy/container/nginx.conf @@ -215,8 +215,18 @@ http { return 404; } + # BEGIN GENARRATIVE MAIN SPA ROUTES + location = / { + try_files /index.html =404; + } + + location ~* "^/(?:bark-battle|big-fish|child-motion-demo|creation|creation/baby-object-match|creation/baby-object-match/generating|creation/baby-object-match/result|creation/bark-battle|creation/bark-battle/generating|creation/bark-battle/result|creation/big-fish|creation/big-fish/generating|creation/big-fish/result|creation/creative-agent|creation/jump-hop|creation/jump-hop/generating|creation/jump-hop/result|creation/match3d|creation/match3d/generating|creation/match3d/result|creation/puzzle|creation/puzzle-clear|creation/puzzle-clear/generating|creation/puzzle-clear/result|creation/puzzle/generating|creation/puzzle/result|creation/rpg|creation/rpg/agent|creation/rpg/generating|creation/rpg/result|creation/square-hole|creation/square-hole/generating|creation/square-hole/result|creation/visual-novel|creation/visual-novel/generating|creation/visual-novel/result|creation/wooden-fish|creation/wooden-fish/generating|creation/wooden-fish/result|editor/canvas|gallery/jump-hop/detail|gallery/puzzle/detail|gallery/visual-novel/detail|match3d|project|puzzle|runtime/baby-love-drawing|runtime/baby-object-match|runtime/bark-battle|runtime/big-fish|runtime/jump-hop|runtime/match3d|runtime/puzzle|runtime/puzzle-clear|runtime/rpg/adventure|runtime/rpg/characters|runtime/square-hole|runtime/visual-novel|runtime/wooden-fish|works/detail|worlds/detail)/?$" { + try_files $uri /index.html =404; + } + # END GENARRATIVE MAIN SPA ROUTES + location / { - try_files $uri $uri/ /index.html; + try_files $uri $uri/ =404; } } } diff --git a/deploy/nginx/genarrative-dev-http.conf b/deploy/nginx/genarrative-dev-http.conf index 62e87f148..b5a33d8e3 100644 --- a/deploy/nginx/genarrative-dev-http.conf +++ b/deploy/nginx/genarrative-dev-http.conf @@ -249,6 +249,28 @@ server { return 404; } + # BEGIN GENARRATIVE MAIN SPA ROUTES + location = / { + error_page 503 /maintenance.html; + + if ($genarrative_maintenance) { + return 503; + } + + try_files /index.html =404; + } + + location ~* "^/(?:bark-battle|big-fish|child-motion-demo|creation|creation/baby-object-match|creation/baby-object-match/generating|creation/baby-object-match/result|creation/bark-battle|creation/bark-battle/generating|creation/bark-battle/result|creation/big-fish|creation/big-fish/generating|creation/big-fish/result|creation/creative-agent|creation/jump-hop|creation/jump-hop/generating|creation/jump-hop/result|creation/match3d|creation/match3d/generating|creation/match3d/result|creation/puzzle|creation/puzzle-clear|creation/puzzle-clear/generating|creation/puzzle-clear/result|creation/puzzle/generating|creation/puzzle/result|creation/rpg|creation/rpg/agent|creation/rpg/generating|creation/rpg/result|creation/square-hole|creation/square-hole/generating|creation/square-hole/result|creation/visual-novel|creation/visual-novel/generating|creation/visual-novel/result|creation/wooden-fish|creation/wooden-fish/generating|creation/wooden-fish/result|editor/canvas|gallery/jump-hop/detail|gallery/puzzle/detail|gallery/visual-novel/detail|match3d|project|puzzle|runtime/baby-love-drawing|runtime/baby-object-match|runtime/bark-battle|runtime/big-fish|runtime/jump-hop|runtime/match3d|runtime/puzzle|runtime/puzzle-clear|runtime/rpg/adventure|runtime/rpg/characters|runtime/square-hole|runtime/visual-novel|runtime/wooden-fish|works/detail|worlds/detail)/?$" { + error_page 503 /maintenance.html; + + if ($genarrative_maintenance) { + return 503; + } + + try_files $uri /index.html =404; + } + # END GENARRATIVE MAIN SPA ROUTES + location / { error_page 503 /maintenance.html; @@ -256,6 +278,6 @@ server { return 503; } - try_files $uri $uri/ /index.html; + try_files $uri $uri/ =404; } } diff --git a/deploy/nginx/genarrative.conf b/deploy/nginx/genarrative.conf index fa1a111b4..f4b3455dc 100644 --- a/deploy/nginx/genarrative.conf +++ b/deploy/nginx/genarrative.conf @@ -269,6 +269,28 @@ server { return 404; } + # BEGIN GENARRATIVE MAIN SPA ROUTES + location = / { + error_page 503 /maintenance.html; + + if ($genarrative_maintenance) { + return 503; + } + + try_files /index.html =404; + } + + location ~* "^/(?:bark-battle|big-fish|child-motion-demo|creation|creation/baby-object-match|creation/baby-object-match/generating|creation/baby-object-match/result|creation/bark-battle|creation/bark-battle/generating|creation/bark-battle/result|creation/big-fish|creation/big-fish/generating|creation/big-fish/result|creation/creative-agent|creation/jump-hop|creation/jump-hop/generating|creation/jump-hop/result|creation/match3d|creation/match3d/generating|creation/match3d/result|creation/puzzle|creation/puzzle-clear|creation/puzzle-clear/generating|creation/puzzle-clear/result|creation/puzzle/generating|creation/puzzle/result|creation/rpg|creation/rpg/agent|creation/rpg/generating|creation/rpg/result|creation/square-hole|creation/square-hole/generating|creation/square-hole/result|creation/visual-novel|creation/visual-novel/generating|creation/visual-novel/result|creation/wooden-fish|creation/wooden-fish/generating|creation/wooden-fish/result|editor/canvas|gallery/jump-hop/detail|gallery/puzzle/detail|gallery/visual-novel/detail|match3d|project|puzzle|runtime/baby-love-drawing|runtime/baby-object-match|runtime/bark-battle|runtime/big-fish|runtime/jump-hop|runtime/match3d|runtime/puzzle|runtime/puzzle-clear|runtime/rpg/adventure|runtime/rpg/characters|runtime/square-hole|runtime/visual-novel|runtime/wooden-fish|works/detail|worlds/detail)/?$" { + error_page 503 /maintenance.html; + + if ($genarrative_maintenance) { + return 503; + } + + try_files $uri /index.html =404; + } + # END GENARRATIVE MAIN SPA ROUTES + location / { error_page 503 /maintenance.html; @@ -276,6 +298,6 @@ server { return 503; } - try_files $uri $uri/ /index.html; + try_files $uri $uri/ =404; } } diff --git a/deploy/pingora/nginx-route-parity.matrix.json b/deploy/pingora/nginx-route-parity.matrix.json index 8642b8651..d923df865 100644 --- a/deploy/pingora/nginx-route-parity.matrix.json +++ b/deploy/pingora/nginx-route-parity.matrix.json @@ -359,17 +359,93 @@ }, { "id": "web_spa_fallback", - "samplePath": "/some/spa/path", + "samplePath": "/creation/puzzle/result", "expect": { "kind": "static", "root": "web", "mode": "spa_fallback" }, "nginx": { - "production": ["location /", "try_files $uri $uri/ /index.html;"], - "development": ["location /", "try_files $uri $uri/ /index.html;"] + "production": [ + "# BEGIN GENARRATIVE MAIN SPA ROUTES", + "try_files $uri /index.html =404;" + ], + "development": [ + "# BEGIN GENARRATIVE MAIN SPA ROUTES", + "try_files $uri /index.html =404;" + ] }, - "docs": ["其它路径", "失败回退 `/index.html`"] + "docs": ["主站 SPA allowlist", "失败回退 `/index.html`"] + }, + { + "id": "web_spa_case_trailing_slash", + "samplePath": "/CREATION/PUZZLE/RESULT/", + "expect": { + "kind": "static", + "root": "web", + "mode": "spa_fallback" + }, + "nginx": { + "production": ["location ~*", "try_files $uri /index.html =404;"], + "development": ["location ~*", "try_files $uri /index.html =404;"] + }, + "docs": ["大小写不敏感", "一个尾部斜杠"] + }, + { + "id": "web_unknown_path_exact", + "samplePath": "/some/spa/path", + "expect": { + "kind": "static", + "root": "web", + "mode": "exact" + }, + "nginx": { + "production": ["location /", "try_files $uri $uri/ =404;"], + "development": ["location /", "try_files $uri $uri/ =404;"] + }, + "docs": ["其它 Web 路径", "缺失时返回真实 404"] + }, + { + "id": "creation_unknown_path_exact", + "samplePath": "/creation/not-exist", + "expect": { + "kind": "static", + "root": "web", + "mode": "exact" + }, + "nginx": { + "production": ["try_files $uri $uri/ =404;"], + "development": ["try_files $uri $uri/ =404;"] + }, + "docs": ["`/creation/not-exist`"] + }, + { + "id": "runtime_unknown_path_exact", + "samplePath": "/runtime/not-exist", + "expect": { + "kind": "static", + "root": "web", + "mode": "exact" + }, + "nginx": { + "production": ["try_files $uri $uri/ =404;"], + "development": ["try_files $uri $uri/ =404;"] + }, + "docs": ["`/runtime/not-exist`"] + }, + { + "id": "puzzle_unknown_path_exact", + "samplePath": "/puzzle/not-exist", + "expect": { + "kind": "static", + "root": "web", + "mode": "exact" + }, + "nginx": { + "production": ["try_files $uri $uri/ =404;"], + "development": ["try_files $uri $uri/ =404;"] + }, + "docs": ["`/puzzle/not-exist`"] } ] } diff --git a/docs/README.md b/docs/README.md index 15d04145a..67c19ef4b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,6 +5,7 @@ ## 快速入口 - [Agent 工作入口与执行准则](./%E3%80%90%E5%8D%8F%E4%BD%9C%E8%A7%84%E8%8C%83%E3%80%91Agent%E5%B7%A5%E4%BD%9C%E5%85%A5%E5%8F%A3%E4%B8%8E%E6%89%A7%E8%A1%8C%E5%87%86%E5%88%99-2026-06-22.md):复杂任务前的 Agent 阅读顺序、执行边界、技能路由、文档规则和验证口径。 +- [官网 SEO 地基实施约定](./technical/【SEO】官网SEO地基实施约定-2026-07-10.md):首页基础 head、robots/sitemap、唯一 H1、精确 SPA 路由与未知路径 404 的长期技术边界。 - [经验沉淀](./experience/README.md):项目开发经验、UI 交接、历史实现经验。 - [审计与复盘](./audits/README.md):工程审查、文本/乱码审计、专项落地审计。 - [系统设计](./design/README.md):玩法、关系、物品与对话设计。 diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index e36f7003c..5ba50491b 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -3949,3 +3949,10 @@ - 性能:`api-server` 只在当前判定涉及的已启用 gate 配置了用户标签白名单时读取用户标签;不因无关 gate 或纯用户 ID / 百分比灰度触发额外标签读取。 - 影响范围:`feature_gate_config`、`spacetime-client` runtime facade、`api-server` 创作入口配置与路由熔断、`apps/admin-web` 灰度发布页。 - 验证方式:`npm run spacetime:generate`、`npm run check:spacetime-schema`、`cargo test -p module-runtime --manifest-path server-rs/Cargo.toml feature_gate`、`cargo test -p api-server --manifest-path server-rs/Cargo.toml creation_entry_feature_gate`、`npm run admin-web:typecheck`、后台灰度页 Vitest、`npm run check:encoding`、`git diff --check`。 + +## 2026-07-10 官网 SEO 与主站 SPA 404 边界 + +- 背景:主站 Nginx 和 Pingora 原先会把任意未知路径回退到 `index.html`,导致 soft 404;共享 `index.html` 也缺少首页 SEO head,robots 和 sitemap 请求会落入 SPA fallback。 +- 决策:新增真实 `robots.txt` 和仅首页的 `sitemap.xml`,首页共享 head 提供基础 SEO/OG/JSON-LD 文本但不使用未确认的 image/logo URL;首页 DOM 只保留一个稳定产品定位 H1。Nginx 与 Pingora 只允许当前完整 SPA 路径回退 `index.html`,同前缀未知路径必须返回 404;`/admin` 继续走独立子应用。路由变化必须同步三套 Nginx、Pingora、route parity matrix 和自动门禁。 +- 影响范围:`index.html`、`public/robots.txt`、`public/sitemap.xml`、首页组件、三套 Nginx、Pingora 网关和路由 parity 门禁。 +- 验证方式:前端定向测试与构建、`npm run check:nginx-spa-routes`、`npm run check:pingora-route-parity`、`npm run check:pingora-gateway-smoke`、`npm run check:encoding`、`git diff --check`,部署后同时抽查根级未知路径和 `/creation/not-exist` 等同前缀未知路径。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 0cda9834e..bb8bc7f5b 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -2937,3 +2937,10 @@ - 处理:产品 UI 不提供模型选择,应用内调用固定 `birefnet`;外部编辑器 OpenAPI 不声明 `segModel`,并保持相关请求 schema 的 `additionalProperties: false`,使外部请求携带该字段时被契约拒绝。只有维护 BgFilter 模型与容量的后端代码可使用该内部字段;若未来需要开放,先完成各模型的内存、并发和超时压测,再明确版本化外部契约。 - 验证:检查 `docs/openapi/genarrative-external-v1.openapi.json` 的图片生成、图标 spritesheet 和 UI 素材提取请求 schema 均未包含 `segModel`,且均保持 `additionalProperties: false`。 - 关联:`server-rs/crates/api-server/src/editor_project.rs`、`src/services/image-editor/editorProjectClient.ts`、`docs/project-memory/shared-memory/decision-log.md`。 +## SPA 路由白名单不能只按一级目录放行 + +- 现象:`/not-exist` 已返回 404,但 `/creation/not-exist`、`/runtime/not-exist` 或 `/puzzle/not-exist` 仍返回 200 首页,搜索引擎继续判定为 soft 404。 +- 原因:Nginx 或 Pingora 使用 `/creation/*`、`/runtime/*` 等宽前缀作为 SPA fallback,前端对未知路径又回到平台首页;只验收根级未知 URL 无法发现该问题。 +- 处理:SPA fallback 必须精确匹配当前真实完整路径,同时允许前端已有的大小写归一和尾部斜杠;最终 catch-all 只提供真实静态文件,失败返回 404。路由增删同步三套 Nginx、Pingora、route parity matrix 和路由门禁。 +- 验证:除全部真实 SPA 路径外,至少检查 `/not-exist`、`/creation/not-exist`、`/runtime/not-exist` 和 `/puzzle/not-exist` 均返回 404;维护模式仍保持页面 503 优先语义。 +- 关联:`src/routing/appRoutes.tsx`、`src/routing/appPageRoutes.ts`、`deploy/nginx/`、`deploy/container/nginx.conf`、`server-rs/crates/pingora-gateway/src/main.rs`。 diff --git a/docs/technical/【SEO】官网SEO地基实施约定-2026-07-10.md b/docs/technical/【SEO】官网SEO地基实施约定-2026-07-10.md new file mode 100644 index 000000000..4c77207ab --- /dev/null +++ b/docs/technical/【SEO】官网SEO地基实施约定-2026-07-10.md @@ -0,0 +1,50 @@ +# 官网 SEO 地基实施约定 + +更新时间:`2026-07-10` + +## 本轮边界 + +本轮只建立陶泥儿中文首页的 SEO 基础设施,不新增英文站、长尾落地页、SSR、动态 sitemap、前端访问埋点或分享图。 + +- `public/robots.txt` 是真实静态文件;非 SEO 产品路径本轮阻止抓取,但不把 `Disallow` 解释为保证不收录。 +- `public/sitemap.xml` 本轮只包含 `https://www.genarrative.world/`,不写难以持续维护的 `lastmod`。 +- `index.html` 提供首页 title、description、canonical、robots、基础 OG/Twitter 文本和 JSON-LD;没有正式分享图时不写 `og:image`、`twitter:image` 或 JSON-LD `logo`。 +- 首页渲染后只有一个稳定、可见的产品定位 H1;动态作品名、卡片标题、按钮内部标题和隐藏 Tab 不作为 H1。 +- Nginx 和 Pingora 只对当前已知完整 SPA 路径回退 `index.html`;未知路径以及 `/creation/not-exist` 等同前缀未知路径返回 HTTP 404。 +- Nginx access log 继续使用现有 `$http_referer`,本轮不新增前端 pageview 或用户身份采集。 + +## 路由事实源 + +主站 SPA 路由以以下源码为事实源: + +- `src/routing/appRoutes.tsx` +- `src/routing/appPageRoutes.ts` + +`/creation/rpg/agent` 仍被现有刷新恢复链路使用,当前作为兼容深链保留。新增或删除前端路由时,必须同步三套 Nginx 配置、Pingora 路由、`deploy/pingora/nginx-route-parity.matrix.json` 和对应自动门禁。不得把 `/creation/*`、`/runtime/*` 等一级目录整体设为 SPA fallback。 + +后续新增 SEO 落地页时,还必须同时满足:返回 200、不被 robots.txt 阻止抓取、加入 sitemap,并提供独立 title、description、canonical、H1、正文和内链入口。纯 SPA 页面需要独立 head 时,应评估构建时静态 HTML、预渲染或 SSR。 + +## 验收口径 + +```bash +npm run build +npm run typecheck +npm run check:nginx-spa-routes +npm run check:pingora-route-parity +npm run check:pingora-gateway-smoke +npm run check:encoding +git diff --check +``` + +部署后至少验证: + +```bash +curl -I https://www.genarrative.world/robots.txt +curl -I https://www.genarrative.world/sitemap.xml +curl -I https://www.genarrative.world/not-exist-test +curl -I https://www.genarrative.world/creation/not-exist-test +curl -I https://www.genarrative.world/runtime/not-exist-test +curl -I https://www.genarrative.world/puzzle/not-exist-test +``` + +真实 SPA 路径不得误 404;以上未知路径必须返回 404。首页浏览器 DOM 应只有一个 H1,并确认桌面和移动端布局、导航、推荐流和创作入口没有因 SEO 文案变形。 diff --git a/docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md b/docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md index 4e22e53b8..9756a2fa7 100644 --- a/docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md +++ b/docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md @@ -42,6 +42,7 @@ cargo run -p pingora-gateway --manifest-path server-rs/Cargo.toml ```bash npm run check:pingora-gateway-smoke +npm run check:nginx-spa-routes npm run check:pingora-route-parity npm run check:nginx-pingora-canary npm run check:pingora-canary-docker @@ -59,9 +60,11 @@ npm run check:pingora-cutover-evidence-audit npm run check:pingora-release-readiness ``` -`check:pingora-gateway-smoke` 会临时启动 mock `api-server`、mock SpacetimeDB、mock Gitea 和 `pingora-gateway`,覆盖 SPA fallback、后台静态路由、HTML / 普通静态资源 `no-cache`、Vite 指纹静态资源 immutable 缓存、静态 `ETag` / `Last-Modified` 与 `304` 协商缓存、静态 `HEAD` 响应、静态 Range、静态 access log method/path/status 对账、gzip 最小长度、小响应不压缩、图片资源不压缩、大响应压缩、ACME、TLS 直连、HTTP/2 ALPN、HTTP 到 HTTPS 重定向、内部路由拒绝、shadow probe、API 代理头(`Host` / `X-Forwarded-Host` / `X-Forwarded-Proto` / `X-Real-IP` / `X-Forwarded-For`)、Gitea Host 整站转发、请求体上限、429 接流保护、上游断连 / 超时 JSON 错误、维护模式、维护模式不拦截 Gitea Host 和 SpacetimeDB WebSocket Upgrade,并复用 `check-pingora-direct-live.mjs` 对临时 HTTPS / HTTP redirect / WSS subscribe 入口做 live smoke。该本地 fixture 会让首页同时引用普通静态资源和 Vite 指纹静态资源,direct live JSON 必须确认指纹资源 GET / HEAD / `Range: bytes=0-0` 以及 access log method/path/status 证据,避免正式直连前只证明普通静态读取。排查失败时可追加 `-- --verbose` 输出网关 stderr / stdout;已确认二进制无需重编时可追加 `-- --skip-build`。 +`check:pingora-gateway-smoke` 会临时启动 mock `api-server`、mock SpacetimeDB、mock Gitea 和 `pingora-gateway`,覆盖精确主站 SPA fallback、大小写与尾部斜杠兼容、同前缀未知路径真实 404、后台静态路由、HTML / 普通静态资源 `no-cache`、Vite 指纹静态资源 immutable 缓存、静态 `ETag` / `Last-Modified` 与 `304` 协商缓存、静态 `HEAD` 响应、静态 Range、静态 access log method/path/status 对账、gzip 最小长度、小响应不压缩、图片资源不压缩、大响应压缩、ACME、TLS 直连、HTTP/2 ALPN、HTTP 到 HTTPS 重定向、内部路由拒绝、shadow probe、API 代理头(`Host` / `X-Forwarded-Host` / `X-Forwarded-Proto` / `X-Real-IP` / `X-Forwarded-For`)、Gitea Host 整站转发、请求体上限、429 接流保护、上游断连 / 超时 JSON 错误、维护模式、维护模式不拦截 Gitea Host 和 SpacetimeDB WebSocket Upgrade,并复用 `check-pingora-direct-live.mjs` 对临时 HTTPS / HTTP redirect / WSS subscribe 入口做 live smoke。该本地 fixture 会让首页同时引用普通静态资源和 Vite 指纹静态资源,direct live JSON 必须确认指纹资源 GET / HEAD / `Range: bytes=0-0` 以及 access log method/path/status 证据,避免正式直连前只证明普通静态读取。排查失败时可追加 `-- --verbose` 输出网关 stderr / stdout;已确认二进制无需重编时可追加 `-- --skip-build`。 -`check:pingora-route-parity` 读取 `deploy/pingora/nginx-route-parity.matrix.json`,静态确认生产 / 开发 Nginx 模板、Pingora Rust 路由单测和本文档都覆盖同一组核心路由。`cargo test -p pingora-gateway --manifest-path server-rs/Cargo.toml matches_nginx_route_parity_matrix` 会读取同一份矩阵,逐条断言 `classify_path` 的路由结果、body limit 和接流保护分组。 +`check:nginx-spa-routes` 从 `appPageRoutes.ts` 的 `STAGE_ROUTE_ENTRIES` / `APP_RUNTIME_ROUTES`、`appRoutes.tsx` 的精确路由判断和兼容恢复路径 `/creation/rpg/agent` 提取当前主站 SPA allowlist,确认生产、开发和容器三套 Nginx 模板集合一致,并验证大小写、尾部斜杠和 `/creation/not-exist`、`/runtime/not-exist`、`/puzzle/not-exist` 等未知反例。 + +`check:pingora-route-parity` 会先执行同一 Nginx SPA 路由门禁,再读取 `deploy/pingora/nginx-route-parity.matrix.json`,静态确认生产 / 开发 Nginx 模板、Pingora Rust 路由 allowlist / 单测和本文档都覆盖同一组核心路由。`cargo test -p pingora-gateway --manifest-path server-rs/Cargo.toml matches_nginx_route_parity_matrix` 会读取同一份矩阵,逐条断言 `classify_path` 的路由结果、body limit 和接流保护分组。 `check:nginx-pingora-canary` 会静态校验 `deploy/nginx/snippets/genarrative-pingora-canary.conf` 的本机来源限制、handoff 响应头、probe token 占位、前缀 rewrite、低缓冲和 WebSocket Upgrade 设置,也会校验 `deploy/nginx/snippets/genarrative-pingora-realpath-canary.conf` 只能作为独立 loopback `server` 片段使用、默认监听 `127.0.0.1:18083`、写独立 access log、没有 rewrite、覆盖真实 `/api` / `/v1` / `/assets` 代表路径。本机安装了 Nginx 时脚本会额外把两个 snippet 包进临时 `http {}` 执行 `nginx -t`;需要在 CI / 目标 agent 上强制要求真实 Nginx 语法检查时执行 `node scripts/check-nginx-pingora-canary.mjs --require-nginx`。 @@ -539,7 +542,8 @@ dev 根盘空间在安装后曾接近满盘;2026-06-17 进入 canary 前已清 | `/v1/database/{db}/subscribe`、`/v1/identity*` | 转发到 SpacetimeDB,保留 WebSocket Upgrade 头。 | | `/__genarrative_pingora/healthz` | 仅在携带 `X-Genarrative-Pingora-Probe` 且匹配配置 token 时返回 shadow JSON,否则 404。 | | `/v1/*`、`/generated-*`、`/healthz*`、`/readyz*` | 返回 404,保持生产公网不暴露口径。 | -| 其它路径 | 先读取静态文件或目录 index,失败回退 `/index.html`,HTML 默认 `no-cache`,并支持条件请求返回 `304` 与单段 `Range: bytes=` 返回 `206` / 越界返回 `416`。 | +| 主站 SPA allowlist | 只对当前前端完整路由及兼容恢复路径 `/creation/rpg/agent` 失败回退 `/index.html`;匹配大小写不敏感并允许一个尾部斜杠,HTML 默认 `no-cache`。 | +| 其它 Web 路径 | 只读取真实静态文件或目录 index,缺失时返回真实 404;`/creation/not-exist`、`/runtime/not-exist`、`/puzzle/not-exist` 不进入 SPA fallback。 | 维护模式下,API-like 路由返回 JSON `503`,Web 静态路由优先返回 `maintenance.html`,不存在时返回纯文本 `503`。 代理失败时,API / SpacetimeDB 等代理路由返回统一 JSON 网关错误;本地静态路由仍保持对应 HTTP 错误状态。 diff --git a/index.html b/index.html index bc00d59ee..1b183cb68 100644 --- a/index.html +++ b/index.html @@ -7,7 +7,45 @@ content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" /> -
登录即送100泥点,可以免费制作50个素材
++ 陶泥儿 Genarrative 以美术 Agent + 与无限画布帮助个人创作者准备角色、场景与宣发素材;登录即送100泥点,可以免费制作50个素材。 +
尼嚎,新陶泥er!
diff --git a/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx b/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx
index 3547f9a88..f4a67f022 100644
--- a/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx
+++ b/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx
@@ -378,6 +378,9 @@ const {
};
});
+const HOME_SEO_HEADING =
+ '游戏美术AI工作台,让个人创作者更快做出角色、场景与宣发素材';
+
const {
mockGetPublicAuthUserByCode,
mockGetPublicAuthUserById,
@@ -4280,6 +4283,39 @@ test('desktop home shows empty states when public shelves are unavailable', () =
expect(screen.getByText('暂时还没有推荐作品。')).toBeTruthy();
});
+test('desktop home exposes one product h1 and section h2 headings', () => {
+ renderLoggedOutHomeView(vi.fn(), {}, 'home', true);
+
+ const productHeading = screen.getByRole('heading', {
+ level: 1,
+ name: HOME_SEO_HEADING,
+ });
+
+ expect(screen.getAllByRole('heading', { level: 1 })).toHaveLength(1);
+ expect(productHeading.closest('button')).toBeNull();
+ expect(
+ screen.getByRole('heading', { level: 2, name: '今日游戏' }),
+ ).toBeTruthy();
+ expect(
+ screen.getByRole('heading', { level: 2, name: '推荐' }),
+ ).toBeTruthy();
+ expect(
+ screen.getByRole('heading', { level: 2, name: '作品分类' }),
+ ).toBeTruthy();
+});
+
+test('mobile shell exposes one product h1 outside interactive content', () => {
+ renderLoggedOutHomeView(vi.fn(), {}, 'profile');
+
+ const productHeading = screen.getByRole('heading', {
+ level: 1,
+ name: HOME_SEO_HEADING,
+ });
+
+ expect(screen.getAllByRole('heading', { level: 1 })).toHaveLength(1);
+ expect(productHeading.closest('button')).toBeNull();
+});
+
test('logged in create tab shows real wallet balance beside the brand', () => {
mockNarrowMobileLayout();
diff --git a/src/components/rpg-entry/RpgEntryHomeView.tsx b/src/components/rpg-entry/RpgEntryHomeView.tsx
index 7670d1fcb..543ddc4d8 100644
--- a/src/components/rpg-entry/RpgEntryHomeView.tsx
+++ b/src/components/rpg-entry/RpgEntryHomeView.tsx
@@ -1020,13 +1020,27 @@ function SectionHeader({ title, detail }: { title: string; detail: string }) {
+ 在陶泥儿 Genarrative,用美术 Agent + 快速生成与迭代素材,再在无限画布中整理角色、场景和宣发灵感。 +
+