Aller au contenu

Créer un thème

Un thème Drupal est un ensemble de fichiers qui contrôle l’apparence visuelle de votre site. Il transforme les données brutes (contenu, menus, blocs) en HTML, CSS et JavaScript pour le navigateur.

┌─────────────────────────────────────┐
│ Contenu (Drupal Core) │ ← Données, logique métier, base de données
├─────────────────────────────────────┤
│ Thème (Twig + CSS + JS) │ ← Présentation, UX, design
├─────────────────────────────────────┤
│ Navigateur (HTML rendu) │ ← Affichage final pour l'utilisateur
└─────────────────────────────────────┘
AspectThèmeModule
RôlePrésentation, affichageLogique métier, fonctionnalités
Fichiers.twig, .css, .js.php, .module, .routing.yml
ExemplesCouleurs, grilles, animationsTypes de contenu, API, formulaires
Localisationthemes/custom/modules/custom/
ModifieComment les choses apparaissentCe que les choses font

Pour TailStore : Le thème gère l’apparence e-commerce (grilles produits, panier visuel, animations), tandis que les modules custom gèrent la logique métier (panier, session, traitement des commandes avec Stripe).

core/themes/stable9/ (base Drupal)
themes/custom/tailstore/ (votre thème)

Deux options :

  1. base theme: false : Thème indépendant (contrôle total, plus de travail)
  2. base theme: olivero : Hérite d’un thème existant (rapide, moins flexible)

Choix pour TailStore : base theme: false car nous créons un design e-commerce sur-mesure avec Tailwind CSS.

Les thèmes personnalisés se placent dans :

drupal/
├── core/
│ └── themes/ # Thèmes du core (ne pas modifier)
│ ├── olivero/
│ ├── claro/
│ └── stark/
├── themes/
│ └── custom/ # Vos thèmes personnalisés
│ └── tailstore/ # Notre thème
└── sites/
  1. Créez le dossier du thème

    Fenêtre de terminal
    mkdir -p themes/custom/tailstore
    cd themes/custom/tailstore
  2. Créez les sous-dossiers

    Fenêtre de terminal
    mkdir -p css/{base,components,layout}
    mkdir -p js
    mkdir -p images
    mkdir -p templates/{layout,block,node,field,views,misc}
    mkdir -p config/install
  • Répertoiretailstore/
    • tailstore.info.yml
    • tailstore.libraries.yml
    • tailstore.theme
    • tailstore.breakpoints.yml
    • Répertoirecss/
      • Répertoirebase/
      • Répertoirecomponents/
      • Répertoirelayout/
      • tailstore.css
    • Répertoirejs/
      • tailstore.js
    • Répertoireimages/
      • logo.svg
    • Répertoiretemplates/
      • Répertoirelayout/
      • Répertoireblock/
      • Répertoirenode/
      • Répertoirefield/
      • Répertoireviews/
      • Répertoiremisc/

Le fichier tailstore.info.yml est obligatoire et déclare le thème :

# tailstore.info.yml
name: TailStore
type: theme
description: 'Thème e-commerce moderne pour la boutique TailStore'
package: Custom
core_version_requirement: ^10 || ^11
# Thème parent (héritage)
base theme: false
# Ou hériter d'un thème : base theme: olivero
# Logo et screenshot
logo: images/logo.svg
screenshot: screenshot.png
# Régions du thème (zones de placement des blocs)
regions:
header: 'Header'
primary_menu: 'Primary menu'
secondary_menu: 'Secondary menu'
highlighted: 'Highlighted'
help: 'Help'
breadcrumb: 'Breadcrumb'
content: 'Content'
sidebar: 'Sidebar'
content_below: 'Content below'
footer_top: 'Footer top'
footer_bottom: 'Footer bottom'
# Librairies attachées globalement
libraries:
- tailstore/global
# Formulaires de configuration du thème
ckeditor5-stylesheets:
- css/ckeditor.css
# Configuration par défaut
libraries-override: {}
libraries-extend: {}

Les régions définissent les zones où vous pouvez placer des blocs. Voici comment elles s’organisent visuellement :

