À proposParcoursCompétencesProjetsBlog Contact
WP
ACCUEIL / BLOG / WORDPRESS
WORDPRESS

Créer un theme WordPress de A à Z avec Tailwind CSS et ACF Pro

Stack 2026 : WordPress 6.6+ · PHP 8.2+ · Tailwind CSS 4 · ACF Pro

ST
Stéphane Donna
21 avril 2026 15 min de lecture
ACF PRO PHP 8.2 TAILWIND CSS THEME CUSTOM TUTORIEL WORDPRESS ACF PRO PHP 8.2 TAILWIND CSS THEME CUSTOM TUTORIEL WORDPRESS

Pourquoi un thème sur mesure en 2026 ?

Créer un thème WordPress sur mesure en 2026, c’est faire le choix de la maîtrise totale : design, performance, accessibilité et maintenabilité. Exit les page builders lourds et les thèmes génériques — place à un code propre, modulaire et pensé pour durer trois à cinq ans sans dette technique.

Un thème pré-fait charge en moyenne 15 à 30 scripts, 10+ feuilles de style et des milliers de règles CSS non utilisées. Résultat : LCP au-delà de 4s, CLS ingérable, et un back-office qui ralentit avec chaque plugin tiers. À l’inverse, un thème custom bien architecturé tient dans 5 fichiers PHP + 1 CSS compilé, charge en moins de 1s et permet des scores Lighthouse 95+ de façon reproductible.

Dans ce guide, nous allons construire un thème WordPress complet en utilisant Tailwind CSS 4 pour le front et ACF Pro pour rendre chaque contenu administrable sans toucher au code. Stack cible : WordPress 6.6+, PHP 8.2+, Node 18+, Tailwind 4 (configuration CSS-first), ACF Pro 6+.

Un bon thème WordPress n'est pas celui qui fait tout — c'est celui qui fait bien ce qu'il doit faire, sans compromis sur la qualité du code.

Prérequis et environnement de développement

Avant toute chose, un environnement local propre : versions à jour, outils de debug actifs et une licence ACF Pro valide. Voici le matériel recommandé pour suivre ce guide dans de bonnes conditions :

  • WordPress 6.6+ installé en local via Local by Flywheel, DDEV ou wp-env (Docker officiel WordPress)
  • PHP 8.2+ avec les extensions mbstring, intl, gd, curl
  • Node.js 18+ et npm 9+ pour le build Tailwind (ou pnpm/bun selon préférences)
  • ACF Pro 6+ (licence active) installé comme plugin — indispensable pour la gestion des champs complexes
  • WP-CLI 2.10+ pour les opérations de maintenance et scripts d’import
  • Un éditeur avec support PHP solide : VS Code + Intelephense + PHP DocBlocker (ou PhpStorm)
  • WP_DEBUG, WP_DEBUG_LOG et WP_DEBUG_DISPLAY activés dans wp-config.php
wp-config.php
// Configuration de debug recommandée en dev
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
define( 'SCRIPT_DEBUG', true );
define( 'DISALLOW_FILE_EDIT', true );
Astuce — wp-env pour un environnement reproductible

Utilisez wp-env pour un environnement Docker standardisé et reproductible entre développeurs. C’est la solution recommandée par l’équipe WordPress Core et elle s’intègre nativement avec Gutenberg.

Architecture du thème

Un thème WordPress bien structuré suit une arborescence claire et prévisible. La philosophie : un fichier, une responsabilité. Le functions.php ne fait qu’orchestrer des modules inc/ — il ne contient jamais de logique métier.

