Files

13 KiB
Raw Permalink Blame History

Web Manager — Integration

How the three apps of Web Manager connect: contracts, flows, caching, env wiring, and cross-app extension checklists.

How the Three Apps Connect

┌───────────────────────┐        ┌──────────────────────────┐
│  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)

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 fileKeys (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)

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)

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.

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.