Après la recherche locale (billet 52, Granulosearch), le miroir statique méritait la deuxième fonction que « tout le monde » délègue à des tiers : le partage. Les boutons de partage classiques sont l'un des pires mouchards du web : le simple affichage d'une page équipée d'un widget officiel déclenche des requêtes vers les plateformes — cookies, empreinte, mesure d'audience — avant tout clic du visiteur. Ce billet raconte l'inverse : un menu complet (cinq réseaux, copie de lien, QR code, e-mail, impression) qui ne charge rien d'externe et ne transmet rien à personne tant que le visiteur n'a pas cliqué.
Le secret bien gardé : partager n'exige aucun SDK
Chaque plateforme expose une URL d'intention : un simple lien qui ouvre sa boîte de partage pré-remplie. Il suffit de les construire soi-même :
https://www.linkedin.com/sharing/share-offsite/?url=<URL>
https://x.com/intent/post?url=<URL>&text=<TITRE>
https://social-plugins.line.me/lineit/share?url=<URL>
https://wa.me/?text=<TITRE + URL>
https://t.me/share/url?url=<URL>&text=<TITRE>
mailto:?subject=<TITRE>&body=<TITRE>%0D%0A%0D%0A<URL>
Tout est encodé par encodeURIComponent, construit localement à l'ouverture du menu, et ouvert dans un nouvel onglet avec rel="noopener noreferrer". La plateforme n'apprend l'existence du visiteur que s'il clique — c'est-à-dire exactement ce qu'il a demandé.
Un menu qui imite l'existant, sans framework
Plutôt qu'un composant de bibliothèque, le menu reprend pixel pour pixel le style du menu de langue déjà présent : fond blanc, bordure noire d'un pixel, angles droits, libellés en capitales. Le site mélangeant deux générations de Bootstrap (attributs data-toggle et data-bs-toggle selon les blocs), le JavaScript du menu est volontairement autonome : ouverture, fermeture, clavier (Échap, flèches), repli dans le viewport et modale QR tiennent en écouteurs d'événements natifs. Accessibilité comprise : role="menu", aria-expanded, retour du focus au déclencheur, et un retour de copie annoncé en aria-live="polite" — jamais d'alert().
Le QR code aussi se fabrique sur place
Le réflexe « API de QR code » (il en existe des dizaines) enverrait chaque URL consultée à un tiers — exactement ce qu'on refuse. La bibliothèque qrcode-generator → (MIT, ~55 Ko, zéro dépendance) dessine le QR en SVG inline, directement dans une modale. Choix doublement gagnant sous CSP stricte : pas de blob:, pas d'unsafe-eval, pas même de data: — la politique de sécurité du site n'a pas bougé d'une virgule.
Qui reçoit le bouton ? Le JSON-LD décide
Sur ~250 pages, pas question de listes manuelles. Chaque page du site porte déjà des données structurées ; ce sont elles qui pilotent : Product, Article, TechArticle, WebPage… reçoivent le bouton, les pages utilitaires (recherche, contact, mentions, remerciements, noindex) en sont exclues. Même logique que le périmètre par attributs du billet 52 : des règles sur les données, pas des listes à maintenir. Cerise contextuelle : sur une fiche produit, le champ model du JSON-LD alimente une entrée « copier la référence » — la référence n'est jamais devinée, seulement lue.
L'injection : une étape de pipeline, idempotente et prudente
Le bouton est injecté au build, par un post-traitement qui insère trois lignes de HTML après les icônes de contact du header — un point d'ancrage unique et identique sur toutes les pages — plus les liens CSS/JS s'ils manquent. Trois règles de survie : idempotence (rejouer l'étape ne crée jamais de doublon), mode SAFE (ancrage introuvable ou ambigu ⇒ page laissée intacte, avertissement journalisé), et un code de décision par page (ajouté, déjà présent, exclu, type non éligible…) qu'un audit statique agrège ensuite en un tableau lisible d'un coup d'œil.
Premier piège : l'éligibilité qui lit des données… pas encore écrites
Premier passage : zéro bouton, zéro erreur. L'audit montrait pourtant 200 pages éligibles. Cause : dans la chaîne de build, le JSON-LD est injecté tard — et l'étape de partage tournait avant. Elle lisait des pages sans données structurées, concluait « aucun type éligible » et passait son chemin, proprement, silencieusement. Moralité : quand une étape dépend d'un enrichissement, sa place dans la chaîne fait partie de sa spécification. L'injection vit désormais juste après le JSON-LD, avant l'indexation et la publication.
Deuxième piège : l'import Python sensible à la casse… même sous Windows
Deuxième passage : ModuleNotFoundError. Le module existait, au caractère près : le dossier du package portait une majuscule que l'import n'avait pas. Windows ouvre volontiers DOSSIER\fichier quelle que soit la casse, alors on croit l'import aussi accommodant — il ne l'est pas : l'importeur de Python vérifie la casse du nom, même sur un système de fichiers qui ne la distingue pas. Parade retenue, plus robuste que de renommer : charger le module par chemin de fichier via importlib.util.spec_from_file_location, en essayant les deux graphies, et afficher le chemin réellement chargé.
spec = importlib.util.spec_from_file_location("_share", chemin_reel)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod) # insensible à la casse du dossier, traçable
Troisième piège : publier, puis injecter
Quatrième piège : le cache de police et les icônes « tofu »
Les icônes viennent de la police d'icônes du site, sous-ensemble ne contenant que les glyphes réellement utilisés (deux à l'origine, treize après l'ajout du menu — la régénération part toujours d'une copie de la police complète, l'opération est rejouable à l'infini). Premier affichage après mise en ligne : des petits carrés avec un code hexadécimal à la place des icônes. Rien de cassé côté serveur — la cmap de la police en ligne contenait bien les treize glyphes — mais les polices sont servies avec un cache navigateur d'un an : le navigateur affichait l'ancienne, à deux glyphes. Un rechargement forcé (ou une fenêtre privée, seul vrai test « nouveau visiteur ») règle le cas individuel ; pour un site à fort trafic, on versionnerait le nom du fichier de police. En plein été, le pragmatisme l'a emporté.
Prouver la promesse
« Aucune requête externe » est une affirmation vérifiable, donc vérifiée : un test en navigateur piloté charge une fiche produit, ouvre le menu, copie le lien, génère le QR — pendant qu'un observateur enregistre toutes les requêtes réseau. La liste des domaines interdits (plateformes sociales, mesure d'audience) est explicite dans le test ; s'il en apparaît un seul au chargement ou à l'ouverture du menu, le test échoue. La confidentialité n'est plus un argument : c'est un test de non-régression. S'y ajoutent des tests unitaires d'encodage (accents, apostrophes, esperluettes, tirets longs dans les titres) exécutant le vrai code du menu sur des pages de fixture locales — sans serveur, donc insensibles aux aléas du réseau.
Bilan
Un menu de partage complet — cinq réseaux, copie, QR, e-mail, impression, référence produit — pour trois fichiers statiques d'environ 80 Ko au total, aucune modification de la politique de sécurité, et zéro octet transmis à des tiers avant un clic volontaire. Les quatre pièges du chemin (ordre des enrichissements, casse des imports Python, ordre de publication, cache de police) sont désormais documentés et testés. Prochaine étape envisagée : des liens courts locaux pour les partages — l'architecture les attend déjà, tout passe par un unique point d'extension qui préfère le lien court quand il existe.