Files
zero-fucks-given/docs/core/web-manager/backend.md
T

7.1 KiB
Raw Blame History

Web Manager — Backend

Backend of Web Manager. Source: apps/backend/app/base_modules/WebManager/. Lifecycle mechanics: Page Lifecycle. Cross-app flows: Integration.

Data Model

web_pages

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

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

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).