# create-wp-reactor

## 0.17.3

### Patch Changes

- 1cf9bcd: Le cache des assets WordPress revalide au lieu d'être `immutable`.

  Les labels Traefik du compose servaient tout `/wp-includes` et `/wp-content` en
  `max-age=31536000, immutable`. Le core WordPress est pourtant versionné par `?ver=`, pas
  par le nom de fichier — et ce `?ver=` ne change pas pour les fichiers qu'une release ne
  touche pas. `private-apis.min.js` a gardé le sien d'une majeure à l'autre : en
  `immutable`, le navigateur ne revalide jamais et garde l'ancien, face à un
  `block-editor.min.js` neuf dont le hash a bougé.

  Résultat, après une montée de core, l'éditeur Gutenberg ne démarre plus :

  ```
  private-apis.min.js: Uncaught Error: Cannot unlock an undefined object
  wp-edit-post-js-after: Cannot read properties of undefined (reading 'initializeEditor')
  ```

  Le serveur, lui, est cohérent — seul le navigateur mélange deux versions. Un rechargement
  forcé débloque, mais chaque éditeur retombe dedans à la montée suivante.

  `immutable` est désormais réservé à `/wp-content/themes/*/build/`, dont les noms de
  fichiers sont réellement fingerprintés (`assets/<name>-<hash>.js`). Le reste passe en
  `max-age=600, stale-while-revalidate=86400`.

## 0.17.2

### Patch Changes

- b94529a: Le cache HTML passe sous `@wp-reactor/headless-core/server` — la barrel racine ne doit pas
  dépendre d'`ioredis`.

  0.9.0 exportait `createHtmlCacheStore` depuis la racine, or celle-ci est importée par des
  composants clients (provider GraphQL, hooks). Vite pré-bundlait donc `ioredis` pour le
  navigateur, qui refusait le module :

  ```
  Uncaught SyntaxError: The requested module '.../ioredis/built/index.js' does not provide
  an export named 'default'
  ```

  Deux corrections, l'une sans l'autre ne suffit pas :

  1. Les trois fabriques (`createHtmlCacheStore`, `createHtmlCacheMiddleware`,
     `handleHtmlCachePurgeRequest`) vivent désormais dans le sous-chemin `/server`.
     `createCacheControlMiddleware` y est ré-exporté par cohérence, mais reste disponible à
     la racine où il était déjà publié.
  2. **`ioredis` est chargé en import dynamique**, à la première connexion. Le sous-chemin
     seul ne règle rien : la coque instancie le store depuis `start.ts`, que TanStack Start
     charge AUSSI côté client — la chaîne
     `client-entry → start.ts → /server → store.ts → ioredis` reste donc dans le graphe
     navigateur. En dynamique, elle n'y est plus qu'en arête jamais parcourue : côté client
     `REDIS_URL` est indéfini, la branche de connexion n'est pas atteinte.

  Corollaire : `isEnabled()` ne teste plus la connexion mais la seule présence de l'URL, et
  le client interne est mémoïsé sous forme de promesse.

  **Migration** (0.9.0 → 0.10.0), uniquement pour qui avait déjà branché le cache :

  ```diff
  -import { createHtmlCacheStore } from "@wp-reactor/headless-core"
  +import { createHtmlCacheStore } from "@wp-reactor/headless-core/server"
  ```

## 0.17.1

### Patch Changes

- 3422421: La CI redéploie WordPress via `POST /api/v1/services/{uuid}/restart?latest=true` au lieu de
  `GET /api/v1/deploy?uuid=…&force=true`.

  WordPress est scaffoldé en ressource **Docker Compose** (« service ») côté Coolify. Sur un
  service, `/api/v1/deploy` exécute `StartService::run($resource)` avec `pullLatestImages: false`
  — donc `docker compose up -d --force-recreate` **sans `docker compose pull`**. Le tag est
  mutable (`:main` / `:development`) et déjà présent sur l'hôte : le conteneur redémarrait sur
  l'image tirée au provisioning, et chaque push laissait WordPress figé pendant que la webapp,
  elle, une _application_ Coolify, se mettait bien à jour. `force=true` ne vaut que pour les
  applications.

  `RestartService` est le seul chemin API qui passe `pullLatestImages: true` à `StartService`.

  Nouvel input `coolify-resource` sur le workflow réutilisable (`application` par défaut, donc
  sans effet sur la webapp) ; le job `wordpress` passe `service`.

## 0.17.0

### Minor Changes

- 13be248: Cache HTML côté origine (Redis) : une page déjà vue ne peut plus répondre 502.

  La résilience au redéploiement reposait jusqu'ici sur Cloudflare
  (`s-maxage` + `stale-while-revalidate`). Beaucoup de clients n'ont pas de CDN, et
  WordPress tourne en ressource Compose Coolify — donc sans rolling update : chaque
  redéploiement coupe `/graphql` une dizaine de secondes, et toute page pas encore à
  l'edge partait en 502. Redis, lui, est déjà provisionné sur chaque stack pour
  l'object cache WordPress.

  Le kernel expose donc `createHtmlCacheStore`, `createHtmlCacheMiddleware` et
  `handleHtmlCachePurgeRequest`. Le middleware se monte en dernier dans
  `start.ts` : sur un HIT il court-circuite le rendu SSR et tout appel WPGraphQL.

  Chaque page vit sous deux clés — le HTML (TTL 24 h) et un marqueur de fraîcheur
  (TTL 1 h). Marqueur expiré ⇒ un rendu est tenté, et s'il échoue l'ancien HTML est
  servi (`x-html-cache: STALE`) au lieu d'une 502. **Purger, c'est donc périmer** :
  le webhook ne supprime que les marqueurs, sinon la purge de déploiement rouvrirait
  la fenêtre de 502 au pire moment.

  Côté scaffolding : route `/api/cache/purge`, store de la coque, `clear-cache.mjs`
  qui périme les deux étages, et `--provision` qui pose `REDIS_URL` +
  `CACHE_PURGE_SECRET` sur la webapp et WordPress.

  Le plugin de base `wp-reactor-headless` (1.3.0) purge les chemins touchés à chaque
  publication et ajoute un bouton _Purger le cache HTML_ dans la barre d'admin —
  inerte sans `CACHE_PURGE_SECRET`. **L'image WordPress de base doit être
  reconstruite** pour que les clients en profitent.

  `REDIS_URL` absent ⇒ cache désactivé, comportement inchangé.

