Credit : Logo officiel
Creer un theme WordPress from scratch
Themes premium a 60 euros et 47 fichiers JS, on arrete
J'ai gere pendant des annees des sites WordPress sous des themes premium type Avada, Divi, Bridge. Le client est content au debut parce que c'est joli et qu'il y a 800 demos. Six mois plus tard il m'appelle parce que la home charge en 6 secondes, le PageSpeed est a 18, et chaque mise a jour casse trois plugins. C'est en passant un client sur un theme maison de 90 Ko que j'ai eu le declic : pour 80% des sites, un theme custom de 500 lignes de PHP suffit largement et roule a 95+ sur PageSpeed sans optimisation supplementaire.
Faire son propre theme WordPress fait peur la premiere fois. On regarde les fichiers d'un theme existant, on voit du PHP, du HTML, des hooks, on panique. En realite, la base c'est une dizaine de fichiers et 200 lignes de code. Une fois que vous avez compris la structure, vous pouvez sortir un theme fonctionnel en une demi-journee. Mieux : il sera plus rapide et plus maintenable que tous les themes premium du marche.
Dans ce guide je vous montre toute la structure, les fichiers obligatoires, les hooks essentiels, et le support Gutenberg moderne. C'est exactement le squelette que j'utilise comme point de depart sur tous les nouveaux projets.
La structure minimale d'un theme
Un theme WordPress vit dans wp-content/themes/montheme/. Voici l'arborescence de base :
montheme/
style.css # Identification + styles principaux
functions.php # Coeur du theme, hooks, supports
index.php # Fallback obligatoire
header.php # Bloc <head> et debut du body
footer.php # Fin du body
sidebar.php # Sidebar (optionnel mais courant)
single.php # Template article unique
page.php # Template page
archive.php # Template archive (categorie, tag, auteur)
search.php # Page de resultats de recherche
404.php # Page d'erreur 404
screenshot.png # Apercu 1200x900 dans Apparence > Themes
theme.json # Config Gutenberg moderne
assets/
css/
custom.css
js/
main.js
img/
inc/
customizer.php
template-tags.php
WordPress impose seulement deux fichiers strictement obligatoires : style.css (avec son entete special) et index.php. Tout le reste est optionnel mais on en aura besoin pour un theme correct.
style.css : l'identite du theme
Le header CSS au debut du fichier est ce que WordPress lit pour reconnaitre votre theme. Sans ces commentaires formates correctement, votre theme n'apparait meme pas dans Apparence > Themes.
/*
Theme Name: MonTheme
Theme URI: https://monsite.fr
Author: Votre Nom
Author URI: https://monsite.fr
Description: Un theme WordPress leger et performant pour 2026.
Version: 1.0.0
Requires at least: 6.4
Tested up to: 6.7
Requires PHP: 8.1
License: GNU General Public License v2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
Text Domain: montheme
Tags: blog, custom-logo, custom-menu, featured-images, full-site-editing
*/
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--color-primary: #2563eb;
--color-text: #1f2937;
--color-bg: #ffffff;
--color-muted: #6b7280;
--font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--font-heading: 'Inter', sans-serif;
}
body {
font-family: var(--font-body);
line-height: 1.6;
color: var(--color-text);
background: var(--color-bg);
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
img {
max-width: 100%;
height: auto;
display: block;
}
Les champs cles du header sont Theme Name, Version et Text Domain. Le Text Domain est utilise par les fonctions de traduction comme __('Texte', 'montheme'). Mettez-le partout ou vous traduisez quelque chose.
functions.php : le coeur du theme
C'est le fichier qui dit a WordPress "voila ce que mon theme sait faire". Tout passe par des hooks (actions et filtres). Voici la base que j'utilise sur tous mes themes :
<?php
/**
* MonTheme - functions.php
*/
if (!defined('ABSPATH')) exit;
if (!function_exists('montheme_setup')) :
function montheme_setup() {
// Charge le textdomain pour les traductions
load_theme_textdomain('montheme', get_template_directory() . '/languages');
// Supports natifs WordPress
add_theme_support('title-tag');
add_theme_support('post-thumbnails');
add_theme_support('automatic-feed-links');
add_theme_support('html5', [
'search-form', 'comment-form', 'comment-list',
'gallery', 'caption', 'style', 'script'
]);
add_theme_support('custom-logo', [
'height' => 60,
'width' => 200,
'flex-height' => true,
'flex-width' => true,
]);
// Supports Gutenberg
add_theme_support('editor-styles');
add_theme_support('responsive-embeds');
add_theme_support('align-wide');
add_theme_support('wp-block-styles');
// Tailles d'images personnalisees
add_image_size('hero', 1920, 800, true);
add_image_size('card', 600, 400, true);
// Menus de navigation
register_nav_menus([
'primary' => __('Menu principal', 'montheme'),
'footer' => __('Menu pied de page', 'montheme'),
]);
}
endif;
add_action('after_setup_theme', 'montheme_setup');
/**
* Charger les CSS et JS
*/
function montheme_enqueue_assets() {
$theme_version = wp_get_theme()->get('Version');
wp_enqueue_style(
'montheme-style',
get_stylesheet_uri(),
[],
$theme_version
);
wp_enqueue_style(
'montheme-custom',
get_template_directory_uri() . '/assets/css/custom.css',
['montheme-style'],
$theme_version
);
wp_enqueue_script(
'montheme-main',
get_template_directory_uri() . '/assets/js/main.js',
[],
$theme_version,
true // dans le footer
);
if (is_singular() && comments_open() && get_option('thread_comments')) {
wp_enqueue_script('comment-reply');
}
}
add_action('wp_enqueue_scripts', 'montheme_enqueue_assets');
/**
* Sidebars
*/
function montheme_widgets_init() {
register_sidebar([
'name' => __('Sidebar principale', 'montheme'),
'id' => 'sidebar-1',
'description' => __('Affichee a droite des articles.', 'montheme'),
'before_widget' => '<section id="%1$s" class="widget %2$s">',
'after_widget' => '</section>',
'before_title' => '<h3 class="widget-title">',
'after_title' => '</h3>',
]);
register_sidebar([
'name' => __('Footer', 'montheme'),
'id' => 'footer-1',
'before_widget' => '<div id="%1$s" class="widget %2$s">',
'after_widget' => '</div>',
'before_title' => '<h4 class="widget-title">',
'after_title' => '</h4>',
]);
}
add_action('widgets_init', 'montheme_widgets_init');
/**
* Includes
*/
require get_template_directory() . '/inc/template-tags.php';
Utilisez TOUJOURS wp_enqueue_style et wp_enqueue_script. Jamais de <link> ou <script> en dur dans header.php. C'est la base du fonctionnement WordPress, et ca permet aux plugins de gerer correctement les dependances et l'ordre de chargement.
header.php : le debut de chaque page
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo('charset'); ?>">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="profile" href="https://gmpg.org/xfn/11">
<?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>
<?php wp_body_open(); ?>
<a class="skip-link screen-reader-text" href="#main"><?php esc_html_e('Aller au contenu', 'montheme'); ?></a>
<header class="site-header">
<div class="container">
<div class="site-branding">
<?php if (has_custom_logo()) : ?>
<?php the_custom_logo(); ?>
<?php else : ?>
<a class="site-title" href="<?php echo esc_url(home_url('/')); ?>" rel="home">
<?php bloginfo('name'); ?>
</a>
<p class="site-description"><?php bloginfo('description'); ?></p>
<?php endif; ?>
</div>
<nav class="main-nav" aria-label="<?php esc_attr_e('Navigation principale', 'montheme'); ?>">
<?php
wp_nav_menu([
'theme_location' => 'primary',
'menu_class' => 'primary-menu',
'container' => false,
'fallback_cb' => false,
]);
?>
</nav>
</div>
</header>
<main id="main" class="site-content container">
wp_head() est OBLIGATOIRE. Sans cet appel, aucun plugin ne fonctionne, aucun script ni style n'est charge correctement, et la balise title est absente. Pareil pour wp_footer() dans footer.php.
wp_body_open() (introduit en WP 5.2) permet aux plugins d'injecter des scripts juste apres l'ouverture du body, c'est la bonne pratique moderne pour Google Tag Manager, Meta Pixel, etc.
footer.php
</main>
<footer class="site-footer">
<div class="container">
<?php if (is_active_sidebar('footer-1')) : ?>
<div class="footer-widgets">
<?php dynamic_sidebar('footer-1'); ?>
</div>
<?php endif; ?>
<?php
wp_nav_menu([
'theme_location' => 'footer',
'menu_class' => 'footer-menu',
'container' => false,
'depth' => 1,
'fallback_cb' => false,
]);
?>
<p class="site-copyright">
© <?php echo date('Y'); ?> <?php bloginfo('name'); ?>.
<?php esc_html_e('Tous droits reserves.', 'montheme'); ?>
</p>
</div>
</footer>
<?php wp_footer(); ?>
</body>
</html>
index.php : la boucle WordPress
index.php est le template fallback : si aucun template plus specifique n'est trouve, WordPress utilise celui-la. Il doit contenir la fameuse "boucle WordPress".
<?php get_header(); ?>
<div class="posts-grid">
<?php if (have_posts()) : ?>
<?php while (have_posts()) : the_post(); ?>
<article id="post-<?php the_ID(); ?>" <?php post_class('post-card'); ?>>
<?php if (has_post_thumbnail()) : ?>
<a class="post-thumb" href="<?php the_permalink(); ?>" aria-hidden="true" tabindex="-1">
<?php the_post_thumbnail('card', ['loading' => 'lazy']); ?>
</a>
<?php endif; ?>
<div class="post-content">
<h2 class="post-title">
<a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
</h2>
<div class="post-meta">
<time datetime="<?php echo esc_attr(get_the_date('c')); ?>">
<?php echo esc_html(get_the_date()); ?>
</time>
<span class="post-author">
<?php esc_html_e('par', 'montheme'); ?> <?php the_author(); ?>
</span>
</div>
<div class="post-excerpt">
<?php the_excerpt(); ?>
</div>
</div>
</article>
<?php endwhile; ?>
<div class="pagination">
<?php
the_posts_pagination([
'mid_size' => 2,
'prev_text' => __('Precedent', 'montheme'),
'next_text' => __('Suivant', 'montheme'),
]);
?>
</div>
<?php else : ?>
<p class="no-results"><?php esc_html_e('Aucun article trouve.', 'montheme'); ?></p>
<?php endif; ?>
</div>
<?php get_sidebar(); ?>
<?php get_footer(); ?>
La boucle while (have_posts()) : the_post(); est le pattern central de WordPress. Tout template qui affiche du contenu (single, archive, search) reutilise cette structure. Une fois que vous l'avez en tete, vous pouvez ecrire n'importe quel template.
single.php : un article unique
<?php get_header(); ?>
<?php while (have_posts()) : the_post(); ?>
<article id="post-<?php the_ID(); ?>" <?php post_class('single-post'); ?>>
<header class="entry-header">
<h1 class="entry-title"><?php the_title(); ?></h1>
<div class="entry-meta">
<time datetime="<?php echo esc_attr(get_the_date('c')); ?>">
<?php echo esc_html(get_the_date()); ?>
</time>
<span class="author"><?php the_author(); ?></span>
<span class="categories"><?php the_category(', '); ?></span>
</div>
</header>
<?php if (has_post_thumbnail()) : ?>
<div class="entry-featured">
<?php the_post_thumbnail('hero'); ?>
</div>
<?php endif; ?>
<div class="entry-content">
<?php
the_content();
wp_link_pages([
'before' => '<div class="page-links">' . __('Pages :', 'montheme'),
'after' => '</div>',
]);
?>
</div>
<footer class="entry-footer">
<?php the_tags('<span class="tags">' . __('Tags : ', 'montheme'), ', ', '</span>'); ?>
</footer>
</article>
<?php
if (comments_open() || get_comments_number()) {
comments_template();
}
?>
<?php endwhile; ?>
<?php get_sidebar(); ?>
<?php get_footer(); ?>
Hooks et filtres utiles
Les hooks c'est la magie de WordPress. Voici ceux que je glisse dans presque tous mes themes :
// Temps de lecture estime avant chaque article
add_filter('the_content', function($content) {
if (is_single() && in_the_loop() && is_main_query()) {
$word_count = str_word_count(strip_tags($content));
$minutes = max(1, ceil($word_count / 200));
$reading = sprintf(
__('Temps de lecture : %d min', 'montheme'),
$minutes
);
$content = '<div class="reading-time">' . esc_html($reading) . '</div>' . $content;
}
return $content;
});
// Longueur de l'extrait (par defaut 55 mots, on passe a 25)
add_filter('excerpt_length', fn() => 25, 999);
// Personnaliser le "Lire la suite"
add_filter('excerpt_more', fn() => '...');
// Ajouter une class custom au body
add_filter('body_class', function($classes) {
if (is_singular('product')) {
$classes[] = 'is-product-page';
}
return $classes;
});
// Desactiver les emojis (ils chargent des KB inutiles)
remove_action('wp_head', 'print_emoji_detection_script', 7);
remove_action('wp_print_styles', 'print_emoji_styles');
// Supprimer le generator (security through obscurity)
remove_action('wp_head', 'wp_generator');
// Limiter les revisions a 5 (mettre dans wp-config en realite)
// define('WP_POST_REVISIONS', 5);
Support Gutenberg moderne avec theme.json
Depuis WordPress 5.8, le fichier theme.json a la racine du theme remplace de nombreux add_theme_support. C'est la maniere moderne de definir palette de couleurs, tailles de police, espacements pour l'editeur Gutenberg.
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 2,
"settings": {
"appearanceTools": true,
"layout": {
"contentSize": "720px",
"wideSize": "1200px"
},
"color": {
"palette": [
{ "name": "Primaire", "slug": "primary", "color": "#2563eb" },
{ "name": "Sombre", "slug": "dark", "color": "#1e293b" },
{ "name": "Clair", "slug": "light", "color": "#f8fafc" },
{ "name": "Accent", "slug": "accent", "color": "#f59e0b" }
],
"custom": false,
"customGradient": false
},
"typography": {
"fontSizes": [
{ "name": "Petit", "slug": "small", "size": "14px" },
{ "name": "Normal", "slug": "medium", "size": "16px" },
{ "name": "Grand", "slug": "large", "size": "20px" },
{ "name": "Tres grand", "slug": "x-large", "size": "28px" }
]
},
"spacing": {
"units": ["px", "em", "rem", "%"]
}
}
}
Ce fichier remplace editor-color-palette, editor-font-sizes et plein d'autres add_theme_support. Plus propre, plus maintenable, et c'est le standard officiel WordPress depuis 2 ans.
Erreurs courantes et leur fix
Theme n'apparait pas dans Apparence > Themes. Le header CSS de style.css est mal forme. Verifiez qu'il y a bien Theme Name: et que vous avez index.php ET style.css dans le dossier. WordPress ignore tout theme qui n'a pas ces deux fichiers.
"Modele perdu" ou page blanche apres activation. Erreur PHP fatale dans functions.php. Activez le mode debug dans wp-config.php :
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);
Puis allez voir /wp-content/debug.log. L'erreur est dedans avec le numero de ligne.
Plugins ne fonctionnent pas (popups, formulaires). Vous avez oublie wp_head() dans header.php ou wp_footer() dans footer.php. C'est l'erreur numero un sur les themes faits a la main.
Images uploadees ne s'affichent pas dans la bonne taille. Vous avez ajoute des add_image_size apres avoir deja uploade des images. Installez le plugin Regenerate Thumbnails et lancez-le pour regenerer toutes les tailles.
Menu personnalise ne s'affiche pas. Vous avez bien fait register_nav_menus mais l'admin n'a pas assigne le menu a l'emplacement. Allez dans Apparence > Menus et cochez "Menu principal" en bas du menu.
Pour aller plus loin
- Optimiser les performances de WordPress
- Configurer Redis comme cache pour WordPress
- Activer le mode debug WordPress
- Securiser WordPress, guide complet anti-hack
- Commandes WP-CLI essentielles
Conclusion : votre theme, votre code, vos regles
Un theme custom de 500 lignes que vous comprenez par coeur sera toujours plus performant et plus fiable qu'un theme premium a 10 000 lignes que personne ne comprend. Le client pense au debut qu'il veut un theme tout fait avec 500 options, mais six mois plus tard il veut juste un site qui charge vite et qui ne casse pas. Faites lui le cadeau d'un theme propre.
Si vous voulez un point de depart plus complet, le starter theme Underscores (_s) reste une excellente base en 2026, ou son evolution _tw qui inclut Tailwind. Mais commencez par le squelette decrit ici, ajoutez ce dont vous avez besoin au fur et a mesure. C'est l'approche la plus saine.