Thème WordPress moderne : la stack Bedrock + Sage + Tailwind + Vite, expliquée pas à pas

Olivier Tech Olivier Tech Dev 36 min de lecture
Thème WordPress moderne : la stack Bedrock + Sage + Tailwind + Vite, expliquée pas à pas

Il y a un moment, dans la vie d’un développeur WordPress, où l’on ouvre le functions.php d’un thème hérité et où l’on comprend que quelque chose ne tourne pas rond. Deux mille lignes.
Des wp_enqueue_script empilés depuis trois ans. Du HTML échappé à la main au milieu de requêtes SQL. Un fichier style.css de 8 000 lignes dont personne n’ose supprimer une règle, parce qu’on ne sait plus qui l’utilise.

Ce n’est pas la faute de WordPress. C’est la faute d’une manière de faire qui n’a pas bougé depuis 2010, alors que tout le reste du développement web a changé. Cet article raconte comment on en sort — concrètement, avec le stack qui fait tourner le site que vous êtes en train de lire.

Pourquoi les thèmes WordPress classiques finissent tous par craquer

Un thème WordPress traditionnel repose sur la hiérarchie de templates : index.php, single.php, archive.php, page.php. WordPress choisit le bon fichier, l’exécute, et ce fichier fait tout : il interroge la base, il met en forme, il affiche. C’est simple à comprendre le premier jour. C’est ingérable au bout de six mois.

Trois problèmes reviennent systématiquement.

Le mélange des responsabilités. Dans un template classique, la logique métier (aller chercher les articles, calculer une durée d’abonnement, décider si l’utilisateur a le droit de voir un contenu) cohabite avec le HTML. Vous vous retrouvez avec des <?php if ( have_posts() ) : while ( have_posts() ) : the_post(); ?> imbriqués sur cinq niveaux, entrecoupés de balises ouvrantes et fermantes. Quand un bug apparaît, vous ne savez pas s’il vient de la donnée ou de l’affichage.

L’absence de structure imposée. WordPress ne vous dit pas où ranger vos choses. Résultat : chaque projet invente sa convention, chaque développeur la sienne, et reprendre le thème de quelqu’un d’autre relève de l’archéologie.

La gestion des dépendances à la main. Les plugins s’installent par l’interface d’administration, les mises à jour se font en cliquant, et le code du cœur WordPress est versionné dans Git au milieu de votre propre code. Personne ne sait exactement quelle version de quoi tourne en production. Le jour où il faut reproduire un bug sur une autre machine, c’est le drame.

À retenir : le problème n’est pas WordPress lui-même, mais l’absence de séparation entre le cœur du CMS, les dépendances et votre application. C’est exactement ce que les outils présentés dans cet article viennent corriger.

Ce que le reste de l’écosystème a résolu depuis longtemps

Pendant que WordPress restait sur ce modèle, l’écosystème PHP a mûri. Composer a réglé la gestion des dépendances. Laravel a popularisé la séparation entre logique et présentation, avec un moteur de templates propre (Blade) et un conteneur d’injection de dépendances. Côté front, les bundlers modernes ont rendu le rechargement à chaud instantané et la compilation de production quasi gratuite.

La bonne nouvelle : rien n’empêche d’apporter tout ça à WordPress. C’est précisément le pari de l’écosystème Roots, et c’est le stack sur lequel Utopiaz est construit :

  • Bedrock pour la structure du projet et la gestion des dépendances par Composer ;
  • Sage pour le thème, avec les templates Blade de Laravel ;
  • Tailwind CSS pour les styles ;
  • Vite pour la compilation des assets ;
  • DDEV pour l’environnement local ;
  • Git + GitHub Actions pour le versionnement et le déploiement automatique.

Aucune de ces briques n’est exotique. Ce sont les outils standards du développement web moderne, appliqués à WordPress. L’objectif : une base propre, maintenable et évolutive, où ajouter une fonctionnalité six mois plus tard ne demande pas de tout relire.

Comprendre la stack WordPress moderne, brique par brique

WordPress : pourquoi il reste incontournable

Commençons par lever un malentendu : choisir un stack moderne ne veut pas dire quitter WordPress. WordPress motorise toujours une part énorme du web, et pour de bonnes raisons. L’interface d’administration est comprise par n’importe quel rédacteur sans formation. L’écosystème de plugins couvre à peu près tous les besoins. Et l’API REST comme la boucle d’action/filtre en font une base extrêmement extensible.

Techniquement, WordPress fonctionne toujours de la même façon : une requête arrive, WordPress détermine ce qui est demandé (un article, une page, une archive), interroge la base MySQL, puis passe la main à un thème pour l’affichage. Ce qu’on va changer, ce n’est pas ce cycle — c’est comment le thème s’organise pour y répondre, et comment le projet dans son ensemble est structuré autour.

Bedrock : remettre WordPress à sa place

Bedrock est une réorganisation du projet WordPress, pensée pour les développeurs professionnels. L’idée fondatrice est simple : WordPress est une dépendance de votre projet, pas votre projet.

Dans une installation classique, tout est mélangé à la racine :

