Sujet déjà traité en 2025, sur cet article avec une solution complètement personnalisable mais overkill dans certains contextes, le sujet de l'obfuscation SEO est récurrent. En gros, rappel… parce que « tant va la cruche à l’eau qu’à la fin elle se casse ». En clair, à force de s’obstiner dans les mêmes erreurs, on en subit les conséquences (ce n'est pas de moi).
L'obfuscation des liens souvent demandée pour l'optimisation SEO pose de sérieux problèmes pour l'accessibilité.
Le remplacement de balises de liens par des span, button ou autres joyeusetés approximatives bloque des utilisateurs car les interactions ne sont plus gérées.
Exemple encore vu en ce mois de novembre 2026.
Légèrement agaçant.
<ul>
<li>
<button type="button" tabindex="0" data-href="aHR0cHMlM0ElMkYlMkZleGFtcGxlLmNvbQ==">
<div>
<i class="icon"></i>
</div>
<div>Le lien anonymisé</div>
</button>
</li>
[… et tout un tas qui suivent]
</ul>
Par charité, cet extrait de code est anonymisé. On pourra noter les jolis div dans des button…
Le tout est transformé en pseudo lien avec un event sur le bouton.
NE PAS FAIRE…
Exemples parfaits de ce qu'IL NE FAUT PAS FAIRE.
- Une agence « engagée ». Engagée probablement, mais pas sur l'inclusion. Article pourtant mis à jour le 4 juin 2026 avec cette merveilleuse affirmation, « […] rappelons que Google n’interprète pas le JS […] ». Ben tiens !
- Une agence canadienne. Ici, on peut lire que
<span class="obflink" data-o="aHR0cHM6Ly93d3cuZXhhbXBsZS5jb20=">Visiter</span>avec un bout de jQuery, « le lien est fonctionnel pour l’utilisateur sans être interprétable par les robots de Google ».
Raté… ce n'est absolument pas fonctionnel et Google voit un mot perdu, le contenu duspan. - Link Juice Optimizer, plugin WordPress, 5 000 installations, recommandé juste par le lien précédent. Trois de ses quatre modes cassent les liens.
<span data-loc="aHR0cHM6Ly…">avec un clic jQuery, un<span onclick="…">avec l'URL en clair, ou un<a>sanshref. Dans les trois cas, plus de rôlelinkni de focus clavier (WCAG 2.2 : 1.3.1, 2.1.1, 4.1.2).- Clic milieu, menu contextuel et Ctrl+clic ne fonctionnent plus. Le base64 est lisible en clair.
- Obfuscation des liens - Fasterize. Un outil SEO qui transforme les liens du footer côté serveur, à partir de sélecteurs CSS.
- Le
<a href="https://…" target="_blank">devient<span rel="aHR0cHM6Ly…" class="frzlinks">. - Plus de rôle
link, plus d'accès au clavier, plus de lien dans la liste des liens des lecteurs d'écran, plus de menu contextuel ni de clic milieu, et l'information d'ouverture dans un nouvel onglet disparaît (WCAG 2.2 : 1.3.1 Information et relations, 2.1.1 Clavier, 4.1.2 Nom, rôle et valeur). - L'attribut
relest détourné pour porter l'URL : il n'est pas valide sur unspan. - Le base64 est en clair, non inversé :
aHR0cHM6Ly(https://) saute aux yeux, donc aucune protection contre un outil qui cherche cette signature, ni contre un robot qui exécute le JavaScript. - Le tutoriel ne dit pas un mot de l'accessibilité. Même les « pros » de la performance web…
Par exemple, un lien, même s'il n'est pas codé avec un attributtarget="_blank", doit pouvoir s'ouvrir dans une nouvelle fenêtre (ou nouvel onglet) à la demande d'un utilisateur.
Comme quoi, avoir pignon sur rue n'est pas un gage de confiance.
- Le
Pour satisfaire tout le monde, une solution simple à mettre en œuvre est possible.
On peut faire…
Redirection interne
Simple mais demande de pouvoir accéder aux règles de redirection via htaccess et robots.txt.
Zéro dépendance JS, natif pour les technologies d’assistance, ouverture dans un nouvel onglet et menu contextuel intacts.
Le lien standard
<!-- rel="nofollow sponsored" selon le cas (affiliation, contenu payant) -->
<a href="/gomachin/partenaire" rel="nofollow sponsored">Libellé explicite</a>
Le robots.txt
# robots.txt : les robots ne suivent pas le chemin de redirection, normalement…
User-agent: *
Disallow: /gomachin/
Mais il faut que ce soit de bons gentils robots qui suivent les directives.
Lien interne et un script
Ce n'est pas très compliqué. Encoder le lien en base64 dans un attribut, le script s'occupe du reste.
Histoire de ne pas dire aux scrappers que c'est un lien masqué, autant ne pas mettre data-href. Ce ne sont pas des neuneus !
Principe
Le lien garde un href="#" : un <a> sans href n'est ni focusable au clavier ni exposé comme lien, alors que href="#" conserve le rôle link et l'ordre de tabulation. L'URL réelle est substituée au href à la première interaction.
Le base64 est stocké inversé (strrev côté PHP). Les débuts de chaîne classiques (aHR0cHM6Ly pour https://, bWFpbHRv pour mailto:, dGVsOi pour tel:) servent de signature aux outils qui cherchent du base64 dans le DOM. Inversés, ils passent en fin de chaîne et le padding = passe en tête : atob() ne fonctionne plus directement.
Le script écoute quatre événements, en délégation sur document (les liens injectés dynamiquement [Ajax] sont couverts) :
focusin: navigation au clavier ;pointeroverettouchstart: souris et tactile, avant le menu contextuel ou le clic milieu ;click: lecteurs d'écran en mode navigation, qui déclenchent un clic sans focus.
Deux variantes de balisage
data-osur le lien : liens écrits à la main ou générés par le back, hors classe PHP. La valeur est le base64 inversé de l'URL.data-o-rel+<template>: liens générés par la classe PHP. Le lien porte l'id d'un<template>qui contient l'URL (data-o) et le texte de révélation optionnel (data-t).
<!-- href="#" conserve le rôle link et le focus clavier ; data-o = URL en base64 inversé -->
<a href="#" data-o="=wWb0hmLlx2YpRnch9SbvNmLjlGdy92auc3d39yL6MHc0RHa">Libellé explicite</a>
Sortie de la classe PHP :
<span data-nosnippet>
<a href="#" data-o-rel="o3fa91c-1">Envoyer un courriel</a>
</span>
<template id="o3fa91c-1" data-o="0N3boxWYj9GbAJXdvpmbvJmOvRHbpFWb"></template>
Le <template> est valide en contenu phrasé, inerte, sans rendu et absent de l'arbre d'accessibilité. Un <meta> dans le body serait invalide sans itemprop. Le lien et son template sont reliés par l'id, l'adjacence dans le DOM n'est pas requise : le template est volontairement sorti du span, qui ne contient que le lien.
Le span data-nosnippet exclut le texte du lien des extraits de recherche de Google.
L'id combine un préfixe aléatoire par requête PHP et un compteur : il reste unique entre la page et les fragments HTML injectés en AJAX.
Le script
Une seule fonction gère les deux variantes. La source des données est soit le lien, soit le template désigné par data-o-rel.
Sécurité : si le base64 n'a pas été inversé, atob lève une erreur ou décode du binaire. Dans les deux cas, le script sort sans toucher au DOM, le lien reste sur href="#" et le template reste en place pour inspection.
data-t (optionnel) remplace le texte du lien, par exemple pour afficher un numéro à la place d'un libellé d'action. Cela modifie le nom accessible du lien au moment de l'interaction : à réserver aux cas où il reste cohérent avec le libellé initial.
À importer en pied de page, par exemple.
Taille brute : 1,93 kilo-octets (~1,04 kilo-octets gzip)
/**
* Obfuscation de liens accessibles (version light, deux variantes).
*
* - a[data-o] : URL (data-o) et texte optionnel (data-t) portés par le lien.
* - a[data-o-rel="id"] : mêmes données portées par le <template id="id">, où qu'il soit dans le document.
*
* Au premier signal d'interaction : href substitué, texte remplacé si data-t,
* puis données retirées pour ne muter le DOM qu'une fois.
*
* focusin : clavier ; pointerover / touchstart : souris et tactile (menu contextuel, clic milieu) ;
* click : lecteurs d'écran en mode navigation (clic sans focus, href lu après les handlers).
* Délégation sur document : couvre aussi les liens injectés dynamiquement.
*/
['focusin', 'pointerover', 'touchstart', 'click'].forEach(t =>
document.addEventListener(t, ({target}) => {
const a = target.closest?.('a[data-o], a[data-o-rel]');
// Source des données : <template> désigné par l'id de data-o-rel, ou le lien lui-même
const s = a?.hasAttribute('data-o-rel') ? document.getElementById(a.dataset.oRel) : a;
if (!s?.dataset.o) return;
// Base64 stocké à l'envers : remise à l'endroit avant atob ; chaîne vide si invalide
const d = v => {
try {
return atob([...v].reverse().join(''));
} catch {
return '';
}
};
const u = d(s.dataset.o);
// Sécurité : base64 non inversé = erreur levée ou binaire décodé ; on sort sans toucher au DOM
if (!/^[\x20-\x7e]+$/.test(u)) return;
a.href = u;
// atob renvoie une chaîne binaire : décodage UTF-8 explicite pour les caractères non ASCII
const x = s.dataset.t && d(s.dataset.t);
if (x) a.textContent = new TextDecoder().decode(Uint8Array.from(x, c => c.charCodeAt(0)));
if (s === a) {
delete a.dataset.o;
delete a.dataset.t;
} else {
s.remove();
a.removeAttribute('data-o-rel');
}
}, {passive: true, capture: true})
);
Télécharger kortic-obfuscation-light.js
La classe PHP
href et data-o-rel sont posés après les attributs fournis : ils ne sont pas surchargeables.
<?php
/**
* Liens obfusqués accessibles (version light).
*
* Sortie : <span data-nosnippet><a href="#" data-o-rel="o3fa91c-1">Libellé</a><template id="o3fa91c-1" data-o="base64"></template></span>
* Le href factice conserve le rôle link et le focus clavier ;
* 300_init_obfuscation-light.js substitue l'URL réelle à la première interaction.
* Le lien et son template sont reliés par un identifiant (data-o-rel = id du template) :
* l'adjacence dans le DOM n'est pas requise. L'id combine un préfixe aléatoire par requête
* et un compteur : unique entre la page et les fragments HTML injectés en AJAX.
*
* @example
* echo ObfuscationLight::link('Mon site', 'https://example.com');
* echo ObfuscationLight::link('Appeler le service client', 'tel:+33123456789', [], '01 23 45 67 89');
* echo ObfuscationLight::external('Partenaire', 'https://example.com', ['rel' => 'nofollow sponsored noopener']);
*/
final class ObfuscationLight
{
/**
* Lien externe : nouvel onglet, rel sécurisé.
* L'indication d'ouverture dans un nouvel onglet reste à la charge du libellé ou du script global.
*/
public static function external(string $label, string $url, array $attrs = [], ?string $reveal = null): string
{
return self::link($label, $url, array_merge(['target' => '_blank', 'rel' => 'noopener noreferrer'], $attrs), $reveal);
}
/**
* Lien obfusqué, enveloppé dans un span data-nosnippet.
*
* @param string $label Texte du lien, échappé (obligatoire)
* @param string $url URL cible (obligatoire), encodée en base64 en sortie ; ASCII : utiliser rawurlencode si besoin
* @param array $attrs Attributs HTML additionnels ; null/false = omis, true = attribut booléen
* @param string|null $reveal Texte affiché à la place du libellé après la première interaction (data-t, base64, porté par le template)
* @throws InvalidArgumentException Si label ou url vide
*/
public static function link(string $label, string $url, array $attrs = [], ?string $reveal = null): string
{
if ($label === '' || $url === '') {
throw new InvalidArgumentException('Label et URL ne peuvent pas être nuls');
}
// Préfixe aléatoire par requête + compteur : id uniques entre la page et les fragments AJAX
static $p, $n = 0;
$id = 'o' . ($p ??= bin2hex(random_bytes(3))) . '-' . ++$n;
$html = '';
// href et data-o-rel posés en dernier : non surchargeables
foreach (array_merge($attrs, ['href' => '#', 'data-o-rel' => $id]) as $name => $value) {
if ($value !== null && $value !== false) {
$html .= ' ' . $name . ($value === true ? '' : '="' . htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '"');
}
}
// Le template est hors du span data-nosnippet : seul le lien y reste et liaison par id via le script JS
return '<span data-nosnippet><a' . $html . '>' . htmlspecialchars($label, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</a></span>'
. '<template id="' . $id . '" data-o="' . strrev(base64_encode($url)) . '"' . ($reveal === null ? '' : ' data-t="' . strrev(base64_encode($reveal)) . '"') . '></template>';
}
}
Ce qui donne ça : Envoyez-moi un courriel.
Note : certains liens du pied de page et les liens vers les réseaux sociaux de Kortic sont gérés de cette manière.
Limites
Le base64, même inversé, n'est pas du chiffrement : c'est un durcissement léger, pas une protection. Il écarte les sélecteurs naïfs de type a[href^="mailto:"], les regex sur le HTML brut et les recherches de signatures base64. Il ne change rien face à un scraper qui exécute le JavaScript et simule un pointerover.
- Sans JavaScript, le lien reste sur
href="#": à ne pas utiliser pour un lien indispensable aux utilisateurs sans alternative. Mais bon, un site sans Javascript en 2026… - Un id dupliqué dans le document (même fragment mis en cache et injecté deux fois) fait pointer le script vers le premier template, donc vers le mauvais lien, sans erreur.
Conclusion
Faire plaisir au SEO, aucun problème ! Mais le SEO ne doit pas prendre le pas sur l'accessibilité. Personne n'a jamais reçu de plainte pour un SEO insuffisant, ce n'est pas une obligation légale. Pour l'accessibilité, oui : en Europe selon le périmètre de l'EAA, aux États-Unis via l'ADA, avec WCAG comme référence dans les deux cas.
That's all Folks !