Catalogue des pages
Éditeur de pages basé sur Puck pour les canaux publics globaux et membres d'organisation.
Vue d'ensemble
Yayaw fournit la gestion des pages à :
/dashboard/content/pages
Le flux dashboard est divisé en :
- une liste/table (
Yayaw DataTable) - une route dédiée d'édition de page (
/dashboard/content/pages/[scope]/[slug])
Docs liées :
- Tableau de bord pour le comportement du shell et de la navigation
- Catalogue des sections pour comprendre comment les documents de page référencent les sections réutilisables
- Modèles de données CMS pour les bindings de données structurées
- Médiathèque pour le stockage des ressources et la génération d'images
- Vue d'ensemble du CMS pour toute la pile de contenu
Les pages sont composées avec ces familles de noeuds :
stack: conteneurs de mise en pagecomponent: références directes de composants pour la composition avancéesection: références de sections réutilisables du catalogue, résolues vers la dernière publicationgenerated_section: sections générées adossées à des recettes, y compris les sections de mise en page initialisées comme l'en-tête/pied de page public de l'accueil ; les sections de contenu générées par IA sont promues en référencessectionréutilisables à la sauvegarde
Rôle CMS
Les pages sont la couche finale de composition du CMS. Elles doivent agencer la mise en page, les métadonnées SEO, les références de sections, les références de composants, les bindings de données et les bindings média. Elles ne doivent pas devenir la source de vérité à long terme pour le contenu répété comme les menus de navigation, offres partagées, blocs de preuve réutilisables ou ressources d'organisation.
Utilisez les sections pour les unités de page réutilisables, les données CMS pour les valeurs structurées partagées, les médias pour les ressources binaires et les design tokens pour le thème visuel public.
Contrat de document
Les pages utilisent un contrat de document Puck natif :
PuckPageDocumentV1- emplacement :
src/lib/shared/pages/puck-document.ts
La persistance des révisions reste dans les tables existantes, et
ui_page_revisions.definition stocke le document Puck versionné comme source de
vérité.
La conversion runtime émet PageSectionNodeV1 pour les sections réutilisables.
Une page stocke un id/slug de section stable plutôt qu'un id de révision figé,
afin que les pages publiées suivent la dernière révision publiée de la section.
La racine du document stocke aussi les réglages de page localisés :
titleByLocaleseo.seoTitleByLocaleseo.metaDescriptionByLocaleseo.socialTitleByLocaleseo.socialDescriptionByLocaleseo.socialImageUrlseo.canonicalUrlseo.noIndex/seo.noFollow
Les métadonnées de registre restent utiles pour les résumés de catalogue, mais le runtime publié utilise la racine du document pour les métadonnées SEO publiques.
Canaux et scope
Les pages prennent en charge deux scopes/canaux :
global(global_public)organization(org_memberspour les pages réservées aux membres)organization(org_publicpour les pages publiques servies depuis des domaines personnalisés vérifiés)
Routes runtime :
- page d'accueil publique globale :
/[locale] - pages publiques globales :
/[locale]/[...slug] - pages membres d'organisation :
/[locale]/o/[orgSlug]/[[...slug]] - pages publiques d'organisation : domaine personnalisé vérifié +
/[locale]/[[...slug]]
Le runtime des pages membres d'organisation exige une adhésion.
Les pages publiques d'organisation n'exigent pas d'adhésion, mais ne sont rendues
que lorsque l'hôte de la requête correspond à une ligne
organization_public_domains vérifiée. Les hôtes personnalisés inconnus
retournent 404 au lieu de retomber sur le site global Yayaw.
Les pages publiques d'organisation peuvent cibler un seul domaine public
d'organisation. Lorsqu'une cible est définie, la page ne se résout que sur cet
hôte. Les pages publiques d'organisation non ciblées restent disponibles sur
tous les domaines publics vérifiés de l'organisation. Les chemins de page sont
des réglages de route explicites: une page publique d'organisation peut donc
utiliser / pour la page d'accueil du domaine personnalisé au lieu d'hériter
d'un chemin issu du nom de page.
Visibilité des sections :
- pages globales : sections intégrées + globales
- pages d'organisation : sections intégrées + globales + sections de la même organisation
UX d'édition dashboard
L'UX d'édition est Puck + shadcn direct :
- édition locale temps réel dans Puck
- sauvegarde automatique avec debounce
- sauvegardes de brouillon sérialisées afin que les fins d'autosave obsolètes ne puissent pas écraser un état plus récent
- sélecteur de locale basé sur
src/config/i18n.config.ts - réglages de route pour mettre à jour après création le chemin de page, le canal de visibilité d'organisation et la cible de domaine public
- champs SEO au niveau page dans l'inspecteur
- contrôles de binding image/média qui sélectionnent des ressources média d'organisation existantes ou génèrent de nouvelles ressources WebP avec OpenAI
- contrôles de binding de données globales qui lient un champ publié complet
depuis
/dashboard/content/data - cycle de vie publication/archivage explicite
- mode lecture seule pour les utilisateurs sans permission de gestion
La palette d'insertion groupe les éléments de page ainsi :
Built-in/Intégrés:Header,FooterSections: sections globales publiées plus sections d'organisation pertinentesComponents: références directes de composantsLayout/Mise en page:PageStack
Les instances de sections intégrées n'exposent aucune prop éditable dans l'inspecteur de page. Le contenu de section est édité depuis le catalogue de sections source, pas dupliqué par page.
Les recettes de sections générées prennent en charge les variantes header et
footer en plus des sections de contenu. Les variantes de contenu actuelles
sont hero, immersive_hero, legal, value_grid, feature_split,
system_map, mcp_console, proof, signal_wall et cta.
Les recettes d'en-tête et de pied de page utilisent itemsJson pour les liens
de menu, groupes de sous-menu, bascules de thème, bascules de langue et menus
utilisateur. Les sections de page Header/Footer initialisées lient ces props aux
données CMS header-menu/main et footer-menu/main, avec le même contrat JSON
comme fallback.
L'état de la barre d'outils expose le cycle d'autosave :
savingsavederrorconflict
Autosave et concurrence
Les sauvegardes de brouillon utilisent le verrouillage optimiste :
saveDraftPageActionexigebaseRevisionId- une révision de base obsolète retourne
conflict - les sauvegardes réussies renvoient le document Puck canonique serveur afin que l'éditeur adopte les IDs de section normalisés et les bindings par défaut sans boucle d'état dirty
- aucune opération serveur de rebase et aucune fusion automatique
Stratégie de résolution de conflit :
- l'utilisateur recharge explicitement le dernier brouillon depuis la bannière de conflit
- l'utilisateur réapplique les modifications souhaitées dans le contexte de la révision courante
Exécution et validation
Le runtime publié se résout directement depuis le document Puck stocké.
Chemin runtime serveur :
- charge le document de révision publiée depuis
ui_page_revisions.definition - normalise le document Puck
- convertit vers la définition runtime validée
- résout les références de sections publiées via
ui_section_publications, en utilisant d'abord l'ID de section courant puis le repli scope/slug pour les anciennes révisions de page dont les IDs sont périmés - rend l'arbre d'aperçu runtime résolu
- résout les métadonnées localisées pour
generateMetadata
Les routes publiques dynamiques exposent les métadonnées SEO via :
/[locale]/page.tsx/[locale]/[...slug]/page.tsx/[locale]/o/[orgSlug]/[[...slug]]/page.tsx
Les pages publiques globales alimentent aussi la découverte Google Search via :
/robots.txt, qui autorise le contenu public tout en excluant API, ingestion analytics, dashboard, auth, maintenance et chemins membres d'organisation/sitemap.xml, qui liste chaque page globale publiée pour chaque locale configurée, inclut les alternativeshreflanget ignore les pages marquéesnoIndex- JSON-LD de niveau site pour l'organisation et le site web Yayaw
Les pages membres d'organisation sont protégées par des contrôles d'adhésion et
émettent des métadonnées noindex même quand une page possède ses propres
champs SEO. Pour la recherche IA générative, gardez un contenu utile aux
personnes et évitez de vous appuyer sur des fichiers spéciaux réservés aux
machines comme levier de classement ; llms.txt reste orienté documentation, ce
n'est pas un chemin d'optimisation Google Search.
Les pages publiques d'organisation utilisent l'hôte de la requête pour les URL
canoniques, alternatives hreflang, robots, entrées sitemap, URL Open Graph et
JSON-LD. Les hôtes personnalisés sont publics uniquement : les surfaces
dashboard, auth, API, docs, ingest et /o/* sont bloquées au niveau proxy. Les
hôtes personnalisés vérifiés résolvent aussi le contenu localisé de page depuis
le préfixe de locale de l'URL et appliquent le thème public et les overrides de
design tokens de l'organisation propriétaire.
Domaines publics d'organisation
Les domaines personnalisés d'organisation sont stockés dans
organization_public_domains et gérés depuis les paramètres d'organisation ou
MCP. Un domaine appartient à exactement une organisation, peut être marqué
primaire une fois vérifié, et stocke le statut fournisseur, les challenges de
vérification TXT, les enregistrements CNAME/A recommandés et l'horodatage du
dernier contrôle. Les anciennes colonnes vercel_* restent utilisées pour la
compatibilité, mais le runtime traite la ligne comme un snapshot générique de
vérification de domaine public.
Le registre de page stocke un public_domain_id optionnel pour les pages
org_public. Les dialogues de création dashboard et de réglages de route de
l'éditeur affichent les domaines publics non archivés de l'organisation active,
avec par défaut le domaine primaire vérifié lorsqu'il existe. La résolution
runtime par hôte filtre les pages ciblées par domaine avec la ligne de domaine
personnalisé correspondante, tout en gardant les anciennes pages non ciblées
visibles sur tous les domaines publics vérifiés de l'organisation.
PUBLIC_DOMAIN_PROVIDER=vercel utilise l'API de domaines projet Vercel pour
ajouter et vérifier les domaines. PUBLIC_DOMAIN_PROVIDER=manual-dns génère un
challenge TXT de propriété et affiche les indications CNAME/A depuis
PUBLIC_DOMAIN_CNAME_TARGET et PUBLIC_DOMAIN_IPV4_TARGETS; les opérateurs
configurent DNS et ingress hors de l'app, puis relancent le contrôle du domaine.
La publication de contenu de page reste pilotée par la DB et ne redéploie pas le
fournisseur d'hébergement.
La publication est bloquée quand les diagnostics contiennent une sévérité
error.
Page d'accueil initialisée
bun run seed crée une page publique globale Home au chemin / lorsqu'aucune
page d'accueil publiée n'existe encore. Le document initialisé est un
PuckPageDocumentV1 normal avec :
- sections générées
headeretfooteradossées aux entrées singleton CMSheader-menu/mainetfooter-menu/main - texte de page et métadonnées SEO localisés en anglais/français
- pile de contenu composée de sections générées hero, value grid, feature split et CTA
Le seed est volontairement non destructeur. Une fois qu'une page d'accueil est déjà publiée, les exécutions ultérieures du seed gardent la révision publiée courante afin de ne pas écraser les éditions du Page Builder.
La route docs (/[locale]/docs) utilise les mêmes wrappers runtime d'en-tête et
de pied de page que la mise en page publique. Ces wrappers lisent les slots CMS
éditables pour la marque cliquable, la navigation centrale et les mini menus
publics à droite, tout en laissant la mise en page Fumadocs gérer la sidebar et
la table des matières des docs.
Pages marketing initialisées
bun run seed crée aussi les pages de lancement Yayaw via le même stockage de
catalogue de pages. Leurs shells de page utilisent les variantes Header/Footer
générées liées aux données de menu globales, tandis que le corps de page reste du
contenu de sections générées :
/est la page d'accueil publique publiée pour la codebase SaaS en code source./codebaseest la page commerciale publiée pour la possession du code et l'architecture./pricingest la page d'offre publiée pour l'accès lifetime au code source.
Le même seed publie les données globales utilisées par ces pages, dont
sales-offer/main et billing-product-content/pro-lifetime, afin que le texte
d'offre reste éditable pendant que noms produits, prix, IDs Stripe et état
checkout restent résolus depuis le catalogue de facturation.
Le seed crée les pages manquantes et peut publier une révision plus récente quand
une page appartient encore à la même clé de seed et que sa seedVersion change.
Les pages Page Builder existantes qui n'appartiennent pas au seed conservent leur
état de publication courant et leurs dernières révisions.
Page Builder IA
Le flux Page AI donne la priorité aux sections réutilisables :
- il génère des sections adossées à des recettes
- il peut générer des recettes d'en-tête/pied de page quand une page a besoin de navigation de mise en page
- il crée et publie ces sections dans le catalogue
- il insère des références
PageSectiondans le document de page - il ne persiste pas de nouveaux noeuds
PageGeneratedSectiondans les documents enregistrés - il reçoit les locales configurées depuis
src/config/i18n.config.ts - il remplit les titres de page et métadonnées SEO localisés pour chaque locale configurée
- les bindings de contenu générés utilisent les indices d'éditeur de recette pour préserver les structures typées (par exemple tableaux de badges et tableaux de cartes) et stocker les valeurs localisées pour chaque locale configurée
- les menus d'en-tête/pied de page générés utilisent
itemsJsonavec des élémentslink,group,themeToggle,languageToggleetuserMenu - il injecte des consignes compactes de composition shadcn/ui afin que les landing pages utilisent des patterns de sections de qualité registre, des tokens sémantiques, des groupes de CTA, des bandes de preuve et de vrais indices d'usage de composants au lieu de dériver vers des contrôles isolés
- il exécute une passe dédiée de localisation après la génération
mise en page/contenu afin que les entrées
valuesByLocalemanquantes soient traduites pour chaque locale du document de page - quand un prompt demande un média généré, ou implique fortement un visuel riche
de landing page, Page AI peut générer au plus une ressource image OpenAI
configurée (défaut
gpt-image-1.5), la persister via la médiathèque de l'organisation active et la lier àheroImageou à un slot média image compatible - les pages globales peuvent utiliser les ressources générées depuis la médiathèque privée de l'organisation active parce que l'URL de fichier stockée est publique ; si aucune organisation active ou permission média n'est disponible, la génération d'image est ignorée avec un avertissement et le brouillon de page réussit quand même
Si la création ou publication de section échoue, la sauvegarde/génération de page échoue avec une erreur explicite au lieu de stocker silencieusement un fallback inline.
La génération Page AI est durable :
- Page AI utilise par défaut le modèle rapide
gpt-5.4-miniavec une courte chaîne de fallback afin de réduire les brouillons de fallback déterministes quand un modèle rapide retourne du JSON invalide. POST /api/ai/pages/runspersiste la requête dans Postgres et réveille un workerGET /api/ai/pages/runs/:runIdrecharge le dernier snapshot d'exécutionGET /api/ai/pages/runs/:runId/events?afterSeq=nrejoue les événements de progression ordonnésPOST /api/ai/pages/runs/:runId/canceldemande une annulation best-effortui_page_ai_runsstocke propriété, payload, statut, résultat partiel, résultat final, erreurs, tentatives et métadonnées de verrouillageui_page_ai_run_eventsstocke les événements de progression ordonnés pour reprendre après rechargement- Vercel Queues est un transport de réveil; Postgres reste la source de vérité.
- En production, le driver de réveil par défaut est
vercel-queueuniquement sur les runtimes Vercel, etdb-workerailleurs. PAGE_AI_QUEUE_DRIVER=db-workerpermet à un processus worker long-lived de traiter les mêmes runs avecbun run worker:page-aisans changer le contrat éditeur/API.
Modèle de données
Tables de persistance des pages :
ui_page_registry_itemsui_page_revisionsui_page_publicationsui_page_ai_runsui_page_ai_run_events
Les sections réutilisables sont stockées séparément dans :
ui_section_registry_itemsui_section_revisionsui_section_publications
Les contraintes de page restent inchangées :
- unicité
(scope, organization_id, slug) - unicité
(scope, organization_id, path) - révisions immuables avec déduplication par hash
- une ligne de publication par élément de registre
API serveur
Les actions de page vivent dans :
src/lib/server/actions/pages/pages-catalog-actions.ts
Actions actuelles :
listPagesForTableActionlistPagesActionopenPageForEditingActioncreatePageActionupdatePageDefinitionJsonActionsaveDraftPageActionpublishPageActionarchivePageAction
La table dashboard utilise les actions groupées personnalisées Yayaw Table pour les raccourcis de cycle de vie des lignes sélectionnées :
- publier les pages sélectionnées via
publishPageAction - archiver les pages sélectionnées via
archivePageAction - garder la suppression groupée sur
deletePagesBulkAction
La disponibilité des sections pour le page builder vient de :
listAvailableSectionsForPageActionlistAvailableSectionsForPage
La génération d'image pour les champs média de page est gérée par le service
partagé de ressources image média, utilisé à la fois par le picker média et Page
AI. Les images générées utilisent le modèle OpenAI configuré, avec
gpt-image-1.5 par défaut, et suivent le même chemin de permissions
d'organisation, quotas, téléversement vers le stockage objet et persistance
media_assets que les téléversements manuels. Le picker média affiche les
miniatures stockées quand elles sont disponibles, tandis que les bindings
conservent le publicUrl original afin que les pages rendues utilisent toujours
la ressource source.
Les champs de données globales publiées sont exposés à l'éditeur de page comme
références de binding. Le rendu runtime des pages résout les bindings
global_data_field et global_data_query depuis le cache de données globales
publiées. Les bindings texte résolvent aussi les tokens de variables CMS
namespacés comme {site.name}, {organization.name} et
{data.global.header-menu.main.brand_label}. Les tokens manquants restent
inchangés.
applyPageOperationsAction a été supprimé.
Validation
Vérifications utiles après des changements du catalogue de pages :
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 --noEmitBase ACL
Ressource : page
Politiques d'organisation recommandées :
- member :
page:list,page:read - manager/admin :
page:manage - super admin : global
page:manage