TASKS-1823 WebManager module

This commit is contained in:
Igor Martyniuk
2026-08-28 08:35:58 +00:00
parent fdb2cf3837
commit c93d8c025b
654 changed files with 26232 additions and 6075 deletions
+118
View File
@@ -0,0 +1,118 @@
# Next.js Web Architecture
Architecture of the public website (`apps/web/`): a Next.js App Router app that server-renders the pages editors publish in [Web Manager](../../core/web-manager/Readme.md). App-specific setup (env vars, dev commands, tests) lives in [apps/web/Readme.md](../../../apps/web/Readme.md).
## How a Request Works
```txt
GET /en/about-us
proxy.ts (next-intl middleware) validates the locale segment
app/[locale]/[[...path]]/page.tsx
resolvePageRenderData('en', 'about-us') web-runtime/api/render-data.ts
GET {BACKEND_API_URL}/web-manager/public/pages/resolve?locale=en&path=about-us
normalizePageResolution(payload) web-runtime/data/normalize.ts
PageRenderer web-runtime/renderer.tsx
SiteHeader → RegisteredPageLayout → SiteFooter
├─ PageHero
├─ Breadcrumbs
└─ PageBlocks → block registry → ContentBlock, FaqBlock, …
```
## Layout
```txt
apps/web/
├── app/ Next.js routes — thin, they only resolve data and delegate
│ ├── [locale]/
│ │ ├── layout.tsx <html>, NextIntlClientProvider, metadataBase, globals.css
│ │ ├── [[...path]]/page.tsx catch-all: every CMS page in this locale
│ │ ├── preview/page.tsx token-gated draft rendering (force-dynamic, noindex)
│ │ └── not-found.tsx 404 screen
│ ├── api/revalidate/route.ts secret-guarded cache purge, called by the backend
│ ├── sitemap.ts, robots.ts generated from the public API
│ └── globals.css one line: @import 'tailwindcss'
├── web-runtime/ the rendering engine — everything that turns CMS data into a page
│ ├── api/ HTTP client, render-data resolution, cache tags
│ ├── data/normalize.ts untrusted payload → typed contract
│ ├── types.ts that contract
│ ├── blocks/ 6 block renderers + registry + unsupported fallback
│ ├── layouts/ 3 page layouts + registry, hero, breadcrumbs
│ ├── chrome/ site header, nav, footer, language switch
│ ├── components/ link, image and rich-text primitives
│ ├── lib/ metadata, site URL, sitemap, structured data
│ └── renderer.tsx header + layout + footer
├── i18n/ next-intl wiring, navigation helpers, translation JSON
├── shared/ui/ design-system primitives (Button)
├── site.config.ts locales and default locale
├── proxy.ts the next-intl middleware
└── tests/ Vitest specs for the pure logic
```
### Layer responsibilities
| Layer | Owns | Never does |
| ------------------- | --------------------------------------------------------------- | ----------------------------- |
| `app/` | Route params, data resolution, `notFound()`, `generateMetadata` | Markup beyond composition |
| `web-runtime/api/` | Every backend call, cache tags, `server-only` | Rendering |
| `web-runtime/data/` | Turning untrusted JSON into `types.ts` shapes | Fetching or throwing |
| `web-runtime/` | All page markup, registries, primitives | Direct `fetch` calls |
| `i18n/` | Locale resolution, `Link`/`getPathname`, message loading | Content |
| `shared/ui/` | Reusable, content-agnostic primitives | Knowing about pages or blocks |
## Backend Boundary
`web-runtime/api/web-runtime-api.ts` is the single door to the backend — a `server-only` module, a lazily constructed singleton, and the only place `fetch` appears.
| Call | Endpoint | Cache |
| ------------------ | ----------------------------------- | ------------------------- |
| `resolvePage` | `/web-manager/public/pages/resolve` | `revalidate: 60` + tags |
| `listBlogPosts` | `/web-manager/public/blog/pages` | `revalidate: 60` + tags |
| `listSitemapPages` | `/web-manager/public/pages/sitemap` | `revalidate: 3600` + tags |
| `getSettings` | `/web-manager/public/settings` | `revalidate: 60` + tags |
| `getPreview` | `/web-manager/public/pages/preview` | `cache: 'no-store'` |
A `404` becomes `null` (the caller renders the 404 page); any other non-OK status throws. The sitemap walk is capped at 50 requests.
Every response goes through `data/normalize.ts` first. Its one rule is **degrade, never throw**: a missing field becomes a default, an unknown block type becomes `UnknownBlock`, a malformed media object disappears. `types.ts` is what the rest of the app may assume after that.
Publishing purges this app's cache: the backend POSTs tag names to `app/api/revalidate/route.ts`, guarded by `REVALIDATE_SECRET`. The tags in `web-runtime/api/cache-tags.ts` mirror the backend's — see [Web Manager caching](../../core/web-manager/integration.md#caching-and-revalidation).
## Rendering
Two registries decide what gets drawn, and both fall back rather than fail:
```txt
layouts/registry.tsx landing → StandardPageLayout unknown → StandardPageLayout
blog → BlogPageLayout
blog-item → EditorialPageLayout
blocks/registry.tsx content, faq, media-content, articles, banner, media
unknown → UnsupportedBlock (dev-only banner, nothing in production)
```
The CMS can publish a page type or block the site has not learned yet — the worst case is a missing section, never a broken deploy.
`StandardPageLayout` is hero + blocks. `EditorialPageLayout` adds breadcrumbs, `Article` JSON-LD, and a narrower reading measure. `BlogPageLayout` ignores the page's blocks and fetches its own post list (12 per page, `?page=` pager). Each sets `data-page-layout` on its `<main>` — the hook for tests and debugging. `Breadcrumbs` renders nothing for a single-item trail and emits `BreadcrumbList` JSON-LD.
Three shared primitives:
- **`components/links.tsx`** — `PageLink` validates the href (`/`, `#`, or `http/https/mailto/tel`) and renders an unusable link as disabled text. Link variants map onto `shared/ui/Button` variants.
- **`components/media-image.tsx`** — a native `<img>`, not `next/image`: media origins are CMS-managed and unrestricted, so the optimizer's allowlist would be a permanent tax.
- **`components/rich-text.tsx`** — `dangerouslySetInnerHTML`. Safe **only** because the backend sanitizes rich text in `PublicContentService`; never render HTML from any other source through it.
## Internationalization
Locales live in `site.config.ts` (`en`, `uk`, `de`). `i18n/routing.ts` sets `localePrefix` to `always` with multiple locales and `never` with one, so a single-language site serves clean URLs.
- Navigate with `Link`, `redirect`, `usePathname`, `useRouter`, `getPathname` from `i18n/navigation.ts` — never `next/link` or `next/navigation` directly, or the locale prefix is lost.
- CMS hrefs arrive locale-prefixed; pass them through `toPublicHref` (`i18n/content-href.ts`).
- Messages are five namespaces (`messages`, `pages`, `components`, `errors`, `form`) under `i18n/translations/`, merged at request time; a missing locale file falls back per namespace.
- `i18n/types.ts` derives the `Messages` type from the English files — a missing key is a type error.
## SEO
- `app/[locale]/layout.tsx` sets `metadataBase` from `siteUrl()` plus the default title template.
- `web-runtime/lib/page-metadata.ts` builds per-page metadata — title, description, canonical, hreflang with `x-default`, and Open Graph / Twitter cards from the hero image. A pure function of the render data, which is why it has direct tests.
- `app/sitemap.ts` walks the paginated backend listing and `buildSitemap` groups translations into `alternates.languages`.
- `app/robots.ts` allows everything except `/*/preview`.
- `web-runtime/lib/structured-data.tsx` emits `BreadcrumbList` and, on editorial pages, `Article`.