Date : 29/12/2024 Priorite : HAUTE - CETTE SEMAINE Projet : QwikPress CMS
Priorité : Haute Complexité : Élevée
Permettre de créer des modules directement depuis l'interface admin sans nécessiter de rebuild du projet.
{
"id": "custom-module-1",
"type": "hot-module",
"template": "HotModule",
"data": {
"name": "Mon Module Custom",
"clientJs": "console.log('Hello client'); document.querySelector('.btn').onclick = () => alert('Clicked!');",
"serverJs": "const data = await fetch('https://api.example.com/data').then(r => r.json()); return { items: data };",
"jsx": "<div class=\"my-module\"><h2>{props.title}</h2><ul>{serverData.items.map(i => <li>{i.name}</li>)}</ul><button class=\"btn\">Click me</button></div>",
"css": ".my-module { padding: 2rem; } .btn { background: blue; color: white; }",
"props": {
"title": "Liste des items"
}
}
}
eval() sandboxé ou VM isolée pour le server JSdangerouslySetInnerHTML pour le JSX compiléhot-modules/ ou table SupabasePriorité : Moyenne Complexité : Moyenne
Séparer le site public et l'administration sur deux domaines distincts pour renforcer la sécurité.
monsite.com → Site public (lecture seule)
admin.monsite.com → Administration (authentifié)
# Mode domaine unique (actuel)
DOMAIN_MODE=single
BASE_URL=https://monsite.com
# Mode multi-domaines
DOMAIN_MODE=split
PUBLIC_DOMAIN=https://monsite.com
ADMIN_DOMAIN=https://admin.monsite.com
/admin/* vers ADMIN_DOMAINPUBLIC_DOMAIN// src/routes/plugin@domain.ts
export const onRequest: RequestHandler = async ({ url, redirect, env }) => {
const isAdminDomain = url.hostname === new URL(env.ADMIN_DOMAIN).hostname;
const isAdminRoute = url.pathname.startsWith('/admin');
if (env.DOMAIN_MODE === 'split') {
// Sur domaine public, bloquer /admin
if (!isAdminDomain && isAdminRoute) {
throw redirect(302, `${env.ADMIN_DOMAIN}${url.pathname}`);
}
// Sur domaine admin, bloquer routes publiques (optionnel)
if (isAdminDomain && !isAdminRoute && url.pathname !== '/') {
throw redirect(302, `${env.PUBLIC_DOMAIN}${url.pathname}`);
}
}
};
# Public
server {
server_name monsite.com;
location / {
proxy_pass http://localhost:4500;
}
location /admin {
return 302 https://admin.monsite.com$request_uri;
}
}
# Admin
server {
server_name admin.monsite.com;
# IP whitelist optionnel
# allow 192.168.1.0/24;
# deny all;
location / {
proxy_pass http://localhost:4500;
}
}
| Feature | Statut | Assigné | Date cible |
|---|---|---|---|
| Hot Module | 🔴 À faire | - | - |
| Multi-domaines | 🔴 À faire | - | - |
Les catchers peuvent dépendre les uns des autres. Exemple :
{
"id": "user-orders",
"name": "Commandes utilisateur",
"connector": "shop-api",
"endpoint": "getOrders",
"depends": ["current-user"],
"params": {
"userId": "${catchers.current-user.id}"
}
}
{
"id": "order-products",
"name": "Produits des commandes",
"connector": "shop-api",
"endpoint": "getProducts",
"depends": ["user-orders"],
"params": {
"ids": "${catchers.user-orders.map(o => o.productIds).flat().join(',')}"
}
}
Le système construit automatiquement un graphe orienté acyclique :
┌─────────────────┐
│ current-user │ (pas de dépendance)
└────────┬────────┘
│
▼
┌─────────────────┐
│ user-orders │ (dépend de current-user)
└────────┬────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
┌────────────┐ ┌─────────────┐ ┌────────────┐
│order-products│ │order-stats │ │ shipping │
└────────────┘ └─────────────┘ └────────────┘
// Pseudo-code du resolver
async function resolveDataCatchers(catchers: DataCatcher[], context: RequestContext) {
const graph = buildDependencyGraph(catchers);
const results: Record<string, any> = {};
// Exécuter par niveaux (catchers sans dépendances d'abord)
for (const level of graph.levels) {
// Tous les catchers d'un même niveau en parallèle
const levelResults = await Promise.all(
level.map(catcher => executeCatcher(catcher, results, context))
);
// Stocker les résultats pour les niveaux suivants
level.forEach((catcher, i) => {
results[catcher.id] = levelResults[i];
});
}
return results;
}
Niveau 0 (parallèle) : current-user, site-settings, featured-products
Niveau 1 (parallèle, après niveau 0) : user-orders, user-favorites
Niveau 2 (parallèle, après niveau 1) : order-products, order-stats, shipping
Dans les params, transform, ou filter d'un catcher :
{
"id": "personalized-products",
"depends": ["current-user", "user-favorites"],
"params": {
"category": "${catchers['current-user'].preferences.category}",
"exclude": "${catchers['user-favorites'].map(f => f.productId)}"
},
"transform": {
"type": "custom",
"code": "items.filter(i => !catchers['user-favorites'].includes(i.id))"
}
}
{
"id": "user-orders",
"depends": ["current-user"],
"onDependencyError": "skip",
"fallback": []
}
Options :
"skip" : Ne pas exécuter ce catcher si une dépendance échoue"fallback" : Utiliser la valeur fallback"throw" : Propager l'erreur (défaut)Le système détecte et refuse les dépendances circulaires :
❌ ERREUR: Dépendance circulaire détectée
catcher-a → catcher-b → catcher-c → catcher-a
Le cache tient compte des dépendances :
{
"id": "user-orders",
"depends": ["current-user"],
"cache": {
"ttl": 300,
"key": "orders-${catchers['current-user'].id}",
"invalidateOn": ["current-user.changed"]
}
}
Si current-user change, le cache de user-orders est invalidé.
┌─────────────────────────────────────────────────────────┐
│ Data Catchers - Graphe de dépendances │
├─────────────────────────────────────────────────────────┤
│ │
│ ○ current-user (0ms) │
│ │ │
│ ├──○ user-orders (45ms) │
│ │ │ │
│ │ ├──○ order-products (120ms) │
│ │ └──○ order-stats (30ms) │
│ │ │
│ └──○ user-favorites (25ms) │
│ │ │
│ └──○ personalized-products (80ms) │
│ │
│ ○ featured-products (50ms) [indépendant] │
│ │
│ Total: 200ms (parallélisé) vs 350ms (séquentiel) │
└─────────────────────────────────────────────────────────┘
Priorité : Haute Complexité : Moyenne
Objectif : Permettre aux développeurs de créer facilement modules, connecteurs et catchers pendant le développement, ET permettre de faire exactement la même chose via le navigateur une fois en production (sans rebuild).
┌────────────────────────────────────────────────────────────────┐
│ DÉVELOPPEMENT │
│ │
│ src/modules/ → Modules compilés (TypeScript/Qwik) │
│ src/connectors/ → Connecteurs compilés │
│ src/catchers/ → Data Catchers compilés │
│ │
│ ✅ Type-safe, performant, testé │
└────────────────────────────────────────────────────────────────┘
│
│ même API, même structure
▼
┌────────────────────────────────────────────────────────────────┐
│ RUNTIME (Browser) │
│ │
│ /admin/custom/modules → Hot Modules (JS + JSX) │
│ /admin/custom/connectors → Connecteurs dynamiques │
│ /admin/custom/catchers → Data Catchers dynamiques │
│ │
│ ✅ Pas de rebuild, déploiement instantané │
└────────────────────────────────────────────────────────────────┘
Avant (config séparée) :
{
"site": { ... },
"header": {
"logo": "...",
"navigation": [...]
},
"footer": {
"columns": [...],
"social": [...]
}
}
Après (modules) :
{
"site": { ... },
"layouts": {
"default": {
"header": {
"module": "HeaderModule",
"data": { "logo": "...", "navigation": [...] },
"options": { "sticky": true, "transparent": false }
},
"footer": {
"module": "FooterModule",
"data": { "columns": [...], "social": [...] },
"options": { "showNewsletter": true }
},
"wrapper": {
"module": "PageWrapperModule",
"options": { "maxWidth": "1200px", "padding": "2rem" }
}
},
"landing": {
"header": {
"module": "MinimalHeaderModule",
"options": { "transparent": true, "absolute": true }
},
"footer": null
},
"admin": {
"header": { "module": "AdminHeaderModule" },
"sidebar": { "module": "AdminSidebarModule" },
"footer": null
}
}
}
Un layout définit :
┌─────────────────────────────────────────────────────────┐
│ HEADER (zone) │
│ [HeaderModule + options] │
├──────────────┬──────────────────────────────────────────┤
│ SIDEBAR │ CONTENT │
│ (zone) │ (zone) │
│ │ │
│ [Module] │ ┌────────────────────────────────┐ │
│ │ │ Module 1 (page content) │ │
│ │ ├────────────────────────────────┤ │
│ │ │ Module 2 │ │
│ │ ├────────────────────────────────┤ │
│ │ │ Module 3 │ │
│ │ └────────────────────────────────┘ │
│ │ │
├──────────────┴──────────────────────────────────────────┤
│ FOOTER (zone) │
│ [FooterModule + options] │
└─────────────────────────────────────────────────────────┘
{
"id": "default",
"name": "Layout par défaut",
"zones": [
{
"id": "header",
"position": "top",
"sticky": true,
"module": {
"type": "HeaderModule",
"data": { ... }
}
},
{
"id": "content",
"position": "main",
"wrapper": {
"maxWidth": "1200px",
"padding": "2rem 1rem"
}
},
{
"id": "footer",
"position": "bottom",
"module": {
"type": "FooterModule",
"data": { ... }
}
}
]
}
{
"meta": {
"id": "landing-page",
"slug": "/promo",
"layout": "landing",
"layoutOverrides": {
"header": {
"options": { "transparent": true }
}
}
}
}
| Layout | Header | Sidebar | Footer | Usage |
|---|---|---|---|---|
default |
✅ Standard | ❌ | ✅ Standard | Pages normales |
landing |
✅ Minimal/Transparent | ❌ | ❌ ou Minimal | Landing pages |
blog |
✅ Standard | ✅ Droite | ✅ Standard | Articles |
docs |
✅ Standard | ✅ Gauche (nav) | ❌ | Documentation |
admin |
✅ Admin | ✅ Menu | ❌ | Administration |
blank |
❌ | ❌ | ❌ | Iframe, embed |
Comme les modules, les layouts peuvent être créés via l'admin :
/admin/layouts
├── Liste des layouts
├── Créer un layout
│ ├── Définir les zones
│ ├── Assigner les modules
│ ├── Options de style
│ └── Preview live
└── Assigner aux pages
┌─────────────────────────────────────────────────────────────────┐
│ QWIKPRESS │
├─────────────────────────────────────────────────────────────────┤
│ │
│ CONNECTEURS ──────► DATA CATCHERS ──────► MODULES │
│ (APIs) (Fetch + Transform) (UI Components) │
│ │ │ │
│ │ ┌───────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ │
│ │ LAYOUTS │ │
│ │ (Zones + CSS) │ │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ PAGES │ │
│ │ (Layout + Mods) │ │
│ └─────────────────┘ │
│ │
├─────────────────────────────────────────────────────────────────┤
│ DEV TIME (compiled) │ RUNTIME (hot/custom) │
│ ───────────────────── │ ──────────────────── │
│ src/modules/* │ /admin/custom/modules │
│ src/connectors/* │ /admin/custom/connectors │
│ src/catchers/* │ /admin/custom/catchers │
│ src/layouts/* │ /admin/custom/layouts │
└─────────────────────────────────────────────────────────────────┘
Tout peut être créé :
Même structure, même API, même résultat.
Priorité : Haute Complexité : Moyenne
Comme Qwik-City, les layouts s'héritent et s'imbriquent via l'arborescence des pages :
pages/
├── _layout.json ← Layout racine (header + footer)
├── home/
├── about/
├── blog/
│ ├── _layout.json ← Layout blog (sidebar articles)
│ ├── index/ ← Liste articles (hérite blog + racine)
│ └── [slug]/ ← Article (hérite blog + racine)
├── docs/
│ ├── _layout.json ← Layout docs (sidebar nav)
│ ├── getting-started/
│ │ ├── _layout.json ← Layout sous-section (breadcrumb)
│ │ ├── install/
│ │ └── config/
│ └── api/
│ ├── _layout.json ← Layout API (version selector)
│ └── endpoints/
└── admin/
├── _layout.json ← Layout admin (remplace tout)
└── ...
Une page /docs/getting-started/install passe par :
┌─────────────────────────────────────────────────────────────┐
│ Layout Racine (_layout.json) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ [HeaderModule] │ │
│ ├─────────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ Layout Docs (docs/_layout.json) │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ [DocsSidebarModule] │ <slot> │ │ │
│ │ │ │ │ │ │
│ │ │ - Getting Started │ Layout Getting-Started │ │ │
│ │ │ - API Reference │ ┌───────────────────┐ │ │ │
│ │ │ - Examples │ │ [BreadcrumbModule]│ │ │ │
│ │ │ │ ├───────────────────┤ │ │ │
│ │ │ │ │ │ │ │ │
│ │ │ │ │ PAGE CONTENT │ │ │ │
│ │ │ │ │ (modules) │ │ │ │
│ │ │ │ │ │ │ │ │
│ │ │ │ └───────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ │ │ │
│ ├─────────────────────────────────────────────────────────┤ │
│ │ [FooterModule] │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
{
"id": "docs-layout",
"name": "Documentation",
"inherit": true,
"zones": {
"before": [
{
"module": "DocsSidebarModule",
"position": "left",
"width": "280px",
"data": {
"source": "auto"
}
}
],
"content": {
"wrapper": {
"class": "docs-content",
"maxWidth": "900px"
}
},
"after": []
},
"options": {
"containerClass": "docs-layout",
"fullHeight": true
}
}
{
"inherit": true,
"inheritMode": "wrap"
}
| Mode | Comportement |
|---|---|
"wrap" (défaut) |
Le layout parent enveloppe ce layout |
"replace" |
Remplace complètement le layout parent |
"extend" |
Fusionne les zones (ajoute sans remplacer) |
Exemple replace (admin) :
{
"id": "admin-layout",
"inherit": false,
"zones": {
"header": { "module": "AdminHeaderModule" },
"sidebar": { "module": "AdminSidebarModule" },
"content": { "wrapper": { "class": "admin-main" } }
}
}
→ Ignore complètement le layout racine
Le layout enfant s'injecte dans le <slot> du parent :
Layout Parent:
┌────────────────────────┐
│ [Header] │
├────────────────────────┤
│ │
│ <slot/> │ ← Layout enfant injecté ici
│ │
├────────────────────────┤
│ [Footer] │
└────────────────────────┘
Les layouts peuvent aussi avoir des data catchers :
{
"id": "blog-layout",
"dataCatchers": [
{
"catcher": "blog-categories",
"as": "categories"
},
{
"catcher": "recent-posts",
"as": "recentPosts",
"options": { "limit": 5 }
}
],
"zones": {
"sidebar": {
"module": "BlogSidebarModule",
"data": {
"categories": "${layoutData.categories}",
"recent": "${layoutData.recentPosts}"
}
}
}
}
→ Les catchers du layout sont disponibles pour toutes les pages enfants via layoutData
globalData (site)
└── layoutData[0] (layout racine)
└── layoutData[1] (layout section)
└── layoutData[2] (layout sous-section)
└── pageData (page)
└── moduleData (module)
Chaque niveau peut accéder aux données des niveaux supérieurs.
/admin/pages
└── Structure des pages
├── 📁 / (racine)
│ ├── ⚙️ _layout.json [Éditer]
│ ├── 📄 home
│ ├── 📁 blog/
│ │ ├── ⚙️ _layout.json [Éditer]
│ │ ├── 📄 index
│ │ └── 📄 [slug]
│ └── 📁 docs/
│ ├── ⚙️ _layout.json [Éditer]
│ ├── 📁 getting-started/
│ │ ├── ⚙️ _layout.json [Éditer]
│ │ └── 📄 install
│ └── 📄 api
Clic sur un _layout.json → Éditeur de layout avec :
URL: /docs/getting-started/install
Layouts appliqués (dans l'ordre):
1. /_layout.json → Header + Footer
2. /docs/_layout.json → + Sidebar docs
3. /docs/getting-started/_layout.json → + Breadcrumb
Data disponible:
- globalData (site)
- layoutData.root (catchers layout racine)
- layoutData.docs (catchers layout docs)
- layoutData.gettingStarted (catchers layout getting-started)
- pageData (catchers page install)
Après réflexion, voici les clarifications et corrections :
Problème : layoutData[0], layoutData[1]... devient confus avec les nested layouts.
Solution : Cascade avec fusion et override
// Données disponibles dans un module
interface ModuleContext {
global: GlobalData; // Catchers globaux (site)
layout: LayoutData; // Catchers fusionnés de tous les layouts (enfant override parent)
page: PageData; // Catchers de la page
module: ModuleData; // Catchers du module lui-même
// Helper pour accéder à tout
get(key: string): any; // Cherche dans module → page → layout → global
}
Fusion des layouts (du plus profond au racine) :
Layout racine : { nav: [...], footer: {...} }
Layout /docs : { sidebar: [...], footer: { override: true } }
Layout /docs/api : { apiVersion: "v2" }
Résultat fusionné :
{
nav: [...], // de racine
sidebar: [...], // de /docs
footer: { override }, // de /docs (override racine)
apiVersion: "v2" // de /docs/api
}
Problème : wrap, replace, extend sont confus.
Solution : Override par zone
{
"id": "docs-layout",
"zones": {
"header": "inherit",
"sidebar": {
"module": "DocsSidebarModule",
"data": { ... }
},
"content": "inherit",
"footer": null
}
}
| Valeur | Comportement |
|---|---|
"inherit" |
Utilise la zone du parent |
null |
Supprime cette zone |
{ module, data } |
Override avec ce module |
Cas spécial : Layout admin qui remplace tout
{
"id": "admin-layout",
"resetParent": true,
"zones": { ... }
}
Zones (niveau Layout) :
Slots (niveau Module) :
CardModule avec slot pour le contenuLAYOUT (zones)
┌─────────────────────────────────┐
│ zone:header [HeaderModule] │
├──────────┬──────────────────────┤
│ zone: │ zone:content │
│ sidebar │ ┌──────────────────┐ │
│ │ │ Module 1 │ │
│ [Sidebar │ │ ┌──────────────┐ │ │
│ Module] │ │ │ slot:default │ │ │ ← SLOT dans le module
│ │ │ └──────────────┘ │ │
│ │ ├──────────────────┤ │
│ │ │ Module 2 │ │
│ │ └──────────────────┘ │
├──────────┴──────────────────────┤
│ zone:footer [FooterModule] │
└─────────────────────────────────┘
Problème : Si header/footer sont des modules, d'où viennent leurs données ?
Solution : Comme tout module, via data catchers
{
"zones": {
"header": {
"module": "HeaderModule",
"catcher": "site-navigation",
"options": { "sticky": true }
},
"footer": {
"module": "FooterModule",
"catcher": "footer-data"
}
}
}
Ou données inline pour les cas simples :
{
"zones": {
"header": {
"module": "HeaderModule",
"data": {
"logo": "/logo.png",
"nav": [...]
}
}
}
}
Mode fichier :
STORAGE/data/
├── layouts/
│ ├── _root.json → Layout racine
│ ├── docs.json → Layout /docs/*
│ └── docs-api.json → Layout /docs/api/*
└── pages/
└── ...
Mapping path → layout :
// Dans config.json
{
"layoutMappings": {
"/": "_root",
"/docs": "docs",
"/docs/api": "docs-api",
"/admin": "admin"
}
}
Mode Supabase :
CREATE TABLE qwikpress.layouts (
id UUID PRIMARY KEY,
site_id UUID,
path VARCHAR(255), -- "/" ou "/docs" ou "/docs/api"
config JSONB,
created_at TIMESTAMP,
updated_at TIMESTAMP,
UNIQUE(site_id, path)
);
Problème : Exécuter du JS arbitraire = dangereux
Solutions :
Client JS :
Server JS :
Exécution dans VM isolée (vm2, isolated-vm)
Timeout strict (5s max)
Pas d'accès filesystem
Imports whitelistés uniquement :
// Autorisé
const { fetch } = require('qwikpress/fetch'); // Proxy sécurisé
const { cache } = require('qwikpress/cache');
const { env } = require('qwikpress/env'); // Seulement les vars exposées
// Interdit
const fs = require('fs'); // ❌ Bloqué
const child = require('child_process'); // ❌ Bloqué
Validation JSX :
<script>, pas de on* events inline┌─────────────────────────────────────────────────────────────┐
│ QWIKPRESS v2 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ CONNECTEURS │────▶│DATA CATCHERS│────▶│ MODULES │ │
│ │ (APIs) │ │(fetch+logic)│ │ (UI) │ │
│ └─────────────┘ └──────┬──────┘ └─────────────┘ │
│ │ │ │
│ ┌───────────────────┼───────────────────┤ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ LAYOUTS │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ _root (zones: header, content, footer) │ │ │
│ │ │ └── docs (override: +sidebar, footer:null) │ │ │
│ │ │ └── api (override: +version selector) │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ PAGES │ │
│ │ - Héritent du layout correspondant à leur path │ │
│ │ - Contiennent les modules de la zone "content" │ │
│ │ - Peuvent avoir leurs propres data catchers │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
├─────────────────────────────────────────────────────────────┤
│ CRÉATION │
│ │
│ DEV (TypeScript, compilé) RUNTIME (Hot, navigateur) │
│ ════════════════════════ ═══════════════════════════ │
│ src/modules/*.tsx /admin/custom/modules │
│ src/connectors/*.ts /admin/custom/connectors │
│ src/catchers/*.ts /admin/custom/catchers │
│ src/layouts/*.tsx /admin/custom/layouts │
│ │
│ → Type-safe, performant → Pas de rebuild, sandbox │
│ → Tests unitaires → Prototypage rapide │
│ → Tree-shaking → Modifications à chaud │
└─────────────────────────────────────────────────────────────┘
Pour une requête sur /docs/api/endpoints :
1. LAYOUTS résolus (du plus spécifique au racine)
└── /docs/api/_layout.json
└── /docs/_layout.json
└── /_layout.json
2. DATA CATCHERS collectés et ordonnés
├── Global : site-settings, main-nav
├── Layout / : footer-data
├── Layout /docs : docs-sidebar, docs-search
├── Layout /docs/api : api-versions
└── Page : endpoint-list
3. GRAPHE DE DÉPENDANCES construit
site-settings (niveau 0)
main-nav (niveau 0)
docs-sidebar ──depends──▶ main-nav (niveau 1)
api-versions (niveau 0)
endpoint-list ──depends──▶ api-versions (niveau 1)
4. EXÉCUTION PARALLÈLE par niveau
Niveau 0: site-settings, main-nav, api-versions (parallèle)
Niveau 1: docs-sidebar, endpoint-list (parallèle, après niveau 0)
5. FUSION DES DONNÉES
{
global: { siteSettings, mainNav },
layout: { footerData, docsSidebar, docsSearch, apiVersions },
page: { endpointList }
}
6. RENDU LAYOUTS (du racine au plus spécifique)
/_layout → /docs/_layout → /docs/api/_layout → page content
7. RENDU MODULES avec données injectées
Les catchers ne sont pas la seule source de données. Le système expose automatiquement les métadonnées de base :
interface AutoData {
// Métadonnées pages (pour construire menus, breadcrumbs, sitemap...)
pages: {
all: PageMeta[]; // Toutes les pages
published: PageMeta[]; // Pages publiées uniquement
tree: PageTreeNode[]; // Arborescence hiérarchique
siblings: PageMeta[]; // Pages au même niveau
children: PageMeta[]; // Pages enfants de la courante
ancestors: PageMeta[]; // Fil d'Ariane (parents)
};
// Page courante
currentPage: {
meta: PageMeta;
slug: string;
path: string[]; // ["docs", "api", "endpoints"]
depth: number;
};
// Site
site: {
name: string;
description: string;
url: string;
logo: string;
};
// Layouts actifs
layouts: {
current: string[]; // ["_root", "docs", "docs-api"]
config: LayoutConfig[];
};
}
Exemple : Menu auto-généré
{
"zones": {
"header": {
"module": "HeaderModule",
"data": {
"logo": "${auto.site.logo}",
"nav": "${auto.pages.tree.filter(p => p.meta.inMenu)}"
}
}
}
}
Exemple : Breadcrumb automatique
{
"module": "BreadcrumbModule",
"data": {
"items": "${auto.currentPage.ancestors}",
"current": "${auto.currentPage.meta.title}"
}
}
Exemple : Sidebar enfants
{
"module": "SidebarNavModule",
"data": {
"title": "Dans cette section",
"links": "${auto.pages.children}"
}
}
Point important : Les layouts s'accumulent (wrap), ils ne se remplacent pas.
Structure:
/ → _root layout
/docs → docs layout
/docs/api → docs-api layout
/docs/api/endpoints → (pas de layout propre)
Requête sur /docs/api/endpoints :
Layouts appliqués (tous, imbriqués) :
┌─────────────────────────────────────────────────────────┐
│ _root layout │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ [HeaderModule] │ │
│ ├─────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ docs layout │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ │ │ │ │
│ │ │ [DocsSide │ docs-api layout │ │ │
│ │ │ barMod] │ ┌────────────────────────┐ │ │ │
│ │ │ │ │ [ApiVersionSelector] │ │ │ │
│ │ │ │ ├────────────────────────┤ │ │ │
│ │ │ │ │ │ │ │ │
│ │ │ │ │ PAGE CONTENT │ │ │ │
│ │ │ │ │ (endpoints modules) │ │ │ │
│ │ │ │ │ │ │ │ │
│ │ │ │ └────────────────────────┘ │ │ │
│ │ │ │ │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ │ │ │
│ ├─────────────────────────────────────────────────────┤ │
│ │ [FooterModule] │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
On a les TROIS layouts, pas juste le plus spécifique.
Pour remplacer au lieu d'accumuler (cas rare) :
{
"id": "admin-layout",
"resetParent": true,
"zones": { ... }
}
Ou pour supprimer une zone héritée :
{
"id": "landing-layout",
"zones": {
"header": "inherit",
"footer": null,
"sidebar": null
}
}
┌─────────────────────────────────────────────────────────────┐
│ SOURCES DE DONNÉES │
├─────────────────────────────────────────────────────────────┤
│ │
│ AUTO (métadonnées système) CATCHERS (fetch externe) │
│ ═══════════════════════════ ════════════════════════ │
│ auto.pages.* Via connecteurs │
│ auto.currentPage.* + Transform │
│ auto.site.* + Cache │
│ auto.layouts.* + Dépendances │
│ │
│ │ │ │
│ └──────────────┬───────────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ CONTEXTE MODULE │ │
│ ├─────────────────┤ │
│ │ auto.* │ ← Toujours dispo │
│ │ global.* │ ← Catchers site │
│ │ layout.* │ ← Catchers layouts fusionnés│
│ │ page.* │ ← Catchers page │
│ │ module.* │ ← Catchers module │
│ │ data.* │ ← Data inline du module │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
| Situation | Comportement |
|---|---|
/ a un layout |
Toutes les pages héritent |
/docs a un layout |
Pages /docs/* ont les DEUX (root + docs) |
/docs/api a un layout |
Pages /docs/api/* ont les TROIS |
resetParent: true |
Coupe la chaîne, repart de zéro |
zone: null |
Supprime cette zone pour ce layout et ses enfants |
zone: "inherit" |
Garde la zone du parent (défaut implicite) |