J’ai eu besoin récemment de contrôler les attributs de certains liens affichés sur le blog, notamment ceux fournis par des sites tiers, des régies publicitaires, des sponsors ou des contenus embarqués.
Le problème est simple : certains fragments HTML arrivent avec des attributs inutiles, obsolètes, gênants ou franchement inadaptés. On peut trouver du style inline partout, des classes inutiles, des attributs propriétaires, ou encore des liens forcés en target="_blank".
Dans certains cas, cela casse la cohérence du site. Dans d’autres, cela perturbe la navigation, ouvre des fenêtres ou onglets supplémentaires sans raison, ou rend le code HTML plus lourd que nécessaire.
J’avais donc écrit un petit script PHP pour supprimer certains attributs de tags HTML. La première version utilisait une expression régulière. Elle avait le mérite d’être courte, mais elle mérite aujourd’hui une version plus robuste.
La première approche : supprimer des attributs avec une regex
Voici l’idée de départ : on définit une liste d’attributs à retirer, puis on parcourt le HTML pour supprimer ces attributs partout où ils apparaissent.
<?php
/**
* Clean up unwanted HTML attributes from a string.
*
* @param string $source Source HTML.
* @return string Cleaned HTML.
*/
function sky_cleanup_attributes( string $source ): string {
$remove = array( 'style', 'class', 'target', 'someattribute' );
$clean_string = $source;
foreach ( $remove as $attribute ) {
$clean_string = preg_replace(
'!\s+' . preg_quote( $attribute, '!' ) . '=("|\')?[-_():;a-z0-9 ]+("|\')?!i',
'',
$clean_string
);
}
return is_string( $clean_string ) ? $clean_string : $source;
}Code language: PHP (php)
On peut ensuite l’utiliser sur un fragment HTML :
<?php
$html = '<span style="font-weight:bold;background:red;">this span has a style attribute</span>
<div class="noborder">this div has a class attribute</div>
<div class="leftclass redclass">this div has two classes applied in one attribute</div>
<a href="https://www.skyminds.net/?p=2523" target="_blank" rel="noopener">Sky Cleanup Attributes by Matt</a>';
$cleaned = sky_cleanup_attributes( $html );
echo '<textarea style="width: 100%; height: 200px;">' . htmlentities( $cleaned, ENT_QUOTES, 'UTF-8' ) . '</textarea>';Code language: HTML, XML (xml)
Cette approche peut suffire pour un contenu très contrôlé. Cependant, elle reste fragile.
Pourquoi ? Parce que HTML n’est pas un format régulier simple. Les attributs peuvent être dans un ordre variable, contenir des apostrophes, des guillemets, des espaces, des URLs, des caractères encodés, des valeurs vides, ou même être booléens. Une regex trop stricte rate des cas. Une regex trop large supprime trop de choses. Bref, le bonheur habituel.
Pourquoi éviter les regex pour nettoyer du HTML
Une expression régulière peut marcher sur un exemple simple, mais elle devient risquée dès que le HTML vient de sources variées.
Voici quelques cas qui peuvent casser une regex naïve :
- un attribut sans guillemets ;
- une URL contenant des paramètres ;
- une valeur contenant des deux-points, des slashs ou des caractères encodés ;
- un attribut vide comme
disabled; - un attribut sur plusieurs lignes ;
- du HTML incomplet mais accepté par le navigateur ;
- des attributs
data-*ouaria-*à préserver.
Pour une démo ou une opération ponctuelle sur du HTML très simple, la regex reste acceptable. Pour du code réutilisable, je préfère utiliser un vrai parseur HTML.
Solution moderne : utiliser DOMDocument
PHP fournit DOMDocument, qui permet de charger du HTML, de parcourir les éléments, puis de supprimer des attributs proprement.
La méthode DOMElement::removeAttribute() sert précisément à supprimer un attribut sur un élément DOM. Elle est disponible dans PHP 5, PHP 7 et PHP 8. Documentation PHP : DOMElement::removeAttribute()
Voici une version plus robuste de la fonction :
<?php
declare(strict_types=1);
/**
* Remove unwanted attributes from an HTML fragment.
*
* This function parses the HTML with DOMDocument instead of relying on regex.
* It is safer for real-world HTML where attributes may contain complex values.
*
* @param string $html Source HTML fragment.
* @param string[] $attributes_remove Attribute names to remove.
* @return string Cleaned HTML fragment.
*/
function sky_cleanup_html_attributes( string $html, array $attributes_remove = array() ): string {
if ( '' === trim( $html ) ) {
return $html;
}
$attributes_remove = array_map(
static fn ( string $attribute ): string => strtolower( trim( $attribute ) ),
$attributes_remove
);
$attributes_remove = array_filter( $attributes_remove );
if ( array() === $attributes_remove ) {
return $html;
}
$dom = new DOMDocument();
$previous = libxml_use_internal_errors( true );
$wrapped_html = '<div id="sky-html-wrapper">' . $html . '</div>';
$dom->loadHTML(
'<!doctype html><html><body>' . $wrapped_html . '</body></html>',
LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
);
libxml_clear_errors();
libxml_use_internal_errors( $previous );
$elements = $dom->getElementsByTagName( '*' );
foreach ( $elements as $element ) {
if ( ! $element instanceof DOMElement ) {
continue;
}
foreach ( $attributes_remove as $attribute ) {
if ( $element->hasAttribute( $attribute ) ) {
$element->removeAttribute( $attribute );
}
}
}
$wrapper = $dom->getElementById( 'sky-html-wrapper' );
if ( ! $wrapper instanceof DOMElement ) {
return $html;
}
$clean_html = '';
foreach ( $wrapper->childNodes as $child ) {
$clean_html .= $dom->saveHTML( $child );
}
return $clean_html;
}Code language: HTML, XML (xml)
On peut ensuite appeler la fonction ainsi :
<?php
$html = '<span style="font-weight:bold;background:red;">this span has a style attribute</span>
<div class="noborder">this div has a class attribute</div>
<a href="https://www.skyminds.net/?p=2523" target="_blank" rel="noopener">Sky Cleanup Attributes by Matt</a>';
$cleaned = sky_cleanup_html_attributes(
$html,
array( 'style', 'class', 'target' )
);
echo htmlentities( $cleaned, ENT_QUOTES, 'UTF-8' );Code language: HTML, XML (xml)
Résultat attendu :
<span>this span has a style attribute</span>
<div>this div has a class attribute</div>
<a href="https://www.skyminds.net/?p=2523" rel="noopener">Sky Cleanup Attributes by Matt</a>Code language: HTML, XML (xml)
Supprimer un attribut seulement sur certaines balises
Dans bien des cas, on ne veut pas supprimer un attribut partout. Par exemple, on peut vouloir retirer target uniquement sur les liens, mais conserver d’autres attributs ailleurs.
Voici une version qui applique une liste d’attributs par balise HTML :
<?php
declare(strict_types=1);
/**
* Remove unwanted attributes from specific HTML tags.
*
* Example:
* array(
* 'a' => array( 'target', 'style' ),
* 'div' => array( 'class', 'style' ),
* )
*
* @param string $html Source HTML fragment.
* @param array<string,array> $rules Attributes to remove, grouped by tag name.
* @return string Cleaned HTML fragment.
*/
function sky_cleanup_html_attributes_by_tag( string $html, array $rules ): string {
if ( '' === trim( $html ) || array() === $rules ) {
return $html;
}
$normalised_rules = array();
foreach ( $rules as $tag_name => $attributes ) {
if ( ! is_string( $tag_name ) || ! is_array( $attributes ) ) {
continue;
}
$tag_name = strtolower( trim( $tag_name ) );
if ( '' === $tag_name ) {
continue;
}
$normalised_rules[ $tag_name ] = array_filter(
array_map(
static fn ( string $attribute ): string => strtolower( trim( $attribute ) ),
$attributes
)
);
}
if ( array() === $normalised_rules ) {
return $html;
}
$dom = new DOMDocument();
$previous = libxml_use_internal_errors( true );
$dom->loadHTML(
'<!doctype html><html><body><div id="sky-html-wrapper">' . $html . '</div></body></html>',
LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
);
libxml_clear_errors();
libxml_use_internal_errors( $previous );
foreach ( $normalised_rules as $tag_name => $attributes ) {
$elements = $dom->getElementsByTagName( $tag_name );
foreach ( $elements as $element ) {
if ( ! $element instanceof DOMElement ) {
continue;
}
foreach ( $attributes as $attribute ) {
if ( $element->hasAttribute( $attribute ) ) {
$element->removeAttribute( $attribute );
}
}
}
}
$wrapper = $dom->getElementById( 'sky-html-wrapper' );
if ( ! $wrapper instanceof DOMElement ) {
return $html;
}
$clean_html = '';
foreach ( $wrapper->childNodes as $child ) {
$clean_html .= $dom->saveHTML( $child );
}
return $clean_html;
}Code language: HTML, XML (xml)
Exemple d’utilisation :
<?php
$cleaned = sky_cleanup_html_attributes_by_tag(
$html,
array(
'a' => array( 'target', 'style' ),
'span' => array( 'style' ),
'div' => array( 'class', 'style' ),
)
);Code language: HTML, XML (xml)
Cette approche évite de nettoyer trop large. Et en nettoyage HTML, nettoyer trop large finit souvent par casser quelque chose qui n’avait rien demandé.
Modifier target=”_blank” au lieu de le supprimer
Supprimer target="_blank" peut être souhaitable si l’on veut éviter l’ouverture de nouveaux onglets. Mais si vous décidez de le conserver, il faut au minimum ajouter rel="noopener".
L’attribut rel="noopener" empêche la page ouverte d’accéder au document d’origine via window.opener. C’est particulièrement utile pour les liens externes non fiables. MDN : rel=”noopener”
Voici une fonction qui ajoute noopener aux liens qui ont un target="_blank", sans écraser les valeurs rel existantes :
<?php
declare(strict_types=1);
/**
* Add rel="noopener" to links using target="_blank".
*
* Existing rel values are preserved.
*
* @param string $html Source HTML fragment.
* @return string Updated HTML fragment.
*/
function sky_add_noopener_to_blank_links( string $html ): string {
if ( '' === trim( $html ) ) {
return $html;
}
$dom = new DOMDocument();
$previous = libxml_use_internal_errors( true );
$dom->loadHTML(
'<!doctype html><html><body><div id="sky-html-wrapper">' . $html . '</div></body></html>',
LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
);
libxml_clear_errors();
libxml_use_internal_errors( $previous );
$links = $dom->getElementsByTagName( 'a' );
foreach ( $links as $link ) {
if ( ! $link instanceof DOMElement ) {
continue;
}
if ( '_blank' !== strtolower( $link->getAttribute( 'target' ) ) ) {
continue;
}
$rel_values = preg_split( '/\s+/', strtolower( $link->getAttribute( 'rel' ) ) ) ?: array();
$rel_values = array_filter( $rel_values );
if ( ! in_array( 'noopener', $rel_values, true ) ) {
$rel_values[] = 'noopener';
}
$link->setAttribute( 'rel', implode( ' ', array_unique( $rel_values ) ) );
}
$wrapper = $dom->getElementById( 'sky-html-wrapper' );
if ( ! $wrapper instanceof DOMElement ) {
return $html;
}
$updated_html = '';
foreach ( $wrapper->childNodes as $child ) {
$updated_html .= $dom->saveHTML( $child );
}
return $updated_html;
}Code language: HTML, XML (xml)
Cette approche est souvent meilleure que de supprimer brutalement target, surtout si l’ouverture dans un nouvel onglet est volontaire.
Utiliser une allowlist plutôt qu’une blocklist
Une autre stratégie consiste à ne garder que certains attributs autorisés. C’est souvent plus sûr quand le HTML vient d’une source externe.
Au lieu de dire “je supprime style, class et target”, on dit plutôt : “sur les liens, je garde seulement href, title et rel”.
<?php
declare(strict_types=1);
/**
* Keep only allowed attributes on selected HTML tags.
*
* @param string $html Source HTML fragment.
* @param array<string,array> $allowed_rules Allowed attributes, grouped by tag name.
* @return string Cleaned HTML fragment.
*/
function sky_keep_allowed_html_attributes( string $html, array $allowed_rules ): string {
if ( '' === trim( $html ) || array() === $allowed_rules ) {
return $html;
}
$normalised_rules = array();
foreach ( $allowed_rules as $tag_name => $attributes ) {
if ( ! is_string( $tag_name ) || ! is_array( $attributes ) ) {
continue;
}
$normalised_rules[ strtolower( trim( $tag_name ) ) ] = array_filter(
array_map(
static fn ( string $attribute ): string => strtolower( trim( $attribute ) ),
$attributes
)
);
}
$dom = new DOMDocument();
$previous = libxml_use_internal_errors( true );
$dom->loadHTML(
'<!doctype html><html><body><div id="sky-html-wrapper">' . $html . '</div></body></html>',
LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
);
libxml_clear_errors();
libxml_use_internal_errors( $previous );
foreach ( $normalised_rules as $tag_name => $allowed_attributes ) {
$elements = $dom->getElementsByTagName( $tag_name );
foreach ( $elements as $element ) {
if ( ! $element instanceof DOMElement || ! $element->hasAttributes() ) {
continue;
}
$attributes_to_remove = array();
foreach ( $element->attributes as $attribute ) {
$attribute_name = strtolower( $attribute->nodeName );
if ( ! in_array( $attribute_name, $allowed_attributes, true ) ) {
$attributes_to_remove[] = $attribute_name;
}
}
foreach ( $attributes_to_remove as $attribute_name ) {
$element->removeAttribute( $attribute_name );
}
}
}
$wrapper = $dom->getElementById( 'sky-html-wrapper' );
if ( ! $wrapper instanceof DOMElement ) {
return $html;
}
$clean_html = '';
foreach ( $wrapper->childNodes as $child ) {
$clean_html .= $dom->saveHTML( $child );
}
return $clean_html;
}Code language: HTML, XML (xml)
Exemple :
<?php
$cleaned = sky_keep_allowed_html_attributes(
$html,
array(
'a' => array( 'href', 'title', 'rel' ),
'img' => array( 'src', 'alt', 'width', 'height', 'loading', 'decoding' ),
'span' => array(),
'div' => array(),
)
);Code language: HTML, XML (xml)
Cette logique est plus stricte. Elle est donc plus adaptée si le HTML vient d’un éditeur tiers, d’un flux externe, d’un import, d’un widget publicitaire ou d’une ancienne base de contenu.
Cas WordPress : utiliser wp_kses()
Dans WordPress, il existe déjà une fonction conçue pour filtrer le HTML autorisé : wp_kses(). Elle permet de définir les balises et attributs autorisés, puis elle supprime le reste. La documentation WordPress précise que wp_kses() s’assure que seuls les éléments HTML, attributs, valeurs d’attributs et entités autorisés restent dans la chaîne filtrée. Documentation WordPress : wp_kses()
Exemple simple :
<?php
/**
* Clean sponsor HTML with a strict allowlist.
*
* @param string $html Sponsor HTML.
* @return string Sanitised HTML.
*/
function sky_clean_sponsor_html( string $html ): string {
$allowed_html = array(
'a' => array(
'href' => true,
'title' => true,
'rel' => true,
),
'span' => array(),
'strong' => array(),
'em' => array(),
);
return wp_kses( $html, $allowed_html );
}Code language: HTML, XML (xml)
Avec cette configuration, WordPress conserve les liens, certains attributs utiles, quelques balises de mise en forme, puis retire le reste.
Pour partir des règles autorisées dans le contenu d’un article, WordPress fournit aussi wp_kses_allowed_html(). Cette fonction retourne les balises et attributs autorisés pour un contexte donné, comme post. Documentation WordPress : wp_kses_allowed_html()
<?php
$allowed_html = wp_kses_allowed_html( 'post' );Code language: HTML, XML (xml)
Ensuite, on peut modifier cette liste selon ses besoins.
Cas WordPress : supprimer target des liens du contenu
Si vous voulez supprimer target dans le contenu des articles, vous pouvez filtrer the_content. Le code suivant utilise DOMDocument et cible uniquement les liens.
<?php
/**
* Plugin Name: Sky Remove Target Blank From Content
* Description: Removes target attributes from links in post content.
* Author: Matt Biscay
* Version: 1.0.0
*/
declare(strict_types=1);
defined( 'ABSPATH' ) || exit;
add_filter( 'the_content', 'sky_remove_link_targets_from_content', 20 );
/**
* Remove target attributes from links in post content.
*
* @param string $content Post content.
* @return string Filtered post content.
*/
function sky_remove_link_targets_from_content( string $content ): string {
if ( ! str_contains( $content, 'target=' ) ) {
return $content;
}
return sky_cleanup_html_attributes_by_tag(
$content,
array(
'a' => array( 'target' ),
)
);
}Code language: HTML, XML (xml)
Ce snippet suppose que la fonction sky_cleanup_html_attributes_by_tag(), présentée plus haut, est disponible dans votre plugin ou votre fichier de fonctions.
Dans un vrai site WordPress, je placerais ce genre de code dans un petit plugin dédié ou dans un mu-plugin, plutôt que dans le fichier functions.php du thème. C’est plus portable, et cela évite de perdre le comportement lors d’un changement de thème.
Conserver target=”_blank” mais ajouter rel=”noopener”
Si votre objectif est surtout de sécuriser les liens qui s’ouvrent dans un nouvel onglet, il vaut mieux conserver target="_blank" et ajouter rel="noopener".
WordPress a d’ailleurs travaillé sur ce sujet dans le cœur, avec l’ajout automatique de rel="noopener" sur les liens en target="_blank". WordPress Core Trac : rel noopener et target blank
Pour un contenu personnalisé, vous pouvez appliquer la fonction présentée plus haut :
<?php
$content = sky_add_noopener_to_blank_links( $content );Code language: HTML, XML (xml)
C’est souvent une meilleure stratégie que de supprimer systématiquement tous les target.
Nettoyer les styles inline
Les attributs style sont souvent les premiers à supprimer. Ils arrivent fréquemment depuis des éditeurs WYSIWYG, des exports HTML, des newsletters, des widgets ou des contenus copiés depuis Word.
Pour les retirer partout :
<?php
$cleaned = sky_cleanup_html_attributes(
$html,
array( 'style' )
);Code language: HTML, XML (xml)
Pour les retirer uniquement de certaines balises :
<?php
$cleaned = sky_cleanup_html_attributes_by_tag(
$html,
array(
'span' => array( 'style' ),
'div' => array( 'style' ),
'p' => array( 'style' ),
)
);Code language: HTML, XML (xml)
C’est très utile lors d’une migration de contenu, notamment quand l’ancien site a stocké des styles directement dans les articles.
Supprimer les classes, sauf certaines
Supprimer toutes les classes peut être utile, mais parfois trop brutal. On peut vouloir conserver certaines classes nécessaires, par exemple alignleft, alignright, aligncenter ou des classes Gutenberg.
Dans ce cas, il vaut mieux nettoyer la valeur de class plutôt que supprimer l’attribut entier.
<?php
declare(strict_types=1);
/**
* Keep only selected CSS classes in an HTML fragment.
*
* @param string $html Source HTML fragment.
* @param string[] $allowed_classes Allowed CSS class names.
* @return string Cleaned HTML fragment.
*/
function sky_keep_allowed_classes( string $html, array $allowed_classes ): string {
if ( '' === trim( $html ) ) {
return $html;
}
$allowed_classes = array_filter(
array_map(
static fn ( string $class_name ): string => strtolower( trim( $class_name ) ),
$allowed_classes
)
);
if ( array() === $allowed_classes ) {
return sky_cleanup_html_attributes( $html, array( 'class' ) );
}
$dom = new DOMDocument();
$previous = libxml_use_internal_errors( true );
$dom->loadHTML(
'<!doctype html><html><body><div id="sky-html-wrapper">' . $html . '</div></body></html>',
LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
);
libxml_clear_errors();
libxml_use_internal_errors( $previous );
$elements = $dom->getElementsByTagName( '*' );
foreach ( $elements as $element ) {
if ( ! $element instanceof DOMElement || ! $element->hasAttribute( 'class' ) ) {
continue;
}
$classes = preg_split( '/\s+/', strtolower( $element->getAttribute( 'class' ) ) ) ?: array();
$classes = array_values(
array_intersect(
array_filter( $classes ),
$allowed_classes
)
);
if ( array() === $classes ) {
$element->removeAttribute( 'class' );
continue;
}
$element->setAttribute( 'class', implode( ' ', $classes ) );
}
$wrapper = $dom->getElementById( 'sky-html-wrapper' );
if ( ! $wrapper instanceof DOMElement ) {
return $html;
}
$clean_html = '';
foreach ( $wrapper->childNodes as $child ) {
$clean_html .= $dom->saveHTML( $child );
}
return $clean_html;
}Code language: HTML, XML (xml)
Exemple :
<?php
$cleaned = sky_keep_allowed_classes(
$html,
array( 'alignleft', 'alignright', 'aligncenter', 'wp-block-image' )
);Code language: HTML, XML (xml)
Cette fonction retire les classes inconnues, mais conserve celles dont vous avez réellement besoin.
Attention aux attributs data-* et aria-*
Avant de supprimer des attributs en masse, attention aux attributs data-* et aria-*.
Les attributs data-* servent souvent à stocker des informations utilisées par JavaScript. Les supprimer peut casser un slider, une modale, un tracking, un accordéon, ou un composant interactif.
Les attributs aria-*, eux, sont liés à l’accessibilité. Les retirer sans réfléchir peut dégrader l’expérience des lecteurs d’écran.
En règle générale, je ne supprime jamais data-* et aria-* globalement. Je les traite uniquement si je sais exactement d’où ils viennent et à quoi ils servent.
Mémo rapide
// Supprimer des attributs partout.
$cleaned = sky_cleanup_html_attributes(
$html,
array( 'style', 'class', 'target' )
);
// Supprimer des attributs selon la balise.
$cleaned = sky_cleanup_html_attributes_by_tag(
$html,
array(
'a' => array( 'target', 'style' ),
'div' => array( 'class', 'style' ),
)
);
// Garder uniquement certains attributs.
$cleaned = sky_keep_allowed_html_attributes(
$html,
array(
'a' => array( 'href', 'title', 'rel' ),
'img' => array( 'src', 'alt', 'width', 'height' ),
)
);
// Ajouter rel="noopener" aux liens target="_blank".
$cleaned = sky_add_noopener_to_blank_links( $html );
// Nettoyer avec WordPress.
$cleaned = wp_kses( $html, $allowed_html );Code language: PHP (php)
Conclusion
Pour supprimer quelques attributs sur un fragment HTML très simple, une expression régulière peut dépanner. Mais pour un script fiable, maintenable et réutilisable, mieux vaut parser le HTML avec DOMDocument.
Si vous travaillez dans WordPress, wp_kses() reste souvent le meilleur outil, surtout si votre objectif est de définir précisément les balises et attributs autorisés.
Enfin, ne supprimez pas les attributs à l’aveugle. style et certaines classes inutiles peuvent souvent disparaître sans drame. En revanche, data-*, aria-*, rel, srcset, sizes ou loading peuvent être utiles, voire essentiels.
Comme toujours avec le nettoyage HTML, le bon objectif n’est pas de tout raser. C’est de retirer le bazar sans casser la maison.
Vous imaginez un projet WordPress ou WooCommerce ? Je vous accompagne à chaque étape pour concrétiser vos ambitions, avec rigueur et transparence.