Arborescence
madebysteph/
├── style.css                     # Métadonnées du thème (obligatoire WP)
├── functions.php                 # Bootstrap (charge les modules inc/)
├── header.php, footer.php        # Parts globales
├── index.php, front-page.php     # Templates racine
├── single.php, page.php          # Détail article / page
├── archive.php, 404.php          # Archives + erreur
├── template-parts/               # Morceaux réutilisables
│   ├── hero.php
│   ├── card-project.php
│   └── card-article.php
├── inc/                          # Modules PHP (1 fichier = 1 responsabilité)
│   ├── setup.php                 # theme_supports, nav menus, image sizes
│   ├── enqueue.php               # Scripts & styles
│   ├── acf-options.php           # ACF options pages
│   ├── post-types.php            # CPT + taxonomies
│   └── security.php              # Hardening WP
├── blocks/                       # Blocs ACF Gutenberg
├── assets/
│   ├── css/source.css            # Source Tailwind
│   ├── css/tailwind.css          # Build compilé
│   └── js/*.js
├── acf-json/                     # Sync ACF local (versionné Git)
└── package.json + postcss.config.js

Le fichier style.css

Obligatoire pour que WordPress reconnaisse le thème. Il ne contient que les métadonnées — tout le CSS réel vit dans Tailwind.

style.css
/*
Theme Name:    Made by Steph
Theme URI:     https://madebysteph.fr
Author:        Stéphane Donna
Description:   Thème WordPress sur mesure — Portfolio & blog
Version:       2.0.0
Requires at least: 6.6
Requires PHP:  8.2
Tested up to:  6.7
Text Domain:   madebysteph
License:       GPL-2.0-or-later
*/

Le fichier functions.php

Le functions.php reste léger : il charge les modules depuis inc/, déclare les supports de thème, et c’est tout. Pas de logique métier ici.

functions.php
<?php
/**
 * Bootstrap du thème.
 *
 * @package Madebysteph
 */

if ( ! defined( 'ABSPATH' ) ) exit;

$modules = array(
    'inc/setup.php',
    'inc/enqueue.php',
    'inc/security.php',
    'inc/post-types.php',
    'inc/acf-options.php',
);

foreach ( $modules as $module ) {
    require_once get_template_directory() . '/' . $module;
}

Theme supports dans inc/setup.php

Les déclarations add_theme_support() vivent dans leur propre module. Cela centralise les capacités du thème : balises title auto, thumbnails, formats Gutenberg, nav menus.

inc/setup.php
<?php
add_action( 'after_setup_theme', 'mbs_theme_setup' );
function mbs_theme_setup(): void {
    add_theme_support( 'title-tag' );
    add_theme_support( 'post-thumbnails' );
    add_theme_support( 'html5', array( 'search-form', 'gallery', 'caption', 'script', 'style' ) );
    add_theme_support( 'responsive-embeds' );
    add_theme_support( 'align-wide' );
    add_theme_support( 'custom-logo', array( 'height' => 60, 'flex-width' => true ) );

    register_nav_menus( array(
        'primary' => __( 'Menu principal', 'madebysteph' ),
        'footer'  => __( 'Menu footer', 'madebysteph' ),
    ) );

    add_image_size( 'card-medium', 800, 500, true );
    add_image_size( 'hero-large', 1920, 1080, true );
}

Intégration de Tailwind CSS 4

Tailwind CSS 4 apporte un nouveau moteur (Oxide, écrit en Rust) et une configuration CSS-first. Fini le tailwind.config.js verbeux — tout se configure directement dans le CSS via la directive @theme. Build jusqu’à 10× plus rapide, scan automatique du code source, zero config pour démarrer.

Installation

Initialisez Node.js à la racine du thème puis installez Tailwind 4 avec PostCSS.

Terminal
# À la racine du thème
npm init -y
npm install -D tailwindcss @tailwindcss/postcss postcss postcss-cli

Scripts npm

Deux scripts suffisent : dev pour le watch, build pour le build de production minifié.

package.json
{"scripts":{"dev":"tailwindcss -i .\/assets\/css\/source.css -o .\/assets\/css\/tailwind.css --watch","build":"tailwindcss -i .\/assets\/css\/source.css -o .\/assets\/css\/tailwind.css --minify"}}

Fichier CSS source avec @theme

Toute la configuration tokens (couleurs, polices, espacements) passe par la directive @theme. Les tokens deviennent automatiquement des utilitaires Tailwind et des variables CSS consommables.

assets/css/source.css
@import "tailwindcss";

@theme {
  --color-primary: #34d399;
  --color-dark:    #050505;

  --font-display: "Orbitron", sans-serif;
  --font-body:    "DM Sans", sans-serif;
  --font-mono:    "JetBrains Mono", monospace;

  --breakpoint-xl: 1280px;
}

/* Utilitaires custom via @utility */
@utility glow-emerald {
  text-shadow: 0 0 18px rgba(52,211,153,0.55), 0 0 40px rgba(52,211,153,0.35);
}

Enqueue côté WordPress

Charger le build Tailwind et — surtout — dégager les CSS core inutiles (libère ~50 KB sur chaque page).