┌────────────────────────────────────────────────┐
│ header │ ← Logo, recherche, langues
├────────────────────────────────────────────────┤
│ primary_menu │ secondary_menu │ ← Navigation principale/secondaire
├────────────────────────────────────────────────┤
│ highlighted │ ← Messages système, bandeau promo
├────────────────────────────────────────────────┤
│ breadcrumb │ ← Fil d'Ariane
├───────────────────────────┬────────────────────┤
│ │ │
│ content │ sidebar │ ← Contenu principal + filtres/widgets
│ (obligatoire) │ │
├───────────────────────────┴────────────────────┤
│ content_below │ ← Produits recommandés, newsletter
├────────────────────────────────────────────────┤
│ footer_top │ ← Liens, réseaux sociaux
│ footer_bottom │ ← Copyright, mentions légales
└────────────────────────────────────────────────┘

Pour le projet TailStore, nous ajouterons des régions supplémentaires adaptées à l’e-commerce. Modifiez la section regions: pour inclure :

regions:
# Régions standards
header: 'Header'
cart_header: 'Cart icon & counter'
primary_menu: 'Primary menu'
secondary_menu: 'Secondary menu'
promo_banner: 'Promotional banner'
highlighted: 'Highlighted'
help: 'Help'
breadcrumb: 'Breadcrumb'
# Zones de contenu
content: 'Content'
sidebar: 'Sidebar (filters, widgets)'
content_below: 'Content below (recommendations)'
# Footer
footer_top: 'Footer top'
footer_bottom: 'Footer bottom'
trust_badges: 'Trust badges (checkout)'

Utilisation des nouvelles régions :

  • cart_header : Icône panier avec compteur d’articles (dynamique)
  • promo_banner : Bandeau promotionnel (ex: “Livraison gratuite dès 50€”)
  • sidebar : Filtres de prix, catégories, disponibilité sur les pages catalogue
  • content_below : “Vous aimerez aussi”, cross-sell, upsell
  • trust_badges : Badges de réassurance (paiement sécurisé, satisfait ou remboursé)

Le fichier tailstore.libraries.yml déclare les CSS et JS selon les standards Drupal :

# tailstore.libraries.yml
# Librairie globale (chargée sur toutes les pages)
global:
version: 1.0.0
css:
base:
css/base/reset.css: {}
css/base/typography.css: {}
layout:
css/layout/grid.css: {}
css/layout/regions.css: {}
component:
css/components/buttons.css: {}
css/components/cards.css: {}
css/components/forms.css: {}
css/components/navigation.css: {}
theme:
css/tailstore.css: {}
js:
js/tailstore.js: {}
dependencies:
- core/drupal
- core/once
# Librairie spécifique pour le slider
slider:
version: 1.0.0
css:
component:
css/components/slider.css: {}
js:
js/slider.js: {}
dependencies:
- tailstore/global
- core/once
# Librairie pour le panier avec drupalSettings
cart:
version: 1.0.0
js:
js/cart.js: { attributes: { defer: true } }
dependencies:
- tailstore/global
- core/drupalSettings
# CDN externes (Swiper)
swiper:
version: 11.0.0
remote: https://swiperjs.com/
license:
name: MIT
url: https://github.com/nolimits4web/swiper/blob/master/LICENSE
gpl-compatible: true
css:
theme:
https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css: { type: external, minified: true }
js:
https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js: { type: external, minified: true }
# Alpine.js avec attributs
alpine:
version: 3.14.1
remote: https://alpinejs.dev/
license:
name: MIT
url: https://github.com/alpinejs/alpine/blob/main/LICENSE.md
gpl-compatible: true
js:
https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js: { type: external, attributes: { defer: true } }
# Librairie chargée dans le header (ex: polices)
fonts:
version: 1.0.0
header: true
css:
theme:
https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap: { type: external }
# Librairie avec JS non agrégé
analytics:
version: 1.0.0
js:
js/analytics.js: { preprocess: false }
dependencies:
- core/drupal

Les fichiers JavaScript Drupal utilisent une structure spécifique appelée IIFE (Immediately Invoked Function Expression) combinée avec Drupal.behaviors.

