Un starter theme FSE, on en croise des dizaines, mais la plupart traînent encore des fichiers inutiles hérités de l’ère PHP classique. Ici, l’objectif est différent : repartir d’une base HTML-first, allégée, et surtout compatible avec les Block Bindings pour connecter vos contenus dynamiques sans plugin superflu. Templates, theme.json, PHP minimal : on construit ensemble une fondation propre, pensée pour durer et évoluer avec les prochaines versions de WordPress.
Structure de fichiers d’un starter theme FSE minimaliste
Avant de coder quoi que ce soit, il faut poser une arborescence claire. Un starter theme FSE minimaliste ne ressemble pas à un thème classique : on va au strict nécessaire, sans fichiers superflus, sans dépendances qu’on n’utilisera jamais. L’idée, c’est de partir d’une base saine qu’on pourra enrichir au fil des projets, sans traîner du code mort.
Arborescence de base et rôle de chaque dossier
Voici la structure minimale qu’on utilise pour un starter theme FSE :
mon-starter-theme/
├── style.css
├── theme.json
├── functions.php
├── templates/
│ ├── index.html
│ ├── single.html
│ ├── archive.html
│ └── page.html
├── parts/
│ ├── header.html
│ └── footer.html
└── patterns/
└── (fichiers .php de patterns)
Chaque dossier a un rôle précis :
- /templates : contient les templates principaux du thème, ceux qui définissent la structure d’une page selon son contexte (accueil, single, archive, page statique).
- /parts : regroupe les éléments réutilisables entre plusieurs templates (header, footer, sidebar éventuellement).
- /patterns : accueille les block patterns enregistrés via PHP, réutilisables dans l’éditeur.
- functions.php : reste léger, il sert surtout à enregistrer les supports du thème (theme supports, menus, tailles d’images) et éventuellement les Block Bindings personnalisés.
Contrairement à un thème classique, on ne trouve ici aucun fichier PHP de template. Pas de header.php, pas de footer.php, pas de page.php. Le FSE repose entièrement sur le HTML et les blocs : la logique de templating passe par theme.json et les fichiers .html, PHP n’intervenant que pour les fonctions utilitaires ou l’enregistrement de patterns.
Le fichier style.css et l’en-tête du thème
Même minimaliste, un thème WordPress a besoin d’un style.css avec un en-tête de métadonnées. C’est ce fichier qui permet à WordPress d’identifier le thème dans l’admin. Voici un exemple type :
/*
Theme Name: Mon Starter FSE
Theme URI: https://dev-wp.fr/mon-starter-fse
Author: Etienne
Author URI: https://dev-wp.fr
Description: Un starter theme FSE minimaliste avec support des Block Bindings.
Version: 1.0.0
Requires at least: 6.4
Tested up to: 6.6
Requires PHP: 8.0
License: GNU General Public License v2 or later
License URI: LICENSE
Text Domain: mon-starter-fse
*/
Ces métadonnées sont obligatoires (Theme Name en tête, sinon WordPress ignore le thème). Notez que le fichier reste quasiment vide côté CSS : dans une logique FSE, on privilégie theme.json pour définir les couleurs, typographies et espacements, plutôt que d’écrire du CSS custom à la main. On évite ainsi la duplication de règles entre les deux fichiers.
Organisation des templates et template parts
Dans /templates, chaque fichier HTML correspond à un contexte d’affichage précis : index.html sert de fallback général, single.html gère l’affichage des articles, archive.html les listes (catégories, tags, auteurs) et page.html les pages statiques. Ce sont des fichiers HTML contenant des commentaires de blocs (block comments), pas du PHP.
Les /parts fonctionnent comme des blocs réutilisables qu’on insère dans les templates via le bloc « Template Part ». Un header.html typique commence ainsi :
<!-- wp:group {"tagName":"header"} -->
<header class="wp-block-group">
<!-- wp:site-title /-->
<!-- wp:navigation /-->
</header>
<!-- /wp:group -->
Cette organisation permet de modifier le header ou le footer une seule fois, l’impact se répercute automatiquement sur tous les templates qui l’appellent. C’est nettement plus maintenable qu’un get_header() dispersé dans dix fichiers PHP différents.
Enfin, on garde volontairement peu de dépendances : pas de framework CSS externe, pas de build tool obligatoire. On s’appuie sur les blocs natifs de WordPress et sur theme.json pour piloter le style global. Cette sobriété facilite la maintenance et prépare le terrain pour intégrer proprement les Block Bindings dans les sections suivantes.
Le theme.json au coeur du starter theme
Si l’arborescence pose les fondations, le theme.json est vraiment le chef d’orchestre de tout starter theme FSE. C’est lui qui définit ce que l’utilisateur peut modifier dans l’éditeur (couleurs, typographie, espacement) et ce qui reste verrouillé. Sans lui, votre thème FSE ne serait qu’une coquille vide. Autant dire qu’il mérite qu’on s’y attarde sérieusement.
Réglages globaux (settings) : couleurs, typographie, espacement
La section settings définit le terrain de jeu du site editor. On y déclare la palette de couleurs, les familles de polices, les tailles de texte (avec des valeurs fluides via clamp(), très pratique pour le responsive) et les espacements disponibles. Voici un exemple concret :
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"settings": {
"color": {
"palette": [
{ "slug": "primary", "color": "#1e293b", "name": "Primaire" },
{ "slug": "accent", "color": "#f59e0b", "name": "Accent" },
{ "slug": "background", "color": "#ffffff", "name": "Fond" }
],
"custom": true,
"customDuotone": false
},
"typography": {
"fontFamilies": [
{
"slug": "system",
"fontFamily": "-apple-system, BlinkMacSystemFont, sans-serif",
"name": "Système"
}
],
"fontSizes": [
{ "slug": "small", "size": "clamp(0.875rem, 0.8rem + 0.3vw, 1rem)", "name": "Petit" },
{ "slug": "large", "size": "clamp(1.5rem, 1.2rem + 1.2vw, 2.5rem)", "name": "Grand" }
]
},
"spacing": {
"units": ["px", "rem", "%"],
"spacingSizes": [
{ "slug": "40", "size": "1rem", "name": "40" },
{ "slug": "60", "size": "1.5rem", "name": "60" }
]
}
}
}
Rien de sorcier ici : on active ce qu’on veut exposer (custom: true pour les couleurs personnalisées, par exemple) et on désactive le reste. C’est cette granularité qui permet de garder un starter theme minimaliste, sans options superflues qui perdent l’utilisateur final.
Styles globaux et styles par bloc
Une fois les réglages posés, place aux styles. La section styles permet d’appliquer des valeurs par défaut globalement, mais aussi bloc par bloc. C’est très utile pour un starter theme : on fixe une base cohérente sans avoir à toucher au CSS.
{
"styles": {
"typography": {
"fontFamily": "var(--wp--preset--font-family--system)",
"lineHeight": "1.6"
},
"blocks": {
"core/paragraph": {
"typography": { "fontSize": "var(--wp--preset--font-size--small)" },
"spacing": { "margin": { "bottom": "var(--wp--preset--spacing--40)" } }
},
"core/heading": {
"typography": {
"fontSize": "var(--wp--preset--font-size--large)",
"fontWeight": "700"
},
"color": { "text": "var(--wp--preset--color--primary)" }
}
}
}
}
On voit bien l’intérêt : chaque bloc hérite d’un style par défaut, cohérent avec la palette et la typographie définies plus haut. Et si l’utilisateur veut modifier ponctuellement un paragraphe dans l’éditeur, il le peut toujours (à condition d’avoir laissé les bons supports activés).
Configuration des templates parts et des zones de contenu
Les templateParts désignent les zones réutilisables du site (header, footer, sidebar…). Dans theme.json, on les déclare avec un name (qui correspond au fichier HTML dans parts/) et une area (header, footer ou uncategorized) :
{
"templateParts": [
{ "name": "header", "title": "En-tête", "area": "header" },
{ "name": "footer", "title": "Pied de page", "area": "footer" }
],
"customTemplates": [
{ "name": "page-large", "title": "Page pleine largeur", "postTypes": ["page"] }
]
}
Cette déclaration permet à l’éditeur de proposer ces zones dans le site editor, avec les bonnes icônes et les bons emplacements. Les customTemplates, eux, servent à proposer des modèles alternatifs pour certains types de contenu (une page sans sidebar, par exemple). Pratique pour donner de la flexibilité sans multiplier les fichiers inutilement.
Activer et préparer le support des Block Bindings dans theme.json
Voilà un point qui prête souvent à confusion : non, theme.json ne configure pas directement les Block Bindings. Ce n’est tout simplement pas une API exposée dans ce fichier. Les Block Bindings, ce sont des sources de données (comme les champs personnalisés) reliées à des attributs de blocs, et ça se gère via PHP, pas via JSON.
Par contre, theme.json prépare le terrain. Concrètement, il faut s’assurer que les bons supports sont activés pour que les sources de bindings fonctionnent correctement dans l’éditeur. Deux étapes essentielles côté functions.php :
- Activer le support des champs personnalisés si nécessaire :
add_theme_support('custom-fields'). - Enregistrer les métadonnées avec
register_post_meta(), en précisantshow_in_rest: true(indispensable pour que le binding fonctionne dans l’éditeur de blocs).
add_action('init', function () {
register_post_meta('post', 'prix_produit', [
'show_in_rest' => true,
'single' => true,
'type' => 'string',
]);
});
Bon, petite précision technique importante : le JSON strict n’accepte pas de commentaires (// ou /* */ provoqueront une erreur de parsing). Si vous voulez documenter votre theme.json, faites-le dans un fichier séparé (README, documentation du thème) plutôt que dans le fichier lui-même. Certains éditeurs supportent le JSON5 qui autorise les commentaires, mais WordPress attend un JSON strict, donc autant ne pas prendre de risques.
En résumé : theme.json fixe le décor (couleurs, typo, structure), mais c’est bien le PHP qui active les mécanismes nécessaires aux Block Bindings. Les deux fichiers travaillent main dans la main, chacun avec son rôle précis.
Implémenter des Block Bindings concrets en PHP
Bon, on rentre dans le dur. La théorie sur theme.json, c’est bien joli, mais les Block Bindings prennent tout leur sens quand on les voit tourner avec du vrai code. Direction functions.php (ou un fichier dédié inc/block-bindings.php, ce qui est plus propre pour la maintenance).
L’idée de base : au lieu d’écrire un template PHP custom pour afficher une donnée dynamique, on « branche » directement un attribut de bloc sur une source de données. Le rendu se fait automatiquement côté front, sans template-parts.php ni fonction de rendu maison. Voyons comment ça marche concrètement.
Enregistrer une source de binding personnalisée avec register_block_bindings_source
Tout commence par l’enregistrement d’une source. C’est elle qui dit à WordPress « voici d’où vient la donnée, et voici comment aller la chercher ».
<?php
/**
* Enregistre une source de binding personnalisée pour le prix produit.
* On accroche ça sur init, comme n'importe quel enregistrement de bloc.
*/
add_action( 'init', 'devwp_register_prix_produit_binding' );
function devwp_register_prix_produit_binding() {
// register_block_bindings_source() attend un identifiant unique
// au format namespace/nom, exactement comme pour un bloc custom.
register_block_bindings_source(
'dev-wp/prix-produit',
array(
// Label affiché dans l'interface de l'éditeur (encore limitée en 2026,
// on y revient plus bas).
'label' => __( 'Prix produit', 'dev-wp' ),
// Le callback appelé pour récupérer la valeur réelle.
'get_value_callback' => 'devwp_get_prix_produit')
);
}
/**
* Callback de récupération de la valeur.
* Reçoit un tableau d'arguments définis dans le binding (source_args),
* le bloc courant, et le nom de l'attribut ciblé.
*/
function devwp_get_prix_produit( $source_args, $block_instance, $attribute_name ) {
// On récupère l'ID du post courant depuis le contexte du bloc.
$post_id = $block_instance->context['postId'] ?? get_the_ID();
if ( ! $post_id ) {
return '';
}
// La meta "prix_produit" doit avoir été enregistrée via register_post_meta
// (voir plus bas), sinon get_post_meta fonctionnera quand même
// mais sans exposition REST.
$prix = get_post_meta( $post_id, 'prix_produit', true );
// On formate un peu la sortie, histoire d'avoir un rendu propre.
return $prix ? number_format_i18n( (float) $prix, 2 ) . ' €' : '';
}
Rien de sorcier ici : get_value_callback fait le travail qu’on ferait normalement dans un template PHP avec the_meta() ou get_post_meta(). Sauf que là, c’est WordPress qui déclenche l’appel au bon moment, via render_block.
Lier des metadonnées personnalisées à un bloc Paragraph ou Image
Une fois la source enregistrée, on peut la brancher sur un bloc directement dans un template ou un pattern. La syntaxe passe par l’attribut metadata.bindings dans le commentaire du bloc.
<!-- wp:paragraph {
"metadata": {
"bindings": {
"content": {
"source": "dev-wp/prix-produit"
}
}
}
} -->
<p>Prix indisponible</p>
<!-- /wp:paragraph -->
Quelques points importants à noter :
- La clé
contentcorrespond à l’attribut du bloc Paragraph qu’on veut lier (pas le contenu HTML global, juste cet attribut précis). - Le texte « Prix indisponible » sert de fallback, affiché si le binding ne retourne rien (utile en preview ou si la meta est vide).
- On peut passer des
argssupplémentaires dans le binding si le callback en a besoin (par exemple unkeydifférent pour réutiliser la même source sur plusieurs metas).
Pour un bloc Image, le principe est identique mais on cible l’attribut url :
<!-- wp:image {
"metadata": {
"bindings": {
"url": {
"source": "dev-wp/image-produit"
}
}
}
} -->
<figure class="wp-block-image"><img src="" alt="Image produit"/></figure>
<!-- /wp:image -->
Ici, src="" reste vide dans le HTML source, mais l’attribut sera rempli dynamiquement au rendu.
Utiliser les bindings avec les champs ACF ou les custom fields natifs
Pour que tout ça fonctionne proprement, la meta doit être exposée à l’API REST via register_post_meta, avec show_in_rest à true. C’est une étape qu’on oublie souvent, et qui bloque silencieusement l’éditeur.
<?php
add_action( 'init', 'devwp_register_meta_produit' );
function devwp_register_meta_produit() {
register_post_meta(
'produit', // Le post type ciblé.
'prix_produit',
array(
'show_in_rest' => true, // Indispensable pour l'éditeur de blocs.
'single' => true,
'type' => 'number',
'auth_callback' => function() {
return current_user_can( 'edit_posts' );
})
);
}
Avec ACF, le principe reste le même : on enregistre une source de binding custom, mais le get_value_callback va chercher la valeur via get_field() plutôt que get_post_meta(). Attention néanmoins à une nuance importante : côté éditeur, l’interface graphique pour lier visuellement un bloc à une source reste limitée en 2026 (pas de menu déroulant natif pour choisir sa source ACF, par exemple). Il faut souvent passer par un plugin complémentaire (Advanced Custom Fields propose son propre support natif des bindings depuis une version récente) ou éditer directement le HTML du template.
Côté front en revanche, tout se passe automatiquement : dès qu’un bloc contient un attribut metadata.bindings, WordPress intercepte le rendu via render_block et injecte la valeur retournée par le callback, sans aucune ligne de template PHP à écrire. C’est là tout l’intérêt : un prix produit, une légende dynamique, une URL d’image ACF s’affichent nativement, avec un thème 100% HTML-first, sans jamais toucher à un fichier single-produit.php.
Un dernier conseil, tiré de l’expérience : testez toujours vos bindings avec une meta vide en base. Si le fallback n’est pas géré proprement, on se retrouve vite avec un layout cassé en production.
Aller plus loin avec le starter theme
Une fois l’ossature du starter theme en place, avec ses templates HTML, son theme.json et ses Block Bindings fonctionnels, reste la question de la durabilité. Un starter theme, ça évolue : nouveaux blocs, nouvelles versions de Gutenberg, nouveaux besoins clients. Autant partir sur de bonnes bases dès maintenant.
Bonnes pratiques de maintenance et de versionnage
Pour un starter theme destiné à être réutilisé sur plusieurs projets, Git est indispensable. On recommande généralement de créer un dépôt dédié au starter theme (indépendant des projets clients), avec des tags de version clairs (v1.0.0, v1.1.0, etc.) pour tracer les évolutions. Chaque projet client peut ensuite forker ou copier ce starter à un instant T, sans dépendance directe.
Côté outillage, si le thème embarque des blocs custom (au delà des simples patterns), l’intégration de npm et wp-scripts devient nécessaire pour compiler le JavaScript et le CSS. Un simple npm run build génère les fichiers de production, et npm run start permet de travailler en mode watch pendant le développement. Cela évite de committer du code non minifié dans le dépôt final.
Le point le plus délicat concerne les mises à jour de theme.json. Si un client personnalise ses couleurs, sa typographie ou ses templates, écraser theme.json lors d’une mise à jour du starter casse tout. Deux approches existent : soit passer par un thème enfant qui surcharge uniquement les valeurs modifiées, soit utiliser le filtre wp_theme_json_data_theme pour injecter des modifications via PHP sans toucher au fichier JSON source. Cette seconde méthode est plus robuste pour un starter theme distribué à grande échelle.
Limites actuelles des Block Bindings et pièges à éviter
Les Block Bindings restent, en 2026, une fonctionnalité perfectible. Il faut le dire clairement pour éviter les mauvaises surprises en production.
Voici les principales limitations à connaître :
- Support partiel dans l’éditeur : tous les attributs de tous les blocs ne sont pas éligibles au binding. Le Paragraph et l’Image fonctionnent bien, mais d’autres blocs (notamment certains blocs de layout) restent hors périmètre.
- Absence d’interface utilisateur native complète : configurer un binding reste souvent une opération technique, via l’éditeur de code ou du PHP. Pour une UI conviviale destinée aux clients non développeurs, des plugins tiers (comme Advanced Custom Fields, qui propose sa propre intégration) restent nécessaires.
- Pas de binding universel : certains attributs complexes (comme les paramètres de style avancés) ne sont tout simplement pas encore bindables nativement.
Ces limites ne doivent pas décourager l’usage des Block Bindings : c’est une API jeune, encore en développement actif dans le core de WordPress. Les prochaines versions de Gutenberg promettent d’élargir la liste des blocs et attributs compatibles, avec potentiellement une interface visuelle native pour gérer les bindings sans passer par le code. À suivre de près, donc, pour ceux qui construisent des starter themes pérennes.