inc/enqueue.php
add_action( 'wp_enqueue_scripts', 'mbs_enqueue_assets' );
function mbs_enqueue_assets(): void {
    $css_path = get_template_directory() . '/assets/css/tailwind.css';
    wp_enqueue_style(
        'mbs-tailwind',
        get_template_directory_uri() . '/assets/css/tailwind.css',
        array(),
        file_exists( $css_path ) ? filemtime( $css_path ) : '2.0.0'
    );
}

// Désactive les CSS core non utilisés (libère ~50 KB)
add_action( 'wp_enqueue_scripts', function (): void {
    wp_dequeue_style( 'wp-block-library' );
    wp_dequeue_style( 'wp-block-library-theme' );
    wp_dequeue_style( 'classic-theme-styles' );
    wp_dequeue_style( 'global-styles' );
}, 100 );
Attention — plus de tailwind.config.js

Tailwind CSS 4 n’utilise plus tailwind.config.js. Si vous migrez depuis v3, supprimez-le et déplacez la config dans @theme. Le scan des classes est automatique (tout fichier .php, .html, .js du projet).

Configuration d’ACF Pro

ACF Pro est la clé pour rendre chaque contenu administrable sans toucher au code. Trois piliers à configurer : le sync JSON (versionner les champs dans Git), les options pages (contenus globaux : header, footer, coordonnées), et les blocs ACF pour enrichir Gutenberg.

Activer le sync JSON

Le sync JSON exporte automatiquement chaque groupe de champs dans acf-json/ à la sauvegarde. Vos champs deviennent versionnables, déployables via Git et synchronisables entre environnements (local/staging/prod).

inc/acf-options.php
add_filter( 'acf/settings/save_json', function (): string {
    return get_template_directory() . '/acf-json';
} );

add_filter( 'acf/settings/load_json', function ( array $paths ): array {
    $paths[] = get_template_directory() . '/acf-json';
    return $paths;
} );

// Options page globale (hooké sur acf/init pour éviter les notices textdomain)
add_action( 'acf/init', function (): void {
    if ( ! function_exists( 'acf_add_options_page' ) ) return;

    acf_add_options_page( array(
        'page_title' => __( 'Options du thème', 'madebysteph' ),
        'menu_title' => __( 'Options thème', 'madebysteph' ),
        'menu_slug'  => 'theme-options',
        'capability' => 'manage_options',
        'icon_url'   => 'dashicons-admin-customizer',
        'redirect'   => false,
    ) );
} );
Bon à savoir — le dossier acf-json

Le dossier acf-json/ doit être créé manuellement à la racine du thème. ACF y dépose automatiquement les fichiers JSON à chaque sauvegarde de groupe de champs. Ces fichiers peuvent être commités dans Git sans risque.

Lire un champ ACF dans un template

Pattern à respecter : get_field() dans le template, escaping systématique à la sortie, check d’existence avant d’afficher.

template-parts/hero.php
<?php
$titre      = get_field( 'hero_titre' );
$sous_titre = get_field( 'hero_sous_titre' );
$cta_url    = get_field( 'hero_cta_url' );
$cta_label  = get_field( 'hero_cta_label' );
?>
<section class="relative py-32">
    <h1 class="font-display text-6xl font-extrabold">
        <?php echo esc_html( $titre ); ?>
    </h1>
    <?php if ( $sous_titre ) : ?>
        <p class="text-neutral-400 mt-4"><?php echo esc_html( $sous_titre ); ?></p>
    <?php endif; ?>
    <?php if ( $cta_url && $cta_label ) : ?>
        <a href="<?php echo esc_url( $cta_url ); ?>" class="btn-primary">
            <?php echo esc_html( $cta_label ); ?>
        </a>
    <?php endif; ?>
</section>

Custom Post Types et taxonomies

Pour un portfolio, il est indispensable de créer un CPT projet avec des taxonomies type_projet et technologie. Cela permet aux projets d’être filtrables, archivables et indexables séparément des articles de blog standards.

