Contenu HTML vide : diagnostic des balises H1-H6 et correctifs

Guide pratique pour détecter et corriger les balises H1–H6 vides sur PrestaShop : diagnostic, causes, correctifs templates/modules, nettoyage WYSIWYG et tests automatisés.

Éditeur de code affichant un avertissement de contenu HTML vide.

Table des matières :

  1. Balises H1–H6 vides : pourquoi c’est un vrai incident (SEO + accessibilité)

  2. Définir “contenu HTML vide” : DOM, entités, rendu CSS et JavaScript

  3. Détecter le problème à l’échelle : crawl, XPath, et différentiel source vs DOM rendu

  4. PrestaShop 8/9 : où naissent les Hn (Smarty/Twig, hooks, CMS) et causes typiques

  5. Correctifs robustes : conditions de rendu, fallbacks, nettoyage WYSIWYG et validation BO

  6. Cas module/hook : éviter d’injecter des H2/H3 vides (et corriger sans casser le front)

  7. Industrialiser : tests, CI, monitoring et garde-fous avant mise en prod

  8. Checklist de correction rapide (sans casser votre thème)


Balises H1–H6 vides : pourquoi c’est un vrai incident (SEO + accessibilité)

Une balise de titre vide (<h1></h1>, <h2>
</h2>
, <h3>&nbsp;</h3>) n’est pas un “détail HTML”. C’est un signal que votre pipeline de rendu (données → template → HTML final) produit un nœud sémantique sans contenu. En SEO technique, ça se traduit par des audits qui remontent “Empty heading” / “Empty H1” et, surtout, par une structure de document dégradée : Google et les outils d’analyse ne peuvent pas s’appuyer sur un titre pour segmenter la page (extraction de sujets, génération de sitelinks, compréhension des sections, etc.).

Le standard HTML est explicite sur le rôle des titres. L’HTML Living Standard (WHATWG) précise :


“The h1, h2, h3, h4, h5, and h6 elements represent section headings.”
— WHATWG, HTML Living Standard : https://html.spec.whatwg.org/

Si vous générez une “section heading” sans texte, vous publiez une structure vide : vous n’aidez ni les moteurs ni les humains.

Côté Google, la documentation de base insiste surtout sur l’usage cohérent des titres (plutôt que sur une “optimisation” magique). Le SEO Starter Guide indique par exemple :


“Use heading tags where it makes sense.”
— Google Search Central, SEO Starter Guide : https://developers.google.com/search/docs/fundamentals/seo-starter-guide

Au-delà du SEO, c’est aussi un sujet d’accessibilité : de nombreux lecteurs d’écran permettent de naviguer par titres (liste des headings de la page). Un heading vide devient alors un “arrêt” inutile et un repère trompeur dans la navigation. Si vous êtes dans un contexte France/UE avec des exigences d’accessibilité (marchés publics, grandes organisations, ou mise en conformité progressive du secteur privé selon les cas), c’est un défaut simple à éviter et à documenter. Référence utile côté France : RGAA (Référentiel général d’amélioration de l’accessibilité) https://www.numerique.gouv.fr/publications/rgaa-accessibilite/

Enfin, si l’incident s’inscrit dans une problématique plus large de pages à faible contenu, faites le lien avec le diagnostic “page vide” (causes + indexation) : https://www.expertise-prestashop.fr/2026/07/27/page-vide-causes-frequentes-et-impact-sur-lindexation-google/.

Définir “contenu HTML vide” : DOM, entités, rendu CSS et JavaScript

Dans un audit, “contenu HTML vide” veut généralement dire : le nœud existe dans le HTML livré, mais après normalisation (suppression des espaces, des sauts de ligne et parfois des entités non imprimables), il ne reste rien.

Quelques cas classiques (tous se voient en production, y compris sur des thèmes “propres”) :

Cas Exemple Pourquoi c’est considéré “vide” en audit
Espaces / retours à la ligne <h2>
\t</h2>
trim() retire tout, aucune information transmise
<br> seul <h3><br></h3> Visuellement “il y a de la hauteur”, sémantiquement zéro texte
NBSP <h3>&nbsp;</h3> Caractère non séparateur : n’apporte aucune information (souvent flagué)
Caractères invisibles U+200B (zero-width space), &shy; Collés par des éditeurs ; difficiles à repérer, mais “vides” fonctionnellement
Commentaires HTML <h2><!-- title --></h2> Le DOM contient un heading sans texte