wp-admin/          ← cœur WordPress (vous n'y touchez jamais)
wp-includes/       ← cœur WordPress (vous n'y touchez jamais)
wp-content/
  ├── themes/      ← votre code
  └── plugins/     ← dépendances tierces
wp-config.php      ← identifiants de base de données EN DUR
index.php

Le cœur de WordPress, les plugins tiers et votre propre code vivent au même niveau. Vos identifiants de base de données sont écrits en dur dans un fichier PHP. Et si vous versionnez tout ça dans Git, vous versionnez 20 000 fichiers du cœur WordPress dont vous ne serez jamais l’auteur.

Bedrock réorganise :

composer.json      ← la liste de vos dépendances (WordPress inclus)
config/            ← la configuration, par environnement
  ├── application.php
  └── environments/
.env               ← les secrets (JAMAIS versionné)
vendor/            ← dépendances Composer (jamais versionné)
web/               ← LA SEULE partie exposée au serveur web
  ├── wp/          ← le cœur WordPress, installé par Composer
  ├── app/         ← l'équivalent de wp-content
  │   ├── themes/
  │   ├── plugins/
  │   └── uploads/
  └── index.php

Trois changements majeurs découlent de cette structure.

WordPress s’installe par Composer. Le cœur est déclaré comme une dépendance, au même titre que n’importe quelle librairie. Sur Utopiaz, c’est roots/wordpress en version 7.0. Vous ne versionnez plus le cœur : vous versionnez la ligne qui dit quelle version utiliser. Un composer install et n’importe quelle machine reconstruit exactement le même WordPress.

Les plugins aussi. Grâce au dépôt WPackagist, les plugins du répertoire officiel deviennent des paquets Composer :

composer require wpackagist-plugin/wordpress-seo

Le plugin est déclaré dans composer.json, sa version est verrouillée dans composer.lock, et il s’installe automatiquement au bon endroit. Fini le « quel plugin a été installé en prod le mois dernier par qui ? ».

Les secrets sortent du code. Bedrock utilise un fichier .env, jamais versionné, pour les identifiants de base de données, les clés d’API, les salts WordPress. Le code, lui, lit ces variables. Vous pouvez ainsi pousser tout votre dépôt sur GitHub sans exposer une seule clé.

DB_NAME='db'
DB_USER='db'
DB_PASSWORD='db'
DB_HOST='db'

WP_ENV='development'
WP_HOME='https://utopiaz.ddev.site'
WP_SITEURL="${WP_HOME}/wp"

Notez le WP_ENV : Bedrock charge une configuration différente selon l’environnement (development, staging, production). En développement, les erreurs PHP s’affichent ; en production, elles sont masquées et journalisées. Vous n’avez plus à penser à commenter/décommenter le mode debug avant un déploiement.

À retenir : Bedrock ne modifie pas le fonctionnement de WordPress. Il déplace les fichiers et introduit Composer + les variables d’environnement. Le gain est immédiat : un dépôt Git propre, des versions reproductibles, et des secrets qui ne fuitent pas. Seul le dossier web/ est exposé au serveur — le reste (config, dépendances, .env) est physiquement hors de portée du navigateur.

Sage : un thème qui ressemble enfin à une application

Sage est le thème de départ de Roots. Sa version 11, celle utilisée sur Utopiaz, embarque Acorn 5 — une couche qui apporte les composants de Laravel 12 à l’intérieur de WordPress. Concrètement, vous récupérez le moteur de templates Blade, un conteneur de services, un système de vues et de composants.

Là où un thème classique mélange tout, Sage sépare :

web/app/themes/sage/
├── app/                  ← la LOGIQUE (PHP)
│   ├── setup.php         ← enregistrement des menus, supports du thème
│   ├── filters.php       ← filtres WordPress
│   └── View/Composers/   ← données préparées pour les vues
├── resources/
│   ├── views/            ← les TEMPLATES (Blade)
│   │   ├── layouts/
│   │   ├── partials/
│   │   ├── components/
│   │   └── sections/
│   ├── css/              ← les STYLES
│   └── js/               ← les SCRIPTS
├── public/build/         ← les assets compilés (généré, non versionné)
├── vite.config.js
└── functions.php         ← ne fait quasiment rien : il démarre Acorn

La règle est nette : le PHP qui réfléchit vit dans app/, le HTML qui affiche vit dans resources/views/.

Blade, c’est le moteur de templates de Laravel. Comparez. En PHP classique :

<?php if ( have_posts() ) : ?>
  <div class="grid">
  <?php while ( have_posts() ) : the_post(); ?>
    <article>
      <h2><?php echo esc_html( get_the_title() ); ?></h2>
      <p><?php echo esc_html( get_the_excerpt() ); ?></p>
    </article>
  <?php endwhile; ?>
  </div>
<?php endif; ?>

En Blade :

@if (have_posts())
  <div class="grid">
    @while (have_posts()) @php(the_post())
      <article>
        <h2>{{ get_the_title() }}</h2>
        <p>{{ get_the_excerpt() }}</p>
      </article>
    @endwhile
  </div>
@endif

Deux détails qui changent la vie. D’abord, la syntaxe {{ }} échappe automatiquement le contenu : plus d’oubli de esc_html(), donc une faille XSS de moins. Ensuite, la structure du HTML redevient lisible — on voit l’imbrication des balises au lieu de la deviner entre les <?php endwhile; ?>.

Blade apporte aussi l’héritage de layouts. Vous définissez un squelette une fois, et chaque page ne décrit que son contenu propre :

{{-- resources/views/single.blade.php --}}
@extends('layouts.app')

@section('content')
  @while(have_posts()) @php(the_post())
    @include('partials.content-single')
  @endwhile
@endsection

Et surtout, les composants : des morceaux de vue réutilisables, avec leurs propres paramètres. Sur Utopiaz, le bloc d’inscription à la newsletter est un composant unique, appelé depuis une dizaine d’endroits différents. Le jour où le design change, on modifie un fichier.

À retenir : si vous connaissez déjà un peu Laravel, Sage vous mettra immédiatement à l’aise. Si vous ne le connaissez pas, sachez que Blade s’apprend en une après-midi : c’est du HTML avec une poignée de directives commençant par @. Le retour sur investissement est énorme dès le deuxième template écrit. Pour aller plus loin sur l’écosystème dont Sage hérite, notre article sur les nouveautés de Laravel 13 donne le contexte.

Tailwind CSS : arrêter d’inventer des noms de classes

Tailwind fait partie de ces outils qui provoquent une réaction de rejet au premier regard, puis qu’on ne veut plus lâcher. Voyons pourquoi.

En CSS classique, écrire un bouton demande deux fichiers et un nom :

<button class="btn btn--primary">S'abonner</button>
.btn {
  display: inline-flex;
  padding: 0.75rem 1.5rem;
  border-radius: 0.5rem;
  font-weight: 600;
}
.btn--primary {
  background-color: #7c3aed;
  color: white;
}
.btn--primary:hover {
  background-color: #6d28d9;
}

Avec Tailwind, on décrit directement l’apparence dans le HTML, avec des classes utilitaires qui font une seule chose :

<button class="inline-flex px-6 py-3 rounded-lg font-semibold bg-primary text-white hover:bg-primary-dim">
  S'abonner
</button>

L’objection arrive tout de suite : « c’est illisible, on met du style dans le HTML, c’est un retour en arrière ». En pratique, trois choses la désamorcent.

1. Vous ne nommez plus rien. Le temps passé à trancher entre .card__header--featured et .featured-card-header disparaît. Et le CSS ne grossit plus indéfiniment : là où chaque nouveau composant ajoutait des règles à un fichier qu’on n’osait plus nettoyer, Tailwind réutilise les mêmes utilitaires.

2. Vous ne cassez plus rien à distance. Modifier .btn dans un CSS global peut impacter quarante pages. Modifier les classes d’un bouton dans son template n’impacte que ce bouton. Le style devient local, donc sûr.

3. Le design system est verrouillé. C’est le point que les juniors sous-estiment le plus. Tailwind vous force à choisir parmi une échelle définie. Vous n’écrivez pas padding: 13px parce que « ça rendait mieux » : vous écrivez p-3 ou p-4. Sur un projet à plusieurs, cette contrainte est ce qui garde un design cohérent.

Tailwind v4, la version actuelle, a supprimé le fichier de configuration JavaScript au profit d’une configuration directement en CSS. Sur Utopiaz, le fichier resources/css/app.css commence ainsi :

@import "tailwindcss";
@plugin "@tailwindcss/typography";

/* Où Tailwind doit chercher les classes utilisées */
@source "../../app/**/*.php";
@source "../views/**/*.blade.php";
@source "../js/**/*.js";

/* Les tokens du design system */
@theme {
  --color-void:          #060612;
  --color-surface:       #0d0d1f;
  --color-primary:       #7c3aed;
  --color-primary-light: #a78bfa;
}

Ce bloc @theme est le cœur du système. En déclarant --color-primary, vous générez automatiquement bg-primary, text-primary, border-primary, hover:bg-primary… La couleur de marque est définie à un seul endroit. Le jour où elle change, une ligne suffit.

Les directives @source indiquent à Tailwind où scanner votre code pour repérer les classes réellement employées. Seules celles-là sont générées. Le CSS final ne contient que ce que vous utilisez, généralement quelques dizaines de kilo-octets — là où un framework CSS classique en embarque des centaines « au cas où ».

À retenir : Tailwind n’est pas « du CSS inline en pire ». C’est un design system exécutable : des contraintes qui produisent de la cohérence, et un CSS de sortie qui ne contient que le strict nécessaire. On détaille ce que la v4 a changé dans notre article dédié à Tailwind CSS v4.

Vite : la fin de l’attente

Dernière brique : Vite, l’outil qui transforme votre code source en fichiers que le navigateur comprend.

Pourquoi un outil pour ça ? Parce que votre code source n’est pas directement utilisable. Vous écrivez du Tailwind qui doit être compilé en CSS. Vous écrivez du JavaScript moderne réparti en modules qu’il faut regrouper. Vous voulez que les fichiers soient minifiés en production, et porter une empreinte unique dans leur nom (app.4f2b9c.js) pour que les navigateurs ne servent pas une vieille version en cache après une mise à jour.

Historiquement, WordPress ignorait ce problème : on écrivait du CSS à la main et on l’incluait avec wp_enqueue_style(). Puis sont venus Grunt, Gulp, Webpack — puissants mais lents et pénibles à configurer. Vite a balayé tout ça grâce à une idée simple : en développement, il ne compile rien à l’avance. Il sert vos fichiers à la demande, en exploitant le support natif des modules ES par les navigateurs modernes.

Le résultat concret : le serveur de développement démarre en une seconde, et le Hot Module Replacement (HMR) met à jour le navigateur instantanément quand vous sauvegardez. Vous changez une classe Tailwind, le style est appliqué avant que votre regard n’ait quitté l’éditeur — sans rechargement de page, sans perdre l’état de la page.

En production, Vite bascule sur un vrai bundler, minifie, découpe le code et versionne les noms de fichiers.

Dans Sage, la configuration Vite est déjà en place. Voici l’essentiel de celle d’Utopiaz :

import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
import laravel from 'laravel-vite-plugin'
import { wordpressPlugin, wordpressThemeJson } from '@roots/vite-plugin'

export default defineConfig({
  base: '/app/themes/sage/public/build/',
  server: {
    host: '0.0.0.0',
    port: 5173,
    strictPort: true,
    origin: 'https://utopiaz.ddev.site:5173',
  },
  plugins: [
    tailwindcss(),
    laravel({
      input: [
        'resources/css/app.css',
        'resources/js/app.js',
        'resources/css/editor.css',
        'resources/js/editor.js',
      ],
      refresh: true,
    }),
    wordpressPlugin(),
    wordpressThemeJson(),
  ],
})

Décryptons les points qui comptent :

  • input liste les points d’entrée. Notez qu’il y en a deux paires : une pour le site public (app.css/app.js), une pour l’éditeur Gutenberg (editor.css/editor.js). Vos styles peuvent ainsi s’appliquer aussi dans l’admin, pour que la rédaction voie le rendu réel.
  • wordpressPlugin() fait le pont entre Vite et WordPress : il génère un manifeste que le thème lit pour savoir quel fichier compilé charger.
  • wordpressThemeJson() génère le theme.json de WordPress à partir de vos tokens Tailwind. Autrement dit, les couleurs et tailles de police définies dans @theme se retrouvent automatiquement proposées à vos rédacteurs dans l’éditeur Gutenberg. Le design system est respecté même par ceux qui ne touchent pas au code. C’est, à mon sens, la fonctionnalité la plus sous-estimée du stack.

À retenir : Vite gère deux modes. En développement, un serveur qui recharge à chaud, quasi instantané. En production, une compilation optimisée et versionnée. Vous ne touchez presque jamais à cette configuration : Sage la fournit prête à l’emploi.

Préparer son environnement avec DDEV

Avant d’écrire la moindre ligne, il faut un environnement local. Et c’est souvent là que les projets WordPress commencent mal.

Le problème que DDEV résout

L’approche classique consiste à installer PHP, MySQL et un serveur web directement sur sa machine (via MAMP, XAMPP, ou à la main). Trois ennuis surgissent invariablement.

Vous n’avez qu’une version de PHP. Un client tourne en PHP 8.1, un autre exige 8.4 ? Vous jonglez, ou vous cassez un projet en réparant l’autre.

Votre machine n’est pas le serveur. Vous développez sur macOS avec PHP 8.2 et MySQL, la production tourne sur Debian avec PHP 8.4 et MariaDB. Le classique « ça marchait chez moi » naît exactement là.

L’onboarding est un cauchemar. Un nouveau développeur rejoint le projet : comptez une journée pour installer et configurer son environnement.

DDEV répond à tout ça avec Docker, mais sans vous demander de connaître Docker. Vous décrivez ce dont votre projet a besoin dans un fichier ; DDEV monte les conteneurs, configure le serveur web, la base de données, et même un certificat HTTPS local valide.

Installation et démarrage

DDEV a besoin de Docker (ou d’OrbStack sur macOS). Ensuite, l’installation tient en une commande :

# macOS / Linux (via Homebrew)
brew install ddev/ddev/ddev

# Linux (script officiel)
curl -fsSL https://ddev.com/install.sh | bash

# Vérification
ddev version

La configuration du projet vit dans .ddev/config.yaml. Celle d’Utopiaz, allégée :

name: utopiaz
type: wordpress
docroot: web
php_version: "8.3"
webserver_type: nginx-fpm
database:
  type: mariadb
  version: "10.11"
nodejs_version: "22"

Chaque ligne compte, et chacune raconte quelque chose du projet :

  • docroot: web — c’est la traduction directe de la structure Bedrock. Seul le dossier web/ est servi. Le .env, la config et le dossier vendor/ restent inaccessibles depuis un navigateur, même en cas de mauvaise configuration serveur.
  • php_version, database, nodejs_version — vous figez les versions. Toute l’équipe travaille sur le même socle, et vous pouvez le faire coller à la production.

Ensuite, tout se joue en trois commandes :

ddev start     # démarre les conteneurs (première fois : quelques minutes)
ddev composer install
ddev launch    # ouvre https://utopiaz.ddev.site dans le navigateur

Vous obtenez un site accessible en HTTPS, avec un certificat local reconnu par votre navigateur. Pas d’avertissement de sécurité, pas de localhost:8080.

DDEV embarque aussi les outils dont vous aurez besoin, préfixés par ddev pour s’exécuter dans le conteneur, avec les bonnes versions :

ddev composer require wpackagist-plugin/wordpress-seo
ddev npm install
ddev wp plugin list          # WP-CLI est inclus
ddev mysql                   # accès direct à la base
ddev ssh                     # shell dans le conteneur
ddev describe                # récapitulatif : URLs, ports, identifiants
ddev stop

Conseil de terrain : la commande ddev snapshot crée un instantané de votre base de données. Prenez-en un avant toute manipulation risquée (import massif, migration, test de plugin douteux). Un ddev snapshot restore et vous revenez en arrière en dix secondes. C’est la fonctionnalité que je regrette le plus quand je travaille sans DDEV.

Créer le projet WordPress avec Bedrock

Place à la pratique. Composer génère le squelette :

composer create-project roots/bedrock mon-projet
cd mon-projet

Vous obtenez la structure décrite plus haut. Configurez ensuite DDEV :

ddev config --project-type=wordpress --docroot=web --php-version=8.3
ddev start
ddev composer install

Le fichier .env

C’est l’étape que les débutants bâclent, et celle qui pose le plus de problèmes ensuite. Copiez l’exemple fourni :

cp .env.example .env

Puis remplissez-le. Avec DDEV, les identifiants de base sont toujours les mêmes — db partout — car la base tourne dans un conteneur dédié :

DB_NAME='db'
DB_USER='db'
DB_PASSWORD='db'
DB_HOST='db'

WP_ENV='development'
WP_HOME='https://mon-projet.ddev.site'
WP_SITEURL="${WP_HOME}/wp"

# Générez ces clés sur https://roots.io/salts.html
AUTH_KEY='...'
SECURE_AUTH_KEY='...'
LOGGED_IN_KEY='...'
NONCE_KEY='...'
AUTH_SALT='...'
SECURE_AUTH_SALT='...'
LOGGED_IN_SALT='...'
NONCE_SALT='...'

Deux points d’attention.

WP_SITEURL contient /wp. C’est spécifique à Bedrock : le cœur WordPress est installé dans web/wp/, tandis que le site est servi depuis web/. L’administration sera donc sur /wp/wp-admin. Déroutant les premières minutes, parfaitement logique ensuite.

Les salts sont des clés de chiffrement utilisées pour sécuriser les cookies de session. Elles doivent être uniques par projet et par environnement. Ne réutilisez jamais celles de développement en production.

Terminez par l’installation de WordPress (une seule commande, à copier telle quelle) :

ddev wp core install --url=https://mon-projet.ddev.site --title="Mon Projet" --admin_user=admin --admin_password=motdepasse --admin_email=vous@exemple.fr

À retenir : le .env ne doit jamais partir dans Git — le .gitignore de Bedrock s’en charge, vérifiez-le. Ce qui se versionne, c’est .env.example : la liste des variables attendues, sans les valeurs. C’est votre documentation vivante pour le prochain développeur.

Installer et configurer le thème Sage

Sage s’installe dans le dossier des thèmes, toujours avec Composer :

cd web/app/themes
ddev composer create-project roots/sage mon-theme
cd mon-theme
ddev composer install

Activez-le, puis installez les dépendances front :

ddev wp theme activate mon-theme
ddev npm install

Vous disposez alors de deux commandes, et il faut bien comprendre ce qu’elles font.

npm run dev — le mode développement

ddev npm run dev

Vite lance un serveur sur le port 5173 et se met à surveiller vos fichiers. Il ne compile rien à l’avance : il tient vos modules prêts et les sert à la demande. Sage détecte que ce serveur tourne et fait charger les assets depuis lui plutôt que depuis les fichiers compilés.

Conséquence : vous modifiez une classe Tailwind dans un fichier Blade, vous sauvegardez, et le navigateur applique le changement sans recharger la page. Ce n’est pas un gadget : sur un formulaire à moitié rempli ou un menu déroulant ouvert, garder l’état de la page pendant qu’on ajuste le style change réellement le rythme de travail.

Une subtilité propre à DDEV mérite d’être signalée, car elle fait perdre du temps à beaucoup de monde. Le serveur Vite tourne dans le conteneur ; votre navigateur, lui, est dehors. Sans configuration, il cherche le serveur sur localhost:5173 et ne trouve rien. D’où les deux réglages vus plus haut :

server: {
  host: '0.0.0.0',                              // écoute hors du conteneur
  origin: 'https://mon-projet.ddev.site:5173',  // URL vue par le navigateur
}

Il faut également exposer le port dans DDEV, via un fichier .ddev/docker-compose.vite.yaml. Si vos assets ne se chargent pas en mode dev, c’est presque toujours de là que vient le problème.

npm run build — le mode production

ddev npm run build

Là, Vite compile pour de bon. Il analyse vos points d’entrée, parcourt vos fichiers pour ne garder que les classes Tailwind réellement utilisées, minifie tout, ajoute une empreinte unique aux noms de fichiers, et écrit le tout dans public/build/ avec un manifeste que le thème saura lire.

Ces fichiers compilés ne sont pas versionnés dans Git : ils sont générés au moment du déploiement. Nous y revenons dans la section GitHub Actions.

Construire une section Hero : Blade + Tailwind en pratique

La théorie, c’est bien. Construisons une vraie section — une bannière d’accueil telle qu’on pourrait en trouver sur Utopiaz — pour voir le stack fonctionner ensemble.

Étape 1 — Le template

Créons resources/views/sections/hero.blade.php :

<section class="relative overflow-hidden bg-void py-24 sm:py-32">
  <div class="mx-auto max-w-4xl px-6 text-center">

    <p class="mb-4 text-sm font-semibold uppercase tracking-widest text-primary-light">
      Le Lab
    </p>

    <h1 class="text-4xl font-bold leading-tight text-white sm:text-6xl">
      Curieux de tout,
      <span class="text-primary-light">passionné par l'essentiel</span>
    </h1>

    <p class="mx-auto mt-6 max-w-2xl text-lg text-white/70">
      Un blog indépendant sur la technologie, la culture musicale
      et le développement web.
    </p>

  </div>
</section>

Puis appelons-la depuis la page d’accueil, dans resources/views/index.blade.php :

@extends('layouts.app')

@section('content')
  @include('sections.hero')

  {{-- la liste des articles… --}}
@endsection

Lancez ddev npm run dev, ouvrez le site : la section est là. Notez que nous n’avons écrit aucune ligne de CSS. Les classes bg-void et text-primary-light viennent directement des tokens déclarés dans le bloc @theme.

Étape 2 — Extraire un composant

Ajoutons un bouton d’appel à l’action. On pourrait écrire ses classes en dur… mais ce bouton va réapparaître partout sur le site. C’est le moment d’en faire un composant.

Créons resources/views/components/button.blade.php :

@props([
  'href' => '#',
  'variant' => 'primary',
])

@php
  $styles = [
    'primary' => 'bg-primary text-white hover:bg-primary-light',
    'ghost'   => 'border border-white/20 text-white hover:bg-white/10',
  ];
@endphp

<a
  href="{{ $href }}"
  {{ $attributes->merge([
    'class' => 'inline-flex items-center rounded-lg px-6 py-3
                font-semibold transition ' . $styles[$variant],
  ]) }}
>
  {{ $slot }}
</a>

Trois mécanismes de Blade sont à l’œuvre ici, et ils valent le détour :

  • @props déclare les paramètres du composant, avec leurs valeurs par défaut.
  • {{ $slot }} reçoit le contenu placé entre les balises ouvrante et fermante — ici, le libellé du bouton.
  • $attributes->merge() fusionne les classes du composant avec celles passées à l’appel. Vous gardez le style de base tout en autorisant des ajustements ponctuels.

L’utilisation, dans le hero, devient limpide :

<div class="mt-10 flex justify-center gap-4">
  <x-button href="/insider">Rejoindre l'Espace Insider</x-button>
  <x-button href="/dev" variant="ghost">Explorer les articles</x-button>
</div>

Comparez avec l’approche classique : il aurait fallu créer un fichier PHP, penser à échapper chaque variable, gérer les valeurs par défaut à la main, et inventer un système de classes CSS. Ici, tout tient en un fichier lisible, et l’appel se lit comme du HTML.

Conseil de terrain : ne créez pas de composant « au cas où ». La règle que j’applique : à la troisième répétition d’un motif, on extrait. Avant, c’est de l’abstraction prématurée — vous figez un design qui n’est pas encore stabilisé, et vous passerez plus de temps à défaire qu’à faire.

Étape 3 — Où va la logique ?

Supposons maintenant que le hero doive afficher le nombre d’articles publiés. La tentation est d’écrire la requête dans le template. Ne le faites pas : c’est exactement le mélange dont nous cherchons à sortir.

Sage fournit les View Composers : des classes qui préparent les données avant que la vue ne s’affiche. Créons app/View/Composers/Hero.php :

<?php

namespace App\View\Composers;

use Roots\Acorn\View\Composer;

class Hero extends Composer
{
    /**
     * Les vues auxquelles ce composer s'applique.
     */
    protected static $views = [
        'sections.hero',
    ];

    /**
     * Les données transmises à la vue.
     */
    public function with(): array
    {
        return [
            'postCount' => wp_count_posts()->publish,
        ];
    }
}

Et dans la vue, la donnée est simplement disponible :

<p class="mt-4 text-sm text-white/50">
  {{ $postCount }} articles publiés
</p>

Le template ne sait pas d’où vient le chiffre, et ce n’est pas son affaire. Il affiche. Le jour où le calcul change — un cache, une autre source, un filtre par catégorie — vous modifiez le composer, jamais la vue. C’est ça, la séparation des responsabilités, et c’est le bénéfice le plus durable de tout ce stack.

Organiser son code comme un professionnel

Le stack ne fait pas tout. Voici les pratiques qui, sur la durée, font vraiment la différence.

Git : deux branches suffisent

Sur Utopiaz, le modèle est volontairement minimaliste :

  • develop — la branche de travail. Tout y est committé.
  • main — la production. Un merge dans main déclenche le déploiement.

Pas de feature/ à rallonge, pas de rituel de branches pour un site géré par une petite équipe. La règle importante n’est pas le nombre de branches, c’est que main reflète exactement ce qui tourne en production. À tout instant. Si vous ne pouvez pas dire avec certitude ce qui est en ligne en regardant une branche, votre modèle de branches ne sert à rien.

Sur les projets où le travail se fait sur plusieurs fronts en parallèle, les worktrees Git évitent le ballet de stash/checkout.

Ce qui ne se versionne jamais

Votre .gitignore doit impérativement exclure :

.env                      # les secrets
vendor/                   # dépendances Composer (composer.lock suffit)
node_modules/             # dépendances npm (package-lock.json suffit)
web/wp/                   # le cœur WordPress (Composer le réinstalle)
web/app/uploads/          # les médias (ils vivent en prod, ou sur un S3)
public/build/             # les assets compilés (générés au déploiement)

Le principe : on versionne ce qu’on écrit et ce qui décrit, pas ce qui se régénère. Les fichiers composer.lock et package-lock.json, eux, sont essentiels : ils figent les versions exactes de chaque dépendance. C’est ce qui garantit que le build de production installera précisément ce que vous avez testé.

Sécurité : ce que le stack vous offre gratuitement

Plusieurs bonnes pratiques sont ici structurelles, pas optionnelles :

  • Racine web restreinte. Seul web/ est exposé. Une erreur de configuration ne peut pas révéler votre .env : il n’est pas dans l’arborescence servie.
  • Secrets hors du code. Ils vivent dans .env en local, et dans les secrets GitHub pour le déploiement.
  • Échappement par défaut. La syntaxe {{ }} de Blade échappe automatiquement. Pour ne pas échapper, il faut l’écrire explicitement ({!! !!}) — l’erreur devient volontaire, donc visible en revue de code.
  • Mises à jour tracées. Un composer update produit un diff lisible dans composer.lock. Vous savez ce qui a bougé, et vous pouvez revenir en arrière.

Performance : les réflexes qui comptent

Tailwind produit un CSS minimal par construction, et Vite versionne les noms de fichiers — donc un cache navigateur agressif ne servira jamais une vieille version. Restent les points sur lesquels vous devez agir vous-même :

  • Compiler les vues Blade en production. Blade transforme vos templates en PHP natif. En développement, il recompile à chaque changement ; en production, ces fichiers doivent être générés une fois et mis en cache. Un cache Blade obsolète après un déploiement est la cause la plus fréquente de « le site affiche l’ancienne version » sur ce stack. Nous verrons comment le vider automatiquement.
  • Optimiser l’autoloader Composer. Le --optimize-autoloader du déploiement génère une carte de classes statique, plus rapide que la résolution dynamique.
  • Charger les images en différé et servir du WebP. Rien de spécifique au stack, mais c’est ce qui pèse réellement sur un site de contenu.

Automatiser le déploiement avec GitHub Actions

Dernière étape, et sans doute celle qui change le plus le quotidien.

Pourquoi automatiser

Le déploiement WordPress « traditionnel » se fait en FTP : on glisse des fichiers, on croise les doigts. Ça fonctionne… jusqu’au jour où l’on oublie un fichier, où l’on écrase le mauvais dossier, ou où l’on déploie du code qui n’a jamais été committé — et qui disparaît quand la machine du développeur meurt.

Avec le stack décrit ici, le FTP n’est de toute façon plus une option : les assets doivent être compilés et les dépendances installées. Il faut une étape de build.

Une pipeline CI/CD (intégration et déploiement continus) est simplement une suite d’étapes exécutées automatiquement par un serveur, à chaque fois qu’un événement survient. Ici : « quand du code arrive sur main, construis le projet et envoie-le en production ». Toujours les mêmes étapes, dans le même ordre, sans facteur humain.

Le workflow, expliqué ligne par ligne

Un workflow GitHub Actions est un fichier YAML placé dans .github/workflows/. Voici celui d’Utopiaz, simplifié pour rester lisible :

name: Deploy to Production

on:
  push:
    branches: [main]        # se déclenche à chaque push sur main

jobs:
  deploy:
    runs-on: ubuntu-latest  # une machine neuve, fournie par GitHub

    steps:
      # 1. Récupérer le code
      - uses: actions/checkout@v6

      # 2. Installer PHP (la version de la PRODUCTION)
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'
          tools: composer:v2

      # 3. Installer Node.js
      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: '22'
          cache: 'npm'
          cache-dependency-path: web/app/themes/sage/package-lock.json

      # 4. Dépendances PHP du projet (WordPress, plugins…)
      - name: Install Bedrock dependencies
        run: composer install --no-dev --optimize-autoloader --no-interaction

      # 5. Dépendances du thème (PHP + JS)
      - name: Install Sage dependencies
        working-directory: web/app/themes/sage
        run: |
          composer install --no-dev --optimize-autoloader --no-interaction
          npm ci

      # 6. Compiler les assets (Tailwind + JS)
      - name: Build Sage assets
        working-directory: web/app/themes/sage
        run: npm run build

      # 7. Envoyer sur le serveur
      - name: Deploy via rsync
        uses: burnett01/rsync-deployments@7.0.1
        with:
          switches: -avz --checksum --delete --exclude='.env' --exclude='web/app/uploads/' --exclude='.git' --exclude='node_modules'
          path: ./
          remote_path: ${{ secrets.DEPLOY_PATH }}
          remote_host: ${{ secrets.DEPLOY_HOST }}
          remote_user: ${{ secrets.DEPLOY_USER }}
          remote_key:  ${{ secrets.DEPLOY_SSH_KEY }}

Reprenons ce qui mérite une explication.

on: push: branches: [main] — le déclencheur. Merger develop dans main est l’acte de déploiement. Il n’y a pas d’autre bouton.

runs-on: ubuntu-latest — GitHub démarre une machine virtuelle vierge pour chaque exécution. C’est un atout majeur : le build ne peut pas dépendre d’un fichier traînant sur votre disque. Si ça compile ici, ça compilera partout.

php-version: '8.4' — on met la version de la production, pas celle du poste de développement. Sur Utopiaz, DDEV tourne en 8.3 et la production en 8.4 : la pipeline est justement l’endroit qui vérifie que le code passe sur la version cible.

composer install --no-devinstall (et non update) installe les versions exactes figées dans composer.lock. C’est fondamental : jamais de mise à jour surprise pendant un déploiement. --no-dev écarte les outils de développement (tests, linters), inutiles et indésirables en production.

npm ci — même logique côté JavaScript. ci (pour clean install) supprime node_modules et réinstalle strictement ce que dit package-lock.json. Plus rapide et parfaitement reproductible, contrairement à npm install qui peut modifier le lockfile.

npm run build — la compilation Vite. C’est ici que les assets de production sont générés, ce qui explique pourquoi ils n’ont pas à être versionnés.

Le déploiement rsync — rsync ne transfère que ce qui a changé (--checksum compare le contenu, pas les dates) et --delete supprime sur le serveur ce qui a disparu du dépôt. Les exclusions sont vitales : sans --exclude='.env', vous écraseriez la configuration de production ; sans --exclude='web/app/uploads/', vous effaceriez toute la médiathèque. Prenez le temps de relire cette ligne deux fois.

${{ secrets.* }} — les identifiants du serveur (hôte, utilisateur, clé SSH) sont stockés dans les secrets du dépôt GitHub, chiffrés. Ils n’apparaissent ni dans le code, ni dans les journaux d’exécution.

Le vrai enseignement : le déploiement ne s’arrête pas au transfert

Le workflow ci-dessus est simplifié. En production, celui d’Utopiaz enchaîne une étape supplémentaire, apprise à la dure : après le rsync, il se connecte en SSH pour vider le cache des vues Blade et remettre les bons droits sur les dossiers de cache et d’uploads.

Pourquoi ? Parce que Blade compile ses templates en fichiers PHP mis en cache. Après un déploiement, ce cache contient l’ancienne version des vues. Le site continue tranquillement d’afficher le code précédent, et vous passez une heure à chercher pourquoi votre modification « n’est pas partie ». Elle est partie. C’est le cache qui n’a pas suivi.

La leçon, valable bien au-delà de ce stack : une pipeline se construit par itérations, en ajoutant une étape à chaque fois qu’un problème vous a mordu. Ne cherchez pas le workflow parfait dès le premier jour. Commencez par checkout / build / rsync, déployez, observez ce qui casse, et corrigez dans le YAML plutôt que sur le serveur. Un correctif appliqué à la main en SSH sera perdu au prochain déploiement ; un correctif dans la pipeline est acquis pour toujours.

À retenir : l’objectif d’une pipeline n’est pas d’être élégante, c’est de rendre le déploiement ennuyeux. Le jour où mettre en production ne provoque plus aucune montée d’adrénaline, vous avez gagné.

Vue d’ensemble : le cycle complet

Pour fixer les idées, voici le trajet d’une modification, de l’éditeur à la production :

   VOTRE MACHINE                    GITHUB                    PRODUCTION
   ─────────────                    ──────                    ──────────

   ┌─────────────┐
   │    DDEV     │   PHP 8.3 · MariaDB · Node 22 · HTTPS local
   │  ┌───────┐  │
   │  │ Vite  │  │   HMR : sauvegarde → navigateur à jour
   │  │  dev  │  │
   │  └───────┘  │
   └──────┬──────┘
          │  git push
          ▼
   ┌─────────────┐            ┌──────────────────┐
   │   develop   │  merge ─▶   │       main       │
   └─────────────┘            └────────┬─────────┘
                                       │ déclenche
                                       ▼
                              ┌──────────────────┐
                              │  GitHub Actions  │
                              │                  │
                              │  composer install│
                              │  npm ci          │
                              │  npm run build   │
                              └────────┬─────────┘
                                       │ rsync (SSH)
                                       ▼
                              ┌──────────────────┐
                              │     Serveur      │
                              │                  │
                              │   cache vidé     │
                              │   site à jour    │
                              └──────────────────┘

Chaque étape est reproductible, traçable, et ne dépend d’aucune manipulation manuelle.

Conclusion : WordPress mérite mieux que ses habitudes

Ce qu’il faut retenir de ce parcours, c’est que rien de tout cela n’est réservé aux grosses équipes ou aux projets à gros budget. Bedrock, Sage, Tailwind, Vite, DDEV et GitHub Actions sont tous gratuits, open source, largement documentés. Le seul vrai investissement, c’est la courbe d’apprentissage — et elle est bien plus douce qu’elle n’en a l’air : chaque brique s’apprend indépendamment, et vous n’êtes pas obligé de tout adopter le premier jour.

Si vous démarrez, une progression raisonnable ressemble à ceci : commencez par DDEV sur un projet existant (bénéfice immédiat, risque nul). Ajoutez Git et un déploiement automatisé, même rudimentaire. Puis, sur votre prochain projet, partez directement sur Bedrock + Sage. Ne migrez pas un site existant du jour au lendemain « pour faire propre » : le meilleur moment pour adopter ce stack, c’est le début d’un projet.

Et gardez en tête la seule chose qui compte vraiment : tout ce dispositif n’a qu’un but, faire en sorte que le développeur qui reprendra ce code dans un an — ce sera probablement vous — comprenne en cinq minutes où se trouve ce qu’il cherche.

Pour aller plus loin

Ce guide ouvre plusieurs chantiers que nous explorerons dans de prochains articles :

  • créer des blocs Gutenberg personnalisés avec Sage et Blade ;
  • utiliser ACF avec Sage pour des champs sur mesure ;
  • optimiser WordPress avec Redis et un cache objet persistant ;
  • déployer WordPress sur un VPS Linux, de zéro jusqu’au HTTPS ;
  • intégrer l’IA au flux de développement WordPress — un sujet que nous avons déjà abordé sous l’angle du métier de développeur en 2026.

En attendant, si vous voulez comprendre ce qui bouge côté cœur WordPress, notre analyse de WordPress 7.0 « Armstrong » détaille ce qui change pour ceux qui maintiennent un thème ou un plugin.

FAQ — Thème WordPress moderne avec Bedrock, Sage, Tailwind et Vite

Faut-il connaître Laravel pour utiliser Sage ?

Non. Sage emprunte à Laravel son moteur de templates (Blade) et quelques composants via Acorn, mais vous n’avez besoin ni de connaître Eloquent, ni le routage, ni l’architecture de Laravel. Blade s’apprend en une après-midi : c’est du HTML enrichi de directives comme @if, @foreach ou @include. Les fonctions WordPress que vous connaissez déjà (get_the_title(), have_posts()) s’utilisent exactement de la même façon.

Bedrock est-il compatible avec tous les hébergeurs WordPress ?

Non, et c’est le principal point à vérifier avant de vous lancer. Bedrock exige un hébergeur qui permet de définir la racine web sur un sous-dossier (web/) et d’exécuter Composer, ou au minimum d’accepter un déploiement par SSH/rsync. Les hébergements mutualisés bas de gamme et certaines offres « WordPress managé » ne le permettent pas. Un VPS, un hébergeur orienté développeurs ou une plateforme cloud conviennent parfaitement.

Tailwind CSS ne rend-il pas le HTML illisible ?

C’est la critique la plus fréquente, et elle est légitime au premier abord. En pratique, deux mécanismes la résolvent : les composants Blade, qui encapsulent les longues listes de classes dans un fichier unique et réutilisable, et la cohérence du design system, qui rend les classes prévisibles une fois qu’on connaît la nomenclature. Le vrai arbitrage est ailleurs : préférez-vous lire beaucoup de classes dans un template, ou chercher dans un fichier CSS de 8 000 lignes quelle règle affecte votre élément ?

Peut-on utiliser Vite sans DDEV ?

Oui, tout à fait. Vite fonctionne avec n’importe quel environnement local — Local, MAMP, Docker, ou une installation PHP native. DDEV apporte un confort supplémentaire (versions figées, HTTPS local, WP-CLI inclus, snapshots de base) mais n’est en rien une dépendance de Vite. La seule précaution spécifique à DDEV concerne l’exposition du port 5173 et le réglage de server.origin, puisque Vite tourne dans un conteneur alors que votre navigateur est en dehors.

Combien de temps faut-il pour être opérationnel sur ce stack ?

Comptez une journée pour monter un premier projet fonctionnel en suivant ce guide, et environ deux semaines de pratique régulière pour être vraiment à l’aise. Le conseil qui fait gagner le plus de temps : adoptez les briques une par une plutôt que toutes d’un coup. DDEV seul apporte déjà un gain immédiat sur un projet existant. Bedrock ensuite. Sage, Tailwind et Vite forment un tout cohérent qu’il est logique d’aborder en bloc, idéalement sur un nouveau projet plutôt qu’en migration.

Olivier Tech

Partager cet article