inc/post-types.php
add_action( 'init', function (): void {

    register_post_type( 'projet', array(
        'labels'       => array(
            'name'          => __( 'Projets', 'madebysteph' ),
            'singular_name' => __( 'Projet', 'madebysteph' ),
            'add_new_item'  => __( 'Ajouter un projet', 'madebysteph' ),
        ),
        'public'       => true,
        'has_archive'  => true,
        'rewrite'      => array( 'slug' => 'projets', 'with_front' => false ),
        'menu_icon'    => 'dashicons-portfolio',
        'supports'     => array( 'title', 'editor', 'thumbnail', 'excerpt', 'revisions' ),
        'show_in_rest' => true, // Gutenberg
    ) );

    register_taxonomy( 'type_projet', 'projet', array(
        'labels'       => array( 'name' => __( 'Types', 'madebysteph' ) ),
        'hierarchical' => true,
        'show_in_rest' => true,
        'rewrite'      => array( 'slug' => 'type' ),
    ) );

    register_taxonomy( 'technologie', 'projet', array(
        'labels'       => array( 'name' => __( 'Technologies', 'madebysteph' ) ),
        'hierarchical' => false,
        'show_in_rest' => true,
        'rewrite'      => array( 'slug' => 'techno' ),
    ) );
} );

Templates WordPress : la hiérarchie

WordPress applique une hiérarchie stricte de templates pour choisir quel fichier PHP rendre une requête donnée. Connaître cette hiérarchie évite les hacks et permet de surcharger finement chaque cas. Pour un CPT projet, l’ordre est : single-projet.phpsingle.phpsingular.phpindex.php.

header.php minimal

header.php
<!DOCTYPE html>
<html <?php language_attributes(); ?> class="scroll-smooth">
<head>
    <meta charset="<?php bloginfo( 'charset' ); ?>">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>
<?php wp_body_open(); ?>

<nav id="navbar" class="fixed top-0 inset-x-0 z-40">
    <div class="max-w-7xl mx-auto px-6 py-5 flex items-center justify-between">
        <a href="<?php echo esc_url( home_url( '/' ) ); ?>" class="font-display font-bold">
            <?php bloginfo( 'name' ); ?>
        </a>
        <?php wp_nav_menu( array(
            'theme_location' => 'primary',
            'container'      => false,
            'items_wrap'     => '%3$s',
            'depth'          => 1,
        ) ); ?>
    </div>
</nav>

<main>

single.php avec template-part

Le détail d’un article : loop WP standard, post_class() pour les classes auto, the_content() pour injecter les blocs.

single.php
<?php get_header(); ?>

<?php while ( have_posts() ) : the_post(); ?>
    <article <?php post_class( 'max-w-3xl mx-auto px-6 py-24' ); ?>>
        <header class="mb-10">
            <h1 class="font-display text-5xl font-extrabold"><?php the_title(); ?></h1>
            <div class="mt-4 text-sm text-neutral-500">
                <?php echo esc_html( get_the_date() ); ?>
                 ·  <?php the_category( ', ' ); ?>
            </div>
        </header>

        <?php if ( has_post_thumbnail() ) : ?>
            <figure class="mb-10"><?php the_post_thumbnail( 'hero-large' ); ?></figure>
        <?php endif; ?>

        <div class="prose-article">
            <?php the_content(); ?>
        </div>
    </article>
<?php endwhile; ?>

<?php get_footer(); ?>

Performance et Core Web Vitals

Google classe trois métriques critiques : LCP (Largest Contentful Paint) doit être < 2.5s, CLS (Cumulative Layout Shift) < 0.1, et INP (Interaction to Next Paint) < 200ms. Un thème correctement architecturé atteint ces cibles sans effort — il suffit de ne pas saboter le navigateur.

  1. Lazy loading natif : loading="lazy" sur toutes les images below-the-fold et les iframes.
  2. Preload des fonts critiques : <link rel="preload" as="font" crossorigin> pour la première font utilisée en hero.
  3. Purge CSS automatique : Tailwind 4 scanne et purge tout seul, le build final ne contient que les classes utilisées.
  4. Images responsives modernes : srcset + sizes + format WebP/AVIF via wp_get_attachment_image().
  5. Cache HTTP côté serveur : headers Cache-Control, ETag, plugin type LiteSpeed Cache ou WP Rocket.
  6. Éviter les layout shifts : dimensions fixes sur images + font-display: swap + aspect-ratio CSS.

Désactiver les scripts WP inutiles

inc/performance.php
// Supprime les emojis WordPress (gain ~4 KB + 1 requête)
remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
remove_action( 'wp_print_styles', 'print_emoji_styles' );
remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );

