YYayaw

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:

  • a list/table (Yayaw DataTable)
  • a dedicated page editor route (/dashboard/content/pages/[scope]/[slug])

Related docs:

Pages are composed with these node families:

  • stack: layout containers
  • component: direct component references for advanced composition
  • section: reusable catalog section references resolved to the latest publication
  • generated_section: recipe-backed generated sections, including seeded layout sections such as the public home header/footer; AI-generated content sections are promoted to reusable section references on save

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:

  • PuckPageDocumentV1
  • location: 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. A page stores a stable section id/slug rather than a frozen revision id, so published pages follow the latest published section revision.

The document root also stores localized page settings:

  • titleByLocale
  • seo.seoTitleByLocale
  • seo.metaDescriptionByLocale
  • seo.socialTitleByLocale
  • seo.socialDescriptionByLocale
  • seo.socialImageUrl
  • seo.canonicalUrl
  • seo.noIndex / seo.noFollow

Registry metadata remains useful for catalog summaries, but the published runtime uses the document root for public SEO metadata.

Channels and Scope

Pages support two scopes/channels:

  • global (global_public)
  • organization (org_members for member-only pages)
  • organization (org_public for 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.ts
  • route 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/data
  • explicit publish/archive lifecycle
  • read-only mode for users without manage permission

The insert palette groups page elements as:

  • Built-in / Intégrés: Header, Footer
  • Sections: published global sections plus relevant organization sections
  • Components: direct component references
  • Layout / Mise en page: PageStack

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, 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:

  • saving
  • saved
  • error
  • conflict

Autosave and Concurrency

Draft saves use optimistic locking:

  • saveDraftPageAction requires baseRevisionId
  • stale base revision returns conflict
  • successful 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.definition
  • normalizes 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 IDs
  • renders resolved runtime preview tree
  • resolves localized metadata for generateMetadata

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, includes hreflang alternates, and skips pages marked noIndex
  • site-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 header and footer sections backed by the CMS singleton entries header-menu/main and footer-menu/main
  • localized 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.
  • /codebase is the published sales page for ownership and architecture.
  • /pricing is 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 PageSection references into the page document
  • it does not persist new PageGeneratedSection nodes in saved documents
  • it receives the configured locales from src/config/i18n.config.ts
  • it 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 itemsJson with link, group, themeToggle, languageToggle, and userMenu items
  • it 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 valuesByLocale entries are translated for every locale in the page document
  • when 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 to heroImage or a compatible image media slot
  • global 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-mini model with a short fallback chain to reduce deterministic fallback drafts when a fast model returns invalid JSON.
  • POST /api/ai/pages/runs persists the request in Postgres and wakes a worker
  • GET /api/ai/pages/runs/:runId reloads the latest run snapshot
  • GET /api/ai/pages/runs/:runId/events?afterSeq=n replays ordered progress events
  • POST /api/ai/pages/runs/:runId/cancel requests best-effort cancellation
  • ui_page_ai_runs stores ownership, payload, status, partial result, final result, errors, attempts, and lock metadata
  • ui_page_ai_run_events stores ordered progress events for resume after reload
  • Vercel Queues is one wake-up transport; Postgres remains the source of truth.
  • In production, the default wake-up driver is vercel-queue only on Vercel runtimes and db-worker elsewhere.
  • PAGE_AI_QUEUE_DRIVER=db-worker lets a long-lived worker process the same runs with bun run worker:page-ai without changing the editor/API contract.

Data Model

Page persistence tables:

  • ui_page_registry_items
  • ui_page_revisions
  • ui_page_publications
  • ui_page_ai_runs
  • ui_page_ai_run_events

Reusable sections are stored separately in:

  • ui_section_registry_items
  • ui_section_revisions
  • ui_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:

  • listPagesForTableAction
  • listPagesAction
  • openPageForEditingAction
  • createPageAction
  • updatePageDefinitionJsonAction
  • saveDraftPageAction
  • publishPageAction
  • archivePageAction

The dashboard table uses Yayaw Table custom bulk actions for the selected-row lifecycle shortcuts:

  • publish selected pages through publishPageAction
  • archive selected pages through archivePageAction
  • keep bulk deletion on deletePagesBulkAction

Section availability for the page builder comes from:

  • listAvailableSectionsForPageAction
  • listAvailableSectionsForPage

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 --noEmit

ACL Baseline

Resource: page

Recommended organization policies:

  • member: page:list, page:read
  • manager/admin: page:manage
  • super admin: global page:manage