Mode opératoire

Maquette d'abord

Construire un site WordPress sur mesure avec un agent IA, sans passer par un page builder et sans produire du contenu que le client ne pourra plus modifier. Le design se fait en HTML local, WordPress n'intervient qu'à la fin.

Produit
Thème bloc sur mesure
Durée
3 à 5 h pour 6 pages
Éditable
100 % blocs natifs
Dépendances
Aucun page builder
§

Le principe

Un agent IA produit un design médiocre quand sa boucle de retour est lente. Toute la méthode consiste à déplacer le travail de conception là où la boucle est rapide, et à ne toucher à WordPress qu'une fois le résultat validé.

Écrire la mise en page directement dans WordPress, que ce soit par un page builder ou par l'API de blocs, impose une boucle de trois à cinq minutes par itération : composer, envoyer, sérialiser, purger le cache, recharger, regarder. En HTML local avec un serveur de développement, la même boucle prend cinq secondes. Le nombre d'itérations possibles est multiplié par cinquante, et la qualité d'un design est à peu près proportionnelle au nombre d'itérations.

La seconde moitié du principe est une contrainte de livraison : le site doit rester modifiable par quelqu'un qui n'écrit pas de code. Cela interdit deux raccourcis tentants, détaillés en règles 4 et 5.

Résumé en une ligne Le design vit dans des fichiers, pas dans le contenu des pages. WordPress reçoit un thème fini, pas des instructions de mise en page.
§

La règle 0, puis les dix autres

Non négociables. Chacune vient d'une erreur réellement commise et de son coût.

Règle 0 · Dessiner la phase 2 comme si la phase 4 n'existait pas Elle passe avant les dix autres parce qu'elle est la seule dont l'infraction ne se voit nulle part : ni dans le code, ni dans la recette, ni dans l'éditeur. Elle ne produit pas une panne, elle produit un site fade — et un site fade passe toutes les portes.

Aucune contrainte de conversion n'entre dans la maquette. « Pas de bloc HTML brut », « blocs natifs », « éditable par le client » sont des problèmes de phase 4. Les anticiper pendant le dessin fait disparaître, une par une et sans qu'on s'en aperçoive, la profondeur, les superpositions, les débordements, le mouvement, l'objet central. Le résultat se convertit parfaitement et ne plaît à personne.

La méthode dessine en HTML local parce que c'est gratuit à cet endroit. S'auto-censurer là, c'est payer le prix de WordPress sans jamais en toucher le bénéfice. Si un composant s'avère trop riche à convertir, c'est un arbitrage de phase 4, pris devant un design fini — jamais un renoncement de phase 2.
1
Exiger une référence visuelleSans direction imposée, un modèle échantillonne le centre statistique de ses données d'entraînement : police neutre, dégradé violet, fond blanc. C'est la définition du générique. Une capture d'écran ou une URL suffit à faire basculer le résultat en une passe.
2
Jamais de maquette sans photos réellesUn aplat gris à la place d'une photo ressemble à une image cassée. Aucun travail de composition ne le rattrape. Les visuels sont réglés avant la première capture, jamais après.
3
Rien ne part sur WordPress avant validationLa phase 2 est la seule où l'itération est bon marché. Tant que la maquette n'est pas validée, le CMS reste hors de la boucle.
4
La page d'accueil est une page, jamais un templateUn contenu rendu par front-page.html via wp:pattern n'est pas modifiable depuis l'éditeur de page. Créer une vraie page et la déclarer en page d'accueil statique.
5
Zéro bloc core/htmlUn bloc HTML brut n'est pas éditable visuellement. Tout composant, même dense, se compose en groupes, paragraphes, titres et images.
6
Images en médiathèque, avec identifiantUne image référencée par URL de fichier de thème n'est pas remplaçable en un clic. Téléverser dans la médiathèque et poser l'attribut id sur le bloc.
7
Les variantes interactives vivent dans la pageOnglets, sélecteurs, accordéons : toutes les variantes sont présentes dans le contenu, le script se contente de basculer une classe. Rien n'est injecté par JavaScript, donc tout reste éditable.
8
Pas de thème tiers sous le designAstra, Kadence ou GeneratePress imposent leur CSS dynamique et leurs options typées. La moitié de la feuille de style finit en surcharges de spécificité. Un thème sur mesure remplace le tout.
9
Recette dans l'éditeur, pas en façadeUne page peut s'afficher parfaitement et être inutilisable dans Gutenberg. Le contrôle se fait en interrogeant le store de l'éditeur, phase 6.
10
Le pixel perfect se mesure, il ne se regarde pasLa recette compare un relevé de styles calculés et de géométrie entre la maquette et le site, sélecteur par sélecteur. « Ça a l'air pareil » a laissé passer 307 px d'écart de rythme vertical : une seule règle de neutralisation avait mis quatorze marges à zéro sans que l'œil le voie.
00