## 0.16.0

### Minor Changes

- 8fe6de0: `X-Forwarded-Proto` suit le schéma de l'URL publique WordPress au lieu d'être figé à `https`.

  `ssrForwardedHeaders()` annonçait `https` en dur sur **toutes** les requêtes GraphQL serveur
  (SSR public + clients authentifiés). Le `auto_prepend_file` de l'image WordPress traduit cet
  en-tête en `$_SERVER['HTTPS'] = 'on'`, donc `is_ssl()` devient vrai et WordPress réécrit les
  URLs de médias en `https://`. En dev local, le backend est servi en clair
  (`http://localhost:8890`) : le HTML SSR embarquait des `<img src="https://localhost:8890/…">`
  et des `<link rel="preload" as="image">` vers un hôte sans TLS — images cassées jusqu'à
  l'hydratation (le client, lui, n'envoie pas l'en-tête).

  `RuntimeEnv` expose désormais `wordpressProtocol` (`"http" | "https"`), dérivé de
  `VITE_WORDPRESS_URL` — la **même** source que `wordpressHost`, c'est-à-dire l'URL publique et
  jamais `WORDPRESS_INTERNAL_URL`. Un déploiement public en https qui joint WordPress par une URL
  interne en clair continue donc d'annoncer `https`, et `https` reste le défaut si l'URL publique
  est illisible.

  **Breaking** : `ssrForwardedHeaders(wordpressHost)` devient `ssrForwardedHeaders(env)`.

  ```diff
  -const headers: Record<string, string> = ssrForwardedHeaders(runtimeEnv.wordpressHost)
  +const headers: Record<string, string> = ssrForwardedHeaders(runtimeEnv)
  ```

  Le `cartMiddleware` du template client est mis à jour.

## 0.15.0

### Minor Changes

- de3f79d: `buildRankMathHead` sait réancrer le SEO sur le domaine public (`rebaseOrigin`) et garantir un
  slash final sur le canonical (`canonicalTrailingSlash: "always"`).

  En headless, WordPress vit sur un domaine backend (`wp.client.com`) et le site sur un autre
  (`www.client.com`). RankMath fabrique tout depuis `home_url` : le graphe schema.org
  (`jsonLd.raw`) et `og:url` portaient donc le domaine **backend**, incohérents avec le canonical
  public (Search Console : « URL non valide dans le champ `id` »). Chaque coque post-traitait le
  `@graph` de son côté ; la logique remonte dans le kernel.

  Nouvelle option **opt-in**, injectée par la coque (contrat ADR 0001 — le kernel ne lit jamais
  l'env) :

  ```ts
  buildRankMathHead(node.seo, {
    titleSuffix: "ACME",
    canonicalTrailingSlash: "always", // routeur en trailingSlash: "always"
    rebaseOrigin: {
      from: runtimeEnv.wordpressHost, // hôte PUBLIC de WordPress, jamais l'URL interne SSR
      to: publicSiteUrl,
    },
  });
  ```

  Ce qui est réécrit : le `canonical`, `og:url`, et toute URL absolue du `@graph` dont l'hôte ∈
  `from` (`@id`, `url`, `author.url`, `sameAs`, `SearchAction.target`, et les références internes
  `isPartOf.@id` / `publisher.@id` par la même passe).

  `canonicalTrailingSlash` accepte désormais `"always"` en plus de `"strip"` (défaut) et
  `"preserve"` : exactement un slash final, requête et fragment préservés à leur place
  (`/page/?a=1`, jamais `/page?a=1/` ni `//`).

  Ce qui ne l'est **pas** : les chemins sous `preservePathPrefixes` (défaut `["/wp-content/"]`,
  les assets réellement servis par le backend — sinon les images tombent en 404), les hôtes
  tiers, les valeurs non-URL.

  Les coques qui réancraient le canonical et forçaient le slash à la main avant d'appeler
  `buildRankMathHead` peuvent supprimer ce pré-traitement et passer les deux options.

  Sans `rebaseOrigin`, la sortie est strictement inchangée. Les breadcrumbs restent la
  responsabilité de la coque : le framework n'expose pas de helper breadcrumb.

  Côté template client : `VITE_FRONTEND_URL` (origine publique du site) est câblée de bout en
  bout — `.env.*.example`, `docker-compose.dev.yml`, `Dockerfile`, `deploy.yml`, provisioning
  GitHub — et consommée par `editorial/model/seo.ts` (`seoHeadOptions`), branché sur les routes
  `/` et `$uri`.

## 0.14.1

### Patch Changes