// Fonction anonyme classique
function() {
console.log('Hello World!');
}
// Devient une IIFE
(function() {
console.log('Hello World!');
})();
// js/tailstore.js
(function($, Drupal, once) {
'use strict';
// Menu mobile toggle
Drupal.behaviors.tailstoreMobileMenu = {
attach: function(context, settings) {
once('mobile-menu', '.mobile-menu-toggle', context).forEach(function(toggle) {
$(toggle).on('click', function(e) {
e.preventDefault();
$('.main-navigation').toggleClass('is-open');
});
});
}
};
})(jQuery, Drupal, once);

Le fichier tailstore.theme contient les fonctions PHP qui utilisent le système de hooks de Drupal.

Un hook est un point d’extension dans Drupal qui permet aux modules et thèmes de modifier ou d’étendre le comportement du système. Les hooks sont des fonctions PHP nommées selon une convention spécifique : nom_du_module_ou_theme_nom_du_hook().

// Hook de préprocessing : Modifie les variables avant le rendu
hook_preprocess_HOOK(&$variables)
// Hook d'altération de formulaire : Modifie les formulaires
hook_form_alter(&$form, $form_state, $form_id)
// Hook de suggestions de template : Ajoute des templates alternatifs
hook_theme_suggestions_HOOK_alter(&$suggestions, $variables)

Le fichier tailstore.theme contient les fonctions PHP :

