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.
La règle 0, puis les dix autres
Non négociables. Chacune vient d'une erreur réellement commise et de son coût.
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.
Préparation
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.
Brief
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
- Le sujet concret : entreprise, audience, mission unique de la page.
- La palette, 4 à 6 valeurs hexadécimales nommées.
- Les typographies et leurs rôles.
- Deux ou trois écarts volontaires avec la référence, justifiés.
Maquette
Une seule page, l'accueil, en HTML et CSS, en local. C'est ici que se joue 90 % de la qualité finale.
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 :
| Source | Coût | Cohérence | Quand |
|---|---|---|---|
| Photos du client | Nulle | Maximale | Dès qu'elles existent |
| Unsplash | Nulle | Moyenne | Démo, secteur générique |
| Génération | Crédits | Élevée | Direction 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
- L'agent écrit la page complète, avec du contenu réel, jamais de lorem.
- Il prend une capture et corrige ce qu'elle montre.
- 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à.
- Le client regarde et nomme précisément ce qui cloche.
- 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.
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.
Déclinaison
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.
| Maquette | Devient en phase 4 |
|---|---|
| Fonction nav() | parts/header.html |
| Constante FOOTER | parts/footer.html |
| Chaque section | Un fichier dans patterns/ |
| styles.css | theme.json + style.css |
| app.js | assets/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.
Thème bloc
Arborescence cible
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 HTML | Bloc natif | Sortie |
|---|---|---|
| Conteneur | core/group | div.wp-block-group.ma-classe |
| Titre | core/heading | h2.wp-block-heading |
| Texte | core/paragraph | p.ma-classe |
| Image | core/image | figure.wp-block-image |
| Bouton | core/buttons | a.wp-block-button__link |
| Liste | core/list | ul.wp-block-list |
| Citation | core/quote | blockquote.wp-block-quote |
| Formulaire | core/shortcode | Contact 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 }
Déploiement
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 :
- Serveur MCP — claude mcp add … -- npx -y @automattic/mcp-wordpress-remote@latest. Nécessite un redémarrage de session.
- HTTP direct — appeler l'endpoint JSON-RPC en curl. Aucun redémarrage, utilisable immédiatement. Script fourni en annexe.
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/.
Recette
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
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.
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.
hb-primary-footer-height dont le défaut est une chaîne vide.ob_start(); try { wp_head(); } catch (\Throwable $e) { … }. La trace donne le fichier et la ligne exacts.get_theme_file_uri('assets/img/nom') au lieu de 'assets/img/nom.jpg'.front-page.html appelle directement des wp:pattern. Le contenu appartient au template, pas à une page.front-page.html à header + post-content + footer, puis show_on_front = page. Règle 4.Error: EPERM: operation not permitted, uv_cwd pour Node, PermissionError pour Python.cwd qui est en cause, pas l'emplacement des fichiers servis./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.figure.wp-block-image, un conteneur div.wp-block-group. Les sélecteurs écrits pour du HTML brut ne matchent plus.className. La classe shot sur un core/image produit figure.wp-block-image.shot : le sélecteur d'origine fonctionne tel quel.post_content par l'API de blocs au lieu d'être écrit en fichiers..wp-block-group > * { margin-block: 0 } écrase toutes les marges déclarées par les composants.blockGap: "0px" dans theme.json + filet de sécurité à spécificité nulle (:where()). Voir la phase 4 et blocs-gutenberg.md.curl le prouve — mais le navigateur affiche l'ancien rendu, purge après purge.max-age=604800, sept jours chez Hostinger), et le cache objet des en-têtes de thème qui fige le ?ver= des assets.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
| Phase | Bloque sur | Livre |
|---|---|---|
| 0 · Préparation | Rien | Outils, dossier de travail |
| 1 · Brief | Référence visuelle | Plan de design validé |
| 2 · Maquette | Photos réelles | Accueil HTML validé |
| 3 · Déclinaison | Phase 2 validée | Toutes les pages + CSS partagé |
| 4 · Thème bloc | Phase 3 | Thème complet, hors ligne |
| 5 · Déploiement | Accès WordPress | Site en ligne |
| 6 · Recette | Phase 5 | Ré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.