Pages Catalog
Puck-based page editor for global public and organization member channels.
Overview
Yayaw provides page management at:
/dashboard/content/pages
The dashboard flow is split into:
the pages list, a dashboard screen (
dashboard.content.pages, see Pages list)a dedicated page editor route (
/dashboard/content/pages/[scope]/[slug])
Related docs:
Dashboard for shell/navigation behavior
Sections Catalog for how page documents reference reusable sections
CMS Data Models for structured data bindings
Media Library for asset storage and image generation
CMS Overview for the full content stack
Pages are composed with these node families:
stack: layout containerscomponent: direct component references for advanced compositionsection: reusable catalog section references resolved to the latest publicationgenerated_section: recipe-backed generated sections, including seeded layout sections such as the public home header/footer; AI-generated content sections are promoted to reusablesectionreferences on save
Pages list
/dashboard/content/pages (route permission page:list) is a dashboard
screen: a full-page table on the system:pages source (see
Dashboard). An organization that customizes its screens can
change the table's default view, but the table always stays.
Who sees which pages: global pages with the global
pageread, list or manage permission, the active organization's pages withpageread, list or manage there, never another organization's. Whether a person may change a page ispage:managein its scope (global, or the organization); every action checks it again on the server.Columns: name, path, scope, channel, status (a live page with unpublished changes reads "Changes · published version live") and the last update; the slug, the publication status, the creation day and the editor link can be shown. Clicking a row opens the page in its editor.
Views: the screen's own view (all pages, the last updated first, 20 a page), then the work queues Drafts to review, Changes to publish, Published pages and Archived pages, then saved views. Any of them can be a person's favorite, and saved views can be shared with the organization.
Modes: table, list, a kanban by status (cards cannot be dragged) and charts.
Search and order: the search reads the name, slug and path; names sort as people read them ("Page 2" before "Page 10"); a page holds at most 100 rows.
Actions: Create page (for whoever may manage pages globally or in the organization) asks for a name, a scope, the organization's visibility (members only, or one of its public domains) and a path, then opens the editor. For a selection: Publish selected reviews the exact revisions before publishing them, Archive selected archives after a confirmation, the bulk edit sets the search engine settings (
noindex,nofollow) as new drafts, and deletion removes the pages the person may delete and names the others. A row can be deleted where the person may change it.
CMS Role
Pages are the final composition layer of the CMS. They should arrange layout, SEO metadata, section references, component references, data bindings, and media bindings. They should not become the long-term source of truth for repeated content such as navigation menus, shared offers, reusable proof blocks, or organization assets.
Use sections for reusable page units, CMS data for shared structured values, media for binary assets, and design tokens for public visual theming.
Document Contract
Pages use a native Puck document contract:
PuckPageDocumentV1location:
src/lib/shared/pages/puck-document.ts
Revision persistence remains in existing tables, and
ui_page_revisions.definition stores the versioned Puck document as the source
of truth.
The runtime conversion emits PageSectionNodeV1 for reusable sections.
Ordinary pages store a stable section id/slug and follow the latest published
section revision. Strict contextual design drafts additionally carry a
server-issued sectionRevisionId; that pin is included in the immutable page
proof and keeps review and publication on the exact section revision.
The document root also stores localized page settings:
titleByLocaleseo.seoTitleByLocaleseo.metaDescriptionByLocaleseo.socialTitleByLocaleseo.socialDescriptionByLocaleseo.socialImageUrlseo.canonicalUrlseo.noIndex/seo.noFollow
Registry metadata remains useful for catalog summaries, but the published runtime uses the document root for public SEO metadata.
PageStack owns three explicit page-layout decisions:
containerselects the tokenized contained width or a full-width bodygapsets the root section rhythmsectionFrameis"token"for the shared public section wrapper and"none"when art-directed sections own their outer spacing and layering
Contextual MCP plans express the same contract as page.layout.bodyWidth,
page.layout.sectionGap, and page.layout.sectionFrame. The plan compiler maps
these values to the root stack instead of forcing every designed page into the
same contained, uniformly framed silhouette.
Contextual drafts also store designProof in the immutable revision document.
It binds the design context, plan hash, selected basis, and pre-proof document
hash. Editing the document outside the contextual plan invalidates or removes
that proof, so registry metadata alone can never rebind an old revision to a
different prototype. Generic editor, import, low-level revision, and ordinary
control-plane save boundaries strip caller-supplied designProof; only strict
contextual persistence can issue it after canonical normalization.
Global page revisions can also carry an optional root.props.designTokens
payload. Contextual plans expose the same layer as page.designTokens. On
create, undefined or null inherits the effective public theme. On rework,
omitting the field preserves the target revision's layer, null explicitly
clears it, and a non-null payload replaces it. A non-null payload is versioned,
supports separate light/dark maps, and accepts
only semantic color roles plus approved font-sans and font-serif stacks.
Layout tokens, shadows, mono fonts, arbitrary CSS, URLs, and unregistered font
families are rejected. The normalized layer participates in the plan hash,
revision hash, contextual proof, draft runtime hash, and publication review.
Changing it therefore creates a new immutable revision and requires fresh
visual evidence.
Publication accepts the layer only with the strict contextual design proof and
review receipt for that exact revision and runtime. A generic editor or
low-level revision write may preserve the field, but cannot publish it as an
unreviewed page theme.
After the first contextual persistence, the design context records the exact page, route, latest page revision, and stable plan-section to catalog-section mapping. Later corrections can only revise that page and those section identities. They cannot fan out to a second route, silently overwrite a manual revision, or recreate every section under suffixed slugs.
Strict persistence commits the page revision, all contextual section revisions,
the session draft target, and contextual media plan hashes in one serializable
transaction. Page ID, path, slug, and each plan-section-to-section ID/slug
mapping are immutable across corrections; optimistic head checks catch external
or concurrent edits. Any failure rolls back the full correction, so the session
cannot point at a partially written page. Because immutable revision hashes are
deduplicated, an A → B → A correction that matches an older non-head revision
returns an explicit revision-reuse conflict instead of silently rebinding that
old revision as the new head.
Channels and Scope
Pages support two scopes/channels:
global(global_public)organization(org_membersfor member-only pages)organization(org_publicfor public pages served from verified custom domains)
Runtime routes:
global public home page:
/[locale]global public pages:
/[locale]/[...slug]organization member pages:
/[locale]/o/[orgSlug]/[[...slug]]organization public pages: verified custom domain +
/[locale]/[[...slug]]
Organization member page runtime requires membership.
Organization public pages do not require membership, but only render when the
request host matches a verified organization_public_domains row. Unknown
custom hosts return 404 instead of falling back to the global Yayaw site.
Public organization pages can optionally target one organization public domain;
when a target is set, that page only resolves on that host. Untargeted public
organization pages remain available on every verified public domain for the
organization. Page paths are explicit route settings, so an organization public
page can use / for the custom-domain homepage instead of inheriting a path
from the page name.
Section visibility:
global pages: built-in + global sections
organization pages: built-in + global + same-organization sections
Dashboard Editing UX
Editor UX is direct Puck + shadcn:
real-time local editing in Puck
autosave with debounce
serialized draft saves so stale autosave completions cannot overwrite newer state
a locale switch based on
src/config/i18n.config.tsroute settings for updating the page path, organization visibility channel, and public-domain target after creation
page-level SEO fields in the inspector
image/media binding controls that select existing organization media assets or generate new WebP assets with OpenAI
global data binding controls that bind a complete published field from
/dashboard/content/dataexplicit publish/archive lifecycle
read-only mode for users without manage permission
The insert palette groups page elements as:
Built-in/Intégrés:Header,FooterSections: published global sections plus relevant organization sectionsComponents: direct component referencesLayout/Mise en page:PageStack
The PageStack inspector exposes contained/full width, section gap, and shared
section-frame behavior. Existing documents normalize to
sectionFrame: "token"; disabling it is an explicit art-direction decision.
Built-in section instances expose no editable props in the page inspector. Section content is edited from the source section catalog, not duplicated per page.
Generated section recipes support header and footer variants in addition to
content sections. Current content variants are hero, immersive_hero,
legal, data_insights, value_grid, feature_split, system_map,
mcp_console, proof, signal_wall, and cta.
Header and footer recipes use itemsJson for menu links, submenu groups, theme
toggles, language toggles, and user menus. Seeded Header/Footer page sections
bind those props to header-menu/main and footer-menu/main CMS data, with the
same JSON contract as fallback.
Toolbar state exposes autosave lifecycle:
savingsavederrorconflict
Autosave and Concurrency
Draft saves use optimistic locking:
saveDraftPageActionrequiresbaseRevisionIdstale base revision returns
conflictsuccessful saves return the server-canonical Puck document so the editor can adopt normalized section IDs and default bindings without dirty-state loops
no server-side operation rebase and no automatic merge
Conflict handling strategy:
user reloads latest draft explicitly from the conflict banner
user reapplies desired edits in current revision context
Runtime and Validation
Published runtime resolves from the stored Puck document directly.
Server runtime path:
loads published revision document from
ui_page_revisions.definitionnormalizes Puck document
converts to validated runtime definition
resolves published section references through
ui_section_publications, using the current section ID first and then the scoped slug/slug fallback for older page revisions with stale IDsrenders resolved runtime preview tree
removes
PublicPageSectionFramearound body sections only when their owning stack declaressectionFrame: "none"applies a revision-owned global-page token layer through the same scoped wrapper for immutable draft previews and published rendering
resolves localized metadata for
generateMetadata
The dashboard Puck canvas round-trips revision-owned page tokens but does not apply them in version 1. Use the immutable preview, not the authoring canvas, for visual proof and review of a page-specific palette or font direction.
Dynamic public routes expose SEO metadata through:
/[locale]/page.tsx/[locale]/[...slug]/page.tsx/[locale]/o/[orgSlug]/[[...slug]]/page.tsx
Global public pages also feed Google Search discovery through:
/robots.txt, which allows public content while excluding API, analytics ingest, dashboard, auth, maintenance, and organization-member paths/sitemap.xml, which lists every published global page for every configured locale, includeshreflangalternates, and skips pages markednoIndexsite-level JSON-LD for the Yayaw organization and website
Organization-member pages are protected by membership checks and emit noindex
metadata even when a page has its own SEO fields. For generative AI search, keep
page content people-first and avoid relying on special machine-only files as a
ranking lever; llms.txt remains documentation-oriented, not a Google Search
optimization path.
Organization-public pages use the request host for canonical URLs, hreflang
alternates, robots, sitemap entries, Open Graph URLs, and JSON-LD. Custom hosts
are public-only: dashboard, auth, API, docs, ingest, and /o/* surfaces are
blocked at proxy level. Verified custom hosts also resolve localized page
content from the URL locale prefix and apply the owning organization's public
theme and design-token overrides.
Organization Public Domains
Organization custom domains are stored in organization_public_domains and
managed from organization settings or MCP. A domain belongs to exactly one
organization, can be marked primary once verified, and stores provider status,
TXT verification challenges, recommended CNAME/A records, and last check
timestamp. The legacy vercel_* columns are still used for compatibility, but
the runtime treats the row as the generic public-domain verification snapshot.
The page registry stores an optional public_domain_id for org_public pages.
The dashboard creation and editor route-settings dialogs show the active
organization's non-archived public domains, defaulting to the primary verified
domain when available. Runtime host resolution filters domain-targeted pages by
the matched custom-domain row while keeping legacy untargeted pages visible on
all verified organization public domains.
PUBLIC_DOMAIN_PROVIDER=vercel uses the Vercel project-domain API to add and
verify domains. PUBLIC_DOMAIN_PROVIDER=manual-dns generates a TXT ownership
challenge and shows CNAME/A hints from PUBLIC_DOMAIN_CNAME_TARGET and
PUBLIC_DOMAIN_IPV4_TARGETS; operators configure DNS and ingress outside the
app, then re-check the domain. Publishing page content stays DB-driven and does
not redeploy the hosting provider.
Publish is blocked when diagnostics contain error severity.
Seeded Home Page
bun run seed creates a global public Home page at path / when no published
home page exists yet. The seeded document is a normal PuckPageDocumentV1 with:
generated
headerandfootersections backed by the CMS singleton entriesheader-menu/mainandfooter-menu/mainlocalized English/French page copy and SEO metadata
a content stack composed of generated hero, value grid, feature split, and CTA sections
The seed is intentionally non-destructive. Once a home page is already published, later seed runs keep the current published revision so Page Builder edits are not overwritten.
The docs route (/[locale]/docs) uses the same runtime header and footer
wrappers as the public layout. These wrappers read the editable CMS slots for
the clickable brand, center navigation, and right-side public mini menus while
leaving the Fumadocs content layout in charge of the docs sidebar and table of
contents.
Seeded Marketing Pages
bun run seed also creates Yayaw launch pages through the same page catalog
storage. Their page shells use the generated Header/Footer variants bound to
global menu data, while the page body remains generated-section content:
/is the published public home page for the source-owned SaaS codebase./codebaseis the published sales page for ownership and architecture./pricingis the published offer page for lifetime codebase access.
The same seed publishes the global data used by those pages, including
sales-offer/main and billing-product-content/pro-lifetime, so offer copy can
stay editable while product names, prices, Stripe IDs, and checkout state remain
resolved from the billing catalog.
The seed creates missing pages and can publish a newer revision when a page is
still owned by the same seed key and its seedVersion changes. Existing Page
Builder pages that are not seed-owned keep their current publication state and
latest revisions.
AI Page Builder
The page AI flow is reusable-section-first:
it generates recipe-backed sections
it can generate header/footer recipes when a page needs layout navigation
it creates and publishes those sections in the catalog
it inserts
PageSectionreferences into the page documentit does not persist new
PageGeneratedSectionnodes in saved documentsit receives the configured locales from
src/config/i18n.config.tsit fills localized page titles and SEO metadata for every configured locale
generated content bindings use recipe editor hints to preserve typed structures (for example badge arrays and card arrays) and store localized values for each configured locale
generated header/footer menus use
itemsJsonwithlink,group,themeToggle,languageToggle, anduserMenuitemsit injects compact shadcn/ui composition guidance so landing pages use registry-quality section patterns, semantic tokens, CTA groups, proof bands, and real component usage hints instead of drifting into isolated controls
it runs a dedicated localization pass after layout/content generation so missing
valuesByLocaleentries are translated for every locale in the page documentwhen a prompt asks for generated media, or strongly implies a rich landing-page visual, Page AI can generate at most one configured OpenAI image asset (default
gpt-image-1.5), persist it through the active organization media library, and bind it toheroImageor a compatible image media slotglobal pages can use generated assets from the active organization's private media library because the stored file URL is public; if no active organization or media permission is available, image generation is skipped with a warning and the page draft still succeeds
If section creation or publication fails, the page save/generation fails with an explicit error instead of silently storing an inline fallback.
Page AI generation is durable:
Page AI defaults to the fast
gpt-5.4-minimodel with a short fallback chain to reduce deterministic fallback drafts when a fast model returns invalid JSON.POST /api/ai/pages/runspersists the request in Postgres and wakes a workerGET /api/ai/pages/runs/:runIdreloads the latest run snapshotGET /api/ai/pages/runs/:runId/events?afterSeq=nreplays ordered progress eventsPOST /api/ai/pages/runs/:runId/cancelrequests best-effort cancellationui_page_ai_runsstores ownership, payload, status, partial result, final result, errors, attempts, and lock metadataui_page_ai_run_eventsstores ordered progress events for resume after reloadVercel Queues is one wake-up transport; Postgres remains the source of truth.
In production, the default wake-up driver is
vercel-queueonly on Vercel runtimes anddb-workerelsewhere.PAGE_AI_QUEUE_DRIVER=db-workerlets a long-lived worker process the same runs withbun run worker:page-aiwithout changing the editor/API contract.
Data Model
Page persistence tables:
ui_page_registry_itemsui_page_revisionsui_page_publicationsui_page_ai_runsui_page_ai_run_events
Reusable sections are stored separately in:
ui_section_registry_itemsui_section_revisionsui_section_publications
Page constraints remain unchanged:
unique
(scope, organization_id, slug)unique
(scope, organization_id, path)immutable revisions with hash dedupe
one publication row per registry item
Server APIs
Page actions live in:
src/lib/server/actions/pages/pages-catalog-actions.ts
Current actions:
getPagesCatalogWorkspaceAction(what the pages list's person may create and change, and the organization's public domains for the create dialog)openPageForEditingActioncreatePageActionupdatePageDefinitionJsonActionsaveDraftPageActionpublishPageActionarchivePageAction
The pages list's rows come from the system:pages screen source, paged,
sorted and searched in SQL. Its table
(src/blocks/dashboard/screens/sources/pages-extension.tsx) uses Yayaw Table
custom bulk actions for the selected-row lifecycle shortcuts:
publish selected pages through
publishPageAction, after a review of the exact revisionsarchive selected pages through
archivePageActiondelete one page through
deletePageByIdAction, a selection throughdeletePagesBulkActionsave the search engine settings of a selection through
saveDraftPageAction
Section availability for the page builder comes from:
listAvailableSectionsForPageActionlistAvailableSectionsForPage
Image generation for page media fields is handled by the shared media image
asset service used by both the media picker and Page AI. Generated images use
the configured OpenAI image model, default to gpt-image-1.5, and follow the
same organization permissions, quotas, object storage upload, and media_assets
persistence path as manual uploads. The media picker displays stored thumbnails
when available, while bindings keep the original publicUrl so rendered pages
still use the source asset.
Published global data fields are exposed to the page editor as binding
references. Runtime page rendering resolves global_data_field and
global_data_query bindings from the published global data cache.
Text bindings also resolve namespaced CMS variable tokens such as {site.name},
{organization.name}, and {data.global.header-menu.main.brand_label}. Missing
tokens remain unchanged.
applyPageOperationsAction has been removed.
Validation
Useful checks after page catalog changes:
bun test src/lib/shared/pages/puck-document.test.ts
bun test src/lib/server/services/pages/page-definition-schema.test.ts
bun test src/lib/server/services/pages/page-definition-runtime.test.ts
bun test src/lib/server/services/pages/page-ai-annotation-runtime.test.ts
bun run check
bunx tsc --noEmitACL Baseline
Resource: page
Recommended organization policies:
member:
page:list,page:readmanager/admin:
page:managesuper admin: global
page:manage
The dashboard, the page server actions and the MCP page tools decide with
can() on page alone, in the organization scope for organization pages and
without scope for global pages. The pages list shows global pages to holders
of a global page read, list or manage policy, and only managers change
them. A Better Auth member role string, owner
included, never grants page access: a member without a group holding a page
policy is refused.