<?php
/**
* @file
* Functions to support theming in the TailStore theme.
*/
use Drupal\Core\Form\FormStateInterface;
/**
* Implements hook_preprocess_HOOK() for html.html.twig.
*
* Ce hook permet de modifier les variables avant le rendu du template html.html.twig.
* Ici, on ajoute des classes CSS au body pour le styling et la logique conditionnelle.
*/
function tailstore_preprocess_html(&$variables) {
// Ajouter une classe globale au thème
$variables['attributes']['class'][] = 'tailstore-theme';
// Ajouter une classe spécifique pour les pages produit
// Utile pour appliquer des styles particuliers aux pages e-commerce
$node = \Drupal::routeMatch()->getParameter('node');
if ($node && $node->bundle() === 'product') {
$variables['attributes']['class'][] = 'page-product';
}
}
## 🧠 Bonnes pratiques essentielles
### 🔒 Sécurité : Validation des données
Ne jamais faire confiance aux données utilisateur ou même aux champs Drupal sans validation.
** Dangereux** :
```php
function tailstore_preprocess_node(&$variables) {
$node = $variables['node'];
$variables['price'] = $node->get('field_price')->value; // Pas de validation !
}

✅ Sécurisé :

function tailstore_preprocess_node(&$variables) {
$node = $variables['node'];
if ($node->hasField('field_price') && !$node->get('field_price')->isEmpty()) {
$price = $node->get('field_price')->value;
// Valider que c'est un nombre positif
if (is_numeric($price) && $price >= 0) {
$variables['price'] = number_format((float) $price, 2, ',', ' ') . ' €';
}
else {
// Logger l'erreur
\Drupal::logger('tailstore')->warning('Invalid price for node @nid', [
'@nid' => $node->id()
]);
}
}
}

Principe : Évitez de charger des librairies lourdes sur toutes les pages.

Exemple : Charger le slider uniquement sur la homepage :

function tailstore_preprocess_page(&$variables) {
// Slider chargé UNIQUEMENT sur la homepage
if (\Drupal::service('path.matcher')->isFrontPage()) {
$variables['#attached']['library'][] = 'tailstore/slider';
$variables['#attached']['library'][] = 'tailstore/swiper';
}
// Panier chargé UNIQUEMENT sur les pages produit et panier
$route_name = \Drupal::routeMatch()->getRouteName();
if (in_array($route_name, ['entity.node.canonical', 'tailstore.cart'])) {
$variables['#attached']['library'][] = 'tailstore/cart';
}
}

Impact :

  • Homepage : 200KB JS
  • Page article : 50KB JS (150KB économisés !)

Chaque hook doit avoir un bloc de documentation PHPDoc.

Template recommandé :

/**
* Implements hook_preprocess_HOOK() for node.html.twig.
*
* Prepare variables for node templates.
*
* @param array $variables
* An associative array containing:
* - elements: Node entity and view mode.
* - node: The node entity.
* - view_mode: View mode (full, teaser, card, etc.).
*/
function tailstore_preprocess_node(&$variables) {
// Code...
}

Le fichier tailstore.theme contient les fonctions PHP :

<?php
/**
* @file
* Functions to support theming in the TailStore theme.
*/
use Drupal\Core\Form\FormStateInterface;
/**
* Implements hook_preprocess_HOOK() for html.html.twig.
*
* Ce hook permet de modifier les variables avant le rendu du template html.html.twig.
* Ici, on ajoute des classes CSS au body pour le styling et la logique conditionnelle.
*/
function tailstore_preprocess_html(&$variables) {
// Ajouter une classe globale au thème
$variables['attributes']['class'][] = 'tailstore-theme';
// Ajouter une classe spécifique pour les pages produit
// Utile pour appliquer des styles particuliers aux pages e-commerce
$node = \Drupal::routeMatch()->getParameter('node');
if ($node && $node->bundle() === 'product') {
$variables['attributes']['class'][] = 'page-product';
}
}
/**
* Implements hook_preprocess_HOOK() for page.html.twig.
*
* Ce hook permet de modifier les variables avant le rendu du template page.html.twig.
* Ici, on charge des librairies JavaScript selon le contexte de la page.
*/
function tailstore_preprocess_page(&$variables) {
// Charger la librairie slider uniquement sur la page d'accueil
// Évite de charger du JS inutile sur les autres pages
if (\Drupal::service('path.matcher')->isFrontPage()) {
$variables['#attached']['library'][] = 'tailstore/slider';
$variables['#attached']['library'][] = 'tailstore/swiper';
}
}
/**
* Implements hook_preprocess_HOOK() for node.html.twig.
*
* Ce hook permet de modifier les variables avant le rendu des nodes.
* Ici, on prépare des données spécifiques pour les produits e-commerce.
*/
function tailstore_preprocess_node(&$variables) {
/** @var \Drupal\node\NodeInterface $node */
$node = $variables['node'];
// Variables personnalisées pour les produits
if ($node->bundle() === 'product') {
// Formater le prix avec séparateur français
if ($node->hasField('field_price') && !$node->get('field_price')->isEmpty()) {
$variables['formatted_price'] = number_format(
$node->get('field_price')->value,
2,
',',
' '
) . ' €';
}
// Vérifier la disponibilité en stock
if ($node->hasField('field_stock')) {
$variables['in_stock'] = $node->get('field_stock')->value > 0;
}
// Gestion des prix barrés pour les promotions
if ($node->hasField('field_old_price') && !$node->get('field_old_price')->isEmpty()) {
$variables['old_price'] = number_format(
$node->get('field_old_price')->value,
2,
',',
' '
) . ' €';
$variables['is_on_sale'] = TRUE;
}
}
}
/**
* Implements hook_preprocess_HOOK() for field.html.twig.
*
* Ce hook permet de modifier les variables avant le rendu des champs.
* Ici, on ajoute des classes CSS spécifiques selon le type de champ.
*/
function tailstore_preprocess_field(&$variables) {
$field_name = $variables['field_name'];
// Ajouter une classe spécifique pour le prix
if ($field_name === 'field_price') {
$variables['attributes']['class'][] = 'product-price';
}
}
/**
* Implements hook_form_alter().
*
* Ce hook permet de modifier tous les formulaires du système.
* Ici, on personnalise l'apparence et le comportement de certains formulaires.
*/
function tailstore_form_alter(&$form, FormStateInterface $form_state, $form_id) {
// Personnaliser le formulaire de recherche
if ($form_id === 'search_block_form') {
$form['keys']['#attributes']['placeholder'] = t('Rechercher...');
$form['keys']['#attributes']['class'][] = 'search-input';
}
// Personnaliser les filtres exposés des vues
if (strpos($form_id, 'views_exposed_form') === 0) {
$form['#attributes']['class'][] = 'filters-form';
}
}
/**
* Implements hook_theme_suggestions_HOOK_alter() for node.
*
* Ce hook permet de suggérer des templates alternatifs selon le contexte.
* Ici, on propose des templates spécifiques pour les produits en promotion.
*/
function tailstore_theme_suggestions_node_alter(array &$suggestions, array $variables) {
/** @var \Drupal\node\NodeInterface $node */
$node = $variables['elements']['#node'];
$view_mode = $variables['elements']['#view_mode'];
// Ajouter une suggestion basée sur un champ personnalisé
if ($node->bundle() === 'product' && $node->hasField('field_featured')) {
if ($node->get('field_featured')->value) {
$suggestions[] = 'node__product__featured';
$suggestions[] = 'node__product__' . $view_mode . '__featured';
}
}
}
## 📐 Fichier .breakpoints.yml
Le fichier **tailstore.breakpoints.yml** définit les points de rupture responsive :
```yaml
# tailstore.breakpoints.yml
tailstore.mobile:
label: Mobile
mediaQuery: ''
weight: 0
multipliers:
- 1x
- 2x
tailstore.narrow:
label: Narrow
mediaQuery: 'all and (min-width: 560px)'
weight: 1
multipliers:
- 1x
- 2x
tailstore.medium:
label: Medium
mediaQuery: 'all and (min-width: 768px)'
weight: 2
multipliers:
- 1x
- 2x
tailstore.wide:
label: Wide
mediaQuery: 'all and (min-width: 1024px)'
weight: 3
multipliers:
- 1x
- 2x
tailstore.extra_wide:
label: Extra Wide
mediaQuery: 'all and (min-width: 1280px)'
weight: 4
multipliers:
- 1x
- 2x
  1. Appearance (/admin/appearance)
  2. Trouvez “TailStore” dans la liste
  3. Cliquez sur Install and set as default
