Add initial project files
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user