// Supprime les liens oEmbed REST
remove_action( 'wp_head', 'wp_oembed_add_discovery_links' );
remove_action( 'wp_head', 'rest_output_link_wp_head' );

// Désactive XML-RPC (surface d'attaque, rarement utile en 2026)
add_filter( 'xmlrpc_enabled', '__return_false' );
add_filter( 'wp_headers', function ( array $headers ): array {
    unset( $headers['X-Pingback'] );
    return $headers;
} );

// Désactive les embeds WP
add_action( 'init', function (): void {
    wp_deregister_script( 'wp-embed' );
} );

JSON-LD structured data

Les données structurées permettent à Google d’afficher des rich snippets (date, auteur, image, note…) dans les résultats. Pour un article, le schéma BlogPosting est attendu.

inc/seo.php
add_action( 'wp_head', function (): void {
    if ( ! is_singular( array( 'post', 'projet' ) ) ) return;

    $post = get_queried_object();
    $img  = get_the_post_thumbnail_url( $post, 'hero-large' ) ?: '';

    $data = array(
        '@context'         => 'https://schema.org',
        '@type'            => 'BlogPosting',
        'headline'         => get_the_title( $post ),
        'description'      => get_the_excerpt( $post ),
        'image'            => $img,
        'datePublished'    => get_the_date( 'c', $post ),
        'dateModified'     => get_the_modified_date( 'c', $post ),
        'author'           => array(
            '@type' => 'Person',
            'name'  => get_the_author_meta( 'display_name', $post->post_author ),
        ),
        'publisher'        => array(
            '@type' => 'Organization',
            'name'  => get_bloginfo( 'name' ),
        ),
        'mainEntityOfPage' => get_permalink( $post ),
    );

    echo '<script type="application/ld+json">'
        . wp_json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE )
        . '</script>';
} );

Sécurité : les bases non-négociables

Un thème bien conçu applique les trois règles de base du WordPress security model : escaping en sortie, sanitization en entrée, nonces pour les actions. Aucune excuse pour les oublier.

  • esc_html() pour du texte affiché dans le DOM
  • esc_attr() pour un attribut HTML
  • esc_url() pour une URL (href, src)
  • wp_kses_post() pour un contenu riche (WYSIWYG)
  • sanitize_text_field() / absint() sur les inputs utilisateur
  • wp_nonce_field() + check_admin_referer() sur toute action sensible
inc/security.php
// Masque la version WP dans les headers HTML et RSS
remove_action( 'wp_head', 'wp_generator' );
add_filter( 'the_generator', '__return_empty_string' );

// Force SSL en prod
if ( ! is_admin() && ! wp_is_json_request() ) {
    add_action( 'template_redirect', function (): void {
        if ( ! is_ssl() && wp_get_environment_type() === 'production' ) {
            wp_safe_redirect( 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'], 301 );
            exit;
        }
    } );
}

// Security headers
add_action( 'send_headers', function (): void {
    header( 'X-Content-Type-Options: nosniff' );
    header( 'X-Frame-Options: SAMEORIGIN' );
    header( 'Referrer-Policy: strict-origin-when-cross-origin' );
    header( 'Permissions-Policy: camera=(), microphone=(), geolocation=()' );
} );

Conclusion

Développer un thème WordPress sur mesure demande un investissement initial plus important qu’utiliser un thème pré-fait, mais les bénéfices sont réels et durables : performance optimale (Lighthouse 95+ reproductible), code maintenable (modules < 200 lignes), contenu 100 % administrable via ACF, et expérience utilisateur soignée sans dette technique.

L’association WordPress 6.6+ · PHP 8.2+ · Tailwind CSS 4 · ACF Pro forme un stack solide et éprouvé pour 2026. La clé est de rester discipliné : architecture claire, séparation des responsabilités, tests à chaque étape, et versionnement Git avec sync ACF JSON. Une fois ces habitudes prises, vous livrez un thème de qualité en quelques jours plutôt qu’en semaines.

Prochaines étapes possibles : blocs ACF Gutenberg custom (hero, stats, timeline), intégration WP-CLI pour les déploiements, pipeline GitHub Actions pour le build Tailwind + déploiement FTP/rsync automatique, tests visuels avec Playwright.

Le meilleur thème WordPress est celui que vous comprenez de bout en bout. Pas de magie noire, pas de dépendances inutiles — juste du code propre et intentionnel.