Fenêtre de terminal
# Activer le thème
drush theme:enable tailstore
# Définir comme thème par défaut
drush config:set system.theme default tailstore -y
# Vider le cache
drush cr
/* Reset moderne */
*,
*::before,
*::after {
box-sizing: border-box;
}
* {
margin: 0;
padding: 0;
}
html {
font-size: 16px;
scroll-behavior: smooth;
}
body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
}
input,
button,
textarea,
select {
font: inherit;
}
p,
h1,
h2,
h3,
h4,
h5,
h6 {
overflow-wrap: break-word;
}
/* Variables de typographie */
:root {
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
--font-mono: 'Fira Code', monospace;
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.25rem;
--text-2xl: 1.5rem;
--text-3xl: 2rem;
--text-4xl: 2.5rem;
}
body {
font-family: var(--font-sans);
font-size: var(--text-base);
color: var(--color-dark, #1a1a2e);
}
h1, h2, h3, h4, h5, h6 {
font-weight: 600;
line-height: 1.2;
}
h1 { font-size: var(--text-4xl); }
h2 { font-size: var(--text-3xl); }
h3 { font-size: var(--text-2xl); }
h4 { font-size: var(--text-xl); }
a {
color: var(--color-primary, #0073e6);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}

Placez votre logo dans images/logo.svg ou configurez-le via :

  1. AppearanceSettingsTailStore
  2. Section Logo image
  3. Uploadez ou spécifiez le chemin
Fenêtre de terminal
# Vérifier que le thème est actif
drush config:get system.theme default
# Devrait retourner : 'tailstore'
# Vider le cache
drush cr
# Visitez le site pour voir le thème

Vérifiez que tout fonctionne correctement :

  1. Vérifier l’activation

    Fenêtre de terminal
    drush config:get system.theme default

    Attendu : 'system.theme:default': tailstore

  2. Vérifier les logs d’erreurs

    Fenêtre de terminal
    drush watchdog:show --severity=Error --count=10

    Attendu : Aucune erreur liée au thème

  3. Tester l’accessibilité du site

    Fenêtre de terminal
    curl -I http://localhost/

    Attendu : HTTP/1.1 200 OK

  4. Inspecter le HTML dans le navigateur

    • Ouvrez le site dans votre navigateur
    • F12 → Onglet Elements
    • Vérifiez que <body> contient class="tailstore-theme"
  5. Vérifier le chargement CSS/JS

    • F12 → Onglet Network
    • Rechargez la page
    • Vérifiez que tailstore.css et tailstore.js sont chargés (statut 200)

Objectif : Créer un thème fonctionnel avec seulement les fichiers obligatoires.

  1. Créez la structure minimale :

    Fenêtre de terminal
    mkdir -p themes/custom/tailstore
    cd themes/custom/tailstore
    touch tailstore.info.yml
  2. Éditez tailstore.info.yml avec le strict minimum :

    name: TailStore
    type: theme
    core_version_requirement: ^10 || ^11
    regions:
    content: 'Content'
  3. Activez le thème :

    Fenêtre de terminal
    drush theme:enable tailstore
    drush config:set system.theme default tailstore -y
    drush cr
  4. Visitez votre site : il doit s’afficher (même sans style)

Validation : ✅ Le site est accessible sans erreur 500


Objectif : Ajouter une classe CSS personnalisée au <body>.

  1. Créez le fichier tailstore.theme :

    Fenêtre de terminal
    touch tailstore.theme
  2. Ajoutez ce code :

    <?php
    function tailstore_preprocess_html(&$variables) {
    $variables['attributes']['class'][] = 'tailstore-active';
    }
  3. Videz le cache :

    Fenêtre de terminal
    drush cr
  4. Inspectez le HTML (F12) : vous devriez voir :

    <body class="tailstore-active">

Validation : ✅ La classe tailstore-active apparaît dans le <body>


Objectif : Créer et charger un fichier CSS de test.

  1. Créez la structure CSS :

    Fenêtre de terminal
    mkdir -p css
  2. Créez css/test.css avec un style visible :

    body {
    background-color: #ffe6e6;
    border-top: 5px solid #dc3545;
    }
  3. Créez tailstore.libraries.yml :

    global:
    version: 1.0.0
    css:
    theme:
    css/test.css: {}
  4. Déclarez la librairie dans tailstore.info.yml :

    libraries:
    - tailstore/global
  5. Videz le cache et rechargez :

    Fenêtre de terminal
    drush cr

Validation : ✅ Le fond de la page doit être rose clair avec une bordure rouge en haut

Si ça ne fonctionne pas :

  • Vérifiez le chemin dans libraries.yml (relatif au dossier du thème)
  • Inspectez F12 → Network : le fichier test.css doit être chargé (statut 200)
  • Vérifiez la console pour des erreurs 404

Objectif : Personnaliser les régions pour le projet TailStore.

  1. Modifiez la section regions: dans tailstore.info.yml pour ajouter :

    regions:
    header: 'Header'
    cart_header: 'Cart icon'
    primary_menu: 'Primary menu'
    content: 'Content'
    sidebar: 'Sidebar'
    footer_top: 'Footer'
  2. Videz le cache :

    Fenêtre de terminal
    drush cr
  3. Allez dans StructureBlock layout (/admin/structure/block)

  4. Vérifiez que vos nouvelles régions apparaissent

Validation : ✅ Les régions “Cart icon” et “Sidebar” sont visibles dans la page de placement des blocs

ÉtapeFichier/ActionStatutCommande de validation
1Créer themes/custom/tailstore/ls themes/custom/tailstore
2tailstore.info.yml (minimal)drush theme:list | grep tailstore
3Régions e-commerce ajoutéesVérifier /admin/structure/block
4tailstore.libraries.ymlValider syntaxe YAML
5tailstore.theme avec hookphp -l themes/custom/tailstore/tailstore.theme
6CSS de base (reset, typography)Inspecter <head> (F12)
7tailstore.breakpoints.ymldrush pm:enable breakpoint
8Activer le thèmedrush config:get system.theme default
9Vider le cachedrush cr
10Site fonctionnelOuvrir dans le navigateur

Temps estimé total : 45 minutes

Le thème est créé ! Passons aux Templates Twig.