Préparation

Une seule fois10 minAgent

Aucun outillage à installer

Rien à installer, aucun plugin à vérifier. Le parti pris esthétique est porté par la phase 2 de cette méthode, et par elle seule.

Espace de travail

Créer le projet dans le dossier auquel l'utilisateur a donné accès — le répertoire de travail de la session. C'est là qu'il ira chercher ses fichiers. Un projet posé ailleurs « par précaution » se solde par un dossier vide qu'il faudra lui expliquer.

Sur macOS, ~/Documents et ~/Desktop sont protégés par TCC — mais cette protection ne joue que si l'autorisation n'a pas été accordée, et elle l'est presque toujours pour l'outil qui vient d'ouvrir ce dossier. Ça se teste en trois secondes, et ça ne se suppose jamais :

mkdir -p "$PWD/.essai-tcc" \
  && node -e "process.chdir('$PWD/.essai-tcc'); require('fs').writeFileSync('t','x'); console.log('acces ok')" \
  ; rm -rf "$PWD/.essai-tcc"

« acces ok » : créer le projet dans le dossier de travail, point final. EPERM: uv_cwd : alors seulement le créer ailleurs, le dire à l'utilisateur, et poser un lien symbolique depuis son dossier pour qu'il retrouve son travail.

Rien à préparer côté WordPress ici : les phases 1 à 4 sont entièrement locales. L'accès au site (plugin et bundle) n'intervient qu'au début du déploiement — il est décrit en tête de phase 5, au moment où on en a besoin.

01

Brief

Client10 minUn seul message

Le brief tient en un message. Plus il est long, moins il est utile ; ce qui compte est la référence visuelle, pas la prose.

Gabarit à copier

Détail du site :
[Nom]
[Activité]
[Description]
[Contact]

Pages du site :
Accueil
[...]

Référence :
Image en pièce jointe

Tout ce que le gabarit ne dit pas est un défaut de la méthode, jamais une question posée au client : le contenu est inventé à partir de l'activité et de la description (le nom et les coordonnées fournis sont repris tels quels), les photos viennent d'Unsplash sauf si le client donne les siennes, rien ne part sur WordPress avant validation de la maquette, et les cinq portes de la recette sont passées d'office.

Ce que l'agent produit en retour, avant de coder

  1. Le sujet concret : entreprise, audience, mission unique de la page.
  2. La palette, 4 à 6 valeurs hexadécimales nommées.
  3. Les typographies et leurs rôles.
  4. Deux ou trois écarts volontaires avec la référence, justifiés.
Point de contrôle Si le plan de design ressemble à ce qu'on produirait pour n'importe quel autre site du même secteur, le faire refaire avant d'écrire une ligne de code.
02

Maquette

Phase critique30 à 60 minItératif

Une seule page, l'accueil, en HTML et CSS, en local. C'est ici que se joue 90 % de la qualité finale.

La porte d'entrée · Règle 0 Avant d'ouvrir un fichier : rien de la phase 4 n'entre ici. Le test se pose à chaque décision de composition — est-ce que j'écarte cette idée parce qu'elle est mauvaise, ou parce que je me demande comment elle passera en blocs ? Si c'est la seconde réponse, l'idée reste.

Le parti pris se décide, il ne se déduit pas

Aucun outil tiers ne porte l'esthétique. Une liste d'interdits ne fabrique pas un design — éviter les dégradés, les ombres molles, les capitales et les flèches ne donne pas une belle page, ça donne une page poncée. Les exigences sont donc formulées en positif, et chacune se constate sur la capture.

  • Un élément domine. Une image qui déborde, un champ de couleur qui prend la moitié de l'écran, un titre hors d'échelle. Sans dominante, dix sections propres font un document, pas un site.
  • La couleur forte occupe du terrain. Ne retenir qu'une seule couleur saturée est une bonne discipline ; ne lui accorder que deux bandeaux sur dix sections en est la caricature.
  • Les sections n'ont pas toutes le même poids. Même marge, même largeur, même alignement, titre-paragraphe-grille à chaque fois : c'est le symptôme le plus fiable d'une page sans intention.
  • Il y a de la profondeur. Au moins un élément sort de son conteneur. Le plat n'est pas le raffiné.
  • La typographie de caractère travaille. Une seconde famille choisie pour son caractère porte du contenu réel, pas une figuration de quatre chiffres.
  • Le concept est visible sans légende. S'il faut l'expliquer pour qu'il existe, il n'est pas dans le design.

Reprendre une référence, c'est reprendre ce qui lui donne son énergie — profondeur, superpositions, objet central, champ de couleur — pas seulement l'ordre de ses sections. Sans ces dispositifs, on en produit le wireframe.

