[CL-PASSPORT-SSG-404] prerender: /objects/<slug>/passport SSG entry (фиксит prod 404) #199

Merged
andrei merged 1 commit from fix/cl-passport-ssg-404 into master 2026-05-23 17:42:13 +00:00
Owner

Что сделано

Добавил SSG-генерацию для /objects/<slug>/passport в app/scripts/prerender.js. Цикл по objectsData:

  1. ROUTE_META[/objects/${obj.slug}/passport] — title {name} — Asset Passport, description Verified asset passport for {name}: financials, ESG grade, occupancy, sources, JSON-LD унаследован из hotelSchema с уникальным @id (/passport#hotel) и mainEntityOfPage указывающим на /passport WebPage.
  2. SSG_ROUTES.push(/objects/${obj.slug}/passport) — теперь prerender генерирует /objects/<slug>/passport/index.html на диске.
  3. console.log обновлён: считает passport routes наряду с object detail + dashboard.

Также добавил инвариант в app/scripts/__tests__/prerender.test.js: source must push passport URL + assign ROUTE_META + jsonLd с @id на /passport.

Зачем

Live evidence prod 404 (CEO репорт скриншот):

GET https://europa-tech.org/objects/hotel-baistrocchi-wellness-longevity-center/passport
→ HTTP 404 Not Found (Server: nginx, ETag 6a100991-eba, static 404.html)

GET https://europa-tech.org/objects/obj-001/passport
→ HTTP 404 (identical ETag — same static 404.html, не SPA fallback)

GET https://api.europa-tech.org/api/objects/hotel-baistrocchi-wellness-longevity-center/passport
→ HTTP 200 (API сам по себе работает по slug)

Root cause: prerender.js loop по objectsData пушит SSG_ROUTES для /objects/<slug> и /objects/<slug>/dashboard, но не для /objects/<slug>/passport. nginx try_files $uri $uri/ =404 ищет /objects/<slug>/passport/index.html на диске — нет файла → 404. SPA-route в React Router определён (AssetPassportPage на /objects/:id/passport) и API работает — ломается только prerender pipeline.

Линки на passport идут из:

  • PassportEditor.tsx (admin UI) — href={/objects/${objectId}/passport}
  • QuickInvestBar.tsx (cabinet object view) — window.open(/objects/${objectId}/passport)
  • AssetPassportButton.tsx — public button

Все эти линки ведут на 404 для любого investor/admin, не только этого hotel-baistrocchi.

План тестирования

  • npx vitest run scripts/__tests__/prerender.test.js — 1/1 PASS (top-level asserts ran at module-load; passport push + ROUTE_META + @id pattern checked)
  • npx vitest run scripts/__tests__ — 3/3 файла, 5/5 тестов PASS (sitemap, prerender, indexable-urls)
  • npx eslint scripts/prerender.js scripts/__tests__/prerender.test.js --max-warnings 0 — clean (exit=0)
  • После merge + deploy через bash scripts/deploy.sh: curl https://europa-tech.org/objects/<slug>/passport должен вернуть 200 (prerendered HTML с <title>{name} — Asset Passport</title>).

Где могу ошибаться

  • smartTrim(title, 60) — title {name} — Asset Passport для длинных name может урезаться. Hotel Baistrocchi даёт ~58 chars — fits. Для очень длинных name fallback: уже короткий suffix.
  • JSON-LD @id использует #hotel fragment (parallel dashboard pattern из PR#85). Google интерпретирует как URI ref, unique within document. Прецедент уже принят CEO.
  • prerender использует cached objectsData (fetched раньше в скрипте). Если новый object добавляется после ребилда — passport-route появится при следующем deploy. Это уже существующее ограничение для /objects/<slug> и /objects/<slug>/dashboard — не вношу регрессии.
  • Sitemap-objects.xml уже декларирует passport URLs (per FIX18+ assertions). Без этого SSG прод возвращал 404 на sitemap-listed URLs — ускорит Google indexation как побочный эффект.
## Что сделано Добавил SSG-генерацию для `/objects/<slug>/passport` в `app/scripts/prerender.js`. Цикл по `objectsData`: 1. `ROUTE_META[/objects/${obj.slug}/passport]` — title `{name} — Asset Passport`, description `Verified asset passport for {name}: financials, ESG grade, occupancy, sources`, JSON-LD унаследован из hotelSchema с уникальным `@id` (`/passport#hotel`) и `mainEntityOfPage` указывающим на `/passport` WebPage. 2. `SSG_ROUTES.push(/objects/${obj.slug}/passport)` — теперь prerender генерирует `/objects/<slug>/passport/index.html` на диске. 3. `console.log` обновлён: считает passport routes наряду с object detail + dashboard. Также добавил инвариант в `app/scripts/__tests__/prerender.test.js`: source must push passport URL + assign ROUTE_META + jsonLd с `@id` на `/passport`. ## Зачем **Live evidence prod 404 (CEO репорт скриншот):** ``` GET https://europa-tech.org/objects/hotel-baistrocchi-wellness-longevity-center/passport → HTTP 404 Not Found (Server: nginx, ETag 6a100991-eba, static 404.html) GET https://europa-tech.org/objects/obj-001/passport → HTTP 404 (identical ETag — same static 404.html, не SPA fallback) GET https://api.europa-tech.org/api/objects/hotel-baistrocchi-wellness-longevity-center/passport → HTTP 200 (API сам по себе работает по slug) ``` Root cause: `prerender.js` loop по `objectsData` пушит `SSG_ROUTES` для `/objects/<slug>` и `/objects/<slug>/dashboard`, **но не для `/objects/<slug>/passport`**. nginx `try_files $uri $uri/ =404` ищет `/objects/<slug>/passport/index.html` на диске — нет файла → 404. SPA-route в React Router определён (`AssetPassportPage` на `/objects/:id/passport`) и API работает — ломается только prerender pipeline. Линки на passport идут из: - `PassportEditor.tsx` (admin UI) — `href={/objects/${objectId}/passport}` - `QuickInvestBar.tsx` (cabinet object view) — `window.open(/objects/${objectId}/passport)` - `AssetPassportButton.tsx` — public button Все эти линки ведут на 404 для **любого** investor/admin, не только этого hotel-baistrocchi. ## План тестирования - `npx vitest run scripts/__tests__/prerender.test.js` — 1/1 PASS (top-level asserts ran at module-load; passport push + ROUTE_META + @id pattern checked) - `npx vitest run scripts/__tests__` — 3/3 файла, 5/5 тестов PASS (sitemap, prerender, indexable-urls) - `npx eslint scripts/prerender.js scripts/__tests__/prerender.test.js --max-warnings 0` — clean (exit=0) - После merge + deploy через `bash scripts/deploy.sh`: `curl https://europa-tech.org/objects/<slug>/passport` должен вернуть 200 (prerendered HTML с `<title>{name} — Asset Passport</title>`). ## Где могу ошибаться - `smartTrim(title, 60)` — title `{name} — Asset Passport` для длинных name может урезаться. Hotel Baistrocchi даёт ~58 chars — fits. Для очень длинных name fallback: уже короткий suffix. - JSON-LD `@id` использует `#hotel` fragment (parallel dashboard pattern из PR#85). Google интерпретирует как URI ref, unique within document. Прецедент уже принят CEO. - prerender использует cached `objectsData` (fetched раньше в скрипте). Если новый object добавляется после ребилда — passport-route появится при следующем deploy. Это уже существующее ограничение для `/objects/<slug>` и `/objects/<slug>/dashboard` — не вношу регрессии. - Sitemap-objects.xml уже декларирует passport URLs (per FIX18+ assertions). Без этого SSG прод возвращал 404 на sitemap-listed URLs — ускорит Google indexation как побочный эффект.
[CL-PASSPORT-SSG-404] prerender: add /objects/<slug>/passport SSG entry
All checks were successful
CI / API (pull_request) Successful in 6m25s
CI / App (pull_request) Successful in 6m34s
CI / Contracts (pull_request) Successful in 1m38s
CI / Telegram Mini App (pull_request) Successful in 1m7s
CI / Python SDK (pull_request) Successful in 25s
CI / Secrets Scan (pull_request) Successful in 11s
CI / Prisma Migrate Gate (pull_request) Successful in 1m14s
React Doctor / React Doctor / App (pull_request) Successful in 1m39s
0949215b29
Root cause: prerender.js loop generated SSG_ROUTES for /objects/<slug> и
/objects/<slug>/dashboard but not /objects/<slug>/passport. nginx try_files
$uri $uri/ =404 -> static 404.html for any passport URL (identical ETag для
slug и obj-id confirmed на prod).

Live evidence pre-fix:
  curl https://europa-tech.org/objects/hotel-baistrocchi-wellness-longevity-center/passport
  -> HTTP 404 (Server: nginx, ETag 6a100991-eba)
  curl https://europa-tech.org/objects/obj-001/passport
  -> HTTP 404 (identical ETag — same static 404.html)
  curl https://api.europa-tech.org/api/objects/hotel-baistrocchi-wellness-longevity-center/passport
  -> HTTP 200 (API works for slug)

Fix:
- ROUTE_META[`/objects/${obj.slug}/passport`] with title/description/jsonLd
  (@id `/passport#hotel` differentiates entity per schema.org URI ref).
- SSG_ROUTES.push(`/objects/${obj.slug}/passport`) -> prerender генерирует
  static index.html на диске.
- console.log updated to count passport routes alongside dashboard.

Test:
- prerender.test.js: passport SSG_ROUTES.push pattern present.
- prerender.test.js: ROUTE_META passport entry assigned.
- prerender.test.js: passport jsonLd has @id pointing to /passport.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
andrei merged commit 59ca0104fe into master 2026-05-23 17:42:13 +00:00
andrei deleted branch fix/cl-passport-ssg-404 2026-05-23 17:42:14 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
europa-tech-srl/europatech!199
No description provided.