- 22faf35: Le scaffold suit le déballage de `@wp-reactor/editorial` : les loaders `$uri` et
  `index` lisent `const node = await …byUriQueryOptions(…)` (nœud direct) au lieu de
  déstructurer l'enveloppe `{ nodeByUri }`.
- 22faf35: Le scaffold expose `usePlatform()` et `usePlatformResource(select)` (dans
  `platform.ts`) : l'équivalent nommé de `useRouteContext({ from: "__root__",
select: (c) => c.platform })` pour accéder, depuis un composant, à la plateforme
  injectée dans le contexte router. `Header` les utilise. Ces hooks vivent dans la
  coque (ils se couplent à l'arbre de routes via `from: "__root__"`, hors périmètre
  d'un package framework — ADR 0001, seam Option B) ; dans un loader, on continue
  de lire `context.platform` directement.
- 5a5a1a7: `update` ne recule plus une dépendance. Le manifeste des versions est figé au
  build de `create-wp-reactor` : un package `@wp-reactor/*` publié APRÈS lui y
  figure encore à sa version précédente, et `update` l'écrivait sans comparer —
  un client à jour se retrouvait downgradé en silence. Les ranges déjà en avant
  sont désormais laissées en place et signalées.

  Les fichiers du template suivent aussi `@wp-reactor/config-biome` (tabulations,
  sans point-virgule). Ils en divergeaient — exclus du formateur du monorepo —
  donc chaque `update` reformatait les fichiers d'infra dans un sens et le
  formateur du client dans l'autre.

## 0.14.0

### Minor Changes

- d9a71fc: `wp-reactor gen-resource` génère une resource de plateforme depuis un descripteur
  JSON : le document GraphQL, le modèle TypeScript, la resource-factory, et le
  câblage du `.use(…)` dans `platform.ts`.

  Les modules du framework fournissent `platform.page` et `platform.product` ; un
  CPT propre au client n'a personne pour le faire, et imposait d'écrire à la main
  trois fichiers dont seule une poignée de lignes variait.

  Formes : `bySlug`, `byUri`, `list` — `bySlug` et `byUri` s'excluent, ce sont deux
  clés pour la même lecture unitaire. Le nommage des méthodes suit celui des
  modules (`bySlugQueryOptions`, `listQueryOptions`).

  Idempotent, refuse d'écraser sans `--overwrite` avant toute écriture, et
  `--delete` défait exactement ce que la génération a fait. Le câblage gère une
  chaîne `.use()` terminée par `;`, forme de toute coque écrite avant la commande.

  Le template gagne le marqueur `// @gen:resource:platform`, la section
  `resources` de `wp-reactor.config.json` et le script `gen:resource`. La
  configuration est entièrement défaillie : la commande marche sur un repo généré
  avant son arrivée, à condition d'y poser le marqueur.

- 7abe410: `--features` découpe le repo généré par domaine. L'éditorial est le socle,
  toujours présent ; `commerce` et `auth` sont indépendants et s'écartent
  entièrement — fichiers, dépendance `@wp-reactor/*` et couture dans
  `platform.ts` / `routes/__root.tsx`. Sans le flag, la variante complète est
  générée : comportement inchangé.

  Les coutures du template sont délimitées par des marqueurs `@wpr:<domaine>`
  retirés au scaffold, en `//`, `#` ou `{/* */}` selon le fichier. La variante
  `:wrap` dégrafe une enveloppe JSX en remontant son contenu d'un cran.

  Le template gagne aussi les scripts par environnement repris de lockimmo —
  `dev:local`, `dev:staging` (Vite `--mode staging`), `push:page:local`,
  `push:page:staging` — et le `.env.staging.example` qui va avec.

- 2f8c529: Le repo généré porte enfin un `biome.json` étendant `@wp-reactor/config-biome`,
  et la dépendance à la racine où cet `extends` doit se résoudre. Sans ce fichier,
  `pnpm lint` d'un repo client tournait sur les défauts de Biome au lieu de la
  config du framework. `@biomejs/biome` est ajouté aux devDeps racine : sans lui,
  `npx biome` résout un paquet npm homonyme en 0.3.3 qui ne vérifie rien.

  `config-biome` revient à `indentStyle: tab`, qui décrit le code réel — la
  bascule en espaces ne correspondait à aucune source.

  Le template livre aussi `scripts/check-block-schemas.mjs` (script
  `check:schemas`), garde-fou sur les `default` des schémas de blocs documenté par
  le skill `gen-block`. Il lit `schemasDir` depuis `wp-reactor.config.json` et
  suit `update`.

### Patch Changes

- fdb58c4: `--provision` abaisse la casse des références d'image GHCR. Un owner ou un repo
  GitHub capitalisé (« Growth-Angels ») produisait `ghcr.io/Growth-Angels/…`, que
  Coolify rejette en 422 depuis la 4.1.2 (`DOCKER_IMAGE_NAME_PATTERN` n'accepte
  que `[a-z0-9]`) et que Docker ne saurait de toute façon pas tirer. La CI
  appliquait déjà le même abaissement au moment de pousser l'image : le
  provisioning pointait donc sur une référence qui n'existait pas.

## 0.12.0

### Minor Changes

- 89cfc56: `--provision` : un seul fichier de configuration, et un flow interactif.

  ```bash
  npx create-wp-reactor@latest --provision
  ```

  Là où il fallait aligner six variables d'environnement devant la commande plus
  quatre flags, tout tient désormais dans un fichier au format `.env` —
  paramètres **et** jetons. Au premier appel, la commande le génère
  (`.env.provision`, `chmod 600`), liste les clés à remplir et attend ; chaque
  Entrée relit le fichier et recalcule ce qui manque.

  Une fois complet, un récapitulatif s'affiche et la confirmation est demandée
  **avant le moindre appel**. Ce point d'arrêt manquait : le provisioning crée de
  l'infra facturée par des appels non idempotents, sans rollback, et rien ne
  séparait la frappe de la commande du premier `POST`. Aucune valeur de jeton
  n'est jamais affichée — seulement présent/absent.

  Précédence : flag CLI > variable du shell > fichier. Les jetons gardent leur nom
  conventionnel (`GITHUB_TOKEN`…) pour qu'un `export` déjà présent dans le shell
  fonctionne tel quel ; les paramètres sont préfixés `WPR_` pour éviter les
  collisions avec des noms génériques comme `DOMAIN`.

  Hors terminal interactif (CI, stdin redirigé), la commande ne bloque jamais :
  `--yes` saute la confirmation, et une configuration incomplète échoue en
  listant les clés manquantes. La forme à flags reste supportée.

  `--tokens-file`, introduit dans la version précédente et jamais publié, est
  supprimé : le fichier de configuration le remplace intégralement.

- 215c0d6: Ajoute `--provision` : scaffolding **et** création de l'infra en une commande.

  `npx create-wp-reactor@latest <nom> --provision --domain <fqdn> --gh-owner <owner>