Serveur de développement

Indispensable : sans lui, les interactions ne fonctionnent pas et l'agent ne peut pas vérifier son travail.

// scripts/serve.js — serveur statique sans dépendance, fourni avec le skill.
// L'essentiel de son contrat (le fichier complet fait une soixantaine de lignes) :
const RACINE_ABS = path.resolve(process.env.RACINE || path.join(__dirname, '..', 'site'));
const PORT = Number(process.env.PORT || 4321);

// - décodage d'URL sous try/catch : un %zz répond 400 au lieu de tuer le serveur ;
// - garde de racine : rien n'est servi hors de RACINE_ABS (préfixe + séparateur) ;
// - Cache-Control: no-store sur tout ;
// - port occupé -> essai automatique du suivant, et le port RÉEL est affiché :
serveur.on('listening', () => {
  console.log('Maquette servie sur http://127.0.0.1:' + serveur.address().port
            + '  (racine : ' + RACINE_ABS + ')');
});

Le lancer depuis /tmp — c'est le répertoire courant du processus qui doit être accessible, jamais RACINE, qui peut pointer n'importe où, y compris dans le dossier de travail de l'utilisateur :

cd /tmp && RACINE=/chemin/projet/site nohup node /chemin/projet/scripts/serve.js > /tmp/serve.log 2>&1 &
sleep 1 && tail -1 /tmp/serve.log   # le serveur affiche le port réellement utilisé

Les visuels avant la première capture

Règle 2. Trois sources possibles, à trancher avant de composer :

SourceCoûtCohérenceQuand
Photos du clientNulleMaximaleDès qu'elles existent
UnsplashNulleMoyenneDémo, secteur générique
GénérationCréditsÉlevéeDirection très typée

Pour Unsplash, récupérer les identifiants de photo depuis la page de résultats plutôt que de deviner des URL :

// dans la console du navigateur, sur une page de recherche Unsplash
[...document.querySelectorAll('figure img[src*="images.unsplash.com/photo-"]')]
  .slice(0,16).map(i => ({
    id: (i.src.match(/photo-[\w-]+/)||[''])[0],
    alt: (i.alt||'').slice(0,70)
  }));

Format de téléchargement : https://images.unsplash.com/{id}?w=1200&q=80&fm=jpg&fit=crop. La licence Unsplash autorise l'usage commercial sans attribution obligatoire.

Protocole d'itération

  1. L'agent écrit la page complète, avec du contenu réel, jamais de lorem.
  2. Il prend une capture et corrige ce qu'elle montre.
  3. Un tour de design avant de vous montrer quoi que ce soit : l'agent reprend la capture et pousse la page, au lieu de la réparer. Un plan validé en prose ne remplace pas ce tour-là.
  4. Le client regarde et nomme précisément ce qui cloche.
  5. Deux à trois tours. Au-delà, c'est le brief qui est en cause, pas l'exécution.

Un tour qui remonte la taille d'une icône ou corrige un prénom est un tour de justesse : nécessaire, et sans le moindre effet sur la qualité du design. Il ne compte pas comme le tour de design.

Piège d'affichage Les captures prises moins de cinq secondes après le chargement montrent souvent des images absentes : les JPEG ne sont pas encore décodés. Vérifier avec img.complete && img.naturalWidth > 0 avant de conclure à un bug.

Points de contrôle avant de valider

  • Aucune décision de composition n'a été prise en pensant à la conversion (règle 0).
  • Un élément domine la page, et on peut le nommer en un mot.
  • La couleur forte couvre une surface réelle.
  • Deux sections voisines ne partagent pas exactement le même gabarit.
  • Au moins un élément sort de son conteneur.
  • Le titre principal tient sur deux lignes au plus, sans césure au milieu d'un mot. Poser hyphens: none et text-wrap: balance.
  • Aucun rectangle vide ni aplat de remplacement.
  • Les composants d'un même type partagent leurs marges intérieures et leurs lignes de base.
  • Le rendu à 390 px de large ne provoque aucun défilement horizontal.
  • Chaque élément qui se répète occupe la même place d'une carte à l'autre.
03

Déclinaison

20 à 30 minAgent seul

L'accueil validé, le système de design l'est aussi. Les pages internes ne rouvrent aucun débat esthétique.

Refactorisation préalable

Extraire le CSS et le JS de la page unique vers des fichiers partagés, puis générer les pages internes depuis une ossature commune. Ce n'est pas cosmétique : c'est déjà la structure du thème.

MaquetteDevient en phase 4
Fonction nav()parts/header.html
Constante FOOTERparts/footer.html
Chaque sectionUn fichier dans patterns/
styles.csstheme.json + style.css
app.jsassets/app.js