Le cas borderline classique est &nbsp; : techniquement, il y a un caractère ; fonctionnellement, c’est du vide. Certains crawlers le comptent comme texte, d’autres le flaguent comme “empty” car il n’apporte aucune information (et surtout ne résout pas la navigation et la compréhension).

Deuxième distinction indispensable : vide dans la source vs vide après exécution JS. PrestaShop rend majoritairement côté serveur (Smarty sur 1.7/8, et de plus en plus Twig/Symfony sur 9 selon zones), donc un heading vide est souvent déjà vide dans view-source. Mais certains thèmes modernes “réécrivent” le DOM (ex. remplacer un H1 par un composant, injecter un titre depuis une config JSON, ou manipuler le contenu via Alpine/React). Résultat : un <h1> non alimenté côté serveur peut rester vide si le JS ne s’exécute pas (timeout, erreur, blocage CSP, bot sans rendu).

Troisième piège : contenu visuellement présent ≠ contenu présent dans le DOM. Exemple fréquent : un titre “injecté” via CSS (h2::before { content: "Livraison"; }). Le visiteur voit un titre, mais les robots et les technologies d’assistance n’ont pas forcément un texte exploitable (le content: CSS n’est pas une bonne source sémantique). Ici on parle bien d’un nœud H1–H6 dont le textContent normalisé est vide. Si votre “fix” consiste à ajouter un texte et le cacher, vous avez juste déplacé le problème, pas corrigé la chaîne de données.

Détecter le problème à l’échelle : crawl, XPath, et différentiel source vs DOM rendu

Pour diagnostiquer correctement, commencez par séparer détection (quelles URL ? quelles balises ?) et attribution (quelle template / quel module ?). Un crawl type Screaming Frog / Sitebulb remonte très vite les “Empty headings” via extraction DOM, mais ce n’est pas toujours suffisant : vous voulez pouvoir reproduire via CLI, sans UI, et comparer HTML brut vs DOM rendu.

Point d’attention avant même les scripts : selon votre site, vous pouvez avoir des divergences de HTML selon langue, devise, géolocalisation, cookies, A/B test, ou statut connecté / non connecté. Pour éviter les faux diagnostics, testez au moins :

  • une URL en langue FR et une en langue secondaire (le cas “titre vide dans une seule langue” est très courant) ;

  • une page catégorie et une page produit (elles n’utilisent pas forcément le même page-header.tpl) ;

  • une URL avec et sans paramètres si vous avez du contenu injecté par JS.

En CLI, un premier passage “source only” (pensez à installer la dépendance si besoin : pip install beautifulsoup4) :

curl -sL https://example.com/ | \
  python3 - <<'PY'
from bs4 import BeautifulSoup
import sys, re
html=sys.stdin.read()
s=BeautifulSoup(html,'html.parser')
for tag in ['h1','h2','h3','h4','h5','h6']:
  for i,h in enumerate(s.find_all(tag),1):
    text=re.sub(r'\s+',' ',h.get_text(' ',strip=True))
    # normalisation agressive de &nbsp; / \xa0
    text=text.replace('\u00a0','').strip()
    if text=='':
      print(f"EMPTY {tag} #{i} -> {str(h)[:120]}")
PY

Si votre audit remonte des Hn vides mais que curl n’en trouve pas, c’est qu’on est probablement sur un différentiel DOM rendu. Là, passez sur un rendu headless (Playwright) et lisez le textContent post-JS. (Astuce : avec import, Node fonctionne mieux en mode module : node --input-type=module -.)

node --input-type=module - <<'NODE'
import playwright from 'playwright';
const url = process.argv[2] || 'https://example.com/';
const browser = await playwright.chromium.launch();
const page = await browser.newPage();
await page.goto(url, {waitUntil: 'networkidle'});
const empty = await page.evaluate(() => {
  const tags=['h1','h2','h3','h4','h5','h6'];
  const out=[];
  for (const t of tags){
    document.querySelectorAll(t).forEach((h,idx)=>{
      const text=(h.textContent||'').replace(/\u00a0/g,'').trim();
      if(!text) out.push({tag:t, idx: idx+1, html: h.outerHTML.slice(0,160)});
    });
  }
  return out;
});
console.log(empty);
await browser.close();
NODE