--coolify-server <nom|uuid>` enchaîne repo GitHub (variables et secrets Actions
  compris), DNS Cloudflare, projet Coolify (MariaDB, Redis, Compose WordPress,
  Application webapp) puis premier push — là où `docs/deploy.md` demandait une
  dizaine d'étapes manuelles réparties sur trois UI.

  Tous les secrets sont désormais générés (`SESSION_SECRET`, mots de passe
  DB/Redis/admin WordPress) au lieu des `change-me` / `admin` du template, et ne
  sont écrits que dans l'environnement Coolify. `--dry-run` imprime le plan
  d'appels sans en émettre aucun.

  Deux étapes manuelles disparaissent au passage :

  - les labels Traefik de la webapp, jusqu'ici copiés-collés dans l'UI, sont posés
    par API depuis une source de vérité unique (`webappTraefikLabels`) ;
  - `WP_AUTO_BOOTSTRAP=1` provisionne WordPress au démarrage du conteneur, à la
    place du `docker exec … bootstrap.sh` sur l'hôte.

  `deploy.yml` ne reconstruit plus que les images concernées par le diff. WordPress
  tourne en Compose, donc sans rolling update : reconstruire les deux images à
  chaque push coupait l'admin et `/graphql` une dizaine de secondes pour un simple
  restyle du front.

  Correctifs inclus :

  - le `.gitignore` du template ignorait les `.env.*.example` qu'il livre — un repo
    fraîchement généré ne les committait pas ;
  - `bootstrap.sh` forçait `--dbhost=db`, valable en dev seulement, au lieu de lire
    `WORDPRESS_DB_HOST` ;
  - `WP_HOME` valait `${WP_HOST}` sans schéma dans `.env.coolify.example`, alors
    qu'il devient une constante WordPress qui en exige un.

## 0.11.1

### Patch Changes

- d1edb7f: Le nom de projet est slugifié avant injection, et le compose dev n'utilise plus le titre comme identifiant

  `__PROJECT_NAME__` recopiait l'argument CLI tel quel. Il atterrit pourtant dans des
  identifiants qui n'acceptent ni espace, ni majuscule, ni accent : `name:` du Docker Compose,
  noms de packages npm, dossier du thème enfant, alias réseau, labels Traefik. `npm create