Générer les pages par script plutôt qu'à la main garantit que l'en-tête et le pied de page sont strictement identiques partout, et rend la conversion mécanique.

04

Thème bloc

45 à 60 minAgent seulLe cœur technique

Arborescence cible

mon-theme/ ├── style.css en-tête de thème + tout le CSS ├── theme.json jetons : couleurs, typo, espacements ├── functions.php polices, styles, script, catégorie de patterns ├── parts/ │ ├── header.html site-title + navigation + bouton │ └── footer.html ├── templates/ │ ├── front-page.html header + post-content + footer │ ├── page.html identique │ ├── index.html boucle de requête │ └── 404.html ├── patterns/ un fichier .php par section réutilisable └── assets/ ├── app.js └── img/ copie des visuels, embarquée dans le zip

theme.json

Il porte les jetons, pas la mise en page. Ce qui compte : désactiver les palettes par défaut, sinon l'éditeur propose les couleurs de WordPress à côté des vôtres.

{
  "$schema": "https://schemas.wp.org/trunk/theme.json",
  "version": 3,
  "settings": {
    "appearanceTools": true,
    "layout": { "contentSize": "1200px", "wideSize": "1400px" },
    "color": {
      "defaultPalette": false, "defaultGradients": false, "defaultDuotone": false,
      "palette": [ { "slug": "encre", "color": "#0A1124", "name": "Encre" } ]
    },
    "typography": {
      "fluid": true, "defaultFontSizes": false,
      "fontFamilies": [ { "slug": "principale", "name": "…",
        "fontFamily": "'…', system-ui, sans-serif" } ]
    }
  },
  "styles": {
    "color": { "background": "var(--wp--preset--color--blanc)" },
    "elements": { "button": { "border": { "radius": "999px" } } }
  },
  "templateParts": [
    { "name": "header", "title": "En-tête", "area": "header" },
    { "name": "footer", "title": "Pied de page", "area": "footer" }
  ]
}

Composer en blocs natifs

Chaque composant de la maquette se traduit en groupes porteurs des classes CSS d'origine. La feuille de style est adaptée au DOM de Gutenberg, pas l'inverse.

Élément HTMLBloc natifSortie
Conteneurcore/groupdiv.wp-block-group.ma-classe
Titrecore/headingh2.wp-block-heading
Textecore/paragraphp.ma-classe
Imagecore/imagefigure.wp-block-image
Boutoncore/buttonsa.wp-block-button__link
Listecore/listul.wp-block-list
Citationcore/quoteblockquote.wp-block-quote
Formulairecore/shortcodeContact Form 7

Cinq substitutions à connaître

  • Icônes SVG inline → masques CSS (mask-image en data URI) ; une simple flèche de bouton reste un pseudo-élément : .btn-arrow a::after{content:"→"}.
  • Graphiques décoratifs → CSS pur. Une barre de plage se dessine avec ::before pour la bande et ::after portant deux radial-gradient pour les poignées.
  • Boutons d'onglet → paragraphes éditables, auxquels le script ajoute role="button", tabindex et la gestion clavier.
  • Accent typographique → <em> nu, stylé par son contexte (.titre-hero em { … }) : un <em class="…"> est invalidé par l'éditeur.
  • Couches décoratives (voiles, lueurs, sigles) → ::before/::after du parent, jamais de groupe vide — un bloc vide dans l'éditeur finit supprimé par le client.

Interactions sans injection

Règle 7. Toutes les variantes sont dans la page ; le script ne fait que basculer une classe.

// Toutes les variantes sont présentes dans le contenu.
// Le script ne crée rien : il bascule "est-actif".
function brancher(selecteurDeclencheur, ciblesParSelecteur) {
  const items = [...document.querySelectorAll(selecteurDeclencheur)];
  items.forEach((el, i) => {
    el.setAttribute('role', 'button');
    el.setAttribute('tabindex', '0');
    const activer = () => {
      items.forEach(x => x.classList.remove('est-actif'));
      el.classList.add('est-actif');
      Object.entries(ciblesParSelecteur).forEach(([sel, prefixe]) => {
        document.querySelectorAll(sel).forEach(c =>
          c.classList.toggle('est-actif', c.classList.contains(prefixe + (i + 1))));
      });
    };
    el.addEventListener('click', activer);
    el.addEventListener('keydown', e => {
      if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); activer(); }
    });
  });
}
brancher('.opt', { '.cap': 'cap-', '.w-deal': 'w-deal-' });

Le CSS masque les variantes inactives avec display:none, et les révèle sur .est-actif. Conséquence : les quatre variantes d'un sélecteur sont toutes éditables dans Gutenberg, y compris celles qui ne sont pas affichées.

Neutraliser l'espacement automatique — sans détruire le vôtre

