Aller au contenu

Skills Celuga

celuga-architecture-payload

Créer, auditer ou structurer un projet Payload 4 + Astro ; décider où placer un fichier.

Quand : Créer, auditer ou structurer un projet Payload 4 + Astro ; décider où placer un fichier.

Portée : machine (~/.agents/skills) · Fichiers : 6

Copier le bloc et le coller dans un terminal. Il recrée tous les fichiers du skill.

dir="$HOME/.agents/skills/celuga-architecture-payload"
mkdir -p "$dir"
cat > "$dir/SKILL.md" <<'CELUGA_SKILL_EOF'
---
name: celuga-architecture-payload
description: Architecture cible des projets Celuga Payload + Astro — monorepo `apps/cms` (Payload 4 sur TanStack Start) + `apps/web` (Astro SSR) + `packages/site-contracts`, registre de contenus publics, modules optionnels, emplacement de chaque fichier, conventions Payload (collections, globals, blocs, champs, hooks, accès, plugins), couche de données front, schéma par migrations, garde-fous qualité. Use when creating or scaffolding a Payload + Astro project, auditing or reviewing its architecture, deciding where a new file, collection, global, field, hook, endpoint, page or helper belongs, changing the database schema, or adding a dependency or workspace.
---
# Architecture Celuga Payload + Astro
Deux applications, une **frontière** nette : Payload possède le contenu, le schéma et l'administration ; Astro possède tout le HTML public. Elles ne communiquent que par HTTP (API REST, endpoints publics dédiés, invalidation) et partagent seulement un package de contrats. Chaque concept a **une seule source de vérité**.
Ce skill décrit la cible. Avant d'appliquer, lire `AGENTS.md`, `README.md`, les manifestes et les procédures `.agents/skills` du projet : ses versions et ses choix explicites priment. Un projet plus ancien peut avoir une autre disposition (`shared/`, `cms/` + `frontend/`) : transposer les règles sans déplacer ses fichiers, sauf demande.
| Besoin | Référence |
| --- | --- |
| arborescence, rôle de chaque dossier, règles d'import | [`references/structure.md`](references/structure.md) |
| collections, globals, blocs, champs, hooks, accès, plugins, schéma et migrations | [`references/payload.md`](references/payload.md) |
| pages Astro, couche de lecture, composants, îlots, styles, typage | [`references/front.md`](references/front.md) |
| noms et règles des variables d'environnement | [`references/environnement.md`](references/environnement.md) |
Hors périmètre, charger le skill dédié : **payload** (API générique, à confronter à la version installée), **celuga-astro-payload-cache** (tags, invalidation, preview, 503), **celuga-shadcnblock-integration** (bloc inspiré de shadcnblocks), **celuga-media-react** (médias, `<Media>`, proxy `/media`), **celuga-seo** (meta, sitemap, JSON-LD, assistant), **celuga-docker-payload-astro** (images et Compose), **mep-celuga** (déploiement et e-mail).
## Règles d'architecture
1. **Topologie** : `apps/cms`, `apps/web`, `packages/site-contracts`, `tests`, `e2e`, `scripts`, `docs`. Pas de troisième app ni de nouveau package sans second consommateur réel.
2. **Frontière vérifiée** : le Web n'importe jamais les sources du CMS ; les contrats n'importent ni une app ni un framework serveur. Un script du dépôt le contrôle (`verify:architecture`). Les types générés vivent dans les contrats. Le code partagé qui exige Node reste sous `server/`.
3. **Registre public** : routes, preview, SEO, sitemap, tags de cache, ordre admin et champs de traduction d'un contenu public se déclarent dans un descripteur partagé (`collections.ts` des contrats). Les consommateurs en dérivent leur comportement ; un contenu absent du registre est refusé. **Les droits restent explicites dans chaque collection** : le registre n'accorde aucun accès.
4. **Modules optionnels** : `celuga.project.json` choisit `modules` et `locales`. Un module désactivé garde ses tables (une seule chaîne de migrations pour toutes les compositions) mais disparaît de l'administration, de l'API, des routes et du rendu.
5. **Contenu → HTML** : le front lit Payload par REST, jamais par la Local API ni par la base. Tout contenu public est dans le HTML initial ; React n'est hydraté que pour un comportement navigateur indispensable.
6. **Types générés** : aucun type de document recopié à la main. `bun run generate` après tout changement de schéma.
7. **Schéma par migrations, partout** : `push: false`, y compris en développement. Un champ ajouté sans migration appliquée casse toutes les lectures, car Payload sélectionne chaque colonne. Migrations et recettes uniquement sur une base locale jetable ou sur la cible explicitement désignée.
8. **Bun seul** : installation, build, commandes Payload, serveurs et images. Typage par `bun check`, formatage par `bun fmt`, tests par `bun test`.
9. **Configuration par l'environnement** : un seul `.env` à la racine, noms de [`environnement.md`](references/environnement.md), sans alias. Aucun secret sous `PUBLIC_`. URL internes jamais exposées au navigateur.
10. **Simplicité** : pas d'abstraction pour un seul appelant, pas de barrel fourre-tout. Une fonction vit à côté de son unique consommateur jusqu'à son deuxième usage.
## Branches
### A. Créer ou porter un projet
Créer avec le générateur `create-celuga` plutôt qu'à la main : il pose les secrets, le manifeste, le lockfile et la chaîne de migrations. Pour un portage, comparer au template du générateur, puis brancher cache, médias et SEO avec les skills dédiés.
**Terminé quand :** la vérification complète du dépôt passe (`bun run verify` : tests, lint, frontières, typage, builds) et la chaîne de migrations s'applique sur une base neuve jetable.
### B. Ajouter une fonctionnalité
Placer chaque fichier avec la table « Où mettre quoi » de [`structure.md`](references/structure.md). Pour une collection, un bloc ou un schéma, suivre d'abord la procédure livrée dans le dépôt (`.agents/skills`). Si le projet a un guide éditorial et que le changement ajoute un geste particulier, le compléter après avoir vérifié les libellés à l'écran.
**Terminé quand :** chaque fichier a son emplacement justifié, la génération ne produit plus de diff, la migration relue est versionnée si le schéma change, la dépendance de cache est déclarée si un bloc lit une autre collection, et les contrôles du dépôt passent.
### C. Auditer
Parcourir les règles 1 à 10 et les références utiles :
```bash
bun run verify:architecture # frontières
bun run schema:check # lecture seule ; identifier d'abord la base visée
rg "@payloadcms|from \"payload\"" apps/web/src # côté front, seuls live-preview et richtext-lexical sont admis
rg "client:(visible|load|idle|only)" apps/web/src # chaque îlot justifié
rg "overrideAccess: true" apps/cms/src # chaque usage justifié, jamais pour une lecture publique
```
**Terminé quand :** chaque règle est notée conforme ou écart, chaque écart a un fichier, une ligne, un risque concret et un correctif, classés par gravité (données/sécurité → prod exposée → lisibilité).
CELUGA_SKILL_EOF
mkdir -p "$dir/agents"
cat > "$dir/agents/openai.yaml" <<'CELUGA_SKILL_EOF'
interface:
display_name: "Celuga Architecture Payload"
short_description: "Architecture cible Payload + Astro : structure, conventions, migrations"
default_prompt: "Utilise $celuga-architecture-payload pour auditer ou structurer l'architecture Payload + Astro de ce projet."
CELUGA_SKILL_EOF
mkdir -p "$dir/references"
cat > "$dir/references/environnement.md" <<'CELUGA_SKILL_EOF'
# Variables d'environnement Payload + Astro
Lire `.env.example`, les lecteurs du code et le Compose du projet, jamais les valeurs du `.env`. Garder ces noms lors d'un portage.
## Contrat
| Variable | Lecteur | Comportement |
| --- | --- | --- |
| `DATABASE_URL`, `PAYLOAD_SECRET` | CMS | Requis. Jamais transmis au Web ni à ses arguments de build. `PAYLOAD_SECRET` protège sessions et clés chiffrées : le conserver à vie sur une installation existante. |
| `SITE_SECRET` | CMS, Web | Requis, 32 caractères minimum, identique des deux côtés, distinct de `PAYLOAD_SECRET`. Le code en dérive par HMAC-SHA-256 une clé par usage (`cache`, `preview`, `form`). |
| `PUBLIC_SITE_URL` | CMS, Web | Origine publique complète avec `https://` (sans schéma, le CMS refuse de démarrer). Canonical, sitemap, preview, OG. |
| `PUBLIC_PAYLOAD_URL` | CMS, Web | Origine publique du CMS : administration, live preview. |
| `PAYLOAD_INTERNAL_URL` | Web serveur | URL du CMS sur le réseau interne ; repli sur `PUBLIC_PAYLOAD_URL`. Fixée par Compose. |
| `WEB_INTERNAL_URL` | CMS | URL d'invalidation ; repli sur `PUBLIC_SITE_URL`. Fixée par Compose. |
| `PUBLIC_ALLOW_INDEXING` | Web, CMS | `true` seulement sur le domaine de production. Défaut `false`. Passe aussi au build : redéployer après changement. |
| `EMAIL_HOST`, `EMAIL_USER` | CMS | Requis, sans valeur par défaut. `EMAIL_USER` : adresse complète, connexion et expéditeur. |
| `EMAIL_PASS` | CMS | Vide : aucune authentification SMTP (relais autorisé par IP, Mailpit). Renseigné : authentification. |
| `EMAIL_PORT` | CMS | Entier, défaut 587 avec STARTTLS obligatoire ; 465 : TLS direct. |
| `FORM_NOTIFICATION_TO` | CMS | Destinataires, séparés par des virgules ; défaut `EMAIL_USER`. |
| `TRUST_PROXY`, `TRUSTED_PROXY_IPS` | Web | `X-Forwarded-For` lu seulement si le pair direct est de confiance. En Docker, liste vide : CIDR privés réellement attachés au conteneur. |
| `FIRST_ADMIN_EMAIL`, `FIRST_ADMIN_PASSWORD` | Commande | Pour `admin:create` ou `setup` en production ; 16 caractères minimum ; à retirer ensuite. |
| `POSTGRES_PASSWORD`, `POSTGRES_PORT`, `MAILPIT_PORT`, `CMS_PORT`, `WEB_PORT` | Compose local | Base et ports locaux. |
| `SHADCNBLOCKS_API_KEY` | CLI shadcn | Développement seul, jamais dans les conteneurs. |
Fixés par le code ou les images, à ne pas mettre dans `.env` : `NODE_ENV`, `ASTRO_BUNDLE_DEPS`, `CELUGA_CMS_SERVER`.
Noms retirés, à ne pas réintroduire : `CACHE_SECRET`, `PREVIEW_SECRET`, `FORM_SECRET` (remplacés par `SITE_SECRET`) ; `SMTP_*` (remplacés par `EMAIL_*`) ; `PAYLOAD_PUBLIC_*`, `CACHE_INVALIDATION_SECRET`, `PAYLOAD_ENABLE_PUSH`. Sur un projet plus ancien, vérifier ce que son code lit réellement avant de renommer. Migrer les anciens secrets : ajouter `SITE_SECRET`, supprimer les trois anciens, redéployer CMS et Web ensemble, régénérer les liens de preview.
## Chargement
- CMS : `lib/load-env.ts` charge le `.env` racine par chemin absolu, sans écraser les variables injectées. Secrets validés au démarrage.
- Web : `vite.envDir` vers la racine, lecture serveur `process.env || import.meta.env` via `lib/env.server.ts`.
- Les `PUBLIC_*` sont aussi figées au build Vite. Une valeur saisie seulement au runtime peut ne pas atteindre le code client.
- En Docker, seul ce que Compose référence arrive au conteneur : renseigner une option facultative dans `.env` ne la transmet pas, il faut un override.
## Vérification
Aucun secret sous `PUBLIC_`. Secrets aléatoires propres à chaque environnement (`openssl rand -hex 32`). Un `.env` de développement ne vise jamais la production. Pour comparer deux valeurs, rapporter seulement présence ou égalité. Les URL internes ne sortent jamais dans le HTML, la canonical, l'OG ou la preview.
CELUGA_SKILL_EOF
mkdir -p "$dir/references"
cat > "$dir/references/front.md" <<'CELUGA_SKILL_EOF'
# Conventions front (`apps/web`)
## Configuration Astro
- `output: "server"`, adaptateur `@astrojs/node` standalone avec `bodySizeLimit` borné, intégrations React et Tailwind 4 (`@tailwindcss/vite`).
- `vite.envDir` vers la racine (un seul `.env`), `dedupe` de React. `ssr.noExternal` seulement quand `ASTRO_BUNDLE_DEPS=true` (image Docker).
- Cache : **celuga-astro-payload-cache**.
## Pages
Une page Astro **orchestre**, elle ne calcule pas :
1. lire les paramètres de route et la langue ;
2. charger via un chargeur commun (`lib/content-page.ts`) : redirection, document, chrome et traductions ;
3. retourner la redirection, ou rendre la 404 ;
4. déclarer le cache **dans le fichier de `pages/`** : `Astro.cache.set` dans un composant enfant n'a pas d'effet fiable ;
5. rendre le composant de page et les blocs préparés.
Une route libre `[...slug].astro` résout le contenu par le registre public ; une route dédiée n'existe que pour un besoin propre. Endpoints privés sous `api/`, proxys sous `media/` et `documents/`.
## Couche de données
- Transport commun (`lib/cms.ts`) : URL interne ou publique, délai, erreur typée, déduplication par requête.
- Requêtes REST avec `depth` minimal, `select` ou `populate` : sans `populate[pages][slug]=true`, chaque lien de menu ramène la page liée entière et ses médias.
- Lecture publique : `locale` explicite, `fallback-locale=none`, statut publié, contrôle de date et de traduction complète.
- Données annexes d'un bloc : dans la préparation des blocs, dédupliquées, avec dégradation limitée au bloc.
- Transformations sans réseau dans `lib/`, testables en unitaire. Fichiers serveur suffixés `.server.ts`.
- Médias : uniquement via le helper et `<Media>` (**celuga-media-react**).
## Composants
- `components/<Nom>/index.tsx`, un dossier par composant. Table de rendu par slug de bloc, à laquelle s'ajoutent les rendus des modules actifs.
- Le même rendu sert la page publique et la preview : un composant de corps de page partagé, pas de seconde logique.
- `components/ui/` : shadcn, adapté à la charte, sans logique métier.
- `SiteLayout.astro` est le seul propriétaire du `<head>`. Chrome en `.astro` s'il est sans état ; JavaScript natif minimal sous `src/scripts/`.
## Îlots React
- Par défaut, un composant React rendu par Astro **sans directive** : zéro JavaScript.
- Directive explicite et justifiée : `client:visible` pour un formulaire ou un graphique, `client:load` pour la preview ou un consentement activé. Préférer HTML/CSS natifs (`<details>`, `popover`, scroll-snap, animations CSS) à un îlot.
- Preuve : inspecter le HTML de la route réelle ; un test de contrat vérifie que chaque bloc du catalogue rend sans `astro-island` ni `<script>`.
## Styles
- Tailwind 4 ; jetons dans `styles/global.css` (`@theme inline`, `@utility`), alignés sur `DESIGN.md`. Pas de valeur hexadécimale dans un composant.
- **Classes sur les composants plutôt que CSS global.** Le lint refuse les valeurs arbitraires dans les `.tsx` : passer par un jeton `@theme` ou une variable `:root` en `(--var)`, et par une variante `cva` sur un composant `ui`. Restent en CSS : jetons, base, utilitaires, clip-path et masques.
- Couleur principale réglable au rendu par valeurs vérifiées, avec texte noir ou blanc choisi par contraste.
- `prefers-reduced-motion` respecté pour toute animation. Une retouche sans changement visuel se prouve par captures à 0 pixel d'écart, suggestions aléatoires figées et images décodées.
## Sécurité et performance
- En-têtes dans le middleware : `nosniff`, `frame-ancestors` limité au site et au CMS, HSTS en HTTPS. Requête identifiée par `x-request-id`.
- Rendu compressé dans le middleware, Brotli **qualité 6** : `CompressionStream("brotli")` force la qualité 11 (bien plus lent pour un gain minime). JS et CSS précompressés au build.
- `scripts/ecoindex.mjs` mesure le front construit. **L'objectif du manifeste ne porte que sur l'accueil** ; les autres pages sont indicatives.
- Polices tierces bloquantes : charger après le premier rendu en `swap`, avec polices de repli calées. Lighthouse se mesure dans un profil vierge, sans extension, sur plusieurs passages, derrière une compression comme en production.
## Typage
- Types de documents importés du package de contrats, projetés par `Pick`/`Partial` si la réponse est partielle.
- Données externes (query string, corps de requête) validées à l'entrée. `.astro` non typés : garder leur logique dans des fichiers `.ts`.
## Checklist d'audit
- [ ] Lectures de contenu via le transport commun ; transports spécialisés justifiés et bornés.
- [ ] Pages limitées à l'orchestration ; cache déclaré dans `pages/`.
- [ ] Chaque directive `client:*` justifiée par un comportement navigateur.
- [ ] Chaque bloc a un rendu, une fixture et une couverture SEO.
- [ ] Aucun type de document recopié à la main.
- [ ] Aucune URL interne Docker dans le HTML.
CELUGA_SKILL_EOF
mkdir -p "$dir/references"
cat > "$dir/references/payload.md" <<'CELUGA_SKILL_EOF'
# Conventions Payload (`apps/cms`)
Pour l'API générique, charger le skill **payload** et confronter chaque point à la version installée.
## payload.config.ts
- `.env` racine chargé par un module dédié (`lib/load-env.ts`) ; les variables injectées restent prioritaires. Des gardes `required()` et `secret()` (32 caractères minimum) arrêtent le démarrage.
- `serverURL`, `cors` et `csrf` limités aux origines du site et du CMS. `routes.admin: "/"` : administration à la racine, `/admin` redirige.
- `postgresAdapter` avec `push: false`, `disableCreateDatabase: true`, `migrationDir`. Les migrations sont une opération explicite, pas un effet du démarrage.
- `typescript.outputFile` dans les contrats ; import map sous `src/app/_payload/`.
- `graphQL: { disable: true }` : le site lit le REST, GraphQL élargirait la surface sans usage.
- `upload.transformers: [sharpTransformer(...)]` et limite de taille explicite.
- `localization` native, `fallback: false`, langues filtrées selon le manifeste. Interface d'administration en français.
- Tâches de fond démarrées dans `onInit` seulement si `CELUGA_CMS_SERVER=1`, posé par `dev`, `start` et l'image : jamais pendant un build ou une migration.
- `/api/health` interroge réellement PostgreSQL et l'état des tâches de fond.
Les plugins qui enveloppent les accès ou journalisent passent après ceux qui ajoutent des collections (formulaires, recherche, SEO) ; le journal d'activité passe après les droits.
## Collections et globals
- Regroupées par domaine ; un module expose les siennes par son registre.
- Un contenu public a un descripteur dans les contrats. Le renommage d'un préfixe d'URL passe par ce descripteur (redirection 301, préfixe réservé).
- Contenu publié : versions et brouillons, statut par langue. Le front exige statut publié, `fallback-locale=none`, date passée et traduction complète.
- Disposition éditoriale en onglets sans nom technique : ils ne changent ni les chemins API ni le schéma.
- Payload 4 crée des tables implicites de versions et d'auteur : poser `versions ??= false` et `authorship ??= false` sur les entités qui n'en ont pas besoin.
- Pièces jointes de formulaire : collection privée, distincte de la médiathèque publique.
## Champs et blocs
- Primitives réutilisables (`text`, `link`, `image`, `hero`) dans `fields/primitives.ts`. `fields/index.ts` construit les blocs depuis le catalogue partagé et leur ajoute options de section, vignette et libellés de lignes.
- Le catalogue et les définitions de champs portent les mêmes identifiants.
- Localiser seulement les feuilles éditoriales : relations, structure des blocs et ordre restent communs.
- Identifiants Postgres tronqués à 63 caractères : garder des noms courts, sinon `dbName`.
- Un slug de bloc persisté crée des tables : ne pas le renommer sans migration qui conserve les données.
## Accès
- Accès toujours explicite dans la collection, avec des règles partagées de `lib/access.ts`. Un plugin de rôles peut ensuite envelopper chaque entité par niveaux (masqué, lecture, modification, suppression) ; les visiteurs gardent la règle d'origine.
- Le dernier administrateur ne peut être ni supprimé ni rétrogradé. En production, le premier compte se crée par une commande (`admin:create`), jamais par le formulaire public.
- Clés d'intégration : champs réservés aux administrateurs, chiffrés avec `PAYLOAD_SECRET`. Changer ce secret rend ces clés illisibles.
- Pas de `overrideAccess: true` dans une route publique. Endpoints POST : session, niveau et contrôle d'origine.
## Hooks
- Passer `req` à tout appel imbriqué : les écritures restent dans la transaction.
- Un autosave de brouillon n'invalide rien ; une dépublication arrive sans `draft=true` et invalide. `req.query.draft` vaut le booléen `true` sur une collection et le texte `"true"` sur un global : comparer via `String(...)`, et tester depuis l'administration, pas seulement en unitaire.
- Un changement de slug publié crée ou met à jour la redirection dans la même transaction.
- Prévoir un drapeau de contexte pour couper l'invalidation dans les scripts. Contrat complet : **celuga-astro-payload-cache**.
## Langues
`localization` native, un document par contenu, un slug et un statut par langue. Langue principale sans préfixe, autres langues sous `/<locale>/`. Une traduction incomplète retourne une 404 et disparaît des liens de langue et du sitemap. Ajouter une langue touche le type de locale, le dictionnaire, la localisation Payload et une migration : le manifeste seul ne suffit pas.
## Admin
- Composants référencés par chaîne (`./components/X#X`), puis `bun run generate` ; import map et arbre de routes sont générés.
- Chercher l'existant avant d'ajouter : tableau de bord, usages des médias, contrôle avant publication, journal d'activité, panneau de santé, export.
- Un moteur d'analyse lourd (Yoast) reste côté serveur : jamais dans le bundle d'administration ni le public.
## Pièges de Payload 4
- `@payloadcms/tanstack-start` : aucun import Next.js dans le code du projet. Routes dans `src/app`, fonctions serveur dans `app/_payload/server.functions.ts`.
- CLI : `bun --bun payload <cmd> --disable-transpile`.
- Formats d'images dans `variants` ; l'ancien `sizes` peut rester lisible : **celuga-media-react**.
- Ne pas ajouter de fonction dans `admin.importMap` : l'objet est copié dans la config client et casse le rendu RSC. Une icône omise de l'import map se déclare dans `admin.dependencies`.
- Montée de v3 vers v4 : sauvegarder base et fichiers, migrer dans un conteneur ponctuel, prévoir une reconnexion des sessions, tester aller et retour sur une base jetable.
## Schéma et migrations
Aucune synchronisation automatique, y compris avec `bun run dev`.
1. Modifier la configuration, puis `bun run generate`, puis corriger les consommateurs typés.
2. Sur une base jetable, `DATABASE_URL=<jetable> bun run migrate:create <nom>`. Relire le SQL : tables, index, contraintes, versions, champs localisés. Un renommage vu comme suppression + création perd les données : le réécrire.
3. Relire `migrations/index.ts`, tester base neuve et base représentative. Toute reprise de données est idempotente et préserve brouillons, publications et traductions.
4. Vérifier que `migrate:create --skip-empty` ne propose rien pour chaque composition de modules.
5. Appliquer seulement sur la cible désignée, après sauvegarde, par un conteneur ponctuel en production. Le CMS n'applique rien au démarrage.
**Base partagée :** si le CMS local et le back-office lisent une base distante non migrée, ajouter un champ persisté casse aussitôt toutes les lectures de cette collection. Prévenir, préparer champ et migration à part, et n'intégrer le champ qu'une fois la migration appliquée avec l'accord de l'utilisateur. Retirer un champ ne casse rien.
Une migration publiée n'est jamais modifiée. `schema:check` (`SELECT ... LIMIT 0` par table) contrôle en lecture que toutes les colonnes existent.
## Checklist d'audit
- [ ] `push: false`, migrations versionnées et relues, `schema:check` passe sur la cible.
- [ ] `cors`/`csrf` limités aux deux origines ; GraphQL désactivé.
- [ ] Chaque collection : accès explicite, `useAsTitle`, groupe admin, brouillons si publiée, descripteur si publique.
- [ ] Types et import map à jour (`bun run generate` ne produit aucun diff).
- [ ] Module désactivé absent de l'admin, de l'API et du rendu, schéma conservé.
- [ ] Aucun identifiant Postgres tronqué sans `dbName`.
CELUGA_SKILL_EOF
mkdir -p "$dir/references"
cat > "$dir/references/structure.md" <<'CELUGA_SKILL_EOF'
# Structure du monorepo
## Arborescence cible
```text
projet/
├── apps/
│ ├── cms/ # Payload 4 sur TanStack Start (Vite, RSC)
│ │ ├── server.mjs # production : srvx, fichiers précompressés, idleTimeout relevé
│ │ ├── vite.config.ts # withPayload + plugins rsc, tanstackStart, react
│ │ ├── scripts/ # precompress.mjs, trace-runtime.ts (node_modules de l'image)
│ │ ├── seed/ # contenus de démonstration
│ │ ├── storage/ # fichiers téléversés (volume en production), non versionné
│ │ └── src/
│ │ ├── app/ # routes TanStack : __root, _payload/ ($, api.$, index,
│ │ │ # server.functions, importMap.js), admin.$, robots[.]txt
│ │ ├── router.tsx, start.ts, routeTree.gen.ts # routeur (le dernier est généré)
│ │ ├── collections/ # regroupées par domaine (content.ts, media.ts, roles.ts…)
│ │ ├── components/ # composants d'administration référencés par chaîne
│ │ ├── endpoints/ # endpoints REST (forms, preview, search…)
│ │ ├── fields/ # index.ts (catalogue → blocs Payload), primitives.ts
│ │ ├── globals/index.ts # accueil, configuration, en-tête, pied de page
│ │ ├── hooks/content.ts # changed, deleted, globalChanged, tâche d'invalidation
│ │ ├── lib/ # access, env, load-env, email, background-tasks…
│ │ ├── migrations/ # générées et relues, avec index.ts
│ │ ├── modules/ # modules optionnels et leurs registres
│ │ ├── plugins/ # effets transversaux sur la configuration
│ │ ├── scripts/ # setup, seed, first-admin, check-schema…
│ │ ├── seo/ # assistant SEO (analyse côté serveur)
│ │ ├── tutorial/ # guide éditorial
│ │ └── payload.config.ts
│ └── web/ # Astro SSR
│ ├── server.mjs # production : adaptateur Node + JS/CSS précompressés
│ ├── astro.config.mjs, components.json, DESIGN.md
│ └── src/
│ ├── components/<Nom>/ # index.tsx ou index.astro, un dossier par composant
│ ├── components/ui/ # composants shadcn, modifiables
│ ├── layouts/SiteLayout.astro # seul propriétaire du <head>
│ ├── lib/ # cms, content-page, page-builder, media, seo, compression…
│ ├── modules/ # rendu et préparation des modules optionnels
│ ├── pages/ # routes publiques, preview, api, media, documents
│ ├── scripts/ # JavaScript natif du site
│ ├── styles/global.css # jetons @theme, @utility
│ └── middleware.ts
├── packages/site-contracts/src/ # contrats partagés CMS ↔ Web
│ ├── collections.ts, public-content.ts # registre des contenus publics
│ ├── content.ts, i18n.ts, cache.ts, seo.ts, schema.ts, project.ts…
│ ├── modules/ # contrats des modules optionnels
│ ├── server/ # secrets dérivés, jetons de preview (Node, serveur seulement)
│ └── generated/payload-types.ts # généré
├── tests/ # bun test, avec tests/fixtures/
├── e2e/ # Playwright
├── scripts/ # dev.ts, ecoindex.mjs, verify-architecture.mjs…
├── docs/ # déploiement, environnement, SEO, médias
├── .agents/skills/ # procédures livrées avec le projet
├── celuga.project.json # modules, langues, titre, objectif EcoIndex
├── compose*.yaml, Dockerfile.cms, Dockerfile.web
└── AGENTS.md, README.md, .env.example
```
Dans tous les skills Celuga, `components/<Nom>/index.tsx` vaut aussi pour `components/<Nom>.tsx` : suivre ce que le projet fait déjà. Les grands projets passent en dossiers, un projet neuf peut rester à plat.
## Où mettre quoi
| Je crée… | Emplacement |
| --- | --- |
| une collection publique | fichier sous `apps/cms/src/collections/`, ajouté à `payload.config.ts`, descripteur dans `collections.ts` des contrats |
| une capacité optionnelle | `modules/<module>` côté CMS, Web et contrats, plus les trois registres `modules/index.ts` |
| un global | `apps/cms/src/globals/` |
| un bloc du page builder | catalogue des contrats (`content.ts`), champs côté CMS, rendu côté Web, puis SEO, traduction et fixture |
| un champ utilisé par plusieurs configs | `apps/cms/src/fields/primitives.ts` |
| une règle d'accès partagée | `apps/cms/src/lib/access.ts` |
| un effet transversal sur la configuration | `apps/cms/src/plugins/<nom>.ts`, ajouté à `plugins` |
| un composant d'administration | `apps/cms/src/components/`, référencé par chaîne, puis `bun run generate` |
| un endpoint REST | `apps/cms/src/endpoints/<nom>.ts`, déclaré dans `endpoints` |
| un script sur les données | `apps/cms/src/scripts/<nom>.ts` + script `package.json` (`payload run --disable-transpile`) |
| un contrat ou une fonction pure partagés | `packages/site-contracts/src/<sujet>.ts` ; avec `node:*` : `server/` |
| une lecture Payload côté front | `apps/web/src/lib/`, via le transport commun, jamais un `fetch` isolé |
| une donnée annexe d'un bloc | préparation des blocs (`lib/page-builder.ts`), tag déclaré au catalogue |
| une route publique | `apps/web/src/pages/` ; son préfixe devient un slug réservé |
| un composant du site | `apps/web/src/components/<Nom>/index.tsx` |
| un composant UI générique | `apps/web/src/components/ui/` (via `bunx shadcn add`) |
| un test | `tests/<sujet>.test.ts(x)` ; parcours navigateur dans `e2e/` |
| une note durable | `docs/` |
## Règles d'import
- `apps/web` : relatifs ou alias du projet pour son code, package de contrats pour le reste. Seuls `@payloadcms/live-preview` et `@payloadcms/richtext-lexical` sont admis côté front.
- `apps/cms` : relatifs dans `src/`, package de contrats. Jamais `apps/web`.
- `packages/site-contracts` : aucun import d'app ni de framework serveur.
- Les fichiers `*.server.ts` ne sont importés que depuis des pages, endpoints, le middleware ou d'autres fichiers serveur.
## Racine du dépôt
Versionner : code, configs, `.env.example`, docs durables, `bun.lock`, `patches/`, migrations.
Ne pas versionner : `.env`, le stockage des uploads, les sorties (`dist`, `standalone`, `.astro`), les rapports (`playwright-report`, `test-results`), les caches.
## Outillage et commandes
Noms habituels, à confirmer dans le `package.json` :
- `bun run dev` lance CMS (Vite) et Astro ; `build` construit les deux puis mesure l'EcoIndex de l'accueil ; `start` sert les versions construites.
- `generate` : types et import map.
- `check` : `bun check` sur CMS, Web, tests et racine. `bun check` ignore les `.astro` : ils ne sont pas typés, limite acceptée.
- `lint` : oxlint avec `@shadcn/lint` sur le Web. Les valeurs arbitraires y sont refusées dans les `.tsx` : passer par des jetons `@theme` ou des variables en `(--var)`.
- `verify` : tests → lint → frontières → typage → builds. Elle n'applique aucune migration et ne remplace pas une recette navigateur.
- `test:e2e` : les configurations Playwright refusent les hôtes de production. Les tests sur base réelle exigent un nom de base jetable (`*_test`).
- `schema:check`, `migrate`, `migrate:create`, `seed`, `setup`, `admin:create`, `seo:backfill` : commandes Payload. Vérifier la cible de `DATABASE_URL` avant chacune : le `.env` peut viser une base partagée.
- Ne pas annoncer CI ou hooks installés sans vérifier leur présence dans le dépôt.
CELUGA_SKILL_EOF
mkdir -p "$HOME/.claude/skills" "$HOME/.codex/skills"
[ -e "$HOME/.claude/skills/celuga-architecture-payload" ] || ln -s "$dir" "$HOME/.claude/skills/celuga-architecture-payload"
[ -e "$HOME/.codex/skills/celuga-architecture-payload" ] || ln -s "$dir" "$HOME/.codex/skills/celuga-architecture-payload"
SKILL.md
---
name: celuga-architecture-payload
description: Architecture cible des projets Celuga Payload + Astro — monorepo `apps/cms` (Payload 4 sur TanStack Start) + `apps/web` (Astro SSR) + `packages/site-contracts`, registre de contenus publics, modules optionnels, emplacement de chaque fichier, conventions Payload (collections, globals, blocs, champs, hooks, accès, plugins), couche de données front, schéma par migrations, garde-fous qualité. Use when creating or scaffolding a Payload + Astro project, auditing or reviewing its architecture, deciding where a new file, collection, global, field, hook, endpoint, page or helper belongs, changing the database schema, or adding a dependency or workspace.
---
# Architecture Celuga Payload + Astro
Deux applications, une **frontière** nette : Payload possède le contenu, le schéma et l'administration ; Astro possède tout le HTML public. Elles ne communiquent que par HTTP (API REST, endpoints publics dédiés, invalidation) et partagent seulement un package de contrats. Chaque concept a **une seule source de vérité**.
Ce skill décrit la cible. Avant d'appliquer, lire `AGENTS.md`, `README.md`, les manifestes et les procédures `.agents/skills` du projet : ses versions et ses choix explicites priment. Un projet plus ancien peut avoir une autre disposition (`shared/`, `cms/` + `frontend/`) : transposer les règles sans déplacer ses fichiers, sauf demande.
| Besoin | Référence |
| --- | --- |
| arborescence, rôle de chaque dossier, règles d'import | [`references/structure.md`](references/structure.md) |
| collections, globals, blocs, champs, hooks, accès, plugins, schéma et migrations | [`references/payload.md`](references/payload.md) |
| pages Astro, couche de lecture, composants, îlots, styles, typage | [`references/front.md`](references/front.md) |
| noms et règles des variables d'environnement | [`references/environnement.md`](references/environnement.md) |
Hors périmètre, charger le skill dédié : **payload** (API générique, à confronter à la version installée), **celuga-astro-payload-cache** (tags, invalidation, preview, 503), **celuga-shadcnblock-integration** (bloc inspiré de shadcnblocks), **celuga-media-react** (médias, `<Media>`, proxy `/media`), **celuga-seo** (meta, sitemap, JSON-LD, assistant), **celuga-docker-payload-astro** (images et Compose), **mep-celuga** (déploiement et e-mail).
## Règles d'architecture
1. **Topologie** : `apps/cms`, `apps/web`, `packages/site-contracts`, `tests`, `e2e`, `scripts`, `docs`. Pas de troisième app ni de nouveau package sans second consommateur réel.
2. **Frontière vérifiée** : le Web n'importe jamais les sources du CMS ; les contrats n'importent ni une app ni un framework serveur. Un script du dépôt le contrôle (`verify:architecture`). Les types générés vivent dans les contrats. Le code partagé qui exige Node reste sous `server/`.
3. **Registre public** : routes, preview, SEO, sitemap, tags de cache, ordre admin et champs de traduction d'un contenu public se déclarent dans un descripteur partagé (`collections.ts` des contrats). Les consommateurs en dérivent leur comportement ; un contenu absent du registre est refusé. **Les droits restent explicites dans chaque collection** : le registre n'accorde aucun accès.
4. **Modules optionnels** : `celuga.project.json` choisit `modules` et `locales`. Un module désactivé garde ses tables (une seule chaîne de migrations pour toutes les compositions) mais disparaît de l'administration, de l'API, des routes et du rendu.
5. **Contenu → HTML** : le front lit Payload par REST, jamais par la Local API ni par la base. Tout contenu public est dans le HTML initial ; React n'est hydraté que pour un comportement navigateur indispensable.
6. **Types générés** : aucun type de document recopié à la main. `bun run generate` après tout changement de schéma.
7. **Schéma par migrations, partout** : `push: false`, y compris en développement. Un champ ajouté sans migration appliquée casse toutes les lectures, car Payload sélectionne chaque colonne. Migrations et recettes uniquement sur une base locale jetable ou sur la cible explicitement désignée.
8. **Bun seul** : installation, build, commandes Payload, serveurs et images. Typage par `bun check`, formatage par `bun fmt`, tests par `bun test`.
9. **Configuration par l'environnement** : un seul `.env` à la racine, noms de [`environnement.md`](references/environnement.md), sans alias. Aucun secret sous `PUBLIC_`. URL internes jamais exposées au navigateur.
10. **Simplicité** : pas d'abstraction pour un seul appelant, pas de barrel fourre-tout. Une fonction vit à côté de son unique consommateur jusqu'à son deuxième usage.
## Branches
### A. Créer ou porter un projet
Créer avec le générateur `create-celuga` plutôt qu'à la main : il pose les secrets, le manifeste, le lockfile et la chaîne de migrations. Pour un portage, comparer au template du générateur, puis brancher cache, médias et SEO avec les skills dédiés.
**Terminé quand :** la vérification complète du dépôt passe (`bun run verify` : tests, lint, frontières, typage, builds) et la chaîne de migrations s'applique sur une base neuve jetable.
### B. Ajouter une fonctionnalité
Placer chaque fichier avec la table « Où mettre quoi » de [`structure.md`](references/structure.md). Pour une collection, un bloc ou un schéma, suivre d'abord la procédure livrée dans le dépôt (`.agents/skills`). Si le projet a un guide éditorial et que le changement ajoute un geste particulier, le compléter après avoir vérifié les libellés à l'écran.
**Terminé quand :** chaque fichier a son emplacement justifié, la génération ne produit plus de diff, la migration relue est versionnée si le schéma change, la dépendance de cache est déclarée si un bloc lit une autre collection, et les contrôles du dépôt passent.
### C. Auditer
Parcourir les règles 1 à 10 et les références utiles :
```bash
bun run verify:architecture # frontières
bun run schema:check # lecture seule ; identifier d'abord la base visée
rg "@payloadcms|from \"payload\"" apps/web/src # côté front, seuls live-preview et richtext-lexical sont admis
rg "client:(visible|load|idle|only)" apps/web/src # chaque îlot justifié
rg "overrideAccess: true" apps/cms/src # chaque usage justifié, jamais pour une lecture publique
```
**Terminé quand :** chaque règle est notée conforme ou écart, chaque écart a un fichier, une ligne, un risque concret et un correctif, classés par gravité (données/sécurité → prod exposée → lisibilité).
agents/openai.yaml
interface:
display_name: "Celuga Architecture Payload"
short_description: "Architecture cible Payload + Astro : structure, conventions, migrations"
default_prompt: "Utilise $celuga-architecture-payload pour auditer ou structurer l'architecture Payload + Astro de ce projet."
references/environnement.md
# Variables d'environnement Payload + Astro
Lire `.env.example`, les lecteurs du code et le Compose du projet, jamais les valeurs du `.env`. Garder ces noms lors d'un portage.
## Contrat
| Variable | Lecteur | Comportement |
| --- | --- | --- |
| `DATABASE_URL`, `PAYLOAD_SECRET` | CMS | Requis. Jamais transmis au Web ni à ses arguments de build. `PAYLOAD_SECRET` protège sessions et clés chiffrées : le conserver à vie sur une installation existante. |
| `SITE_SECRET` | CMS, Web | Requis, 32 caractères minimum, identique des deux côtés, distinct de `PAYLOAD_SECRET`. Le code en dérive par HMAC-SHA-256 une clé par usage (`cache`, `preview`, `form`). |
| `PUBLIC_SITE_URL` | CMS, Web | Origine publique complète avec `https://` (sans schéma, le CMS refuse de démarrer). Canonical, sitemap, preview, OG. |
| `PUBLIC_PAYLOAD_URL` | CMS, Web | Origine publique du CMS : administration, live preview. |
| `PAYLOAD_INTERNAL_URL` | Web serveur | URL du CMS sur le réseau interne ; repli sur `PUBLIC_PAYLOAD_URL`. Fixée par Compose. |
| `WEB_INTERNAL_URL` | CMS | URL d'invalidation ; repli sur `PUBLIC_SITE_URL`. Fixée par Compose. |
| `PUBLIC_ALLOW_INDEXING` | Web, CMS | `true` seulement sur le domaine de production. Défaut `false`. Passe aussi au build : redéployer après changement. |
| `EMAIL_HOST`, `EMAIL_USER` | CMS | Requis, sans valeur par défaut. `EMAIL_USER` : adresse complète, connexion et expéditeur. |
| `EMAIL_PASS` | CMS | Vide : aucune authentification SMTP (relais autorisé par IP, Mailpit). Renseigné : authentification. |
| `EMAIL_PORT` | CMS | Entier, défaut 587 avec STARTTLS obligatoire ; 465 : TLS direct. |
| `FORM_NOTIFICATION_TO` | CMS | Destinataires, séparés par des virgules ; défaut `EMAIL_USER`. |
| `TRUST_PROXY`, `TRUSTED_PROXY_IPS` | Web | `X-Forwarded-For` lu seulement si le pair direct est de confiance. En Docker, liste vide : CIDR privés réellement attachés au conteneur. |
| `FIRST_ADMIN_EMAIL`, `FIRST_ADMIN_PASSWORD` | Commande | Pour `admin:create` ou `setup` en production ; 16 caractères minimum ; à retirer ensuite. |
| `POSTGRES_PASSWORD`, `POSTGRES_PORT`, `MAILPIT_PORT`, `CMS_PORT`, `WEB_PORT` | Compose local | Base et ports locaux. |
| `SHADCNBLOCKS_API_KEY` | CLI shadcn | Développement seul, jamais dans les conteneurs. |
Fixés par le code ou les images, à ne pas mettre dans `.env` : `NODE_ENV`, `ASTRO_BUNDLE_DEPS`, `CELUGA_CMS_SERVER`.
Noms retirés, à ne pas réintroduire : `CACHE_SECRET`, `PREVIEW_SECRET`, `FORM_SECRET` (remplacés par `SITE_SECRET`) ; `SMTP_*` (remplacés par `EMAIL_*`) ; `PAYLOAD_PUBLIC_*`, `CACHE_INVALIDATION_SECRET`, `PAYLOAD_ENABLE_PUSH`. Sur un projet plus ancien, vérifier ce que son code lit réellement avant de renommer. Migrer les anciens secrets : ajouter `SITE_SECRET`, supprimer les trois anciens, redéployer CMS et Web ensemble, régénérer les liens de preview.
## Chargement
- CMS : `lib/load-env.ts` charge le `.env` racine par chemin absolu, sans écraser les variables injectées. Secrets validés au démarrage.
- Web : `vite.envDir` vers la racine, lecture serveur `process.env || import.meta.env` via `lib/env.server.ts`.
- Les `PUBLIC_*` sont aussi figées au build Vite. Une valeur saisie seulement au runtime peut ne pas atteindre le code client.
- En Docker, seul ce que Compose référence arrive au conteneur : renseigner une option facultative dans `.env` ne la transmet pas, il faut un override.
## Vérification
Aucun secret sous `PUBLIC_`. Secrets aléatoires propres à chaque environnement (`openssl rand -hex 32`). Un `.env` de développement ne vise jamais la production. Pour comparer deux valeurs, rapporter seulement présence ou égalité. Les URL internes ne sortent jamais dans le HTML, la canonical, l'OG ou la preview.
references/front.md
# Conventions front (`apps/web`)
## Configuration Astro
- `output: "server"`, adaptateur `@astrojs/node` standalone avec `bodySizeLimit` borné, intégrations React et Tailwind 4 (`@tailwindcss/vite`).
- `vite.envDir` vers la racine (un seul `.env`), `dedupe` de React. `ssr.noExternal` seulement quand `ASTRO_BUNDLE_DEPS=true` (image Docker).
- Cache : **celuga-astro-payload-cache**.
## Pages
Une page Astro **orchestre**, elle ne calcule pas :
1. lire les paramètres de route et la langue ;
2. charger via un chargeur commun (`lib/content-page.ts`) : redirection, document, chrome et traductions ;
3. retourner la redirection, ou rendre la 404 ;
4. déclarer le cache **dans le fichier de `pages/`** : `Astro.cache.set` dans un composant enfant n'a pas d'effet fiable ;
5. rendre le composant de page et les blocs préparés.
Une route libre `[...slug].astro` résout le contenu par le registre public ; une route dédiée n'existe que pour un besoin propre. Endpoints privés sous `api/`, proxys sous `media/` et `documents/`.
## Couche de données
- Transport commun (`lib/cms.ts`) : URL interne ou publique, délai, erreur typée, déduplication par requête.
- Requêtes REST avec `depth` minimal, `select` ou `populate` : sans `populate[pages][slug]=true`, chaque lien de menu ramène la page liée entière et ses médias.
- Lecture publique : `locale` explicite, `fallback-locale=none`, statut publié, contrôle de date et de traduction complète.
- Données annexes d'un bloc : dans la préparation des blocs, dédupliquées, avec dégradation limitée au bloc.
- Transformations sans réseau dans `lib/`, testables en unitaire. Fichiers serveur suffixés `.server.ts`.
- Médias : uniquement via le helper et `<Media>` (**celuga-media-react**).
## Composants
- `components/<Nom>/index.tsx`, un dossier par composant. Table de rendu par slug de bloc, à laquelle s'ajoutent les rendus des modules actifs.
- Le même rendu sert la page publique et la preview : un composant de corps de page partagé, pas de seconde logique.
- `components/ui/` : shadcn, adapté à la charte, sans logique métier.
- `SiteLayout.astro` est le seul propriétaire du `<head>`. Chrome en `.astro` s'il est sans état ; JavaScript natif minimal sous `src/scripts/`.
## Îlots React
- Par défaut, un composant React rendu par Astro **sans directive** : zéro JavaScript.
- Directive explicite et justifiée : `client:visible` pour un formulaire ou un graphique, `client:load` pour la preview ou un consentement activé. Préférer HTML/CSS natifs (`<details>`, `popover`, scroll-snap, animations CSS) à un îlot.
- Preuve : inspecter le HTML de la route réelle ; un test de contrat vérifie que chaque bloc du catalogue rend sans `astro-island` ni `<script>`.
## Styles
- Tailwind 4 ; jetons dans `styles/global.css` (`@theme inline`, `@utility`), alignés sur `DESIGN.md`. Pas de valeur hexadécimale dans un composant.
- **Classes sur les composants plutôt que CSS global.** Le lint refuse les valeurs arbitraires dans les `.tsx` : passer par un jeton `@theme` ou une variable `:root` en `(--var)`, et par une variante `cva` sur un composant `ui`. Restent en CSS : jetons, base, utilitaires, clip-path et masques.
- Couleur principale réglable au rendu par valeurs vérifiées, avec texte noir ou blanc choisi par contraste.
- `prefers-reduced-motion` respecté pour toute animation. Une retouche sans changement visuel se prouve par captures à 0 pixel d'écart, suggestions aléatoires figées et images décodées.
## Sécurité et performance
- En-têtes dans le middleware : `nosniff`, `frame-ancestors` limité au site et au CMS, HSTS en HTTPS. Requête identifiée par `x-request-id`.
- Rendu compressé dans le middleware, Brotli **qualité 6** : `CompressionStream("brotli")` force la qualité 11 (bien plus lent pour un gain minime). JS et CSS précompressés au build.
- `scripts/ecoindex.mjs` mesure le front construit. **L'objectif du manifeste ne porte que sur l'accueil** ; les autres pages sont indicatives.
- Polices tierces bloquantes : charger après le premier rendu en `swap`, avec polices de repli calées. Lighthouse se mesure dans un profil vierge, sans extension, sur plusieurs passages, derrière une compression comme en production.
## Typage
- Types de documents importés du package de contrats, projetés par `Pick`/`Partial` si la réponse est partielle.
- Données externes (query string, corps de requête) validées à l'entrée. `.astro` non typés : garder leur logique dans des fichiers `.ts`.
## Checklist d'audit
- [ ] Lectures de contenu via le transport commun ; transports spécialisés justifiés et bornés.
- [ ] Pages limitées à l'orchestration ; cache déclaré dans `pages/`.
- [ ] Chaque directive `client:*` justifiée par un comportement navigateur.
- [ ] Chaque bloc a un rendu, une fixture et une couverture SEO.
- [ ] Aucun type de document recopié à la main.
- [ ] Aucune URL interne Docker dans le HTML.
references/payload.md
# Conventions Payload (`apps/cms`)
Pour l'API générique, charger le skill **payload** et confronter chaque point à la version installée.
## payload.config.ts
- `.env` racine chargé par un module dédié (`lib/load-env.ts`) ; les variables injectées restent prioritaires. Des gardes `required()` et `secret()` (32 caractères minimum) arrêtent le démarrage.
- `serverURL`, `cors` et `csrf` limités aux origines du site et du CMS. `routes.admin: "/"` : administration à la racine, `/admin` redirige.
- `postgresAdapter` avec `push: false`, `disableCreateDatabase: true`, `migrationDir`. Les migrations sont une opération explicite, pas un effet du démarrage.
- `typescript.outputFile` dans les contrats ; import map sous `src/app/_payload/`.
- `graphQL: { disable: true }` : le site lit le REST, GraphQL élargirait la surface sans usage.
- `upload.transformers: [sharpTransformer(...)]` et limite de taille explicite.
- `localization` native, `fallback: false`, langues filtrées selon le manifeste. Interface d'administration en français.
- Tâches de fond démarrées dans `onInit` seulement si `CELUGA_CMS_SERVER=1`, posé par `dev`, `start` et l'image : jamais pendant un build ou une migration.
- `/api/health` interroge réellement PostgreSQL et l'état des tâches de fond.
Les plugins qui enveloppent les accès ou journalisent passent après ceux qui ajoutent des collections (formulaires, recherche, SEO) ; le journal d'activité passe après les droits.
## Collections et globals
- Regroupées par domaine ; un module expose les siennes par son registre.
- Un contenu public a un descripteur dans les contrats. Le renommage d'un préfixe d'URL passe par ce descripteur (redirection 301, préfixe réservé).
- Contenu publié : versions et brouillons, statut par langue. Le front exige statut publié, `fallback-locale=none`, date passée et traduction complète.
- Disposition éditoriale en onglets sans nom technique : ils ne changent ni les chemins API ni le schéma.
- Payload 4 crée des tables implicites de versions et d'auteur : poser `versions ??= false` et `authorship ??= false` sur les entités qui n'en ont pas besoin.
- Pièces jointes de formulaire : collection privée, distincte de la médiathèque publique.
## Champs et blocs
- Primitives réutilisables (`text`, `link`, `image`, `hero`) dans `fields/primitives.ts`. `fields/index.ts` construit les blocs depuis le catalogue partagé et leur ajoute options de section, vignette et libellés de lignes.
- Le catalogue et les définitions de champs portent les mêmes identifiants.
- Localiser seulement les feuilles éditoriales : relations, structure des blocs et ordre restent communs.
- Identifiants Postgres tronqués à 63 caractères : garder des noms courts, sinon `dbName`.
- Un slug de bloc persisté crée des tables : ne pas le renommer sans migration qui conserve les données.
## Accès
- Accès toujours explicite dans la collection, avec des règles partagées de `lib/access.ts`. Un plugin de rôles peut ensuite envelopper chaque entité par niveaux (masqué, lecture, modification, suppression) ; les visiteurs gardent la règle d'origine.
- Le dernier administrateur ne peut être ni supprimé ni rétrogradé. En production, le premier compte se crée par une commande (`admin:create`), jamais par le formulaire public.
- Clés d'intégration : champs réservés aux administrateurs, chiffrés avec `PAYLOAD_SECRET`. Changer ce secret rend ces clés illisibles.
- Pas de `overrideAccess: true` dans une route publique. Endpoints POST : session, niveau et contrôle d'origine.
## Hooks
- Passer `req` à tout appel imbriqué : les écritures restent dans la transaction.
- Un autosave de brouillon n'invalide rien ; une dépublication arrive sans `draft=true` et invalide. `req.query.draft` vaut le booléen `true` sur une collection et le texte `"true"` sur un global : comparer via `String(...)`, et tester depuis l'administration, pas seulement en unitaire.
- Un changement de slug publié crée ou met à jour la redirection dans la même transaction.
- Prévoir un drapeau de contexte pour couper l'invalidation dans les scripts. Contrat complet : **celuga-astro-payload-cache**.
## Langues
`localization` native, un document par contenu, un slug et un statut par langue. Langue principale sans préfixe, autres langues sous `/<locale>/`. Une traduction incomplète retourne une 404 et disparaît des liens de langue et du sitemap. Ajouter une langue touche le type de locale, le dictionnaire, la localisation Payload et une migration : le manifeste seul ne suffit pas.
## Admin
- Composants référencés par chaîne (`./components/X#X`), puis `bun run generate` ; import map et arbre de routes sont générés.
- Chercher l'existant avant d'ajouter : tableau de bord, usages des médias, contrôle avant publication, journal d'activité, panneau de santé, export.
- Un moteur d'analyse lourd (Yoast) reste côté serveur : jamais dans le bundle d'administration ni le public.
## Pièges de Payload 4
- `@payloadcms/tanstack-start` : aucun import Next.js dans le code du projet. Routes dans `src/app`, fonctions serveur dans `app/_payload/server.functions.ts`.
- CLI : `bun --bun payload <cmd> --disable-transpile`.
- Formats d'images dans `variants` ; l'ancien `sizes` peut rester lisible : **celuga-media-react**.
- Ne pas ajouter de fonction dans `admin.importMap` : l'objet est copié dans la config client et casse le rendu RSC. Une icône omise de l'import map se déclare dans `admin.dependencies`.
- Montée de v3 vers v4 : sauvegarder base et fichiers, migrer dans un conteneur ponctuel, prévoir une reconnexion des sessions, tester aller et retour sur une base jetable.
## Schéma et migrations
Aucune synchronisation automatique, y compris avec `bun run dev`.
1. Modifier la configuration, puis `bun run generate`, puis corriger les consommateurs typés.
2. Sur une base jetable, `DATABASE_URL=<jetable> bun run migrate:create <nom>`. Relire le SQL : tables, index, contraintes, versions, champs localisés. Un renommage vu comme suppression + création perd les données : le réécrire.
3. Relire `migrations/index.ts`, tester base neuve et base représentative. Toute reprise de données est idempotente et préserve brouillons, publications et traductions.
4. Vérifier que `migrate:create --skip-empty` ne propose rien pour chaque composition de modules.
5. Appliquer seulement sur la cible désignée, après sauvegarde, par un conteneur ponctuel en production. Le CMS n'applique rien au démarrage.
**Base partagée :** si le CMS local et le back-office lisent une base distante non migrée, ajouter un champ persisté casse aussitôt toutes les lectures de cette collection. Prévenir, préparer champ et migration à part, et n'intégrer le champ qu'une fois la migration appliquée avec l'accord de l'utilisateur. Retirer un champ ne casse rien.
Une migration publiée n'est jamais modifiée. `schema:check` (`SELECT ... LIMIT 0` par table) contrôle en lecture que toutes les colonnes existent.
## Checklist d'audit
- [ ] `push: false`, migrations versionnées et relues, `schema:check` passe sur la cible.
- [ ] `cors`/`csrf` limités aux deux origines ; GraphQL désactivé.
- [ ] Chaque collection : accès explicite, `useAsTitle`, groupe admin, brouillons si publiée, descripteur si publique.
- [ ] Types et import map à jour (`bun run generate` ne produit aucun diff).
- [ ] Module désactivé absent de l'admin, de l'API et du rendu, schéma conservé.
- [ ] Aucun identifiant Postgres tronqué sans `dbName`.
references/structure.md
# Structure du monorepo
## Arborescence cible
```text
projet/
├── apps/
│ ├── cms/ # Payload 4 sur TanStack Start (Vite, RSC)
│ │ ├── server.mjs # production : srvx, fichiers précompressés, idleTimeout relevé
│ │ ├── vite.config.ts # withPayload + plugins rsc, tanstackStart, react
│ │ ├── scripts/ # precompress.mjs, trace-runtime.ts (node_modules de l'image)
│ │ ├── seed/ # contenus de démonstration
│ │ ├── storage/ # fichiers téléversés (volume en production), non versionné
│ │ └── src/
│ │ ├── app/ # routes TanStack : __root, _payload/ ($, api.$, index,
│ │ │ # server.functions, importMap.js), admin.$, robots[.]txt
│ │ ├── router.tsx, start.ts, routeTree.gen.ts # routeur (le dernier est généré)
│ │ ├── collections/ # regroupées par domaine (content.ts, media.ts, roles.ts…)
│ │ ├── components/ # composants d'administration référencés par chaîne
│ │ ├── endpoints/ # endpoints REST (forms, preview, search…)
│ │ ├── fields/ # index.ts (catalogue → blocs Payload), primitives.ts
│ │ ├── globals/index.ts # accueil, configuration, en-tête, pied de page
│ │ ├── hooks/content.ts # changed, deleted, globalChanged, tâche d'invalidation
│ │ ├── lib/ # access, env, load-env, email, background-tasks…
│ │ ├── migrations/ # générées et relues, avec index.ts
│ │ ├── modules/ # modules optionnels et leurs registres
│ │ ├── plugins/ # effets transversaux sur la configuration
│ │ ├── scripts/ # setup, seed, first-admin, check-schema…
│ │ ├── seo/ # assistant SEO (analyse côté serveur)
│ │ ├── tutorial/ # guide éditorial
│ │ └── payload.config.ts
│ └── web/ # Astro SSR
│ ├── server.mjs # production : adaptateur Node + JS/CSS précompressés
│ ├── astro.config.mjs, components.json, DESIGN.md
│ └── src/
│ ├── components/<Nom>/ # index.tsx ou index.astro, un dossier par composant
│ ├── components/ui/ # composants shadcn, modifiables
│ ├── layouts/SiteLayout.astro # seul propriétaire du <head>
│ ├── lib/ # cms, content-page, page-builder, media, seo, compression…
│ ├── modules/ # rendu et préparation des modules optionnels
│ ├── pages/ # routes publiques, preview, api, media, documents
│ ├── scripts/ # JavaScript natif du site
│ ├── styles/global.css # jetons @theme, @utility
│ └── middleware.ts
├── packages/site-contracts/src/ # contrats partagés CMS ↔ Web
│ ├── collections.ts, public-content.ts # registre des contenus publics
│ ├── content.ts, i18n.ts, cache.ts, seo.ts, schema.ts, project.ts…
│ ├── modules/ # contrats des modules optionnels
│ ├── server/ # secrets dérivés, jetons de preview (Node, serveur seulement)
│ └── generated/payload-types.ts # généré
├── tests/ # bun test, avec tests/fixtures/
├── e2e/ # Playwright
├── scripts/ # dev.ts, ecoindex.mjs, verify-architecture.mjs…
├── docs/ # déploiement, environnement, SEO, médias
├── .agents/skills/ # procédures livrées avec le projet
├── celuga.project.json # modules, langues, titre, objectif EcoIndex
├── compose*.yaml, Dockerfile.cms, Dockerfile.web
└── AGENTS.md, README.md, .env.example
```
Dans tous les skills Celuga, `components/<Nom>/index.tsx` vaut aussi pour `components/<Nom>.tsx` : suivre ce que le projet fait déjà. Les grands projets passent en dossiers, un projet neuf peut rester à plat.
## Où mettre quoi
| Je crée… | Emplacement |
| --- | --- |
| une collection publique | fichier sous `apps/cms/src/collections/`, ajouté à `payload.config.ts`, descripteur dans `collections.ts` des contrats |
| une capacité optionnelle | `modules/<module>` côté CMS, Web et contrats, plus les trois registres `modules/index.ts` |
| un global | `apps/cms/src/globals/` |
| un bloc du page builder | catalogue des contrats (`content.ts`), champs côté CMS, rendu côté Web, puis SEO, traduction et fixture |
| un champ utilisé par plusieurs configs | `apps/cms/src/fields/primitives.ts` |
| une règle d'accès partagée | `apps/cms/src/lib/access.ts` |
| un effet transversal sur la configuration | `apps/cms/src/plugins/<nom>.ts`, ajouté à `plugins` |
| un composant d'administration | `apps/cms/src/components/`, référencé par chaîne, puis `bun run generate` |
| un endpoint REST | `apps/cms/src/endpoints/<nom>.ts`, déclaré dans `endpoints` |
| un script sur les données | `apps/cms/src/scripts/<nom>.ts` + script `package.json` (`payload run --disable-transpile`) |
| un contrat ou une fonction pure partagés | `packages/site-contracts/src/<sujet>.ts` ; avec `node:*` : `server/` |
| une lecture Payload côté front | `apps/web/src/lib/`, via le transport commun, jamais un `fetch` isolé |
| une donnée annexe d'un bloc | préparation des blocs (`lib/page-builder.ts`), tag déclaré au catalogue |
| une route publique | `apps/web/src/pages/` ; son préfixe devient un slug réservé |
| un composant du site | `apps/web/src/components/<Nom>/index.tsx` |
| un composant UI générique | `apps/web/src/components/ui/` (via `bunx shadcn add`) |
| un test | `tests/<sujet>.test.ts(x)` ; parcours navigateur dans `e2e/` |
| une note durable | `docs/` |
## Règles d'import
- `apps/web` : relatifs ou alias du projet pour son code, package de contrats pour le reste. Seuls `@payloadcms/live-preview` et `@payloadcms/richtext-lexical` sont admis côté front.
- `apps/cms` : relatifs dans `src/`, package de contrats. Jamais `apps/web`.
- `packages/site-contracts` : aucun import d'app ni de framework serveur.
- Les fichiers `*.server.ts` ne sont importés que depuis des pages, endpoints, le middleware ou d'autres fichiers serveur.
## Racine du dépôt
Versionner : code, configs, `.env.example`, docs durables, `bun.lock`, `patches/`, migrations.
Ne pas versionner : `.env`, le stockage des uploads, les sorties (`dist`, `standalone`, `.astro`), les rapports (`playwright-report`, `test-results`), les caches.
## Outillage et commandes
Noms habituels, à confirmer dans le `package.json` :
- `bun run dev` lance CMS (Vite) et Astro ; `build` construit les deux puis mesure l'EcoIndex de l'accueil ; `start` sert les versions construites.
- `generate` : types et import map.
- `check` : `bun check` sur CMS, Web, tests et racine. `bun check` ignore les `.astro` : ils ne sont pas typés, limite acceptée.
- `lint` : oxlint avec `@shadcn/lint` sur le Web. Les valeurs arbitraires y sont refusées dans les `.tsx` : passer par des jetons `@theme` ou des variables en `(--var)`.
- `verify` : tests → lint → frontières → typage → builds. Elle n'applique aucune migration et ne remplace pas une recette navigateur.
- `test:e2e` : les configurations Playwright refusent les hôtes de production. Les tests sur base réelle exigent un nom de base jetable (`*_test`).
- `schema:check`, `migrate`, `migrate:create`, `seed`, `setup`, `admin:create`, `seo:backfill` : commandes Payload. Vérifier la cible de `DATABASE_URL` avant chacune : le `.env` peut viser une base partagée.
- Ne pas annoncer CI ou hooks installés sans vérifier leur présence dans le dépôt.