Pour industrialiser la détection, l’approche la plus robuste est : crawl + extraction XPath/CSS + normalisation + seuils. Exemple (logique) : extraire //h1 et //h2 et déclencher une alerte si normalize-space() est vide. Même sans développer un crawler maison, vous pouvez déjà configurer :

  • des extractions ciblées (H1/H2) ;

  • une comparaison “HTML source” vs “Rendered HTML” si votre outil le permet ;

  • une segmentation par type de page (produit/catégorie/CMS) pour remonter rapidement au bon template.

Cette logique s’intègre bien dans un audit SEO technique plus large (garde-fous indexation/perf) : https://www.expertise-prestashop.fr/2026/08/06/seo-prestashop-checklist-technique-2025-pour-indexation-et-performance/.


PrestaShop 8/9 : où naissent les Hn (Smarty/Twig, hooks, CMS) et causes typiques

Contexte versions (à adapter à votre stack) : les exemples ci-dessous sont valables pour PrestaShop 8.1/8.2 (Smarty) et PrestaShop 9.x (hybride Smarty + Symfony/Twig selon pages/back-office), avec PHP 8.2/8.3 côté serveur. Avant de patcher, vérifiez votre couple PrestaShop/PHP et les incohérences de doc/compatibilité : https://www.expertise-prestashop.fr/2026/07/23/prestashop-9-versions-php-recommandees-et-incoherences-de-documentation/.

Les headings “structurants” sortent presque toujours de :

  • templates thème (themes/<theme>/templates/...) : product.tpl, category.tpl, cms.tpl, errors/404.tpl, et surtout les partials page-header.tpl.

  • modules injectant du contenu via hooks (ex. displayHeader, displayTop, displayFooter, displayLeftColumn) : certains modules encapsulent leur titre dans un <hX> pour “faire joli”, et le laissent vide si la config n’est pas remplie.

  • contenu éditorial BO (CMS, descriptions catégories/produits) : un WYSIWYG peut laisser des headings “fantômes” (<h2><br></h2>), souvent après copier-coller depuis Word/Google Docs.

Causes typiques observées en production :
1) Variable Smarty/Twig vide : {$page.meta.title} ou {$category.name} est vide dans une langue donnée → le template rend quand même <h1>.
2) Fallback de données absent : meta title vide, name vide, ou champ non synchronisé lors d’une migration.
3) Multi-boutique / multi-langue : un produit existe, mais sa traduction n’a pas été créée pour une langue donnée (ou a été “nettoyée” par un import).
4) Surcharge thème / override module : un override supprime le contenu mais garde le wrapper <h2 class="h3">.
5) Minification/concat (CCC) + JS : si le titre est injecté en JS, une erreur bloquante (souvent visible dans la console) laisse l’élément vide.

Mini-scénario courant : migration 1.7 → 8/9 + ajout d’une langue. Les pages FR sont correctes, mais sur EN le meta_title n’est pas renseigné sur certains CMS. Le thème rend <h1>{$page.meta.title}</h1> : sur EN, H1 vide (et parfois title de page peu exploitable). Un crawl en mode “liste d’URL EN” permet de mesurer immédiatement le volume et d’éviter de “corriger” uniquement le template alors que la donnée est réellement manquante.

Sur PrestaShop, les optimisations front (CCC/CDN) peuvent aggraver le debugging ; gardez en tête la méthodologie perf/caches : https://www.expertise-prestashop.fr/2026/07/30/prestashop-optimiser-performance-via-cache-smarty-ccc-et-cdn/.


Correctifs robustes : conditions de rendu, fallbacks, nettoyage WYSIWYG et validation BO

Le correctif “propre” est presque toujours : ne pas rendre la balise si le contenu est vide. Dans un thème Smarty (PrestaShop 8), au lieu de :

{assign var=heading value=$page.meta.title|default:''}
{assign var=heading value=$heading|regex_replace:'/\s+/':' '|trim}
{assign var=heading value=$heading|replace:"\u00a0":""|trim}
{if $heading != ''}
  <h1 class="h1">{$heading|escape:'html':'UTF-8'}</h1>
{/if}