Surtout pas .wp-block-group > * { margin-block: 0 } : cette règle, à spécificité (0,1,0) en fin de feuille, écrase toutes les marges déclarées par les composants — quatorze marges à zéro et 307 px de rythme vertical perdus en conditions réelles. La coupure se fait à la source dans theme.json, et le filet de sécurité CSS s'écrit à spécificité nulle.

/* theme.json */
"settings": { "spacing": { "blockGap": true } },
"styles":   { "spacing": { "blockGap": "0px" } }

/* style.css — filet de sécurité, spécificité nulle */
:where(.wp-block-group) > :where(*), :where(.wp-block-buttons) { margin-block: 0 }
figure.wp-block-image { margin: 0 }
Contrainte côté serveur L'écriture de fichiers .php y est verrouillée hors d'un bac à sable (le détail est en phase 5). Les patterns étant des fichiers PHP, ils ne peuvent pas être écrits un par un : ils partent dans l'archive du thème. C'est de toute façon plus rapide qu'une trentaine d'écritures.
05

Déploiement

15 minAgent seulReproductible

Prérequis · L'accès au site

C'est seulement maintenant que WordPress entre en scène — les phases 0 à 4 n'ont eu besoin d'aucun accès au site. Prérequis : le plugin Novamira actif sur le site cible et son bundle .mcpb téléchargé (leur mise en place est montrée dans la formation à ce moment précis). Il expose quatre capacités indispensables : exécution PHP (novamira/execute-php), écriture de fichiers non-PHP, lien de téléversement temporaire, et session wp-admin sans mot de passe pour la recette. Extraire les identifiants du bundle :

# le .mcpb est une archive zip
unzip -o site.mcpb -d bundle
python3 -c "import json;print(json.load(open('bundle/manifest.json'))['server']['mcp_config']['env'])"

Deux voies d'accès, au choix :

  1. Serveur MCP — claude mcp add … -- npx -y @automattic/mcp-wordpress-remote@latest. Nécessite un redémarrage de session.
  2. HTTP direct — appeler l'endpoint JSON-RPC en curl. Aucun redémarrage, utilisable immédiatement. Script fourni en annexe.
Sécurité Le bundle contient un mot de passe applicatif en clair. Ne jamais le committer. Le stocker au niveau utilisateur, pas dans le dossier du projet. S'il a transité par un canal non fiable, le révoquer depuis wp-admin → Utilisateurs → Mots de passe d'application.

Huit opérations, numérotées de 0 à 7, dans cet ordre. L'ordre n'est pas décoratif : les visuels s'importent depuis le dossier du thème côté serveur, donc après son téléversement.

0 · Sauvegarder l'existant

Le site cible n'est jamais supposé vide. Sérialiser pages, articles et réglages (thème actif, page d'accueil, titre du site) dans wp-content/uploads/sauvegarde-avant-theme.json, et signaler les collisions de slug : une page « accueil » ou « contact » déjà en place sera écrasée, son contenu d'origine ne survivra que dans la sauvegarde. L'ancien thème reste installé — le retour en arrière tient en un switch_theme() plus ce fichier. Le code exact est dans references/phases.md.

1 · Empaqueter et téléverser le thème

zip -rq mon-theme.zip mon-theme -x '*.DS_Store'

# créer un point de dépôt temporaire côté WordPress
novamira/create-upload-link { "path": "wp-content/uploads/theme.zip", "overwrite": true }

# puis déposer le fichier
curl -X PUT -H "X-Novamira-Upload-Token: $TOKEN" \
     --data-binary @mon-theme.zip "$UPLOAD_URL"

L'archive contient un dossier portant exactement le slug du thème : c'est lui que switch_theme() attend. La réponse de create-upload-link fournit upload_url et upload_token — ce sont eux qui remplissent les variables du curl.

2 · Décompresser et activer

require_once ABSPATH . 'wp-admin/includes/file.php';
WP_Filesystem();

// vider les dossiers versionnés avant de réécrire,
// sinon d'anciens patterns survivent au déploiement
foreach (['patterns','templates','parts'] as $d) {
  foreach (glob(get_theme_root() . "/mon-theme/$d/*") as $f) @unlink($f);
}

unzip_file(ABSPATH . 'wp-content/uploads/theme.zip', get_theme_root());
wp_clean_themes_cache();
switch_theme('mon-theme');

3 · Importer les visuels dans la médiathèque

Règle 6 — et seulement maintenant : avant le téléversement du thème, il n'y a rien à copier côté serveur. Conserver la table slug → identifiant, elle alimente l'attribut id de chaque bloc image.

require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/image.php';

$tmp = wp_tempnam($slug);
copy(get_theme_root() . "/mon-theme/assets/img/$slug.jpg", $tmp);
$id = media_handle_sideload(['name' => "$slug.jpg", 'tmp_name' => $tmp], 0, $alt);
wp_update_post(['ID' => $id, 'post_name' => $slug]);
update_post_meta($id, '_wp_attachment_image_alt', $alt);

