Files

128 lines
7.1 KiB
Markdown
Raw Permalink 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 — Backend
Backend of [Web Manager](./Readme.md). Source: `apps/backend/app/base_modules/WebManager/`.
Lifecycle mechanics: [Page Lifecycle](./Readme.md#page-lifecycle). Cross-app flows: [Integration](./integration.md).
## Data Model
### `web_pages`
```txt
id
status -- draft | published | unpublished
draft_of_id -- nullable FK to web_pages, ON DELETE CASCADE
locale -- en, uk, de
path -- '' for home, else 'about-us' or 'blog/post/about-me' (no leading slash)
group_key -- links localized versions of the same page
page_type -- landing | blog | blog-item
title
hero -- jsonb, nullable
seo_title -- defaults to ''
seo_description
published_at -- nullable
created_at / updated_at
```
Indexes: `(group_key, status)`, `(locale, path, status)`, a partial unique index on
`(locale, path) WHERE status = 'published'`, and a partial unique index on
`(draft_of_id) WHERE draft_of_id IS NOT NULL` — one draft clone per page, enforced by the database.
Rules:
- `path` is the full route after the locale, without leading or trailing slashes: `/en/blog/post/x`
is `locale = en`, `path = blog/post/x`. The editor form shows it with a leading slash.
- Public reads only ever match `status = published`.
- A root row is `draft_of_id = null`; a draft clone points at its published original.
- Route uniqueness is enforced twice: the partial index for live collisions, and
`assertNewRouteAvailable` for root rows of any status, so an unpublished page keeps its address.
### `web_components`
```txt
id
page_id -- FK to web_pages, ON DELETE CASCADE
type -- content | faq | media-content | articles | banner | media
data -- jsonb, block-specific shape validated by validator/web_content_validator.ts
sort_order -- position in the page, index of the submitted array
created_at / updated_at
```
Indexed on `(page_id, sort_order)`. Saving a draft deletes and reinserts the whole set rather than
diffing it.
### `web_settings`
```txt
id
locale -- unique
header -- jsonb { menus: [{ title, items }], ctaItems }
footer -- jsonb { copyright, menus }
legal -- jsonb { privacyPath, termsPath }
created_at / updated_at
```
One row per locale, upserted by locale. Shape lives in `WebStudio/config/siteSettings.ts`.
## HTTP API
Admin routes are under `/web-manager` behind `middleware.auth()`; every action authorizes through
`WebManagerPolicy`.
| Method | Route | Permission | Purpose |
| ------ | -------------------------------------- | ---------------------- | ------------------------------------------- |
| GET | `/web-manager/pages` | `web_manager.view` | Paginated root rows with a `draft` relation |
| GET | `/web-manager/pages/group-keys` | `web_manager.view` | Distinct group keys, alphabetized |
| POST | `/web-manager/pages` | `web_manager.create` | Create a draft (multipart) |
| GET | `/web-manager/pages/:id` | `web_manager.view` | Page + components |
| PUT | `/web-manager/pages/:id/draft` | `web_manager.update` | Save a draft (multipart) |
| POST | `/web-manager/pages/:id/publish` | `web_manager.publish` | Publish, returns `meta.revalidation` |
| POST | `/web-manager/pages/:id/unpublish` | `web_manager.publish` | Take down, returns `meta.revalidation` |
| POST | `/web-manager/pages/:id/preview-token` | `web_manager.view` | Signed preview token |
| DELETE | `/web-manager/pages/:id` | `web_manager.delete` | Delete page or discard draft |
| GET | `/web-manager/settings` | `web_manager.view` | Settings, all locales or one |
| PUT | `/web-manager/settings` | `web_manager.settings` | Upsert settings for a locale |
Public routes are under `/web-manager/public`, unauthenticated, and return published content only —
except `preview`, which is gated by the signed token.
| Route | Returns |
| ------------------------------------ | ----------------------------------------------------------------------------------- |
| `/pages/resolve?locale=&path=` | Page, components, settings, breadcrumbs, `languageAlternatives` |
| `/pages/preview?token=` | The same shape for any status, for a valid token |
| `/pages/sitemap?page=&perPage=` | `locale`, `path`, `groupKey`, `pageType`, `publishedAt`, `updatedAt` + `pagination` |
| `/blog/pages?locale=&page=&perPage=` | Published `blog-item` pages, newest first, with hero media |
| `/settings?locale=` | Header, footer, legal for one locale |
The sitemap is the one public read proportional to the site, so it is paginated
(`SITEMAP_PAGE_SIZE` 1000, capped at `SITEMAP_MAX_PAGE_SIZE` 5000) and the caller walks it;
`groupKey` is included so hreflang alternates need no second lookup. The resolve payload carries
`publishedAt` / `updatedAt`, which the web app emits as `datePublished` / `dateModified`.
Permissions are seeded from `WebManager/config/permissions.ts`: `web_manager.view`, `.create`,
`.update`, `.publish`, `.delete`, `.settings`.
## Public Resolve Query Budget
| Query | Bounded by |
| --------------------- | ----------------------------------------------------------- |
| the page row | route lookup |
| its components | blocks on the page |
| site settings | one row per locale |
| language alternatives | locales sharing the `group_key` |
| breadcrumbs | path depth (~4–6 rows), plus one query per group-key jump |
| internal page links | internal links in the page's own content, skipped when none |
Two details keep that true. **Link map by id**: `collectInternalPageIds` walks hero, components and
settings first, then `createPageLinkMap` fetches only those ids — unknown or unpublished ids are
absent and render as an empty href. **Breadcrumbs by candidate**: the walk can only climb to a
shorter prefix of the current path, the home page, or the group key in
`BREADCRUMB_PAGE_TYPE_PARENTS`, so `buildBreadcrumbs` asks for precisely those rows; a second query
happens only when a group-key jump lands on a branch whose ancestors were not already loaded
(`ancestorsAlreadyLoaded`). Both are served by the existing indexes.
## Tuning Constants
Backend tuning constants live in `WebManager/config/general.ts`: media upload path and 10 MB size
limit, preview token TTLs, sitemap page sizes, and `BREADCRUMB_PAGE_TYPE_PARENTS` (which makes
`blog` the breadcrumb parent of every `blog-item`).