Pourquoi créer votre propre thème WordPress
Les thèmes pré-faits sont pratiques mais limités : code surchargé, dépendance à un page builder, personnalisation restreinte, et des dizaines de milliers de sites qui ressemblent au vôtre. Créer un thème from scratch vous donne un contrôle total sur le code, de meilleures performances (pas de bloat), et une compréhension profonde de WordPress qui fait de vous un meilleur développeur.
La structure minimale d’un thème WordPress
Un thème WordPress fonctionnel nécessite seulement 2 fichiers. Créez un dossier dans wp-content/themes/montheme/ :
montheme/
├── style.css (obligatoire : métadonnées du thème)
├── index.php (obligatoire : template par défaut)
├── functions.php (configuration et fonctionnalités)
├── header.php (en-tête HTML commun)
├── footer.php (pied de page commun)
├── single.php (template article)
├── page.php (template page)
├── archive.php (template archives/catégories)
├── 404.php (page erreur 404)
├── sidebar.php (barre latérale)
├── screenshot.png (aperçu 1200×900 dans le dashboard)
├── css/
│ └── custom.css (vos styles)
└── js/
└── main.js (vos scripts)
Étape 1 : style.css — la carte d’identité du thème
/*
Theme Name: MonThème
Theme URI: https://monsite.com
Author: Votre Nom
Author URI: https://monsite.com
Description: Thème WordPress personnalisé, léger et performant
Version: 1.0.0
Requires at least: 6.0
Tested up to: 6.5
Requires PHP: 8.0
License: GNU General Public License v2
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: montheme
Tags: custom, lightweight, responsive
*/
Ce commentaire CSS est lu par WordPress pour afficher les infos du thème dans Apparence → Thèmes.
Étape 2 : functions.php — le cerveau du thème
<?php
/**
* Configuration du thème MonThème
*/
// Support des fonctionnalités WordPress
function montheme_setup() {
// Support du titre dynamique dans <title>
add_theme_support('title-tag');
// Support des images mises en avant
add_theme_support('post-thumbnails');
// Tailles d'images personnalisées
add_image_size('card-thumbnail', 400, 300, true);
add_image_size('hero-image', 1600, 900, true);
// Support du logo personnalisé
add_theme_support('custom-logo', array(
'height' => 60,
'width' => 200,
'flex-width' => true,
'flex-height' => true,
));
// Menus de navigation
register_nav_menus(array(
'primary' => 'Menu Principal',
'footer' => 'Menu Footer',
));
// Support HTML5
add_theme_support('html5', array(
'search-form', 'comment-form', 'comment-list',
'gallery', 'caption', 'style', 'script'
));
// Support de l'éditeur Gutenberg
add_theme_support('align-wide');
add_theme_support('responsive-embeds');
add_theme_support('editor-styles');
}
add_action('after_setup_theme', 'montheme_setup');
// Charger les styles et scripts
function montheme_enqueue_assets() {
// Style principal
wp_enqueue_style(
'montheme-style',
get_stylesheet_uri(),
array(),
filemtime(get_stylesheet_directory() . '/style.css')
);
// CSS personnalisé
wp_enqueue_style(
'montheme-custom',
get_template_directory_uri() . '/css/custom.css',
array('montheme-style'),
filemtime(get_template_directory() . '/css/custom.css')
);
// JavaScript (dans le footer)
wp_enqueue_script(
'montheme-main',
get_template_directory_uri() . '/js/main.js',
array(),
filemtime(get_template_directory() . '/js/main.js'),
true
);
}
add_action('wp_enqueue_scripts', 'montheme_enqueue_assets');
// Zone de widgets (sidebar)
function montheme_widgets() {
register_sidebar(array(
'name' => 'Sidebar',
'id' => 'sidebar-1',
'before_widget' => '<div class="widget %2$s">',
'after_widget' => '</div>',
'before_title' => '<h3 class="widget-title">',
'after_title' => '</h3>',
));
register_sidebar(array(
'name' => 'Footer',
'id' => 'footer-1',
'before_widget' => '<div class="footer-widget %2$s">',
'after_widget' => '</div>',
'before_title' => '<h4 class="footer-widget-title">',
'after_title' => '</h4>',
));
}
add_action('widgets_init', 'montheme_widgets');
Étape 3 : header.php — l’en-tête commun
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<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(); ?>
<header class="site-header">
<div class="container">
<div class="header-inner">
<!-- Logo -->
<div class="site-branding">
<?php if (has_custom_logo()) : ?>
<?php the_custom_logo(); ?>
<?php else : ?>
<a href="<?php echo home_url('/'); ?>" class="site-title">
<?php bloginfo('name'); ?>
</a>
<?php endif; ?>
</div>
<!-- Navigation -->
<nav class="main-nav">
<?php wp_nav_menu(array(
'theme_location' => 'primary',
'container' => false,
'menu_class' => 'nav-menu',
'fallback_cb' => false,
)); ?>
</nav>
<!-- Bouton menu mobile -->
<button class="menu-toggle" aria-label="Menu">
<span></span>
<span></span>
<span></span>
</button>
</div>
</div>
</header>
<main class="site-main">
Étape 4 : footer.php — le pied de page
</main><!-- .site-main -->
<footer class="site-footer">
<div class="container">
<div class="footer-grid">
<div class="footer-col">
<h4><?php bloginfo('name'); ?></h4>
<p><?php bloginfo('description'); ?></p>
</div>
<div class="footer-col">
<h4>Navigation</h4>
<?php wp_nav_menu(array(
'theme_location' => 'footer',
'container' => false,
'menu_class' => 'footer-menu',
'depth' => 1,
)); ?>
</div>
<div class="footer-col">
<h4>Contact</h4>
<p>Dakar, Sénégal<br>
<a href="tel:+221770000000">+221 77 000 00 00</a><br>
<a href="mailto:contact@monsite.com">contact@monsite.com</a></p>
</div>
<?php if (is_active_sidebar('footer-1')) : ?>
<div class="footer-col">
<?php dynamic_sidebar('footer-1'); ?>
</div>
<?php endif; ?>
</div>
<div class="footer-bottom">
<p>© <?php echo date('Y'); ?> <?php bloginfo('name'); ?>. Tous droits réservés.</p>
</div>
</div>
</footer>
<?php wp_footer(); ?>
</body>
</html>
Étape 5 : index.php — le template principal
<?php get_header(); ?>
<div class="container">
<div class="content-area">
<?php if (have_posts()) : ?>
<div class="posts-grid">
<?php while (have_posts()) : the_post(); ?>
<article <?php post_class('post-card'); ?>>
<?php if (has_post_thumbnail()) : ?>
<a href="<?php the_permalink(); ?>" class="post-card-image">
<?php the_post_thumbnail('card-thumbnail'); ?>
</a>
<?php endif; ?>
<div class="post-card-content">
<div class="post-card-meta">
<span class="post-category">
<?php the_category(', '); ?>
</span>
<time datetime="<?php the_time('c'); ?>">
<?php the_time('j F Y'); ?>
</time>
</div>
<h2 class="post-card-title">
<a href="<?php the_permalink(); ?>">
<?php the_title(); ?>
</a>
</h2>
<p class="post-card-excerpt">
<?php echo wp_trim_words(get_the_excerpt(), 20); ?>
</p>
</div>
</article>
<?php endwhile; ?>
</div>
<!-- Pagination -->
<div class="pagination">
<?php the_posts_pagination(array(
'mid_size' => 2,
'prev_text' => '← Précédent',
'next_text' => 'Suivant →',
)); ?>
</div>
<?php else : ?>
<p>Aucun article trouvé.</p>
<?php endif; ?>
</div>
</div>
<?php get_footer(); ?>
Étape 6 : single.php — template article
<?php get_header(); ?>
<div class="container">
<div class="content-with-sidebar">
<article <?php post_class('single-post'); ?>>
<header class="post-header">
<div class="post-category"><?php the_category(', '); ?></div>
<h1><?php the_title(); ?></h1>
<div class="post-meta">
<span>Par <?php the_author(); ?></span>
<time datetime="<?php the_time('c'); ?>">
<?php the_time('j F Y'); ?>
</time>
<span><?php echo ceil(str_word_count(get_the_content()) / 200); ?> min de lecture</span>
</div>
</header>
<?php if (has_post_thumbnail()) : ?>
<div class="post-featured-image">
<?php the_post_thumbnail('hero-image'); ?>
</div>
<?php endif; ?>
<div class="post-content">
<?php the_content(); ?>
</div>
<!-- Navigation articles -->
<nav class="post-navigation">
<div class="nav-previous">
<?php previous_post_link('%link', '← %title'); ?>
</div>
<div class="nav-next">
<?php next_post_link('%link', '%title →'); ?>
</div>
</nav>
<?php comments_template(); ?>
</article>
<aside class="sidebar">
<?php get_sidebar(); ?>
</aside>
</div>
</div>
<?php get_footer(); ?>
Étape 7 : Le CSS de base
Créez css/custom.css avec une base solide :
/* Reset et variables */
:root {
--primary: #2563EB;
--primary-hover: #1D4ED8;
--text: #1F2937;
--text-light: #6B7280;
--bg: #FFFFFF;
--bg-alt: #F9FAFB;
--border: #E5E7EB;
--radius: 8px;
--container: 1200px;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Inter', -apple-system, sans-serif;
font-size: 1rem;
line-height: 1.6;
color: var(--text);
background: var(--bg);
}
img { max-width: 100%; height: auto; display: block; }
a { color: var(--primary); text-decoration: none; }
a:hover { color: var(--primary-hover); }
.container { max-width: var(--container); margin: 0 auto; padding: 0 24px; }
/* Header */
.site-header {
background: white;
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
z-index: 100;
}
.header-inner {
display: flex;
align-items: center;
justify-content: space-between;
height: 72px;
}
.site-title { font-size: 1.25rem; font-weight: 800; color: var(--text); }
.nav-menu { display: flex; list-style: none; gap: 32px; }
.nav-menu a { color: var(--text); font-weight: 500; font-size: 0.9375rem; }
.nav-menu a:hover { color: var(--primary); }
/* Grille d'articles */
.posts-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 32px;
padding: 48px 0;
}
.post-card {
background: white;
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
transition: box-shadow 0.2s;
}
.post-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.08); }
.post-card-content { padding: 20px; }
.post-card-title { font-size: 1.125rem; margin: 8px 0; }
.post-card-title a { color: var(--text); }
.post-card-excerpt { color: var(--text-light); font-size: 0.9375rem; }
/* Article single */
.single-post { max-width: 780px; }
.post-header h1 { font-size: 2.5rem; line-height: 1.2; margin: 12px 0 16px; }
.post-content { margin-top: 32px; }
.post-content h2 { margin-top: 48px; margin-bottom: 16px; font-size: 1.75rem; }
.post-content h3 { margin-top: 32px; margin-bottom: 12px; font-size: 1.375rem; }
.post-content p { margin-bottom: 16px; }
.post-content pre { background: #1F2937; color: #F9FAFB; padding: 24px; border-radius: var(--radius); overflow-x: auto; margin: 24px 0; }
.post-content code { font-family: 'JetBrains Mono', monospace; font-size: 0.875rem; }
/* Footer */
.site-footer { background: #111827; color: #D1D5DB; padding: 64px 0 24px; margin-top: 80px; }
.footer-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 40px; }
.footer-bottom { margin-top: 48px; padding-top: 24px; border-top: 1px solid #374151; text-align: center; font-size: 0.875rem; }
/* Menu mobile */
.menu-toggle { display: none; }
@media (max-width: 768px) {
.menu-toggle { display: block; background: none; border: none; cursor: pointer; }
.menu-toggle span { display: block; width: 24px; height: 2px; background: var(--text); margin: 5px 0; }
.main-nav { display: none; }
.main-nav.activé { display: block; position: absolute; top: 72px; left: 0; right: 0; background: white; border-bottom: 1px solid var(--border); padding: 24px; }
.main-nav.activé .nav-menu { flex-direction: column; gap: 16px; }
.posts-grid { grid-template-columns: 1fr; }
.post-header h1 { font-size: 1.75rem; }
}
Étape 8 : JavaScript — menu mobile
Créez js/main.js :
// Menu mobile toggle
document.addEventListener('DOMContentLoaded', function() {
const toggle = document.querySelector('.menu-toggle');
const nav = document.querySelector('.main-nav');
if (toggle && nav) {
toggle.addEventListener('click', function() {
nav.classList.toggle('activé');
this.classList.toggle('activé');
});
}
});
Activer et tester votre thème
- Apparence → Thèmes → votre thème apparaît avec le screenshot
- Cliquez « Activer »
- Créez vos menus : Apparence → Menus → assignez aux emplacements « Menu Principal » et « Menu Footer »
- Testez toutes les pages : accueil, article, page, 404, archives
Aller plus loin
Ce thème de base est votre fondation. Vous pouvez l’enrichir avec :
- search.php : Template pour la page de résultats de recherche
- archive.php : Template pour les pages catégories et tags
- page-contact.php : Template personnalisé pour la page contact
- template-parts/ : Fragments réutilisables avec
get_template_part() - Customizer : Options personnalisables depuis Apparence → Personnaliser
- Support FSE : Ajoutez
templates/etparts/pour le Full Site Editing
Vous préférez un site déjà en ligne ?
Pack complet : conception, domaine 1 an, hébergement 1 an, formation et support 6 mois inclus. Code et accès livrés.
À partir de 350 000 FCFA
Classic theme ou Block theme (FSE) — que choisir en 2026 ?
Depuis WordPress 5.9 (janvier 2022) et plus encore avec WordPress 6.x, l’écosystème connaît une transition lente mais réelle vers les block themes et le Full Site Editing (FSE), où l’ensemble du thème — y compris le header, le footer et les archives — se construit avec l’éditeur de blocs. Le fichier central devient theme.json, qui déclare la palette de couleurs, la typographie, les espacements et les variantes de styles. Les templates ne sont plus des fichiers PHP mais des fichiers HTML stockés dans le dossier templates/.
Pour un projet professionnel en 2026, la question n’est plus « est-ce que FSE est prêt ? » — il l’est — mais « quel modèle correspond à votre équipe et à votre client ? ». Un thème classique reste préférable lorsque l’on veut le contrôle total du markup HTML, qu’on intègre WordPress comme back-end pour un front Next.js ou Astro, ou que le client a déjà des templates HTML existants à porter. Un block theme s’impose lorsque l’on travaille avec une équipe marketing non technique qui doit pouvoir modifier la mise en page sans toucher au code, ou lorsque l’on souhaite minimiser la maintenance.
Le squelette présenté plus haut (fichiers PHP classiques) fonctionne dans les deux scénarios : il sert directement de thème classique, et il peut être progressivement transformé en thème hybride en ajoutant un fichier theme.json et en remplaçant page.php par templates/page.html à mesure que les besoins évoluent.
Performance et Core Web Vitals
L’argument principal pour un thème maison reste la performance. Les thèmes commerciaux multipurpose intègrent typiquement 50 à 200 Ko de JavaScript inutile par page (slideshows, page builder, animations, polices Google chargées en double). Sur les indicateurs Core Web Vitals que Google intègre dans son classement depuis 2021, ce surpoids coûte des secondes de LCP (Largest Contentful Paint) et fait basculer les pages d’un seuil Good à Needs Improvement.
Trois pratiques élémentaires garantissent un thème performant. D’abord, charger les feuilles de style et scripts par wp_enqueue_style et wp_enqueue_scripts avec la directive $in_footer = true et defer pour les scripts non critiques. Ensuite, embarquer les polices web localement via @font-face avec font-display: swap au lieu d’utiliser Google Fonts en CDN externe — cela élimine un DNS lookup et le risque de blocage. Enfin, n’inclure dans le HTML inline (wp_head) que le CSS critique au-dessus de la ligne de flottaison, et différer le reste. Avec ces trois disciplines, un thème de blog atteint typiquement 95-100 sur Lighthouse mobile sans effort additionnel.
Sécurité du thème — règles non négociables
Un thème mal écrit est un vecteur d’attaque autant que n’importe quel plugin. Quatre règles structurent un thème sûr. Échapper toute sortie : à chaque fois qu’une variable est affichée dans le HTML, utiliser esc_html() pour du texte, esc_attr() pour les attributs, esc_url() pour les URLs, et wp_kses_post() pour le contenu HTML riche autorisé. Valider toute entrée : les paramètres GET et POST sont vérifiés avec sanitize_text_field(), absint(), ou une fonction métier dédiée. Vérifier les nonces sur tout traitement de formulaire avec wp_verify_nonce(). Contrôler les capabilities avec current_user_can() avant toute action sensible. Une partie importante des compromissions WordPress vient de thèmes qui injectent directement $_GET['x'] dans le HTML sans échappement, créant des XSS exploitables.
Internationalisation (i18n) — penser dès le jour 1
Même si votre site n’est qu’en français aujourd’hui, faire l’i18n dès le départ coûte 5 % d’effort en plus et évite une réécriture coûteuse plus tard. Toutes les chaînes de texte du thème passent par __() ou _e() avec un text-domain unique cohérent avec le slug du thème :
// functions.php
function montheme_setup() {
load_theme_textdomain( 'montheme', get_template_directory() . '/languages' );
add_theme_support( 'post-thumbnails' );
add_theme_support( 'title-tag' );
add_theme_support( 'html5', array('search-form','comment-form','comment-list','gallery','caption') );
register_nav_menus( array(
'primary' => __( 'Menu principal', 'montheme' ),
'footer' => __( 'Menu footer', 'montheme' ),
) );
}
add_action( 'after_setup_theme', 'montheme_setup' );
Génération du fichier .pot avec WP-CLI : wp i18n make-pot . languages/montheme.pot. Les traductions .po sont éditées avec Poedit puis compilées en .mo chargé automatiquement par WordPress selon la locale du site.
Child theme et workflow Git
Quand le thème principal est livré à un client, la règle d’or est de séparer code et contenu. Le code du thème vit dans un dépôt Git, le contenu vit dans la base de données. Pour permettre au client de personnaliser sans casser les futures mises à jour, créer un child theme minimal :
// wp-content/themes/montheme-child/style.css
/*
Theme Name: Mon Theme Child
Template: montheme
Version: 1.0.0
*/
// wp-content/themes/montheme-child/functions.php
<?php
add_action( 'wp_enqueue_scripts', function () {
wp_enqueue_style( 'parent-style', get_template_directory_uri() . '/style.css' );
wp_enqueue_style( 'child-style',
get_stylesheet_directory_uri() . '/style.css',
array( 'parent-style' ),
filemtime( get_stylesheet_directory() . '/style.css' )
);
} );
Côté workflow, le dépôt Git inclut uniquement le code du thème (PHP, CSS, JS, templates) et ignore wp-content/uploads/, wp-content/cache/, et la base de données. La synchronisation entre environnements (local, staging, production) utilise WP-CLI pour exporter/importer les options et la base. Des outils comme WP Migrate ou WP CLI db export automatisent les transferts.
Tests, debug et observabilité
Trois outils sont indispensables pour développer un thème sereinement. WP_DEBUG activé dans wp-config.php avec WP_DEBUG_LOG = true écrit toutes les erreurs PHP dans wp-content/debug.log. Query Monitor (plugin) affiche dans la barre admin chaque requête SQL exécutée, les hooks déclenchés, les scripts enqueues, et le temps total de génération — outil indispensable pour diagnostiquer un thème lent. PHPCS avec le ruleset WordPress Coding Standards détecte automatiquement les écarts de style et les vulnérabilités courantes (XSS, SQL injection) avant qu’elles n’atteignent la production.
Pour les tests automatisés, WP-CLI exécute wp eval-file tests/test-helpers.php. Pour des tests d’intégration plus poussés, WP Browser (basé sur Codeception) lance le thème dans un environnement isolé et vérifie le rendu HTML, les redirections et les comportements front-end. Un thème de production sérieux a au minimum une suite de tests qui vérifie : page d’accueil 200 OK, article 200 OK avec H1 présent, archive paginée fonctionnelle, formulaire de recherche, formulaire de commentaire, et menu mobile.
Déploiement en production
La mise en production d’un thème suit un cycle reproductible. En local, le développeur travaille sur une branche Git. À chaque commit, un workflow GitHub Actions exécute PHPCS, lance les tests, et sur la branche main, déploie automatiquement via SSH/Rsync ou via le plugin WP Pusher (qui synchronise un dépôt Git directement avec WordPress). Sur Hostinger ou un VPS, un script bash de 20 lignes lancé par webhook GitHub fait l’affaire :
#!/bin/bash
# deploy.sh — copié dans /usr/local/bin/
cd /var/www/html/wp-content/themes/montheme
git pull origin main
wp cache flush --allow-root
wp litespeed-purge all --allow-root || true
Les rollbacks se font en revenant à un commit précédent (git checkout TAG puis wp cache flush), pratique qui suppose que toutes les modifications passent par Git et jamais par l’éditeur de thème de l’admin WordPress — éditeur qu’il faut désactiver via define('DISALLOW_FILE_EDIT', true); dans wp-config.php pour des raisons à la fois de discipline et de sécurité.
FAQ
Combien de temps pour créer un thème de qualité production ?
Pour un site éditorial (blog + pages statiques) avec un développeur expérimenté, comptez 40 à 80 heures pour le thème, plus 20 à 40 heures pour l’intégration design et les ajustements responsive. Un thème e-commerce avec WooCommerce double ces estimations en raison des templates spécifiques (panier, checkout, mon compte, archive produit).
Un thème custom est-il compatible avec les page builders comme Elementor ?
Oui, à condition d’enregistrer correctement les theme supports et les hooks attendus. Elementor s’adapte à tout thème qui respecte les standards WordPress (wp_head, wp_footer, the_content). Cela dit, mélanger un thème custom et un page builder lourd contredit l’objectif initial de performance — si Elementor est requis, autant partir directement d’un thème léger compatible Elementor comme Hello.
Comment gérer les mises à jour WordPress sans casser le thème ?
Tester d’abord en staging, vérifier les fonctions dépréciées (le code reste typiquement compatible 2 à 4 versions majeures sans modification), garder le thème à jour sur les nouveaux theme supports introduits (responsive-embeds, editor-styles, wp-block-styles), et exécuter régulièrement le Theme Check Plugin de WordPress qui vérifie la conformité aux standards courants.
Faut-il publier le thème sur WordPress.org ?
La soumission au directory officiel exige le respect strict des Theme Review Guidelines : licence GPL-compatible pour tout le code et toutes les ressources, absence de cross-promotion, séparation stricte plugin/thème, accessibilité WCAG. C’est un excellent exercice de qualité mais ce n’est pas obligatoire pour un thème projet — l’hébergement Git privé suffit largement pour une utilisation en agence ou en client unique.
Références et ressources officielles
- WordPress Theme Handbook — developer.wordpress.org/themes
- WordPress Coding Standards — developer.wordpress.org/coding-standards
- Block Editor Handbook — developer.wordpress.org/block-editor
- theme.json reference — developer.wordpress.org/themes/global-settings-and-styles
- Theme Check Plugin — wordpress.org/plugins/theme-check
- Query Monitor — wordpress.org/plugins/query-monitor
- PHPCS WordPress Standards — github.com/WordPress/WordPress-Coding-Standards
- WP-CLI documentation — developer.wordpress.org/cli
- Core Web Vitals (web.dev) — web.dev/articles/vitals