4 · Créer le formulaire Contact Form 7

Avant les pages, puisque leur contenu référence son shortcode. Vérifier class_exists('WPCF7_ContactForm') — rien dans la méthode ne l'installe implicitement ; au besoin, installer et activer l'extension contact-form-7 d'abord. Gabarit avec les classes de la maquette, textarea limité (40x6 : CF7 en met 10 par défaut, une centaine de pixels de trop), Reply-To sur le courriel du visiteur.

5 · Créer les pages

Passer le contenu par un fichier JSON téléversé plutôt qu'en ligne : un contenu de page fait facilement 90 Ko.

$pages = json_decode(file_get_contents(ABSPATH . 'wp-content/uploads/content.json'), true);

foreach ($pages as $pg) {
  $existe = get_page_by_path($pg['slug']);
  $data = [
    'post_type'    => 'page',
    'post_title'   => $pg['title'],
    'post_name'    => $pg['slug'],
    'post_status'  => 'publish',
    'post_content' => $pg['content'],
  ];
  if ($existe) { $data['ID'] = $existe->ID; wp_update_post($data); }
  else { wp_insert_post($data); }
}

6 · Déclarer la page d'accueil statique

Règle 4. L'étape la plus facile à oublier, et celle qui rend le site inutilisable si on la saute.

$accueil = get_page_by_path('accueil');
update_option('show_on_front', 'page');
update_option('page_on_front', $accueil->ID);

// permaliens propres, tant qu'on y est
global $wp_rewrite;
$wp_rewrite->set_permalink_structure('/%postname%/');
$wp_rewrite->flush_rules();

7 · Purger, versionner, vérifier ce qui est réellement servi

Trois caches se superposent : la page (LiteSpeed), le navigateur (le HTML est servi avec max-age=604800 — sept jours — chez Hostinger) et les en-têtes de thème qui figent le ?ver= des assets. Après chaque modification de style.css : incrémenter Version:, purger, comparer par curl le fichier servi au fichier local à l'octet près, et contrôler avec ?frais=n — jamais ?m= ni un autre paramètre réservé. Supprimer ensuite theme.zip et content.json : ils sont servis publiquement depuis uploads/.

06

Recette

Obligatoire15 minCinq portes

Une page peut s'afficher parfaitement et rester inutilisable dans l'éditeur — ou fidèle à l'œil et fausse au pixel. Les cinq contrôles se font dans cet ordre, d'office, sans que le client les demande ; aucun ne remplace les autres.

Porte 1 · Intégrité des blocs, côté serveur

Confirme que le contenu se parse et ne contient ni HTML brut ni bloc tiers.

function compter($blocs, &$n) {
  foreach ($blocs as $b) {
    if (empty($b['blockName'])) continue;
    $n[$b['blockName']] = ($n[$b['blockName']] ?? 0) + 1;
    if (!empty($b['innerBlocks'])) compter($b['innerBlocks'], $n);
  }
}
$n = []; compter(parse_blocks(get_page_by_path('accueil')->post_content), $n);
return [
  'total'     => array_sum($n),
  'html_brut' => $n['core/html'] ?? 0,          // doit valoir 0
  'hors_core' => count(array_filter(array_keys($n),
                   fn($k) => strpos($k, 'core/') !== 0)),  // doit valoir 0
];

Porte 2 · Validation dans l'éditeur

Le seul contrôle qui détecte les blocs marqués « contenu inattendu ou invalide ». Ouvrir la page en édition, puis interroger le store.

// dans la console, sur post.php?post=ID&action=edit
const aplatir = bs => bs.reduce((a, b) => a.concat([b], aplatir(b.innerBlocks || [])), []);
const tous = aplatir(wp.data.select('core/block-editor').getBlocks());
const invalides = tous.filter(b => b.isValid === false);

({
  total: tous.length,
  invalides: invalides.length,              // doit valoir 0
  noms: invalides.map(b => b.name),
  images_total: tous.filter(b => b.name === 'core/image').length,
  images_liees: tous.filter(b => b.name === 'core/image' && b.attributes.id).length
});   // une expression, pas un return : un return de premier niveau est une SyntaxError en console
Critère de réception invalides === 0 et images_liees === images_total. Tant que ce n'est pas le cas, le client ne peut pas travailler.

Porte 3 · Rendu et responsive

  • Toutes les images servies en HTTP 200, taille non nulle.
  • Polices réellement appliquées : contrôler getComputedStyle(document.body).fontFamily.
  • Interactions fonctionnelles après déconnexion, pas seulement en session administrateur.
  • Rendu à 390 px sans défilement horizontal.
  • La barre d'administration ne recouvre pas l'en-tête en position absolue.
