Files
admin/docs/core/web-manager/integration.md
T

186 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Web Manager — Integration
How the three apps of [Web Manager](./Readme.md) connect: contracts, flows, caching, env wiring, and cross-app extension checklists.
## How the Three Apps Connect
```txt
┌───────────────────────┐ ┌──────────────────────────┐
│ Admin (Web Studio) │───────►│ Backend (AdonisJS) │
│ editor, drafts, │◄───────│ pages, components, │
│ publish, preview │ DTOs │ settings, transactions │
└───────────┬───────────┘ └────────────┬─────────────┘
│ │
│ iframe /preview?token=… │ POST /api/revalidate (tag purge)
▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ Public site (Next.js) │
│ server-renders GET /web-manager/public/*, tagged 60s cache │
└─────────────────────────────────────────────────────────────┘
```
The three never share code, only contracts:
| Contract | Defined by | Consumed by |
| --------------------- | ------------------------------------------------------------ | ------------------------------------------------------- |
| Page / block payloads | `types/web_content.ts`, `validator/web_content_validator.ts` | Admin DTOs, `web-runtime/types.ts` |
| Public read API | `WebManager/routes.ts` (`/web-manager/public/*`) | `web-runtime/api/web-runtime-api.ts` |
| Cache tag names | `services/web_revalidation_service.ts` | `web-runtime/api/cache-tags.ts` — **must stay in sync** |
| Preview token | `services/preview_token_service.ts` | Admin preview sheet, `app/[locale]/preview/` |
| Block / page types | `types/web_content.ts` | `WebStudio/config/blocks.ts`, `web-runtime/*/registry` |
## Save Flow (admin → backend)
```txt
editor submits React Hook Form values
PagePayloadSerializer → FormData: `payload` JSON string + one binary part per new file
POST /web-manager/pages | PUT /web-manager/pages/:id/draft (multipart/form-data)
PagePayloadService parses `payload`, validates, stores files through FileManager,
rewrites { name, fileKey } media values into { name, ref }
WebManagerService writes page + components in one transaction
```
Media inside `hero` and block `data` is either `{ name, ref }` (already stored) or
`{ name, fileKey }` (new upload); only `ref` values are ever persisted. Part names are the
`fileKey`s (`file-0`, `file-1`, …). A request without a `payload` part is read as a plain JSON body,
so the endpoints stay usable from tests and scripts. The payload travels as one JSON string so
booleans, numbers, nulls and empty arrays survive the multipart transport.
## Publish Flow (backend → public site)
```txt
publish/unpublish/settings commit
WebRevalidationService POSTs { tags } to WEB_APP_URL/api/revalidate with X-Revalidate-Secret
Next.js revalidateTag(tag, 'max') per tag
response meta.revalidation = purged | retrying | disabled → admin toast
```
## Public Request Flow (visitor → web → backend)
```txt
GET /en/about-us
next-intl middleware resolves the locale segment
app/[locale]/[[...path]]/page.tsx → resolvePageRenderData(locale, path)
GET /web-manager/public/pages/resolve?locale=en&path=about-us (next: revalidate 60 + tags)
serializePublicPage returns page, components, settings, breadcrumbs, languageAlternatives
renderer.tsx → header + RegisteredPageLayout + footer; blocks via the block registry
```
Before it leaves the backend, `PublicContentService.prepare` resolves internal links to hrefs,
turns stored file refs into URLs, and sanitizes rich text (`content_sanitizer.ts`: allowlisted tags,
`http/https/mailto/tel` only, `rel` limited to `nofollow`/`noopener`/`noreferrer`). The web app
renders rich text as HTML and trusts that sanitization.
## Caching and Revalidation
Every public fetch is tagged and time-boxed, so a purge that never lands costs about a minute of
stale content rather than correctness.
| Fetch | `revalidate` | Tags |
| --------- | ------------ | --------------------------------------------------------------- |
| page | 60s | `web-manager`, `web-manager:page:{locale}:{path}`, settings tag |
| settings | 60s | `web-manager`, `web-manager:settings:{locale}` |
| blog list | 60s | `web-manager`, `web-manager:blog:{locale}` |
| sitemap | 3600s | `web-manager` only — it spans every locale and page |
Tag builders are duplicated in `web_revalidation_service.ts` (backend) and `cache-tags.ts` (web) and
must stay in sync; both sides have tests over the names, because a drift stops purges from landing
without anything failing.
Delivery is one attempt inline with the request (so the outcome can be reported to the editor) and
attempts 2–3 in the background with exponential backoff and jitter. Publish, unpublish and settings
save therefore return `meta.revalidation`:
| `status` | Meaning |
| ---------- | ----------------------------------------------------------------------- |
| `purged` | The web app confirmed the purge. |
| `retrying` | The first attempt failed; background retries are running. |
| `disabled` | `WEB_APP_URL` or `WEB_REVALIDATE_SECRET` is unset, so nothing was sent. |
The admin shows a warning toast on anything but `purged`, so an editor is never told the site is up
to date when it is not. Retries live in memory only — a restart drops them onto the same 60s floor a
failed purge already has. Exhausting all attempts logs at `error`; publishing again sends a fresh
purge. A deployment missing either variable logs each skipped purge at `warn` in production.
## Configuration
| App | Variable | Purpose |
| ------- | ----------------------- | ----------------------------------------------------------------------------- |
| backend | `APP_KEY` | Signs preview tokens. |
| backend | `WEB_APP_URL` | Where to POST cache purges. Server-side URL (compose service name in Docker). |
| backend | `WEB_REVALIDATE_SECRET` | Must equal the web app's `REVALIDATE_SECRET`. |
| web | `BACKEND_API_URL` | Backend base URL, without the `/web-manager/public/...` part. Required. |
| web | `REVALIDATE_SECRET` | Guards `/api/revalidate`. |
| web | `SITE_URL` | Canonical origin for metadata. Required in production. |
| admin | `VITE_WEB_URL` | Public site origin used to build preview and "view page" links. |
Backend tuning constants live in `WebManager/config/general.ts` — see
[Backend → Tuning Constants](./backend.md#tuning-constants).
## Extending
### Add a block type
1. Backend: add the value to `BLOCK_TYPES` and its data type in `WebManager/types/web_content.ts`,
and its shape to the component union in `validator/web_content_validator.ts`, so the payload is
validated server-side.
2. Admin: add the enum value to `entities/BlockType.ts`, the block shape under `entities/block/`,
an editor under `view/forms/blocks/<Name>Block/` with its `schema.ts`, and an entry in
`config/blocks.ts` (label, description, preview image, `createBlock` factory). Add it to the
`allowedBlockTypes` of every page type that should offer it in `config/pages.ts`.
3. Web: add the renderer under `web-runtime/blocks/components/` and register it in
`web-runtime/blocks/registry.tsx`, plus the type in `web-runtime/types.ts`.
4. Translations: add the label and description keys to both `config/translation/en.json` and
`uk.json` — the two files are kept at exact key parity.
`apps/web/tests/registry.test.ts` keeps a hand-mirrored copy of `BLOCK_TYPES` and `PAGE_TYPES` and
asserts the registries match it exactly — add the type there too, and it becomes the tripwire for a
block the backend can publish but the site cannot draw.
### Block preview images
Wireframe SVGs in `apps/admin/public/media/web-studio/blocks/<type>.svg`, referenced by
`previewImage` in `config/blocks.ts` and rendered by `BlockPickerSheet` as a 4:3 thumbnail. They are
schematic, not screenshots: `viewBox="0 0 400 300"`, no `width`/`height`, content inside
`x: 40–360` / `y: 32–268`, and no text — everything reads as bars and shapes at 112px wide.
| Token | Hex | Used for |
| -------------- | --------- | --------------------------------------------- |
| background | `#f7f4ec` | full-canvas rect, always the first element |
| surface | `#fffdf8` | cards, accordion rows, banner strip |
| surface border | `#d9dacf` | 1px stroke on surfaces and image placeholders |
| image fill | `#e7e9dc` | image placeholder rects |
| ink | `#1f291f` | titles, button inner bars, icons |
| text line | `#ccd1c0` | body copy bars |
| muted line | `#b9bfab` | subtitles, captions, image glyphs |
| accent | `#c7ff3f` | primary CTA pills, sun circle, active state |
| accent border | `#a8c930` | stroke marking an active or open element |
Reuse the shared vocabulary so previews read as one family: title bar (dark rect, `height 14–18`,
`rx = height/2`), body line (`height 9–10`, last line ~70% width), card (surface + border,
`rx 10–14`), CTA pill (accent, `height 24–28`, dark label bar inside), image placeholder (mountain
polygon plus accent sun). Draw the distinguishing structure only — three cards means "a list",
2–4 lines mean "a paragraph" — and keep at most one accent element per preview.
### Add a page type
Add the value to `PAGE_TYPES` in `WebManager/types/web_content.ts` and `entities/PageType.ts`, a
`WebStudioPageConfig` in `config/pages.ts` (allowed blocks, default path, label keys), a layout
component in `web-runtime/layouts/components/`, its entry in `PAGE_LAYOUT_REGISTRY`, and the
mirrored list in `registry.test.ts`. An unregistered type still renders through the standard
layout, so the site cannot break mid-rollout.
## Tests
| Suite | Command | Location | Covers |
| ------- | ---------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Backend | `npm run test:backend` | `apps/backend/tests/functional/web_manager/` | Preview token, revalidation, public content, sanitizer, breadcrumbs; page lifecycle (`web_manager_service.spec.ts`: draft/clone/publish/unpublish, route reservation, purge tags); HTTP surface (`pages_api.spec.ts`: 401/403 per permission, multipart, public endpoints). |
| Admin | `npm run test:admin` | `apps/admin/tests/unit/core/WebStudio/` | Menu, SEO helpers, list mapper, payload serializer, editor-value mapping and validation, `toCreatePayload`, block factory↔schema round-trips, field schemas, page paths, permission helper. |
| Web | `npm run test:web` | `apps/web/tests/` | Normalization, cache tags, block/layout registries, page metadata, sitemap, site URL, structured data, `safeHref`, published-only gate, `/api/revalidate`, API client, blog pager. |
Notes:
- DB-backed backend specs run against a dedicated `adonis_test` database (`.env.test`): the runner migrates up/down and each test runs in a global transaction (fixtures in `tests/utils/web_manager.ts`). `.env.test` leaves `WEB_REVALIDATE_SECRET` empty, so revalidation reports `disabled` while still exposing computed tags for assertions.
- `apps/web/vitest.config.ts` maps `#*` to the app root (Vitest ignores `tsconfig` `paths`) and stubs `server-only`, which only resolves inside a Next build.
- Not covered: rendering the block and layout components — needs jsdom and `@testing-library/react`, neither a dependency yet.