wp-reactor "Café Déco"` produisait `theme-Café Déco`, `@Café Déco/source` et un projet
  compose que Docker mutile en `cafdco`.

  - **`toProjectSlug`** (exporté) : NFD → suppression des diacritiques → minuscules → tout ce
    qui n'est pas `[a-z0-9]` devient `-`. `"Café Déco Paris"` → `cafe-deco-paris`. Un nom sans
    aucun caractère utilisable échoue explicitement au lieu de générer un repo cassé.
    `__PROJECT_TITLE__` reste dérivé de la saisie brute : le branding lisible ne change pas.
  - **`--namespace`** est slugifié aussi (un namespace de bloc WP est `[a-z][a-z0-9-]*`).
  - **`bin/create-app.js`** : le dossier créé par défaut et l'instruction `cd` suivent le slug,
    et le CLI annonce la normalisation quand elle a lieu.
  - **`docker-compose.dev.yml`** : `name:` et le filtre pnpm du service `webapp` utilisaient
    `__PROJECT_TITLE__`. `pnpm --filter @Acme/webapp` ne matchait aucun package — le vrai nom
    est `@acme/webapp` — donc le service webapp de la stack dev ne démarrait pour **aucun**
    client, même avec un nom déjà en kebab. Les deux passent à `__PROJECT_NAME__`.

  ⚠️ Migration : pour un client dont le nom fait plusieurs mots, `create-wp-reactor update`
  renomme le projet compose dev (Docker écrasait `Client 2-dev` en `client2-dev`, il devient
  `client-2-dev`). Les volumes dev existants (`db_data`, `wp_data`) apparaîtront orphelins —
  `docker compose -p client2-dev down -v` pour repartir propre, la stack dev étant jetable.

## 0.11.0

### Minor Changes

- 147f284: La home du repo scaffoldé est pilotée par WordPress, plus par un tableau de blocs en dur

  `routes/index.tsx` rendait un `demoBlocks` statique : le scaffold montrait le dispatch du
  moteur de blocs, mais rien ne prouvait la chaîne complète WordPress → GraphQL → registre, et
  la première chose qu'un client faisait était de réécrire la route. Elle résout maintenant
  `nodeByUri("/")` comme n'importe quelle page, avec son `<head>` RankMath.

  - **`editorial/components/Content.tsx`** (nouveau) : dispatche les `editorBlocks` d'une page.
    Ceux du namespace passent par le registre (sous `Suspense`, les renderers étant `lazy`), les
    blocs **core** rendent leur `renderedHtml` produit par WordPress. L'`anchor` retombe sur le
    `clientId` quand il n'est pas saisi.
  - **`shared/graphql/editorBlocks.ts`** : la query remonte `clientId`, `parentClientId` et
    `renderedHtml`. `editorBlocks` est **aplati** par `wp-graphql-content-blocks` — sans
    `parentClientId`, les enfants d'un `core/group` seraient rendus une seconde fois en frères.
  - **`editorial/components/HtmlContent.tsx`** (nouveau, + dép `html-react-parser`) : parse le
    HTML WordPress et réécrit les URLs `/wp-content` et `/wp-includes` vers `VITE_WORDPRESS_URL`
    (`src`, `srcset`, `poster`, liens vers un média) — en headless, les chemins relatifs de WP
    pointent sinon sur la webapp.
  - **`__root.tsx`** : la route `/preview` (iframe de live-preview de l'éditeur Gutenberg) rend
    son bloc **sans header ni footer**, qui faussaient le cadrage de l'aperçu.
  - **`bootstrap.sh`** : provisionne une page « Accueil » **statique** (`show_on_front` +
    `page_on_front`). Sans elle, `nodeByUri("/")` renvoie l'archive des articles, qui n'a pas
    d'`editorBlocks` — la home serait vide au premier lancement.

- 90a82ac: Déploiement multi-environnements : `main` → production, `development` → staging

  La chaîne de déploiement ne connaissait qu'une cible. Les secrets et variables étaient lus
  au niveau du **repo**, donc un même repo client ne pouvait pas pousser une préprod et une
  prod avec des `VITE_WORDPRESS_URL` ou des UUID Coolify différents.

  Le workflow réutilisable (`build-and-deploy-image.yml`, framework **et** copie vendorisée du
  template) prend désormais une entrée `environment` et la porte sur son job — c'est le seul
  moyen de résoudre les secrets d'un environnement GitHub, `environment:` étant interdit sur
  un job qui _appelle_ un workflow réutilisable. Côté client, `deploy.yml` gagne un job
  `config` qui résout les `vars` de l'environnement et les passe en `build-args`, et déclenche
  sur `main` **et** `development` (idem `ci.yml`).

  - Les secrets sont lus dans le repo **appelant** : `secrets: inherit` devient obligatoire.
  - L'UUID Coolify se passe désormais par **nom** (`coolify-uuid-secret: COOLIFY_WEBAPP_UUID`),
    résolu via `secrets[...]` dans l'environnement — au lieu d'un secret `COOLIFY_UUID`
    recâblé à chaque appel.
  - `NODE_AUTH_TOKEN` passe en build-arg depuis les secrets de l'environnement.
  - Le tag `latest` est réservé à `main` ; les autres branches ne taguent que `<branche>` et
    `<sha>`.

  ⚠️ **Repos clients existants** : `environment` est une entrée **requise**. Après un
  `npx create-wp-reactor@latest update`, il faut créer les environnements GitHub
  `production` et `staging` et y placer `NODE_AUTH_TOKEN`, `COOLIFY_URL`, `COOLIFY_TOKEN`,
  `COOLIFY_WEBAPP_UUID`, `COOLIFY_WORDPRESS_UUID` et les `VITE_*` — sans quoi le déploiement
  échoue au démarrage du workflow.

- d69ad3c: Le skill agent `gen-block` est livré au repo client

  `.claude/skills/gen-block/SKILL.md` fait maintenant partie du template client : un
  repo scaffoldé arrive avec le skill qui pilote gen-block de bout en bout (lien Figma
  ou capture → schéma inféré → génération → renderer à implémenter). Il vivait jusqu'ici
  dans la config globale de la machine, hors versionnement — donc invisible pour l'équipe
  et libre de dériver du CLI qu'il documente.

  Le chemin `.claude/skills` rejoint les `INFRA_PATHS` : `create-wp-reactor update` le
  re-pousse dans un repo client existant, comme les workflows CI ou le compose dev. Un
  skill propre au client posé à côté n'est jamais touché (`renderInto` écrit, ne purge pas).

- 9e68b89: Le repo client est scaffoldé avec une **master page**, et le skill `gen-block` l'alimente

  Le template livre `pages/master-page.ts` : une page décrite en TypeScript (`definePage` +
  `block(...)`, poussée par `wp-reactor push-page`) qui tient **un bloc par variante utile** —
  chaque valeur de `Select`, `Toggle` dans les deux sens, avec et sans les repeaters optionnels.
  C'était le maillon manquant du pipeline de blocs : jusqu'ici un bloc généré n'était rendu pour
  de vrai qu'une fois qu'un client s'en servait, donc une régression ne se voyait pas.

  - **template** : `pages/master-page.ts` pré-rempli avec les deux états `inverted` du bloc de
    démo `cta-banner` (donc utilisable dès le scaffold : `pnpm push:page … --dry-run`), script
    `push:page` dans le `package.json` racine, devDep `dotenv`, `WP_USER`/`WP_APP_PASSWORD`
    dans `.env.local.example` et dans `docs/environment.md`, section « La master page » au README.
  - **skill `gen-block`** : nouvelle étape 6 — après le renderer, l'agent ajoute au fichier une
    instance par variante du bloc qu'il vient de générer, puis vérifie avec `--dry-run`. Le skill
    encadre deux pièges au passage : la copy de la maquette va **là** (jamais dans un `default`
    de schéma, qui n'est pas sérialisé dans le `post_content`), et `push-page` n'émet pas de
    `__wpReactorItemId` — la clé React d'un repeater doit retomber sur l'index.
  - La master page est **hors `INFRA_PATHS`** : elle accumule les blocs du client, donc `update`
    ne la re-pousse jamais. Les repos scaffoldés avant cette version ne la reçoivent pas
    automatiquement ; le skill la crée si elle manque.

- c5b59c8: `update` bumpe les deps `@wp-reactor/*` en plus de l'infra

  `create-wp-reactor update` re-poussait les fichiers d'infra et les skills agent, mais
  laissait les ranges `@wp-reactor/*` du repo client là où le scaffold les avait posées :
  récupérer une version de framework restait un bump manuel, package par package, dans
  trois `package.json`. Un repo client dérivait donc silencieusement — infra à jour,
  logique gelée.

  `update` réécrit maintenant chaque range `@wp-reactor/*` en `^<version>` dans **tous**
  les `package.json` du repo (racine, `apps/webapp`, thème enfant, packages ajoutés par le
  client). Le remplacement est chirurgical — seule la chaîne de version est touchée, donc
  l'indentation et l'ordre des clés survivent et le `git diff` ne montre que les bumps.
  Les versions viennent du manifeste embarqué au build (aucun appel réseau) : d'où
  `npx create-wp-reactor@latest update`, puis `pnpm install`.

  Garde-fous : les deps hors `@wp-reactor/*` (react, vite, turbo…) ne sont jamais bumpées
  — les toucher pourrait casser l'app du client ; les ranges `workspace:`/`link:`/`file:`/
  `portal:` sont préservées (le dogfood monorepo garde ses liens locaux) ; `node_modules`
  et les dossiers de build sont ignorés ; une dep `@wp-reactor/*` inconnue du manifeste
  (renommée ou retirée) est laissée telle quelle et signalée en `⚠` plutôt que bumpée dans
  le vide. La commande reste idempotente : au second passage, les fichiers sont
  octet-pour-octet identiques.

  `update()` retourne désormais `UpdateResult` (`ScaffoldResult` + `bumps` + `unknownDeps`)
  et le CLI liste chaque bump `dep from → to (fichier)`.

- c5b59c8: `update` re-pousse le dossier `.github/workflows` entier

  `INFRA_PATHS` listait les workflows un par un (`ci.yml`, `deploy.yml`) et avait donc
  manqué `build-and-deploy-image.yml` — la copie **vendorisée** du workflow réutilisable du
  framework (vendorisée parce qu'un reusable workflow d'un repo privé n'est pas accessible
  cross-org via le `GITHUB_TOKEN`). Conséquence : le seul fichier de la chaîne de déploiement
  qui contient la logique de build/push GHCR était aussi le seul qu'`update` ne rafraîchissait
  jamais. Toute évolution du workflow framework demandait un report à la main, silencieusement.

  L'entrée devient le **dossier** `.github/workflows` : le template ne livre que des
  workflows d'infra, et une liste fichier-par-fichier dérive à chaque ajout — c'est
  exactement ce qui s'est produit. Un workflow propre au client posé à côté n'est jamais
  touché (`renderInto` écrit, ne purge pas), comme pour `.claude/skills`.

  À noter pour les repos existants : `build-and-deploy-image.yml` était jusqu'ici préservé
  par `update`, il est maintenant écrasé par la version du template. Si tu l'avais modifié
  à la main, relis le `git diff` avant de committer.

### Patch Changes

- d69ad3c: Contrat de schéma publié : `TermSelect` et `taxonomy` reconnus

  Le JSON Schema servi sur le CDN (`schema/block-contract.schema.json`, référencé par
  le `$schema` de chaque `schemas/<slug>.json`) ignorait l'UI `TermSelect` et la clé
  `taxonomy`, alors que gen-block les supporte depuis `schema/ui.ts`. Avec
  `additionalProperties: false`, un champ de sélection de terme était signalé invalide
  dans l'éditeur — sans que la génération, elle, échoue. Ajout additif, rétrocompatible.

- a2ae219: Nouvelle UI de champ `Title` : niveau `h1`→`h6` + texte enrichi restreint

  Un titre de section porte deux informations indissociables — sa balise sémantique et son
  texte — jusqu'ici éclatées en un `Select` et un `TextInput` qu'il fallait penser à garder
  cohérents. `{ "ui": "Title" }` les réunit en un attribut objet
  `{ level: "h1" | … | "h6"; text: string }`.

  Son éditeur (`TitleFieldEditor`) est un select de niveau plus un champ enrichi
  **volontairement restreint au gras et au `<span>`** : un titre n'accueille ni listes, ni
  liens, ni paragraphes. Le `text` sort donc en HTML _inline_ — le renderer l'insère tel quel
  dans la balise (`<Tag dangerouslySetInnerHTML={{ __html: text }} />`), et un `<p>` y serait
  invalide.

  `RichTextEditor` gagne pour cela deux options, utiles à tout champ enrichi : `tools`
  (restreint le jeu de marques — l'extension est retirée, pas seulement son bouton, sinon le
  raccourci clavier resterait actif) et `inline` (ligne unique, sortie non enveloppée).

- 93bd932: Le `pnpm install` d'un repo fraîchement scaffoldé ne casse plus sur `@parcel/watcher`

  pnpm 11 fait **échouer** `install` (et pas seulement avertir) dès qu'une dépendance a un
  script de build non arbitré : `[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts:
@parcel/watcher@2.6.0`. Le paquet arrive comme dep optionnelle de `sass` (thème enfant).

  Le `pnpm-workspace.yaml` du template porte désormais un bloc `allowBuilds` qui refuse
  explicitement `@parcel/watcher` (sass retombe sur son watcher JS, aucun impact) — la
  première commande après le scaffold repasse.

  `pnpm-workspace.yaml` n'est pas dans `INFRA_PATHS` (le client peut y ajouter ses propres
  workspaces), donc `update` ne le re-pousse pas : sur un repo déjà scaffoldé, ajouter le
  bloc à la main.

- 57954b3: wp-cli est baké dans l'image WordPress base — le sidecar `wordpress:cli` disparaît

  L'image `ghcr.io/wp-reactor/wordpress-base` embarque désormais le phar wp-cli (repris de
  l'image officielle `wordpress:cli-php8.4`, +7 Mo) et `WP_CLI_CACHE_DIR=/tmp/wp-cli-cache`
  (sinon wp-cli râle sur `/var/www/.wp-cli` à chaque commande).

  Conséquence : le provisioning se fait par `docker exec` **dans** le conteneur WordPress,
  qui hérite de son environnement (`WORDPRESS_DB_*`, `WP_HOME`) au lieu de le recopier à la
  main dans un conteneur jetable.

  - **Prod** : `bootstrap.sh` est baké dans l'image client comme
    `/usr/local/bin/<projet>-bootstrap.sh` (+ `ENV CHILD_THEME`). L'étape 4 de
    `docs/deploy.md` passe d'un `docker run --volumes-from` avec 10 variables à un
    `docker exec -u 33:33` avec les 4 variables d'admin.
  - **Dev** : le service `wp-bootstrap` de `docker-compose.dev.yml` est supprimé ;
    `pnpm docker:bootstrap` fait un `exec` dans le service `wordpress`.

  `-u 33:33` reste requis : les plugins installés par wp-cli doivent appartenir à www-data
  pour rester gérables depuis l'admin.

  Le client MySQL n'est **pas** embarqué : seules les sous-commandes `wp db` en dépendent
  (tout le reste passe par mysqli/PHP), et `mariadb-client` pèse +85 Mo. `deploy.md`
  documente le dump via un conteneur `mariadb:11` jetable.

  ⚠️ Nécessite une image base republiée (`release-wordpress-base.yml`) avant qu'un repo
  client en profite.

  Corrige au passage le bind-mount du thème enfant en dev, qui pointait
  `theme-__PROJECT_TITLE__` (titleCase) au lieu de `theme-__PROJECT_NAME__` — le dossier
  monté n'existait pas, donc le thème n'était jamais activable.

## 0.10.0

### Minor Changes

- 337cff1: gen-block câble le registre en **chargement asynchrone** : un chunk JS par bloc

  Le renderer d'un bloc généré est désormais déclaré en `React.lazy` au lieu d'un
  import statique — chaque bloc a son chunk, chargé à la demande, au lieu d'un
  bundle unique parsé et évalué sur chaque page, blocs absents compris.

  - **cli** : `wireRegistry` écrit `const XBlock = lazy(() => import(…))` (et
    l'import de `lazy`) ; nouveau flag **`--eager`** (+ question en mode
    interactif) pour garder un bloc au-dessus de la ligne de flottaison (LCP) en
    import statique. Le mode fait foi à chaque run : `--overwrite` bascule la
    déclaration d'une forme à l'autre, et `--delete` retire les deux formes ainsi
    que l'import de `lazy` devenu inutile.
  - **editorial** : `EditorBlocks` enveloppe chaque bloc d'un `<Suspense>` (une
    frontière par bloc, pour qu'un chunk lent n'en retienne pas d'autres) et
    `PreviewBlockHost` fait de même — un renderer lazy suspend, la frontière est
    fournie par le framework, rien à câbler côté client. Le rendu reste SSR
    (streaming) : seul le JS d'hydratation est différé.
  - **create-wp-reactor** : le registre du template déclare son bloc de démo en
    `lazy` et documente le compromis eager/lazy.

## 0.9.0

### Minor Changes

- b3c50f8: Le schéma de blocs (`schema/block-contract.schema.json`, servi via CDN) expose désormais `conditional` (`field` + `value`) : un champ peut n'être affiché dans l'éditeur que si `context[field] === value`, y compris par-ligne dans un ArrayRepeater/Object. Republie le scaffolder pour synchroniser le schéma CDN avec le template.

## 0.8.1

### Patch Changes

- 8e63a5c: Clarifie les fichiers `.env` du template par destination et fait de `SESSION_SECRET` un secret runtime/Coolify uniquement.

  - `.env.local.example` (dev), `.env.github.example` (CI), `apps/{webapp,wordpress}/.env.coolify.example` (runtime).
  - `SESSION_SECRET` retiré du build (Dockerfile + workflows) : il est lu au runtime, donc défini côté Coolify uniquement.
  - `.env.github.example` complété avec `VITE_GRAPHQL_PATH` ; références `.env.example` → `.env.local.example` corrigées ; doc `environment.md` harmonisée.

## 0.8.0

### Minor Changes

- ac6ec8a: Les deps `@wp-reactor/*` des projets scaffoldés résolvent désormais `^<version>` **par package** (manifeste embarqué au build via `versions.generated.ts`), au lieu d'une range partagée `^0.1.0` qui excluait les packages déjà publiés en 0.3.x. L'override `pkgRange` (dogfood `workspace:*`) et `$WP_REACTOR_PKG_RANGE` restent prioritaires.

  Le build éditeur du thème (Vite IIFE) est corrigé pour embarquer une lib React tierce (TipTap) : `react-dom` externalisé vers le global `ReactDOM`, `process.env.NODE_ENV` défini, et shim `require` scopé pour les deps CJS. L'exemple cta-banner illustre les nouveaux champs.

## 0.7.1

### Patch Changes

- Patch hardcoded projet title

## 0.7.0

### Minor Changes

- Updating client to match with reality

## 0.6.0

### Minor Changes

- 4c9906b: Source de vérité unique = le template client bundlé ; dogfood par sandbox générée.

  - Le starter `webapp-template/` (racine) et le script `sync-template.mjs` sont
    supprimés : `templates/client/` (webapp + thème enfant) devient la **source de
    vérité unique**. Plus de sync à maintenir.
  - Les deps `@wp-reactor/*` du template portent un token `__PKG_RANGE__` rendu au
    scaffold : `^x.y.z` publié pour un client npx, `workspace:*` pour le dogfood
    interne. Nouvelle option `scaffold({ pkgRange })`.
  - Correctif `vite.config.ts` du template : les `@wp-reactor/*` se résolvent par
    `node_modules` (et non par un alias `../packages/*/src` qui supposait une
    topologie de repo) ; `envDir` et `fs.allow` corrigés de `..` → `../..` pour la
    profondeur réelle `apps/webapp`. Le build d'un client scaffoldé est désormais
    correct quelle que soit son emplacement.

## 0.4.0

### Minor Changes

- Câblage de l'import éditeur du thème pour les blocs générés.

  - `@wp-reactor/cli` : `gen-block`/`remove` câblent désormais l'import à effet de
    bord de chaque bloc dans l'entrée éditeur du thème (`src/index.tsx`) via le
    nouveau module `wiring/theme-editor`. Config étendue : `theme.editorEntry`,
    `theme.editorEntryMarker`, `theme.editorImportBase` (no-op si `editorEntry`
    absent).
  - `create-wp-reactor` : le template client scaffolde l'app thème
    (`apps/wordpress/theme-__PROJECT_NAME__/` : `package.json`, `src/index.tsx`,
    `tsconfig.json`, `vite.config.ts`) et pré-configure `editorEntry` +
    `renderersImportBase` dans `wp-reactor.config.json`.

## 0.3.2

### Patch

- Schéma de blocs CENTRALISÉ : le `$schema` des `schemas/<slug>.json` pointe
  désormais vers le CDN jsDelivr du package
  (`https://cdn.jsdelivr.net/npm/create-wp-reactor/schema/block-contract.schema.json`).
  Tous les clients (existants compris) bénéficient des mises à jour du contrat
  sans copier de fichier. Le schéma canonique vit dans `schema/` du package.