Toujours vérifier déconnecté Une session administrateur ajoute la barre d'outils et, chez certains hébergeurs, un panneau qui recouvre tout l'écran mobile (le chatbot Hostinger, par exemple). Le rendu réel se constate déconnecté, avec un paramètre d'URL neutre pour contourner le cache — ?frais=1, jamais ?m= ni un autre paramètre réservé de WordPress.

Porte 4 · Fidélité au pixel

Le seul contrôle qui attrape ce que l'œil rate : marges écrasées, blockGap résiduel, champ de formulaire trop haut. Relever la géométrie et les styles calculés des mêmes sélecteurs sur la maquette locale puis sur le site, et différer les deux relevés. Réception : hauteur de page identique à ±2 px, aucun composant au-delà de 2 px d'écart. Le script de relevé est dans references/phases.md.

Porte 5 · Fidélité de l'éditeur

Dans le canevas de Gutenberg (l'iframe editor-canvas), mesurer que les couches sorties du flux le sont restées (position: absolute), que l'enveloppe occupe toute la largeur du canevas et qu'aucun bouton ne s'affiche en texte vertical. Si ça échoue, c'est assets/editor.css qui manque : Gutenberg force position: relative sur chaque bloc, et sans cette feuille l'aperçu s'effondre.

§

Pièges recensés

Rencontrés en conditions réelles, avec leur symptôme exact et leur correctif.

Page blanche après écriture d'une option de thème
Symptôme
« Il y a eu une erreur critique sur ce site ». Le journal d'erreurs ne contient rien d'utile.
Cause
Une option typée a reçu un tableau responsive là où le thème attendait une chaîne. Exemple vécu : hb-primary-footer-height dont le défaut est une chaîne vide.
Diagnostic
Reproduire le rendu en PHP : ob_start(); try { wp_head(); } catch (\Throwable $e) { … }. La trace donne le fichier et la ligne exacts.
Correctif
Lire le défaut avant d'écrire, et respecter son type. Plus généralement : ne pas empiler un thème tiers sous un design sur mesure, règle 8.
Toutes les images cassées après déploiement
Symptôme
Le texte alternatif s'affiche à la place de chaque image. Les URL se terminent sans extension.
Cause
Extension oubliée dans le générateur de patterns : get_theme_file_uri('assets/img/nom') au lieu de 'assets/img/nom.jpg'.
Correctif
Corriger, régénérer, et ne renvoyer qu'une archive des patterns : 11 Ko au lieu de 2,3 Mo.
Page d'accueil impossible à modifier
Symptôme
Aucune page « Accueil » dans la liste des pages. Le contenu n'apparaît que dans l'éditeur de site.
Cause
front-page.html appelle directement des wp:pattern. Le contenu appartient au template, pas à une page.
Correctif
Créer une page réelle, y placer le contenu, réduire front-page.html à header + post-content + footer, puis show_on_front = page. Règle 4.
Le serveur de développement refuse de démarrer
Symptôme
Error: EPERM: operation not permitted, uv_cwd pour Node, PermissionError pour Python.
Cause
Le processus est lancé avec un répertoire courant protégé par macOS sans que l'autorisation ait été accordée. C'est le cwd qui est en cause, pas l'emplacement des fichiers servis.
Correctif
Lancer depuis /tmp en passant RACINE en chemin absolu : le serveur lit sans difficulté un dossier que son cwd ne pourrait pas occuper. Ce n'est qu'en cas d'échec de lecture qu'il faut déplacer le projet.
« Pourquoi mon dossier est vide ? »
Symptôme
Le site est en ligne, tout fonctionne, et l'utilisateur ne trouve aucun fichier là où il les attendait.
Cause
Le projet a été créé hors du dossier de travail « par précaution TCC », sans vérifier que la protection s'appliquait, et sans le redire ensuite.
Correctif
Tester l'accès en phase 0, et ne sortir du dossier de travail que sur un échec réel. Une consigne de prudence appliquée sans vérification ne casse rien, donc rien ne la signale — seul l'utilisateur finit par buter dessus.
Composants sans style après passage en blocs natifs
Symptôme
Les grilles s'empilent, les cartes perdent leurs marges intérieures.
Cause
Le DOM a changé : une image est devenue figure.wp-block-image, un conteneur div.wp-block-group. Les sélecteurs écrits pour du HTML brut ne matchent plus.
Correctif
Poser les classes du composant directement sur le bloc via className. La classe shot sur un core/image produit figure.wp-block-image.shot : le sélecteur d'origine fonctionne tel quel.
Une itération de mise en page coûte quatre minutes
Symptôme
Chaque retouche exige de composer, mettre en file, sérialiser dans un onglet d'administration ouvert, purger le cache, recharger.
Cause
Le design est poussé dans le post_content par l'API de blocs au lieu d'être écrit en fichiers.
Correctif
C'est le principe même de la méthode. La file de blocs sert au contenu, jamais à la mise en forme.
Le rythme vertical disparaît après le passage en blocs
Symptôme
Rien ne semble cassé, mais la page est ~300 px plus courte que la maquette : titres collés aux paragraphes, décalages volontaires remis à plat. L'œil ne le voit pas ; la porte 4 le mesure.
Cause
La neutralisation naïve de l'espacement : .wp-block-group > * { margin-block: 0 } écrase toutes les marges déclarées par les composants.
Correctif
blockGap: "0px" dans theme.json + filet de sécurité à spécificité nulle (:where()). Voir la phase 4 et blocs-gutenberg.md.
Un correctif CSS ne « prend » pas
Symptôme
Le fichier est corrigé sur le serveur — curl le prouve — mais le navigateur affiche l'ancien rendu, purge après purge.
Cause
Trois caches superposés : la page LiteSpeed, le navigateur (HTML servi avec max-age=604800, sept jours chez Hostinger), et le cache objet des en-têtes de thème qui fige le ?ver= des assets.
Correctif
Incrémenter Version: dans style.css, purger, comparer par curl le fichier servi au fichier local à l'octet près, contrôler avec ?frais=n. Phase 5, étape 7.
§

Annexes

Appeler Novamira en HTTP direct

Évite d'installer le serveur MCP et de redémarrer la session. Le protocole exige une session : ouvrir, mémoriser l'identifiant, le réutiliser.

#!/bin/bash
# wp.sh <nom-de-l-ability> '<json des parametres>'
A="https://SITE/wp-json/mcp/novamira"
U="…"; P="…"
SID_FILE="$(dirname "$0")/.mcp-sid"

ouvrir() {
  H=$(curl -s -D - -o /dev/null -u "$U:$P" -X POST "$A" \
    -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
    -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
         "protocolVersion":"2024-11-05","capabilities":{},
         "clientInfo":{"name":"cc","version":"1"}}}')
  SID=$(printf '%s' "$H" | grep -i '^mcp-session-id:' | tr -d '\r' | awk '{print $2}')
  printf '%s' "$SID" > "$SID_FILE"
  curl -s -o /dev/null -u "$U:$P" -X POST "$A" \
    -H 'Content-Type: application/json' -H "Mcp-Session-Id: $SID" \
    -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
  echo "$SID"
}