Trois points importants : (1) normaliser les espaces (dont NBSP), (2) échapper correctement, (3) ne pas générer de H1 “placeholder” (type “Produit” ou “Catégorie”) : c’est souvent pire en SEO et en UX. Si le heading est manquant parce que les données sont réellement manquantes, corrigez d’abord la donnée.

Si vous êtes déjà sur Twig (certaines pages / PS9), l’équivalent d’intention est simple : condition + trim. Exemple (principe) :

{% set heading = (page.meta.title ?? '')|replace({'\u00A0':''})|trim %}
{% if heading is not empty %}
  <h1 class="h1">{{ heading }}</h1>
{% endif %}

Quand la donnée est vide en base, commencez par objectiver le périmètre via SQL (backup obligatoire ; exécutez sur une réplique ou un dump). Attention : selon votre moteur (souvent MySQL/MariaDB), \u00A0 n’est pas interprété comme “NBSP” dans REPLACE(). Un test plus réaliste utilise CHAR(160).

Exemple pour les produits multilingues (table préfixée ps_ à adapter) :

-- Noms produits vides ou composés uniquement d'espaces/NBSP
SELECT id_product, id_lang, name
FROM ps_product_lang
WHERE name IS NULL
   OR TRIM(REPLACE(name, CHAR(160), '')) = '';

-- Noms catégories vides
SELECT id_category, id_lang, name
FROM ps_category_lang
WHERE name IS NULL
   OR TRIM(REPLACE(name, CHAR(160), '')) = '';

Si vous trouvez des trous, vous avez un problème amont (import, ERP, traduction). Il faut corriger la source, sinon vous allez multiplier les fallbacks incohérents (et vous retrouvez rapidement à générer des titres SEO sans contenu exploitable) : https://www.expertise-prestashop.fr/2026/07/21/titres-seo-generer-une-strategie-sans-contenu-source-exploitable/.

Pour les headings vides issus du WYSIWYG (CMS, descriptions), le pattern est : le HTML contient des tags de titre, mais le contenu est un <br>, un &nbsp; ou du whitespace. Ne “nettoyez” pas en SQL à l’aveugle. Procédez plutôt ainsi :

  • exporter un échantillon (les 50 pages qui posent problème) ;

  • parser et journaliser chaque modification (avant/après) ;

  • supprimer uniquement les headings dont le textContent est vide après normalisation ;

  • réimporter et recrawler.

Le cœur de PrestaShop ne fournit pas de pipeline de lint HTML natif : si vous laissez les contributeurs coller du contenu, vous devez mettre un garde-fou (validation côté module, contrainte dans un formulaire Symfony en PrestaShop 9, ou lint côté CI si vos contenus sont versionnés).


Cas module/hook : éviter d’injecter des H2/H3 vides (et corriger sans casser le front)

Beaucoup d’“empty headings” viennent de modules qui encapsulent leurs blocs avec un titre optionnel : title en configuration, traduisible, parfois vide en une langue. C’est fréquent sur des modules de réassurance, de blocs HTML, de menus, ou des intégrations marketplace. Techniquement, le bug est simple : le template rend <h3>{$title}</h3> même si $title est vide.

Le correctif côté module doit se faire à deux niveaux :

  • Template : conditionner le rendu du heading.

  • Back-office : empêcher la sauvegarde d’un titre vide si le module dépend de ce champ pour la structure (ou au minimum afficher un warning et fournir un fallback explicite).

Exemple côté template Smarty (idée minimale) :

{if isset($title) && $title|strip_tags|trim != ''}
  <h3 class="h3">{$title|escape:'html':'UTF-8'}</h3>
{/if}

Si votre design a absolument besoin d’un “en-tête visuel” même sans titre (cas rare), préférez un élément non sémantique (ex. <div class="block-title">) plutôt qu’un heading vide : vous évitez d’abîmer l’arbre sémantique tout en gardant le rendu CSS.

Sur PrestaShop 9, si votre module utilise des formulaires Symfony, mettez des contraintes explicites (ex. NotBlank) sur les champs requis. Sinon, vous allez continuer à produire du HTML “structurel” incomplet et vous ne le verrez qu’en audit. Et si vous êtes tenté de “réparer” via CSS (masquer les headings vides), notez que les crawlers et outils d’accessibilité lisent le DOM, pas votre intention : vous gardez un nœud sémantique vide et vous polluez les arbres d’accessibilité.