## 0.3.1

### Patch

- Le `$schema` des fichiers `schemas/<slug>.json` pointait vers un lien public
  inexistant (`https://wp-reactor.dev/...`). Remplacé par une référence INTERNE
  relative `./block-contract.schema.json`, fichier embarqué dans `schemas/` →
  validation/autocomplétion éditeur sans dépendance externe.

## 0.3.0

### Minor

- Nouvelle commande `create-wp-reactor update` : dans un repo client existant,
  re-pousse les fichiers d'**infra** (docker-compose dev, `docs/`, Dockerfiles,
  workflows CI) depuis la version courante du template, sans toucher la coque
  `src/` ni le branding. Permet de bénéficier des améliorations d'infra après coup.

## 0.2.1

### Patch

- Fix : le tarball publié n'embarquait pas `dist/` (le `prepack` ne faisait que
  le sync, pas le build tsup) → `npm create wp-reactor` échouait avec « Cannot
  find module dist/index.js ». Le `prepack` build désormais avant de packer.
  (0.2.0 est cassé — utiliser 0.2.1.)

## 0.2.0

### Minor

- Stack dev local : `docker-compose.dev.yml` (WordPress image base + MariaDB +
  Redis, thème enfant bind-monté) + `bootstrap.sh` + scripts `pnpm docker:*`.
- Docs déploiement : `docs/deploy.md` (procédure Coolify ordonnée + Cloudflare),
  `docs/environment.md` (variables local / GitHub / Coolify), `docs/traefik-labels.md`.
- Script de purge edge Cloudflare (`scripts/clear-cache.mjs`) en post-deploy.
- Renommage `GA_FRONTEND_URL` → `FRONTEND_URL` dans les templates.

### Patch

- Fix : le `Dockerfile` de la webapp cliente était absent du repo généré
  (effacé par le sync). Déplacé dans le starter → porté correctement au scaffold.

## 0.1.0

- Première publication. Scaffolder de repo client mince : `npm create wp-reactor <nom>`.