[ -s "$SID_FILE" ] && SID=$(cat "$SID_FILE") || SID=$(ouvrir)

curl -s -u "$U:$P" -X POST "$A" \
  -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -H "Mcp-Session-Id: $SID" \
  -d "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{
       \"name\":\"mcp-adapter-execute-ability\",
       \"arguments\":{\"ability_name\":\"$1\",\"parameters\":$2}}}"

La réponse peut arriver en text/event-stream : lire la ligne préfixée data: avant de parser le JSON.

Ouvrir wp-admin sans mot de passe

Pour la porte 2 de la recette, un agent a besoin d'une session d'administration dans un navigateur. Novamira fournit un échange à usage unique.

# 1. demander un jeton
novamira/create-admin-access-link {
  "expires_in": 180,
  "admin_path": "post.php?post=38&action=edit"
}

# 2. l'echanger contre une URL de connexion (valable 60 s)
curl -s -X POST \
  -H "X-Novamira-Admin-Access-Token: $TOKEN" \
  -H "X-Novamira-Admin-Access-Nonce: $NONCE" \
  "$EXCHANGE_URL"

# 3. ouvrir immediatement l'URL renvoyee dans le navigateur

Ordre de dépendance des phases

PhaseBloque surLivre
0 · PréparationRienOutils, dossier de travail
1 · BriefRéférence visuellePlan de design validé
2 · MaquettePhotos réellesAccueil HTML validé
3 · DéclinaisonPhase 2 validéeToutes les pages + CSS partagé
4 · Thème blocPhase 3Thème complet, hors ligne
5 · DéploiementAccès WordPressSite en ligne
6 · RecettePhase 5Réception, ou retour en 4

Ce que la méthode ne couvre pas

  • Formulaires — aucun bloc natif n'existe. Contact Form 7 ou équivalent, posé par core/shortcode. Les champs se règlent dans l'interface du plugin, pas dans Gutenberg.
  • Commerce — WooCommerce impose ses propres templates et sort du cadre.
  • Multilingue — à traiter avant la phase 4, le choix de l'extension conditionne la structure des templates.
  • Performance — les visuels sont livrés en JPEG pleine taille. Prévoir une passe de compression et de formats modernes avant mise en production réelle.