Industrialiser : tests, CI, monitoring et garde-fous avant mise en prod

Une fois le correctif appliqué, le vrai sujet est d’éviter la régression (nouveau module, nouvelle langue, import ERP, refonte thème). La stratégie efficace : un test automatique qui échoue si un heading vide apparaît.

Approche pragmatique :

  • choisir un petit set d’URL critiques (home, top catégorie, top produit, CMS “livraison/CGV”, 404) ;

  • exécuter un test headless après déploiement en préprod ;

  • bloquer la mise en prod si un H1 vide est détecté (et, selon votre politique, si un H2/H3 vide apparaît dans des zones clés).

Même un test très court suffit à attraper les régressions “grosses” (ex. page-header.tpl modifié, variable renommée, traduction manquante). Ce n’est pas un “test SEO”, c’est un test de rendu sémantique.

Côté CI/CD, ajoutez une étape “lint HTML” après build thème / déploiement en préprod. Si vous faites du blue/green, ce contrôle s’insère avant bascule. En complément, gardez une observabilité minimale : erreurs JS front (Sentry/ELK), logs PHP-FPM et métriques infra. Les incidents “heading injecté en JS mais JS cassé” se détectent souvent d’abord comme une hausse de 4xx/5xx, du temps de réponse, ou un pic d’erreurs console.

Pour la brique monitoring, vous pouvez vous appuyer sur vos collectors temps réel (Netdata) : https://www.expertise-prestashop.fr/2026/08/06/netdata-activer-et-configurer-les-collectors-pour-monitoring-temps-reel/ et, si vous êtes équipé, sur des alertes Prometheus : https://www.expertise-prestashop.fr/2026/08/05/prometheus-configuration-des-alertes-metriques-et-requetes-promql/.

Enfin, avant mise en prod : purgez proprement les caches PrestaShop (cache Smarty, cache Symfony sur PS9, et caches reverse proxy/CDN si présents) et refaites un crawl rapide. Les faux positifs “corrigé mais toujours vu” viennent souvent d’un cache qui sert une ancienne version du template. Si votre architecture a du multi-front (Nginx+PHP-FPM, Apache+PHP-FPM), gardez en tête le chemin d’exécution et où se situe la cache : https://www.expertise-prestashop.fr/2026/08/06/php-fpm-comprendre-le-flux-dexecution-avec-nginx-et-apache/.


Checklist de correction rapide (sans casser votre thème)

Commencez par isoler : quelles URL et quel heading (H1 ou H2-H6) est vide. Sur e-commerce, priorisez les pages à fort volume et fort trafic (catégories, produits, pages d’atterrissage facettées si indexées). Une anomalie H1 vide sur 5 pages est un bug local ; sur 50 000 produits, c’est un problème de données ou de template global.

Ensuite, attribuez : comparez view-source vs DOM rendu.

  • Vide dans la source : bug de données (champ vide en BDD / traduction manquante) ou bug template (variable non définie, override).

  • Vide uniquement après rendu : JS qui manipule le titre, composant qui remplace le contenu, ou erreur JS qui stoppe l’injection.

Pour aller vite sans “refonte” :

  • cherchez d’abord le fichier qui génère le <h1> (page-header.tpl, product.tpl, category.tpl) ;

  • ajoutez une condition de rendu (ne pas imprimer la balise si texte vide) ;

  • corrigez la donnée si le vide provient de la base (import/traduction) ;

  • vérifiez qu’aucun CSS/JS ne remplace le heading par un artefact (pseudo-élément, injection tardive, etc.).

Enfin, validez :
1) un crawl après purge caches,
2) un test e2e minimal en CI,
3) un contrôle éditorial sur les champs WYSIWYG (interdiction ou nettoyage des headings vides).

Si votre audit initial était lié à des symptômes plus larges de pages “creuses”, enchaînez avec une revue structurée des pages concernées : https://www.expertise-prestashop.fr/2026/07/22/page-vide-methodologie-danalyse-seo-et-reprise-editoriale-structuree/.



À lire aussi