Après plusieurs années à développer des blocs personnalisés pour différents clients, l’équipe s’est retrouvée avec sept dépôts Git distincts, chacun réimplémentant sensiblement les mêmes briques : un composant de sélection d’icône, un hook pour récupérer les catégories d’un custom post type, une configuration wp-scripts quasi identique copiée-collée d’un projet à l’autre. Chaque correction de bug dans ce composant d’icône devait être répliquée manuellement dans les sept dépôts — un cauchemar de maintenance qui a fini par coûter plus cher que le temps gagné à l’origine en dupliquant plutôt qu’en mutualisant.
Le passage à un monorepo structuré avec les workspaces npm a résolu ce problème de fond, sans pour autant transformer chaque extension client en dépendance rigide d’un package central figé.
Structure générale du dépôt
L’arborescence retenue sépare clairement les extensions destinées aux clients (chacune restant un plugin WordPress autonome et installable seule) et les packages internes partagés, jamais installés directement sur un site.
agence-monorepo/
├── package.json (racine, définit les workspaces)
├── packages/
│ ├── composants-partages/ (icônes, sélecteurs, hooks communs)
│ │ ├── package.json
│ │ └── src/
│ └── config-wp-scripts/ (configuration webpack commune)
│ ├── package.json
│ └── webpack.config.js
└── extensions/
├── client-a-blocs-catalogue/
│ ├── package.json
│ └── src/
├── client-b-blocs-evenements/
│ ├── package.json
│ └── src/
└── client-c-blocs-temoignages/
├── package.json
└── src/
Déclarer les workspaces
Le fichier package.json racine référence les deux dossiers contenant des paquets npm distincts, sans que ceux-ci ne partagent nécessairement la même version de dépendances tierces — chaque workspace conserve ses propres devDependencies si nécessaire.
{
"name": "agence-monorepo",
"private": true,
"workspaces": [
"packages/*",
"extensions/*"
]
}
Une extension cliente référence ensuite le package partagé comme une dépendance npm ordinaire, résolue localement par npm grâce au mécanisme de workspaces, sans publication sur le registre npm public :
{
"name": "client-a-blocs-catalogue",
"dependencies": {
"@agence/composants-partages": "workspace:*"
}
}

Ce qui mérite vraiment d’être partagé
Le piège inverse existe aussi : tout centraliser par principe, y compris des composants qui n’ont en réalité rien de commun d’un client à l’autre, au risque de créer un package fourre-tout difficile à faire évoluer sans casser une extension cliente qui l’utilise différemment. La règle appliquée sur ce monorepo : un composant ou un hook ne migre vers composants-partages qu’après avoir été dupliqué identiquement dans au moins deux extensions différentes, jamais de façon anticipée sur une seule utilisation.
- Le sélecteur d’icônes SVG, utilisé par cinq des sept extensions avec exactement la même interface.
- Le hook
useCategoriesPostType, une fine surcouche deuseSelectpour récupérer les taxonomies d’un custom post type donné. - Les composants de panneau d’inspecteur récurrents : un sélecteur de couleur limité à la palette du thème, un champ de lien avec validation d’URL.
À l’inverse, la logique métier propre à chaque client (le calcul de prix du configurateur du client A, le système de badges d’événements du client B) reste strictement dans son extension respective, sans jamais remonter dans le package partagé.
Configuration wp-scripts centralisée
Plutôt que de dupliquer un fichier webpack.config.js presque identique dans chaque extension, un package config-wp-scripts exporte une fonction qui étend la configuration par défaut de @wordpress/scripts avec les ajustements communs à l’agence (alias de résolution vers les composants partagés, notamment) :
// packages/config-wp-scripts/webpack.config.js
const defaultConfig = require( '@wordpress/scripts/config/webpack.config' );
module.exports = {
...defaultConfig,
resolve: {
...defaultConfig.resolve,
alias: {
'@agence/composants-partages': require.resolve( '@agence/composants-partages' ),
},
},
};
Versionnage indépendant
Chaque extension cliente garde son propre numéro de version et son propre changelog, publiable indépendamment vers le client concerné sans jamais forcer une mise à jour groupée des six autres extensions. Le package partagé, lui, suit un versionnage sémantique classique : une modification qui casse la compatibilité (changement de signature d’un hook, par exemple) passe en version majeure, ce qui oblige à mettre à jour explicitement chaque extension une par une plutôt que de subir une rupture silencieuse.
Le conseil qu’on donne à toute agence qui envisage cette structure : ne migrez jamais tous vos projets existants d’un coup. On a commencé par extraire un seul composant partagé entre deux extensions, validé que le mécanisme de workspace fonctionnait sans accroc en conditions réelles de build et de déploiement, puis étendu progressivement.
En résumé
Un monorepo npm workspaces pour plusieurs extensions de blocs d’agence n’a de sens que si la mutualisation reste guidée par une duplication réellement constatée, pas par anticipation. Sur ces sept extensions, ce passage a réduit à un seul emplacement la maintenance du sélecteur d’icônes et de la configuration de build commune, tout en gardant chaque extension cliente indépendante dans son cycle de publication et sa logique métier propre.