<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Blog on Zwindler's Reflection</title><link>https://blog.zwindler.fr/tags/blog/</link><description>Recent content in Blog on Zwindler's Reflection</description><generator>Hugo -- gohugo.io</generator><language>fr</language><copyright>Licensed under CC BY-SA 4.0</copyright><lastBuildDate>Fri, 13 Mar 2026 20:00:00 +0100</lastBuildDate><atom:link href="https://blog.zwindler.fr/tags/blog/index.xml" rel="self" type="application/rss+xml"/><item><title>Refonte de la page Conférences : data-driven avec Hugo</title><link>https://blog.zwindler.fr/2026/03/13/refonte-page-conferences-hugo/</link><pubDate>Fri, 13 Mar 2026 20:00:00 +0100</pubDate><guid>https://blog.zwindler.fr/2026/03/13/refonte-page-conferences-hugo/</guid><description>&lt;img src="https://blog.zwindler.fr/2026/03/conferences-apres.webp" alt="Featured image of post Refonte de la page Conférences : data-driven avec Hugo" /&gt;&lt;p&gt;Grosse refonte de ma page &amp;ldquo;Conférences&amp;rdquo; qui était un bazar sans nom, dans lequel j&amp;rsquo;avais aussi mélangé les podcasts, les publications écrites, etc. Maintenant tout est présenté avec des &amp;ldquo;cartes&amp;rdquo;, et bien séparé en 3 pages distinctes.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Tous les fichiers sources (YAML, layouts Hugo, CSS) sont disponibles dans &lt;a class="link" href="https://blog.zwindler.fr/misc/conferences-refonte/" &gt;/misc/conferences-refonte/&lt;/a&gt; si vous voulez reproduire ou vous en inspirer.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="le-problème"&gt;Le problème
&lt;/h2&gt;&lt;p&gt;Ma page &lt;a class="link" href="https://blog.zwindler.fr/conf%c3%a9rences/" &gt;Conférences&lt;/a&gt; était un &lt;strong&gt;gros fichier Markdown&lt;/strong&gt; monolithique. Talks, podcasts, publications, orga : tout mélangé dans des listes à puces, avec des doublons, et &lt;strong&gt;de plus en plus pénible à maintenir&lt;/strong&gt; (surtout depuis le passage en multi-langue FR/EN).&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/03/conferences-avant.webp"
loading="lazy"
alt="La page Conférences avant la refonte - un long fichier Markdown avec des listes à puces"
&gt;&lt;/p&gt;
&lt;h2 id="lapproche--données--layouts--css"&gt;L&amp;rsquo;approche : données + layouts + CSS
&lt;/h2&gt;&lt;p&gt;Plutôt que de continuer à maintenir du Markdown, je suis passé en &lt;strong&gt;data-driven&lt;/strong&gt;, comme je l&amp;rsquo;ai fait pour mon side project &amp;ldquo;101 ways to deploy Kubernetes&amp;rdquo; :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Données&lt;/strong&gt; dans des fichiers YAML (&lt;code&gt;data/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Présentation&lt;/strong&gt; dans des layouts Hugo custom (&lt;code&gt;layouts/page/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Style&lt;/strong&gt; dans des CSS dédiés (&lt;code&gt;static/css/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pages Markdown&lt;/strong&gt; réduites au strict minimum (front matter uniquement)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Et au passage, j&amp;rsquo;ai découpé &lt;strong&gt;une page fourre-tout en trois&lt;/strong&gt; : Conférences, Podcasts &amp;amp; Lives, Publications.&lt;/p&gt;
&lt;h2 id="les-données-yaml"&gt;Les données YAML
&lt;/h2&gt;&lt;h3 id="dataconferencesyaml"&gt;&lt;code&gt;data/conferences.yaml&lt;/code&gt;
&lt;/h3&gt;&lt;p&gt;Le plus gros fichier. L&amp;rsquo;idée clé : un &lt;strong&gt;talk&lt;/strong&gt; (sujet unique) peut être présenté à &lt;strong&gt;plusieurs events&lt;/strong&gt;. Fini la duplication.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;talks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;k8s-scheduling&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Limits, Requests, QoS, PriorityClasses, ...&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Limits, Requests, QoS, PriorityClasses: ...&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;cospeaker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Quentin Joly&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;slides&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;/talks/2025-lrqppobcqvpsslsdk/...&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;talk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;k8s-scheduling &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# référence l&amp;#39;id du talk&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;DevoxxFR 2026&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ld"&gt;2026-03-22&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;conference&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;talk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;k8s-scheduling&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;TNT 26&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ld"&gt;2026-02-12&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;conference&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;video&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;https://www.youtube.com/watch?v=...&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;organizer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Membre de l&amp;#39;équipe d&amp;#39;organisation du Meetup CNCF Bordeaux&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Les titres sont bilingues (FR/EN), Hugo sélectionne la bonne langue automatiquement. Les fichiers &lt;code&gt;data/podcasts.yaml&lt;/code&gt; et &lt;code&gt;data/publications.yaml&lt;/code&gt; suivent le même principe (voir les &lt;a class="link" href="https://blog.zwindler.fr/misc/conferences-refonte/" &gt;fichiers sources&lt;/a&gt;).&lt;/p&gt;
&lt;h2 id="les-pages-markdown-minimalistes"&gt;Les pages Markdown minimalistes
&lt;/h2&gt;&lt;p&gt;L&amp;rsquo;ancienne page &lt;code&gt;content/page/conferences.md&lt;/code&gt; faisait 100+ lignes avec le contenu complet. Voici la nouvelle :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nn"&gt;---&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Conférences&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;authors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;zwindler&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;page&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;conferences&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ld"&gt;2022-03-14T18:00:00&lt;/span&gt;&lt;span class="m"&gt;+00&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="m"&gt;00&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;menu&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&lt;span class="m"&gt;40&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;messages&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;toc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nn"&gt;---&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Le point important, c&amp;rsquo;est &lt;code&gt;layout: conferences&lt;/code&gt; dit à Hugo d&amp;rsquo;utiliser &lt;code&gt;layouts/page/conferences.html&lt;/code&gt; au lieu du layout par défaut. Même chose pour &lt;code&gt;podcasts&lt;/code&gt; et &lt;code&gt;publications&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="le-layout-hugo"&gt;Le layout Hugo
&lt;/h2&gt;&lt;p&gt;Le layout charge les données YAML, calcule des stats, et génère du HTML. Si vous avez déjà manipulé du GoTemplate (helm notamment), ça vous semblera facile à comprendre.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Chargement et indexation des talks :&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go-html-template" data-lang="go-html-template"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;.Site.Data.conferences&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$talks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dict&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$data&lt;/span&gt;&lt;span class="na"&gt;.talks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$talks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;merge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$talks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dict&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;.id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;.&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Compteur dynamique&lt;/strong&gt; ex. combien de fois un talk a été présenté :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go-html-template" data-lang="go-html-template"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$count&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$talkId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;.id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$data&lt;/span&gt;&lt;span class="na"&gt;.events&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;.talk&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$talkId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$count&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$count&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;talk-card-count&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Présenté &lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$count&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;x&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Événements groupés par année&lt;/strong&gt;, les 2 plus récentes dépliées, le reste en archive pliable via &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go-html-template" data-lang="go-html-template"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$i&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$year&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$sortedYears&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;lt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;details&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;year-group&amp;#34;&lt;/span&gt; &lt;span class="na"&gt;open&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;details&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;year-group&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Les événements &lt;strong&gt;à venir&lt;/strong&gt; (date dans le futur) reçoivent automatiquement un badge &amp;ldquo;à venir&amp;rdquo;. Les layouts complets sont dans les &lt;a class="link" href="https://blog.zwindler.fr/misc/conferences-refonte/" &gt;fichiers sources&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="le-css--des-cartes-partout"&gt;Le CSS : des cartes partout
&lt;/h2&gt;&lt;p&gt;Le design repose sur des &lt;strong&gt;cartes&lt;/strong&gt; CSS grid, avec des badges colorés par type (conf, meetup, BBL), du responsive, et un support dark mode. L&amp;rsquo;essentiel :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-css" data-lang="css"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;talks-grid&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;auto&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="kc"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;minmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;280&lt;/span&gt;&lt;span class="kt"&gt;px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="kt"&gt;rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;talk-card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;background&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="kt"&gt;px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="kt"&gt;rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;border-color&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="kt"&gt;s&lt;/span&gt; &lt;span class="kc"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;box-shadow&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="kt"&gt;s&lt;/span&gt; &lt;span class="kc"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;type-conference&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#dbeafe&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#1e40af&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;type-meetup&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#dcfce7&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#166534&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;type-bbl&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#fef9c3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#854d0e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;type-upcoming&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#fee2e2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#991b1b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Les CSS complets gèrent aussi le dark mode et les breakpoints mobile. Ils sont dans les &lt;a class="link" href="https://blog.zwindler.fr/misc/conferences-refonte/" &gt;fichiers sources&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="bilan"&gt;Bilan
&lt;/h2&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Avant&lt;/th&gt;
&lt;th&gt;Après&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pages&lt;/td&gt;
&lt;td&gt;1 fourre-tout&lt;/td&gt;
&lt;td&gt;3 spécialisées&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Données&lt;/td&gt;
&lt;td&gt;Markdown libre&lt;/td&gt;
&lt;td&gt;YAML structuré&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplication&lt;/td&gt;
&lt;td&gt;Oui&lt;/td&gt;
&lt;td&gt;Non (relation talk → events)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stats&lt;/td&gt;
&lt;td&gt;Aucune&lt;/td&gt;
&lt;td&gt;Automatiques&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design&lt;/td&gt;
&lt;td&gt;Listes à puces&lt;/td&gt;
&lt;td&gt;Cartes + badges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-langue&lt;/td&gt;
&lt;td&gt;Copier-coller&lt;/td&gt;
&lt;td&gt;Titres FR/EN dans le YAML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Archives&lt;/td&gt;
&lt;td&gt;Tout d&amp;rsquo;un bloc&lt;/td&gt;
&lt;td&gt;Années pliables&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;C&amp;rsquo;était un gros morceau : 19 fichiers et ~2400 lignes (une grosse partie du taf, en particulier la traduction markdown =&amp;gt; YAML et le CSS, a été faite par un LLM), mais la logique est simple : &lt;strong&gt;données dans YAML, présentation dans les layouts, style dans le CSS&lt;/strong&gt;. Hugo fait le lien au build, sans JavaScript.&lt;/p&gt;
&lt;p&gt;Pour ajouter un nouveau talk maintenant, il me suffit d&amp;rsquo;ajouter une entrée dans &lt;code&gt;data/conferences.yaml&lt;/code&gt; et les stats se mettent à jour toutes seules.&lt;/p&gt;
&lt;p&gt;Avouez que c&amp;rsquo;est quand même plus propre qu&amp;rsquo;un copier-coller dans plusieurs fichiers Markdown :).&lt;/p&gt;</description></item><item><title>De F à A+ sur HTTP Observatory : sécuriser les headers de mon blog Hugo</title><link>https://blog.zwindler.fr/2026/02/20/securite-headers-http-observatory-hugo/</link><pubDate>Sat, 21 Feb 2026 10:00:00 +0200</pubDate><guid>https://blog.zwindler.fr/2026/02/20/securite-headers-http-observatory-hugo/</guid><description>&lt;img src="https://blog.zwindler.fr/2026/02/observatory-a-plus.webp" alt="Featured image of post De F à A+ sur HTTP Observatory : sécuriser les headers de mon blog Hugo" /&gt;&lt;p&gt;Après &lt;a class="link" href="https://blog.zwindler.fr/2026/02/19/optimisation-webperf-avif-precompression/" &gt;l&amp;rsquo;optimisation des performances du blog&lt;/a&gt; (AVIF, pré-compression), j&amp;rsquo;étais super content de moi. 🥳&lt;/p&gt;
&lt;p&gt;Mais c&amp;rsquo;était sans compter sur Antoine Caron, qui est venu (à très juste titre) me chatouiller sur un autre aspect que j&amp;rsquo;avais ignoré, le rapport Mozilla Observatory&amp;hellip;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/02/antoine-mozilla-observatory.avif"
loading="lazy"
alt="https://bsky.app/profile/slashgear.dev/post/3mfc62ckw3s2g"
&gt;&lt;/p&gt;
&lt;p&gt;J&amp;rsquo;ai donc lancé un scan &lt;a class="link" href="https://developer.mozilla.org/en-US/observatory" target="_blank" rel="noopener"
&gt;Mozilla HTTP Observatory&lt;/a&gt; sur mon blog et le résultat était : &lt;strong&gt;F&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Cool, je peux pas faire pire.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/02/observatory-initial.avif"
loading="lazy"
alt="Le scan initial : F avec 0/100"
&gt;&lt;/p&gt;
&lt;h2 id="le-déclic"&gt;Le déclic
&lt;/h2&gt;&lt;p&gt;En relisant &lt;a class="link" href="https://codeka.io/2026/02/20/optimiser-les-perfs-et-la-s%C3%A9curit%C3%A9-dun-site-hugo/" target="_blank" rel="noopener"
&gt;l&amp;rsquo;article de Julien sur codeka.io&lt;/a&gt;, qui a aussi parlé avec Antoine, a aussi un blog statique avec Hugo, MAIS qui lui avait fait le taf, j&amp;rsquo;ai réalisé que c&amp;rsquo;était à la fois &lt;strong&gt;important et peut-être pas si compliqué&lt;/strong&gt; à corriger.&lt;/p&gt;
&lt;p&gt;Julien y détaille les headers de sécurité à ajouter sur un site Hugo, avec des exemples pour Caddy. Moi je suis sur nginx, mais le principe est le même.&lt;/p&gt;
&lt;p&gt;Note : si ça vous intéresse, les liens des épisodes précédents sur l&amp;rsquo;infra du blog sont ici :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2019/12/24/ca-bouge-pas-mal-sur-le-blog/" &gt;Ça bouge pas mal sur le blog !&lt;/a&gt; (2019) - De 5s à 1s en virant Wordpress pour Hugo&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2025/01/15/ca-bouge-encore-sur-le-blog/" &gt;Ça bouge encore sur le blog&lt;/a&gt; (2025) - Nettoyage massif, retour auto-hébergé&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2023/08/01/automatiser-hugo-sans-github-action/" &gt;Automatiser son site Hugo sans Github Action ou Vercel&lt;/a&gt; - Le setup nginx + webhook actuel&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2026/02/19/optimisation-webperf-avif-precompression/" &gt;Optimisation webperf : AVIF et pré-compression&lt;/a&gt; - Le round précédent (focus sur les images)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="ce-que-teste-http-observatory"&gt;Ce que teste HTTP Observatory
&lt;/h2&gt;&lt;p&gt;HTTP Observatory vérifie une série de headers HTTP que votre serveur devrait envoyer pour protéger vos visiteurs. Les principaux :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Strict-Transport-Security (HSTS)&lt;/strong&gt; : force le navigateur à toujours utiliser HTTPS&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Content-Security-Policy (CSP)&lt;/strong&gt; : contrôle quelles ressources le navigateur peut charger (scripts, styles, images, iframes&amp;hellip;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;X-Content-Type-Options&lt;/strong&gt; : empêche le navigateur de &amp;ldquo;deviner&amp;rdquo; le type MIME des fichiers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Referrer-Policy&lt;/strong&gt; : contrôle les informations de provenance envoyées aux sites tiers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;X-Frame-Options&lt;/strong&gt; / &lt;strong&gt;frame-ancestors&lt;/strong&gt; : empêche l&amp;rsquo;inclusion de votre site dans une iframe (clickjacking)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sur un blog statique, on pourrait se dire que ce n&amp;rsquo;est pas critique. Pas de base de données, pas de sessions utilisateur, pas de formulaires sensibles. Mais ces headers protègent quand même contre des attaques réelles :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Un &lt;strong&gt;script injecté&lt;/strong&gt; (XSS) pourrait rediriger vos lecteurs vers un site malveillant&lt;/li&gt;
&lt;li&gt;Sans HSTS, un attaquant sur un Wi-Fi public pourrait intercepter la connexion initiale en HTTP&lt;/li&gt;
&lt;li&gt;Sans &lt;code&gt;frame-ancestors&lt;/code&gt;, quelqu&amp;rsquo;un pourrait inclure le blog dans une iframe piégée&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Bref, même pour un blog statique, ça peut valoir le coup de faire l&amp;rsquo;effort, a minima pour s&amp;rsquo;habituer aux bonnes pratiques.&lt;/p&gt;
&lt;h2 id="étape-1--les-headers-faciles"&gt;Étape 1 : les headers &amp;ldquo;faciles&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Le piège nginx : &lt;code&gt;add_header&lt;/code&gt; et l&amp;rsquo;héritage&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;La première chose, c&amp;rsquo;est que je suis tombé dans un piège classique avec mon nginx. Je l&amp;rsquo;ignorais, mais les directives &lt;code&gt;add_header&lt;/code&gt; dans un bloc &lt;code&gt;location&lt;/code&gt; &lt;strong&gt;écrasent&lt;/strong&gt; (et ne complètent pas) celles du bloc &lt;code&gt;server&lt;/code&gt; parent.&lt;/p&gt;
&lt;p&gt;Ça veut dire que si vous avez un &lt;code&gt;add_header Cache-Control&lt;/code&gt; dans une &lt;code&gt;location&lt;/code&gt; (c&amp;rsquo;était mon cas), tous vos headers de sécurité définis au niveau &lt;code&gt;server&lt;/code&gt; &lt;strong&gt;disparaissent&lt;/strong&gt; pour cette location. J&amp;rsquo;ai pété un plomb pendant quelques minutes à reload / restart en boucle sans comprendre.&lt;/p&gt;
&lt;p&gt;La solution : créer un fichier snippet et l&amp;rsquo;inclure partout.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat &amp;gt; /etc/nginx/snippets/security-headers.conf &lt;span class="s"&gt;&amp;lt;&amp;lt; &amp;#39;EOF&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;add_header Strict-Transport-Security &amp;#34;max-age=63072000; includeSubDomains; preload&amp;#34; always;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;add_header X-Content-Type-Options &amp;#34;nosniff&amp;#34; always;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;add_header Referrer-Policy &amp;#34;strict-origin-when-cross-origin&amp;#34; always;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;add_header Content-Security-Policy &amp;#34;...&amp;#34; always;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;EOF&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Puis dans la config nginx :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nginx" data-lang="nginx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Headers de sécurité au niveau server
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;include&lt;/span&gt; &lt;span class="s"&gt;/etc/nginx/snippets/security-headers.conf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Cache des assets statiques
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt; &lt;span class="s"&gt;\.(jpg|jpeg|png|gif|ico|css|js|woff|woff2|ttf|svg|webp|avif)&lt;/span&gt;$ &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;expires&lt;/span&gt; &lt;span class="s"&gt;1y&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Cache-Control&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;public,&lt;/span&gt; &lt;span class="s"&gt;immutable&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# OBLIGATOIRE : ré-inclure les headers ici !
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;include&lt;/span&gt; &lt;span class="s"&gt;/etc/nginx/snippets/security-headers.conf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt; &lt;span class="s"&gt;\.html&lt;/span&gt;$ &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;expires&lt;/span&gt; &lt;span class="s"&gt;1h&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Cache-Control&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;public,&lt;/span&gt; &lt;span class="s"&gt;must-revalidate&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;include&lt;/span&gt; &lt;span class="s"&gt;/etc/nginx/snippets/security-headers.conf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;HSTS, X-Content-Type-Options, Referrer-Policy&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Ces trois-là sont les plus simples à ajouter et &amp;ldquo;ne cassent rien&amp;rdquo; :&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;HSTS&lt;/strong&gt; avec &lt;code&gt;max-age=63072000&lt;/code&gt; (2 ans) et &lt;code&gt;preload&lt;/code&gt; permet à terme (pas encore fait) de s&amp;rsquo;inscrire sur la &lt;a class="link" href="https://hstspreload.org/" target="_blank" rel="noopener"
&gt;HSTS preload list&lt;/a&gt;. Le navigateur refusera même la toute première connexion HTTP initiale.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;nosniff&lt;/strong&gt; empêche les navigateurs d&amp;rsquo;interpréter un fichier texte comme du JavaScript.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;strict-origin-when-cross-origin&lt;/strong&gt; envoie l&amp;rsquo;URL complète en referrer pour les requêtes same-origin, mais seulement l&amp;rsquo;origine (domaine) pour les requêtes cross-origin.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="étape-2--content-security-policy-la-vraie-difficulté"&gt;Étape 2 : Content-Security-Policy (la vraie difficulté)
&lt;/h2&gt;&lt;p&gt;La CSP est un header puissant et a priori complexe. C&amp;rsquo;est lui qui rapporte le plus de points sur Observatory, mais c&amp;rsquo;est aussi lui qui peut tout casser si on va trop vite.&lt;/p&gt;
&lt;p&gt;Le principe : vous déclarez au navigateur &lt;strong&gt;exactement&lt;/strong&gt; quelles ressources votre site a le droit de charger (scripts, styles, images, iframes, fonts&amp;hellip;) et depuis quelles origines. Tout le reste est bloqué. C&amp;rsquo;est la meilleure défense contre le XSS, parce que même si un attaquant arrive à injecter un &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; dans votre HTML, le navigateur refusera de l&amp;rsquo;exécuter s&amp;rsquo;il n&amp;rsquo;est pas dans la liste autorisée.&lt;/p&gt;
&lt;p&gt;Le revers de la médaille : si vous oubliez une ressource légitime dans votre CSP, elle sera bloquée et votre site casse silencieusement : un script qui ne charge plus, une font qui disparaît, une iframe vide. Il faut donc faire un inventaire exhaustif &lt;em&gt;avant&lt;/em&gt; de verrouiller.&lt;/p&gt;
&lt;p&gt;Je me suis fait épauler par un LLM (Opus 4.6) pour les modifs à réaliser et on a fait plusieurs passes jusqu&amp;rsquo;à ce que tout fonctionne.&lt;/p&gt;
&lt;h3 id="inventaire-des-ressources-externes"&gt;Inventaire des ressources externes
&lt;/h3&gt;&lt;p&gt;Avant d&amp;rsquo;écrire la CSP, il faut faire l&amp;rsquo;inventaire de &lt;strong&gt;tout&lt;/strong&gt; ce que le site charge. Voilà une petite synthèse de mon pote &lt;em&gt;Claudio&lt;/em&gt; :&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Ressource&lt;/th&gt;
&lt;th&gt;Origine&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Script&lt;/td&gt;
&lt;td&gt;GoatCounter analytics&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://gc.zgo.at/count.js&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Script&lt;/td&gt;
&lt;td&gt;Umami analytics&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://cloud.umami.is/script.js&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connect&lt;/td&gt;
&lt;td&gt;GoatCounter API&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://blogzwindler.goatcounter.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connect&lt;/td&gt;
&lt;td&gt;Umami API&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://api-gateway.umami.dev&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Script&lt;/td&gt;
&lt;td&gt;De mon thème &amp;lsquo;stack&amp;rsquo; - vibrant.js (couleurs d&amp;rsquo;images)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://cdn.jsdelivr.net&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Script&lt;/td&gt;
&lt;td&gt;De mon thème &amp;lsquo;stack&amp;rsquo; - PhotoSwipe (lightbox images)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://cdn.jsdelivr.net&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Style&lt;/td&gt;
&lt;td&gt;De mon thème &amp;lsquo;stack&amp;rsquo; - PhotoSwipe CSS (lightbox)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://cdn.jsdelivr.net&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Style&lt;/td&gt;
&lt;td&gt;De mon thème &amp;lsquo;stack&amp;rsquo; - CSS du thème&lt;/td&gt;
&lt;td&gt;local (&lt;code&gt;'self'&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Font&lt;/td&gt;
&lt;td&gt;Luciole&lt;/td&gt;
&lt;td&gt;locale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frame&lt;/td&gt;
&lt;td&gt;Widget Deezer (inclu dans un seul article 😖)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://widget.deezer.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image&lt;/td&gt;
&lt;td&gt;data: URIs (SVGs inline)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;data:&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Form&lt;/td&gt;
&lt;td&gt;Mailchimp (abonnement newsletter)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://zwindler.us15.list-manage.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="le-problème-des-scripts-danalytics-externes"&gt;Le problème des scripts d&amp;rsquo;analytics externes
&lt;/h3&gt;&lt;p&gt;Avec une CSP comme &lt;code&gt;script-src 'self' https://gc.zgo.at https://cloud.umami.is&lt;/code&gt;, je devais soit :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Faire confiance à ces CDN&lt;/strong&gt; -&amp;gt; si le CDN est compromis, le script malveillant s&amp;rsquo;exécute sur mon blog. Nope.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ajouter des hashes SRI&lt;/strong&gt; (Subresource Integrity). Problème : ces scripts peuvent changer côté serveur sans prévenir, ce qui casserait le hash.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;J&amp;rsquo;ai choisi une troisième voie : &lt;strong&gt;télécharger les scripts à chaque build&lt;/strong&gt; et les servir localement.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Dans mon petit script blog_refresh.sh, avant le hugo --minify&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir -p static/js
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sf --max-time &lt;span class="m"&gt;10&lt;/span&gt; https://gc.zgo.at/count.js -o static/js/count.js &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;WARN: failed to download GoatCounter script&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sf --max-time &lt;span class="m"&gt;10&lt;/span&gt; https://cloud.umami.is/script.js -o static/js/umami.js &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;WARN: failed to download Umami script&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Et dans le template Hugo :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt; &lt;span class="na"&gt;data-goatcounter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;https://blogzwindler.goatcounter.com/count&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="na"&gt;async&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/js/count.js&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt; &lt;span class="na"&gt;defer&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/js/umami.js&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="na"&gt;data-website-id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;3d8f0ea9-...&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Est-ce que c&amp;rsquo;est &amp;ldquo;tricher&amp;rdquo; ?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Une fois que j&amp;rsquo;avais fait la modif, je me suis demandé : est-ce qu&amp;rsquo;héberger les scripts localement à chaque build ne contourne pas l&amp;rsquo;esprit des vérifications d&amp;rsquo;Observatory ? Sur ce point précis j&amp;rsquo;ai un petit doute, n&amp;rsquo;étant pas expert sur ces points là. J&amp;rsquo;ai demandé à un LLM qui m&amp;rsquo;a dit que &lt;em&gt;non&lt;/em&gt;, mais je veux bien un avis tiers.&lt;/p&gt;
&lt;p&gt;Dans tous les cas, ça semble être l&amp;rsquo;approche recommandée par &lt;a class="link" href="https://www.goatcounter.com/help/csp" target="_blank" rel="noopener"
&gt;la documentation GoatCounter&lt;/a&gt; pour les sites avec une CSP stricte.&lt;/p&gt;
&lt;h3 id="supprimer-unsafe-inline-de-script-src"&gt;Supprimer &lt;code&gt;unsafe-inline&lt;/code&gt; de &lt;code&gt;script-src&lt;/code&gt;
&lt;/h3&gt;&lt;p&gt;Observatory pénalise fortement &lt;code&gt;'unsafe-inline'&lt;/code&gt; dans &lt;code&gt;script-src&lt;/code&gt;. Et c&amp;rsquo;est normal, car c&amp;rsquo;est la porte ouverte au XSS : n&amp;rsquo;importe quel script injecté dans le HTML s&amp;rsquo;exécute.&lt;/p&gt;
&lt;p&gt;Le problème : mon thème Hugo (&lt;a class="link" href="https://github.com/CaiJimmy/hugo-theme-stack" target="_blank" rel="noopener"
&gt;hugo-theme-stack&lt;/a&gt;) générait &lt;strong&gt;4 scripts inline&lt;/strong&gt; :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Color scheme init&lt;/strong&gt; — lit le localStorage pour appliquer le thème clair/sombre avant le premier rendu&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Color scheme detection&lt;/strong&gt; — détecte la préférence système (&lt;code&gt;prefers-color-scheme: dark&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Language switcher&lt;/strong&gt; — le sélecteur de langue &lt;code&gt;&amp;lt;select onchange=&amp;quot;window.location.href=this.selectedOptions[0].value&amp;quot;&amp;gt;&lt;/code&gt; dans la sidebar, qui est un attribut d&amp;rsquo;événement inline (&lt;code&gt;script-src-attr&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Font loader&lt;/strong&gt; — charge la CSS de &lt;a class="link" href="https://luciole-vision.com/" target="_blank" rel="noopener"
&gt;la font Luciole&lt;/a&gt; dynamiquement&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;Luciole: A typeface for visual impairment&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Pour chacun, j&amp;rsquo;ai extrait le code dans un fichier &lt;code&gt;.js&lt;/code&gt; externe dans &lt;code&gt;static/js/&lt;/code&gt; et remplacé le script inline par une balise &lt;code&gt;&amp;lt;script src=&amp;quot;...&amp;quot;&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Par exemple, pour le color scheme, le thème avait dans &lt;code&gt;layouts/partials/head/colorScheme.html&lt;/code&gt; :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;colorSchemeKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;StackColorScheme&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;colorSchemeKey&lt;/span&gt;&lt;span class="p"&gt;)){&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;colorSchemeKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;auto&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Que j&amp;rsquo;ai remplacé par un &lt;a class="link" href="https://gohugo.io/templates/lookup-order/" target="_blank" rel="noopener"
&gt;override de template Hugo&lt;/a&gt; :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{{- /* Override: external JS file instead of inline (CSP compliance) */ -}}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/js/color-scheme.js&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Avec le contenu correspondant dans &lt;code&gt;static/js/color-scheme.js&lt;/code&gt;. Hugo permet de surcharger n&amp;rsquo;importe quel template du thème en plaçant un fichier au même chemin dans le dossier &lt;code&gt;layouts/&lt;/code&gt; du projet.&lt;/p&gt;
&lt;p&gt;Pour le &lt;strong&gt;language switcher&lt;/strong&gt;, même approche : j&amp;rsquo;ai surchargé &lt;code&gt;layouts/partials/sidebar/left.html&lt;/code&gt; pour retirer l&amp;rsquo;attribut &lt;code&gt;onchange&lt;/code&gt; du &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; et ajouté un &lt;code&gt;id=&amp;quot;language-select&amp;quot;&lt;/code&gt;, puis créé un fichier &lt;code&gt;static/js/language-switcher.js&lt;/code&gt; :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;select&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;language-select&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;select&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;select&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;change&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;selectedOptions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Note importante :&lt;/strong&gt; il m&amp;rsquo;a fallu plusieurs allers-retours avec la console du navigateur (F12 -&amp;gt; Console) avant de trouver la totalité des violations CSP. Chaque correction en révélait de nouvelles, il fallait bien regarder tous les différents types de pages sur le blog (pas juste la page d&amp;rsquo;accueil et les posts), et certaines erreurs affichées dans la console venaient en fait d&amp;rsquo;&lt;strong&gt;extensions navigateur&lt;/strong&gt; (typiquement les gestionnaires de mots de passe comme Bitwarden, dont le &lt;code&gt;bootstrap-autofill-overlay.js&lt;/code&gt; génère des violations &lt;code&gt;style-src-elem&lt;/code&gt;) et non du site lui-même. Il faut donc bien distinguer les erreurs du site de celles injectées par les extensions.&lt;/p&gt;
&lt;h3 id="supprimer-unsafe-inline-de-style-src-la-suite"&gt;Supprimer &lt;code&gt;unsafe-inline&lt;/code&gt; de &lt;code&gt;style-src&lt;/code&gt;, la suite
&lt;/h3&gt;&lt;p&gt;Même logique pour les styles. J&amp;rsquo;avais quatre sources d&amp;rsquo;inline CSS :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Un bloc &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt;&lt;/strong&gt; dans &lt;code&gt;custom.html&lt;/code&gt; — les CSS custom properties pour ma font Luciole&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Des attributs &lt;code&gt;style=&amp;quot;&amp;quot;&lt;/code&gt;&lt;/strong&gt; sur les badges de catégories — &lt;code&gt;background-color: #2a9d8f; color: #fff;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Un attribut &lt;code&gt;style=&amp;quot;enable-background:...&amp;quot;&lt;/code&gt;&lt;/strong&gt; sur un SVG (export Adobe Illustrator)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Des attributs &lt;code&gt;style=&amp;quot;&amp;quot;&lt;/code&gt;&lt;/strong&gt; dans le formulaire Mailchimp du footer — &lt;code&gt;display:none&lt;/code&gt; et un positionnement off-screen pour le honeypot anti-spam&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Le &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; bloc&lt;/strong&gt; : déplacé dans &lt;code&gt;assets/scss/custom.scss&lt;/code&gt;, le fichier de personnalisation SCSS prévu par le thème.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-scss" data-lang="scss"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cm"&gt;/* Font family CSS custom properties */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="na"&gt;--article-font-family&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Luciole&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="na"&gt;--base-font-family&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Luciole&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Les badges de catégories&lt;/strong&gt; : toutes mes catégories utilisent les mêmes couleurs (#2a9d8f / #fff), définies dans le front matter de chaque &lt;code&gt;_index.md&lt;/code&gt;. Le thème les injectait en &lt;code&gt;style=&amp;quot;&amp;quot;&lt;/code&gt; via le template &lt;code&gt;article/components/details.html&lt;/code&gt;. J&amp;rsquo;ai surchargé ce template pour retirer le &lt;code&gt;style&lt;/code&gt; inline et ajouté une règle CSS dans &lt;code&gt;custom.scss&lt;/code&gt; :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-scss" data-lang="scss"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nc"&gt;.article-category&lt;/span&gt; &lt;span class="nt"&gt;a&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="na"&gt;background-color&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#2a9d8f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#fff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Le SVG&lt;/strong&gt; : j&amp;rsquo;avais un SVG que j&amp;rsquo;avais ajouté à la main (un petit Robot mignon pour mon &amp;ldquo;&lt;a class="link" href="https://blog.zwindler.fr/ai-manifesto/" target="_blank" rel="noopener"
&gt;AI Manifesto&lt;/a&gt;&amp;rdquo;) avec &lt;code&gt;enable-background&lt;/code&gt;, qui est un attribut SVG 1.1 déprécié. Je l&amp;rsquo;ai simplement supprimé du fichier SVG source, sans impact visuel.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Le formulaire Mailchimp&lt;/strong&gt; : le code HTML Mailchimp utilise &lt;code&gt;style=&amp;quot;display:none&amp;quot;&lt;/code&gt; pour masquer les divs de feedback et &lt;code&gt;style=&amp;quot;position: absolute; left: -5000px;&amp;quot;&lt;/code&gt; pour le champ honeypot (pour les robots qui s&amp;rsquo;inscrivent à ma newsletter 🤔). J&amp;rsquo;ai remplacé tout ça par des classes CSS dans &lt;code&gt;custom.scss&lt;/code&gt; :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-scss" data-lang="scss"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nn"&gt;#mce-error-response&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nn"&gt;#mce-success-response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="na"&gt;display&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="ni"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nc"&gt;.mce-honeypot&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="ni"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="na"&gt;left&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="kt"&gt;px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="la-csp-finale"&gt;La CSP finale
&lt;/h3&gt;&lt;p&gt;Après tous ces changements, plus aucun script ni style inline sur le site. La CSP peut donc être stricte :&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;default-src &amp;#39;none&amp;#39;;
script-src &amp;#39;self&amp;#39; https://cdn.jsdelivr.net;
style-src &amp;#39;self&amp;#39; https://cdn.jsdelivr.net;
img-src &amp;#39;self&amp;#39; data:;
connect-src &amp;#39;self&amp;#39; https://blogzwindler.goatcounter.com https://api-gateway.umami.dev https://cdn.jsdelivr.net;
font-src &amp;#39;self&amp;#39;;
frame-src https://widget.deezer.com;
frame-ancestors &amp;#39;none&amp;#39;;
base-uri &amp;#39;self&amp;#39;;
form-action &amp;#39;self&amp;#39; https://zwindler.us15.list-manage.com;
manifest-src &amp;#39;self&amp;#39;;
media-src &amp;#39;self&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Points notables :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;default-src 'none'&lt;/code&gt;&lt;/strong&gt; : par défaut, rien n&amp;rsquo;est autorisé. Chaque type de ressource doit être explicitement listé. C&amp;rsquo;est la politique la plus restrictive.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pas de &lt;code&gt;unsafe-inline&lt;/code&gt;&lt;/strong&gt; nulle part : ni dans &lt;code&gt;script-src&lt;/code&gt;, ni dans &lt;code&gt;style-src&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;frame-ancestors 'none'&lt;/code&gt;&lt;/strong&gt; remplace &lt;code&gt;X-Frame-Options: DENY&lt;/code&gt; (plus moderne)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;cdn.jsdelivr.net&lt;/code&gt;&lt;/strong&gt; dans &lt;code&gt;script-src&lt;/code&gt;, &lt;code&gt;style-src&lt;/code&gt; ET &lt;code&gt;connect-src&lt;/code&gt; : le thème charge &lt;code&gt;vibrant.js&lt;/code&gt; (JS avec SRI), les CSS de PhotoSwipe (lightbox d&amp;rsquo;images) et les source maps associées depuis ce CDN&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;form-action&lt;/code&gt;&lt;/strong&gt; autorise Mailchimp pour le formulaire d&amp;rsquo;abonnement dans le footer&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Au final, de nombreux morceaux du thème de mon blog ont du être réécrit / overridés. Je suis un peu gêné d&amp;rsquo;avoir du en arriver là, mais c&amp;rsquo;était le prix à payer pour une note maximale 😂.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/02/rework-theme.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;h2 id="nettoyage-bonus--mailchimp"&gt;Nettoyage bonus : Mailchimp
&lt;/h2&gt;&lt;p&gt;En faisant l&amp;rsquo;inventaire des ressources externes, j&amp;rsquo;ai découvert un vieux widget Mailchimp dans la sidebar qui chargeait du CSS et du JavaScript depuis &lt;code&gt;chimpstatic.com&lt;/code&gt;. Je ne l&amp;rsquo;utilisais plus : supprimé. Le formulaire d&amp;rsquo;abonnement dans le footer des articles a été gardé, mais nettoyé de ses &lt;code&gt;style=&amp;quot;&amp;quot;&lt;/code&gt; inline (voir plus haut).&lt;/p&gt;
&lt;h2 id="résultat-final"&gt;Résultat final
&lt;/h2&gt;&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/02/observatory-a-plus.avif"
loading="lazy"
alt="A&amp;#43; sur HTTP Observatory"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;De F (0/100) à A+ (125/100).&lt;/strong&gt; Tous les tests sont verts.&lt;/p&gt;
&lt;p&gt;Un grand merci à Antoine pour sa patience avec les débutants naïfs comme moi 😂 et ses super talks !&lt;/p&gt;</description></item><item><title>Optimisation webperf : AVIF et pré-compression pour le blog</title><link>https://blog.zwindler.fr/2026/02/19/optimisation-webperf-avif-precompression/</link><pubDate>Thu, 19 Feb 2026 17:00:00 +0200</pubDate><guid>https://blog.zwindler.fr/2026/02/19/optimisation-webperf-avif-precompression/</guid><description>&lt;img src="https://blog.zwindler.fr/2026/02/webperf0.webp" alt="Featured image of post Optimisation webperf : AVIF et pré-compression pour le blog" /&gt;&lt;p&gt;Ce blog a presque 16 ans d&amp;rsquo;existence. Sur cette période, j&amp;rsquo;ai accumulé plus de 530 articles avec plus de 2700 images. Il y a quelques années, j&amp;rsquo;avais commencé à taper des limites (notamment quand j&amp;rsquo;ai essayer Gitlab pages chez Froggit) en atteignant les 500 Mo de medias.&lt;/p&gt;
&lt;p&gt;J&amp;rsquo;avais fait une première passe d&amp;rsquo;optimisation, à grand coup de resize, &lt;code&gt;jpegoptim&lt;/code&gt; et &lt;code&gt;optipng&lt;/code&gt; et j&amp;rsquo;étais redescendu sous les 300 Mo. C&amp;rsquo;était pas mal, mais pas satisfaisant.&lt;/p&gt;
&lt;p&gt;Puis j&amp;rsquo;ai vu &lt;a class="link" href="https://blog.zwindler.fr/2026/02/13/recap-touraine-tech-2026-jour2/#au-secours--mes-images-pourrissent-mes-perfs" &gt;le talk d&amp;rsquo;Antoine Caron (slashgear) et Mathieu Mure à Touraine Tech 2026&lt;/a&gt; et j&amp;rsquo;ai enfin pris le temps de lâcher les &amp;ldquo;formats morts&amp;rdquo;.&lt;/p&gt;
&lt;h2 id="un-peu-de-contexte"&gt;Un peu de contexte
&lt;/h2&gt;&lt;p&gt;Ça fait un moment que je bricole l&amp;rsquo;infra et les perfs de ce blog. Si ça vous intéresse, les épisodes précédents sont ici :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2019/12/24/ca-bouge-pas-mal-sur-le-blog/" &gt;Ça bouge pas mal sur le blog !&lt;/a&gt; (2019) - De 5s à 1s en virant Wordpress pour Hugo&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2025/01/15/ca-bouge-encore-sur-le-blog/" &gt;Ça bouge encore sur le blog&lt;/a&gt; (2025) - Nettoyage massif, retour auto-hébergé&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2023/08/01/automatiser-hugo-sans-github-action/" &gt;Automatiser son site Hugo sans Github Action ou Vercel&lt;/a&gt; - Le setup nginx + webhook actuel&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Quand j&amp;rsquo;ai commencé ce round d&amp;rsquo;optimisation, la note PageSpeed de la page d&amp;rsquo;accueil tournait autour de 70-85 en mobile selon les articles. Pas dramatique, mais on peut faire mieux !&lt;/p&gt;
&lt;h2 id="le-talk-qui-a-tout-déclenché"&gt;Le talk qui a tout déclenché
&lt;/h2&gt;&lt;p&gt;À &lt;a class="link" href="https://blog.zwindler.fr/2026/02/13/recap-touraine-tech-2026-jour2/" &gt;Touraine Tech 2026&lt;/a&gt;, Antoine Caron et Mathieu Mure ont fait un talk très clair sur l&amp;rsquo;optimisation des images web, qui pourrissent les perfs des sites web aujourd&amp;rsquo;hui.&lt;/p&gt;
&lt;p&gt;Le message principal : les formats comme JPEG et PNG sont des &amp;ldquo;formats morts&amp;rdquo; (ou en tout cas vieillissants). À compression équivalente, les formats modernes comme AVIF prennent beaucoup moins de place, mais surtout, ils affichent des artefacts visuels bien moindres dans les hauts niveaux de compression.&lt;/p&gt;
&lt;p&gt;J&amp;rsquo;avais failli faire une migration vers le WebP il y a quelques années, puis j&amp;rsquo;avais laissé tomber, par flemme et après quelques soucis techniques dont je ne me souviens plus trop.&lt;/p&gt;
&lt;p&gt;Et finalement, c&amp;rsquo;est presque tant mieux, parce qu&amp;rsquo;avec AVIF, on peut compresser &lt;strong&gt;encore plus fort&lt;/strong&gt;, sans que ça se voit. C&amp;rsquo;est exactement ce qu&amp;rsquo;il me fallait.&lt;/p&gt;
&lt;p&gt;Side note : on m&amp;rsquo;a demandé une comparaison WebP vs AVIF, et Joseph a trouvé ça. C&amp;rsquo;est plutôt intéressant :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://elementor.com/blog/fr/avif-vs-webp-quel-format-dimage-regne-en-maitre-en-2024/" target="_blank" rel="noopener"
&gt;elementor blog - AVIF vs WebP : Quel format d’image règne en maître en 2024 ?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="conversion-massive-en-avif"&gt;Conversion massive en AVIF
&lt;/h2&gt;&lt;p&gt;J&amp;rsquo;ai donc écrit un script qui :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Redimensionne&lt;/strong&gt; les images trop grandes (&amp;gt; 1500px) pour les ramener à ~1 mégapixel&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Convertit en AVIF&lt;/strong&gt; avec &lt;code&gt;avifenc&lt;/code&gt; (qualité 50, speed 6)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Met à jour les références&lt;/strong&gt; dans tous les articles markdown&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Et je l&amp;rsquo;ai lancé année par année, de 2026 jusqu&amp;rsquo;à 2010.&lt;/p&gt;
&lt;h3 id="les-résultats"&gt;Les résultats
&lt;/h3&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Année&lt;/th&gt;
&lt;th&gt;Fichiers&lt;/th&gt;
&lt;th&gt;Originaux&lt;/th&gt;
&lt;th&gt;AVIF&lt;/th&gt;
&lt;th&gt;Réduction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2010&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;0.4 MiB&lt;/td&gt;
&lt;td&gt;0.1 MiB&lt;/td&gt;
&lt;td&gt;74%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2011&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;1.8 MiB&lt;/td&gt;
&lt;td&gt;0.7 MiB&lt;/td&gt;
&lt;td&gt;63%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2012&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;0.4 MiB&lt;/td&gt;
&lt;td&gt;0.1 MiB&lt;/td&gt;
&lt;td&gt;78%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2013&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0.1 MiB&lt;/td&gt;
&lt;td&gt;0.1 MiB&lt;/td&gt;
&lt;td&gt;56%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2014&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;5.3 MiB&lt;/td&gt;
&lt;td&gt;1.0 MiB&lt;/td&gt;
&lt;td&gt;81%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2015&lt;/td&gt;
&lt;td&gt;214&lt;/td&gt;
&lt;td&gt;10.7 MiB&lt;/td&gt;
&lt;td&gt;3.7 MiB&lt;/td&gt;
&lt;td&gt;65%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2016&lt;/td&gt;
&lt;td&gt;231&lt;/td&gt;
&lt;td&gt;12.1 MiB&lt;/td&gt;
&lt;td&gt;4.1 MiB&lt;/td&gt;
&lt;td&gt;66%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2017&lt;/td&gt;
&lt;td&gt;428&lt;/td&gt;
&lt;td&gt;23.7 MiB&lt;/td&gt;
&lt;td&gt;9.1 MiB&lt;/td&gt;
&lt;td&gt;62%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2018&lt;/td&gt;
&lt;td&gt;142&lt;/td&gt;
&lt;td&gt;7.5 MiB&lt;/td&gt;
&lt;td&gt;2.4 MiB&lt;/td&gt;
&lt;td&gt;68%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2019&lt;/td&gt;
&lt;td&gt;136&lt;/td&gt;
&lt;td&gt;12.1 MiB&lt;/td&gt;
&lt;td&gt;2.9 MiB&lt;/td&gt;
&lt;td&gt;76%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2020&lt;/td&gt;
&lt;td&gt;228&lt;/td&gt;
&lt;td&gt;23.6 MiB&lt;/td&gt;
&lt;td&gt;5.6 MiB&lt;/td&gt;
&lt;td&gt;77%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2021&lt;/td&gt;
&lt;td&gt;187&lt;/td&gt;
&lt;td&gt;31.6 MiB&lt;/td&gt;
&lt;td&gt;4.8 MiB&lt;/td&gt;
&lt;td&gt;85%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2022&lt;/td&gt;
&lt;td&gt;237&lt;/td&gt;
&lt;td&gt;29.2 MiB&lt;/td&gt;
&lt;td&gt;7.5 MiB&lt;/td&gt;
&lt;td&gt;74%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2023&lt;/td&gt;
&lt;td&gt;253&lt;/td&gt;
&lt;td&gt;43.2 MiB&lt;/td&gt;
&lt;td&gt;7.8 MiB&lt;/td&gt;
&lt;td&gt;82%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024&lt;/td&gt;
&lt;td&gt;259&lt;/td&gt;
&lt;td&gt;34.2 MiB&lt;/td&gt;
&lt;td&gt;8.0 MiB&lt;/td&gt;
&lt;td&gt;77%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025&lt;/td&gt;
&lt;td&gt;255&lt;/td&gt;
&lt;td&gt;31.2 MiB&lt;/td&gt;
&lt;td&gt;6.8 MiB&lt;/td&gt;
&lt;td&gt;78%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026&lt;/td&gt;
&lt;td&gt;44&lt;/td&gt;
&lt;td&gt;6.8 MiB&lt;/td&gt;
&lt;td&gt;1.8 MiB&lt;/td&gt;
&lt;td&gt;74%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2704&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~274 MiB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~66 MiB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~76%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;De PNG/JPEG qualité 90 à AVIF qualité 50 : entre 4 et 5 fois moins lourd.&lt;/p&gt;
&lt;p&gt;Le plus satisfaisant, c&amp;rsquo;est que je ne suis pas capable de détecter visuellement de perte de qualité. Les screenshots de terminal et mes photos passent très bien en AVIF 50.&lt;/p&gt;
&lt;h2 id="pré-compression-des-documents-html"&gt;Pré-compression des documents HTML
&lt;/h2&gt;&lt;p&gt;Après la conversion AVIF, &lt;a class="link" href="https://bsky.app/profile/slashgear.dev/post/3metqwv5aas2t" target="_blank" rel="noopener"
&gt;Antoine Caron (@slashgear.dev)&lt;/a&gt; m&amp;rsquo;a fait remarquer un truc :&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Alors c&amp;rsquo;est pas mal déjà ! Je vois aussi que tes documents HTML ne sont pas compressés. Si ton blog est purement static, hésite pas à précompresser à balle et dire à ton server de servir les versions précompressées.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Effectivement, j&amp;rsquo;avais déjà &lt;code&gt;gzip on;&lt;/code&gt; dans ma config nginx, mais c&amp;rsquo;est de la compression &lt;strong&gt;à la volée&lt;/strong&gt;. Nginx utilise par défaut un niveau de compression modéré (niveau 6 sur 9) pour ne pas consommer trop de CPU.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nginx" data-lang="nginx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;blog.zwindler.fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;root&lt;/span&gt; &lt;span class="s"&gt;/usr/share/nginx/html/blog.zwindler.fr/public&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;gzip&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Or, mon blog est &lt;strong&gt;100% statique&lt;/strong&gt;. Les fichiers ne changent qu&amp;rsquo;au rebuild. Ça veut dire qu&amp;rsquo;on peut les compresser une seule fois, avec le niveau maximum, et demander à nginx de servir directement les fichiers pré-compressés. Zéro CPU à chaque requête, mais surtout un meilleur ratio au final, car on peut compresser plus fort.&lt;/p&gt;
&lt;h3 id="côté-build--blog_refreshsh"&gt;Côté build : &lt;code&gt;blog_refresh.sh&lt;/code&gt;
&lt;/h3&gt;&lt;p&gt;J&amp;rsquo;ai ajouté les commandes de pré-compression après le &lt;code&gt;hugo --minify&lt;/code&gt; :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Pre-compress static files (gzip + brotli) for nginx gzip_static/brotli_static&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Zopfli: compatible gzip mais ~3-8% plus petit que gzip -9&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;command&lt;/span&gt; -v zopfli &lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&amp;gt; /dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; find public -type f &lt;span class="se"&gt;\(&lt;/span&gt; -name &lt;span class="s2"&gt;&amp;#34;*.html&amp;#34;&lt;/span&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.css&amp;#34;&lt;/span&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.js&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.xml&amp;#34;&lt;/span&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.json&amp;#34;&lt;/span&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.svg&amp;#34;&lt;/span&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.txt&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\)&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -exec zopfli --i1023 &lt;span class="o"&gt;{}&lt;/span&gt; +
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; find public -type f &lt;span class="se"&gt;\(&lt;/span&gt; -name &lt;span class="s2"&gt;&amp;#34;*.html&amp;#34;&lt;/span&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.css&amp;#34;&lt;/span&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.js&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.xml&amp;#34;&lt;/span&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.json&amp;#34;&lt;/span&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.svg&amp;#34;&lt;/span&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.txt&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\)&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -exec gzip -k -f -9 &lt;span class="o"&gt;{}&lt;/span&gt; +
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Brotli pre-compression (better ratio than gzip, ~15-25% smaller)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;command&lt;/span&gt; -v brotli &lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&amp;gt; /dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; find public -type f &lt;span class="se"&gt;\(&lt;/span&gt; -name &lt;span class="s2"&gt;&amp;#34;*.html&amp;#34;&lt;/span&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.css&amp;#34;&lt;/span&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.js&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.xml&amp;#34;&lt;/span&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.json&amp;#34;&lt;/span&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.svg&amp;#34;&lt;/span&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.txt&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\)&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -exec brotli -k -f -q &lt;span class="m"&gt;11&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt; +
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Pour la compression gzip, Zigazou m&amp;rsquo;a conseillé d&amp;rsquo;utiliser &lt;a class="link" href="https://github.com/google/zopfli" target="_blank" rel="noopener"
&gt;Zopfli&lt;/a&gt; (&lt;code&gt;apt install zopfli&lt;/code&gt;) plutôt que &lt;code&gt;gzip -9&lt;/code&gt;. Zopfli produit des fichiers 100% compatibles gzip mais avec un meilleur ratio (~3-8% en moins). C&amp;rsquo;est plus lent, mais sur un blog statique où on compresse une seule fois au build, on s&amp;rsquo;en fiche.&lt;/p&gt;
&lt;p&gt;En pratique, le gain de Zopfli est surtout un bonus : la majorité des navigateurs modernes supportent Brotli et recevront les &lt;code&gt;.br&lt;/code&gt;, qui sont de toute façon plus petits. Le &lt;code&gt;.gz&lt;/code&gt; ne sert que de fallback.&lt;/p&gt;
&lt;p&gt;Chaque fichier &lt;code&gt;index.html&lt;/code&gt; se retrouve ainsi avec un &lt;code&gt;index.html.gz&lt;/code&gt; et un &lt;code&gt;index.html.br&lt;/code&gt; à côté de lui.&lt;/p&gt;
&lt;h3 id="côté-nginx"&gt;Côté nginx
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nginx" data-lang="nginx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Serve pre-compressed files generated at build time
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;gzip_static&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;brotli_static&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;# nécessite libnginx-mod-http-brotli-static
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Fallback pour les contenus non pré-compressés
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;gzip&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;gzip_vary&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;gzip_min_length&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;gzip_types&lt;/span&gt; &lt;span class="s"&gt;text/plain&lt;/span&gt; &lt;span class="s"&gt;text/css&lt;/span&gt; &lt;span class="s"&gt;text/xml&lt;/span&gt; &lt;span class="s"&gt;text/javascript&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;application/javascript&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;application/xml&lt;/span&gt; &lt;span class="s"&gt;image/svg+xml&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Pour Brotli, sur &lt;strong&gt;Ubuntu 24.04+&lt;/strong&gt;, les paquets sont dans les dépôts officiels :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install libnginx-mod-http-brotli-filter libnginx-mod-http-brotli-static
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="le-gain-mesuré"&gt;Le gain mesuré
&lt;/h3&gt;&lt;p&gt;Un petit &lt;code&gt;curl&lt;/code&gt; pour comparer la page d&amp;rsquo;accueil :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Avec Brotli (ce que reçoivent les navigateurs)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -so /dev/null -w &lt;span class="s2"&gt;&amp;#34;%{size_download}&amp;#34;&lt;/span&gt; -H &lt;span class="s2"&gt;&amp;#34;Accept-Encoding: br, gzip&amp;#34;&lt;/span&gt; https://blog.zwindler.fr/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# =&amp;gt; 6 206 bytes&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Sans compression&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -so /dev/null -w &lt;span class="s2"&gt;&amp;#34;%{size_download}&amp;#34;&lt;/span&gt; -H &lt;span class="s2"&gt;&amp;#34;Accept-Encoding: identity&amp;#34;&lt;/span&gt; https://blog.zwindler.fr/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# =&amp;gt; 32 886 bytes&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;-81% sur le HTML&lt;/strong&gt;, de 33 Ko à 6 Ko transférés. Et on le vérifie dans Chrome DevTools : le header &lt;code&gt;Content-Encoding: br&lt;/code&gt; confirme que Brotli est bien servi.&lt;/p&gt;
&lt;p&gt;Et c&amp;rsquo;est pareil pour les autres fichiers textes statiques (CSS, JS).&lt;/p&gt;
&lt;h2 id="bilan"&gt;Bilan
&lt;/h2&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Optimisation&lt;/th&gt;
&lt;th&gt;Avant&lt;/th&gt;
&lt;th&gt;Après&lt;/th&gt;
&lt;th&gt;Gain&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Images (AVIF)&lt;/td&gt;
&lt;td&gt;274 MiB&lt;/td&gt;
&lt;td&gt;66 MiB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-76%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTML page d&amp;rsquo;accueil (Brotli)&lt;/td&gt;
&lt;td&gt;33 Ko transférés&lt;/td&gt;
&lt;td&gt;6 Ko transférés&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-81%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Le tout sans aucune dégradation visible de la qualité des images, et zéro impact CPU côté serveur pour la compression (puisqu&amp;rsquo;elle est faite au build).&lt;/p&gt;
&lt;p&gt;Et des webperfs qui ont bien progressé, même en mobile :&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/02/webperf.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;h2 id="addendum"&gt;Addendum
&lt;/h2&gt;&lt;p&gt;Un peu en vrac&amp;hellip;&lt;/p&gt;
&lt;p&gt;J&amp;rsquo;ai du repasser sur du webp pour les images en frontmatter car les sites sociaux (bluesky / slack / linkedin) ne supportent pas AVIF :-/.&lt;/p&gt;
&lt;p&gt;On m&amp;rsquo;a conseillé &lt;a class="link" href="https://github.com/google/zopfli" target="_blank" rel="noopener"
&gt;zopfli&lt;/a&gt; en remplacement de gzip.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Pourquoi ne pas utiliser zopfli &amp;ndash;i1023 en lieu et place de gzip -9 ?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Sauf que :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;la très grande majorité supportent le brotli donc le gain sera marginal&lt;/li&gt;
&lt;li&gt;Sur ma mini VM, le zopfli n&amp;rsquo;aboutissait jamais (bug ou trop intensif en CPU)&lt;/li&gt;
&lt;li&gt;le projet n&amp;rsquo;est plus maintenu et qu&amp;rsquo;il a été archivé en octobre dernier.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sinon, il y a &lt;a class="link" href="https://github.com/ebiggers/libdeflate" target="_blank" rel="noopener"
&gt;libdeflate-gzip&lt;/a&gt;, sur le papier aussi bon que zopfli (pas testé) et semble encore maintenu.&lt;/p&gt;
&lt;p&gt;On m&amp;rsquo;a aussi indiqué qu&amp;rsquo;il y a un paramètre nginx pour hériter les headers&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://nginx.org/en/docs/http/ngx_http_headers_module.html#add_header_inherit" target="_blank" rel="noopener"
&gt;https://nginx.org/en/docs/http/ngx_http_headers_module.html#add_header_inherit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Ce blog est (enfin) multilingue</title><link>https://blog.zwindler.fr/2025/11/06/blog-multilingue-enfin/</link><pubDate>Thu, 06 Nov 2025 12:00:00 +0200</pubDate><guid>https://blog.zwindler.fr/2025/11/06/blog-multilingue-enfin/</guid><description>&lt;img src="https://blog.zwindler.fr/2025/11/multilang.webp" alt="Featured image of post Ce blog est (enfin) multilingue" /&gt;&lt;h2 id="une-évolution-qui-simposait"&gt;Une évolution qui s&amp;rsquo;imposait
&lt;/h2&gt;&lt;p&gt;Après 15 ans d&amp;rsquo;existence de ce blog, et probablement 6 avec Hugo, j&amp;rsquo;ai &lt;strong&gt;enfin&lt;/strong&gt; activé le mode multilingue propre de Hugo.&lt;/p&gt;
&lt;p&gt;Pourquoi maintenant ? Plusieurs raisons :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;D&amp;rsquo;abord parce que Quentin m&amp;rsquo;a trollé&amp;hellip;&lt;/li&gt;
&lt;li&gt;Mon lectorat est principalement francophone, mais j&amp;rsquo;ai aussi des lecteurs anglophones qui arrivent sur des articles techniques spécifiques. Jusqu&amp;rsquo;à présent, tout était mélangé sur la page d&amp;rsquo;accueil.&lt;/li&gt;
&lt;li&gt;Côté référencement, c&amp;rsquo;était probablement un peu le bazar. Avoir des articles en français et en anglais mélangés sans distinction claire, ce n&amp;rsquo;est probablement pas optimal pour les moteurs de recherche.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Maintenant, chaque langue a son espace :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;blog.zwindler.fr&lt;/strong&gt; → page principale en français (n&amp;rsquo;affiche plus les articles en anglais)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;blog.zwindler.fr/en&lt;/strong&gt; → version anglaise (uniquement les articles en anglais, une dizaine pour le moment)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="urls-conservées"&gt;URLs conservées
&lt;/h2&gt;&lt;p&gt;J&amp;rsquo;aurais pu tout flinguer côté anglais et réécrire toutes les URLs pour rajouter le &amp;ldquo;/en&amp;rdquo;. Ou rajouter des alias avec Hugo (feature bien pratique que j&amp;rsquo;utilise quand je me suis foiré sur le lien mais que je l&amp;rsquo;ai déjà partagé un peu partout).&lt;/p&gt;
&lt;p&gt;Pour l&amp;rsquo;instant, les URLs existantes restent inchangées. Les articles déjà publiés en anglais gardent leur URL sans le &lt;code&gt;/en/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Je verrai pour les prochains articles comment je gère ça.&lt;/p&gt;
&lt;h2 id="limplémentation-technique"&gt;L&amp;rsquo;implémentation technique
&lt;/h2&gt;&lt;p&gt;Pour les curieux, Hugo propose deux façons de gérer le multilingue :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Translation by file name&lt;/strong&gt; : ajouter &lt;code&gt;.en&lt;/code&gt; ou &lt;code&gt;.fr&lt;/code&gt; avant &lt;code&gt;.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Translation by content directory&lt;/strong&gt; : séparer les contenus dans des dossiers distincts&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;J&amp;rsquo;ai opté pour la seconde option, plus claire à mon goût :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;languages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;contentDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;content-en&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;languageName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;English&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;contentDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;content&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;languageName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Français&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Je ne sais pas si ça fonctionne avec &lt;strong&gt;tous&lt;/strong&gt; les thèmes &amp;ldquo;out of the box&amp;rdquo;, mais le miens (&lt;a class="link" href="https://stack.jimmycai.com/" target="_blank" rel="noopener"
&gt;https://stack.jimmycai.com/&lt;/a&gt;) mettait en avant que c&amp;rsquo;était supporté, donc j&amp;rsquo;en déduis qu&amp;rsquo;il faut faire gaffe à ça.&lt;/p&gt;
&lt;h2 id="des-traductions-à-venir-peut-être-"&gt;Des traductions à venir (peut-être ?)
&lt;/h2&gt;&lt;p&gt;Dans les prochains jours ou semaines, je ferai probablement quelques traductions d&amp;rsquo;articles, &lt;strong&gt;dans les deux sens&lt;/strong&gt; :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Anglais → Français&lt;/strong&gt; : certains articles très techniques et niches (comme &lt;a class="link" href="https://blog.zwindler.fr/2024/12/12/recompile-mimir-distributed-grafana-dashboards/" &gt;Recompile Mimir&amp;rsquo;s &amp;ldquo;MetaMonitoring&amp;rdquo; Grafana Dashboards for Kubernetes&lt;/a&gt;) pourraient être traduits en français (ou non, c&amp;rsquo;est quand même giga niche)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Français → Anglais&lt;/strong&gt; : des articles qui marchent bien en français et qui pourraient intéresser un public anglophone plus large&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Je ne me mets &lt;strong&gt;aucun objectif&lt;/strong&gt;, ni aucune pression. Si je trouve qu&amp;rsquo;un article mérite d&amp;rsquo;être traduit et que j&amp;rsquo;ai le temps/l&amp;rsquo;envie, je le ferai. Sinon, tant pis !&lt;/p&gt;
&lt;p&gt;L&amp;rsquo;important, c&amp;rsquo;est d&amp;rsquo;avoir sauté le pas.&lt;/p&gt;
&lt;p&gt;N&amp;rsquo;hésitez pas à me faire vos retours si vous constatez des problèmes de navigation ou d&amp;rsquo;affichage, j&amp;rsquo;ai un peu fait ça à l&amp;rsquo;arrache lundi soir pendant &lt;a class="link" href="https://www.twitch.tv/cuistops" target="_blank" rel="noopener"
&gt;le live des copains Cuistops&lt;/a&gt; !&lt;/p&gt;
&lt;h2 id="références"&gt;Références
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://gohugo.io/content-management/multilingual/" target="_blank" rel="noopener"
&gt;Documentation officielle Hugo - Multilingual Mode&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>3 choses que vous ne savez pas sur Zwindler</title><link>https://blog.zwindler.fr/2025/06/29/3-choses-que-vous-ne-savez-pas-sur-zwindler/</link><pubDate>Sun, 29 Jun 2025 14:00:00 +0200</pubDate><guid>https://blog.zwindler.fr/2025/06/29/3-choses-que-vous-ne-savez-pas-sur-zwindler/</guid><description>&lt;img src="https://blog.zwindler.fr/2025/06/zwindler-2025.webp" alt="Featured image of post 3 choses que vous ne savez pas sur Zwindler" /&gt;&lt;p&gt;Après toutes ces années à vous parler de Kubernetes, de containers et d&amp;rsquo;infrastructure, j&amp;rsquo;ai pensé qu&amp;rsquo;il était temps de vous révéler quelques petits secrets sur votre humble serviteur. Parce que derrière chaque nerd se cache parfois des surprises&amp;hellip;&lt;/p&gt;
&lt;h2 id="1-jai-brièvement-monté-une-entreprise-en-2013-2014-qui-na-jamais-eu-un-seul-client"&gt;1. J&amp;rsquo;ai brièvement monté une entreprise en 2013-2014 qui n&amp;rsquo;a jamais eu un seul client
&lt;/h2&gt;&lt;p&gt;Ah, l&amp;rsquo;entrepreneuriat ! Ce rêve français de monter sa boîte entre copains et de révolutionner le monde. En 2013, avec 4 ex-collègues, nous étions pleins d&amp;rsquo;optimisme et convaincus que nous allions changer la donne.&lt;/p&gt;
&lt;p&gt;L&amp;rsquo;idée ? Créer une ESN (Entreprise de Services du Numérique) qui capitaliserait fortement sur notre expertise en logiciels libres et open source pour faire de l&amp;rsquo;infogérance. Sur le papier, c&amp;rsquo;était génial : nous étions tous compétents techniquement, nous avions de l&amp;rsquo;expérience, et &lt;strong&gt;on se pensait plus malins que tout le monde&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Dovosys était créée !&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/06/dovosys.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Nous avons même intégré une pépinière d&amp;rsquo;entreprise pour nous accompagner dans cette aventure. Mais très vite, la réalité nous a rattrapés&amp;hellip;&lt;/p&gt;
&lt;p&gt;D&amp;rsquo;abord, nous étions tous en poste (et tous insatisfaits de ces postes) mais aucun d&amp;rsquo;entre nous n&amp;rsquo;était prêt à faire les sacrifices nécessaires pour vraiment démarrer l&amp;rsquo;activité. Le mieux aurait été qu&amp;rsquo;un d&amp;rsquo;entre nous démissionne pour bootstraper l&amp;rsquo;entreprise et chercher activement des clients, mais personne n&amp;rsquo;a osé franchir le pas. Donc tout était fait en cachette sur les pauses ou le soir. &lt;em&gt;Fake it until you make it&lt;/em&gt; ça marche pas souvent super bien.&lt;/p&gt;
&lt;p&gt;Ensuite, prendre des décisions à 5 (rapidement 4 car une des membres est partie dès le début), c&amp;rsquo;est un cauchemar. Les travaux n&amp;rsquo;étaient pas faits en temps et en heure. Certaines tàches n&amp;rsquo;étaient pas faites en temps et en heure. L&amp;rsquo;accompagnement de la pépinière n&amp;rsquo;était pas extraordinaire non plus, probablement du fait de notre manque de disponibilité en heures ouvrées.&lt;/p&gt;
&lt;p&gt;À vrai dire, le mieux aurait été que je crée la boîte tout seul. Avec le recul, j&amp;rsquo;ai monté toute l&amp;rsquo;infra de virtualisation, écrit la plupart des parties techniques sur les dossiers, pondu la plupart des réponses aux appels d&amp;rsquo;offres publics&amp;hellip;&lt;/p&gt;
&lt;p&gt;Au final, nous n&amp;rsquo;avons jamais eu un seul client, et il a fallu clôturer l&amp;rsquo;entreprise. Sauf que l&amp;rsquo;entreprise n&amp;rsquo;a pas été correctement clôturée et je l&amp;rsquo;ai découvert un an plus tard quand les impôts ont toqué à notre porte&amp;hellip; avec des pénalités de retard (un chiffre d&amp;rsquo;affaire à 0 n&amp;rsquo;empêche pas de payer certaines taxes).&lt;/p&gt;
&lt;p&gt;A ce moment là (autour de 2015), mes associés étant totalement désengagés, j&amp;rsquo;ai dû faire tout le travail administratif de dissolution et liquidation tout seul. Un vrai parcours du combattant que j&amp;rsquo;ai d&amp;rsquo;ailleurs documenté dans deux articles détaillés : &lt;a class="link" href="https://blog.zwindler.fr/2015/11/25/dissolution-amiable-sas-mode-emploi/" &gt;la dissolution&lt;/a&gt; et &lt;a class="link" href="https://blog.zwindler.fr/2015/12/26/liquidation-amiable-sas-mode-emploi/" &gt;la liquidation&lt;/a&gt;, probablement plus à jour mais représentatif de ma galère.&lt;/p&gt;
&lt;p&gt;Bref, cette aventure est une leçon d&amp;rsquo;humilité qui m&amp;rsquo;a appris que l&amp;rsquo;entrepreneuriat, ce n&amp;rsquo;est pas si facile, et que ce n&amp;rsquo;est certainement pas que de la technique. S&amp;rsquo;associer, en particulier avec des copains, c&amp;rsquo;est probablement souvent plus compliqué que prévu.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Je ne dirais pas que c&amp;rsquo;est un échec, je dirais que ça&amp;hellip; ouais, en fait si, c&amp;rsquo;est un échec.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Les deux seuls points positifs que j&amp;rsquo;en retire :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;avoir pu rencontrer des gens qui ont eu la même idée, en même temps, mais qui eux s&amp;rsquo;en sont sortis. Coucou Alexis, la bise à &lt;a class="link" href="https://www.sysnove.fr/apropos" target="_blank" rel="noopener"
&gt;Sysnove&lt;/a&gt; (je ne sais pas si tu te souviens de moi, cela dit).&lt;/li&gt;
&lt;li&gt;avoir pu en profiter pour me payer un avocat du travail pour décortiquer mon contrat de travail de l&amp;rsquo;époque (abusif sur plusieurs points). J&amp;rsquo;ai appris plusieurs tips sur le droit du travail à cette occasion :-p.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note : j&amp;rsquo;ai tout gardé, les fichiers et les documents, je retombe dessus parfois, ça fait bizarre.&lt;/p&gt;
&lt;h2 id="2-jai-longtemps-souffert-danxiété-sociale-que-jai-soignée-à-grand-coup-de-conférences-tech"&gt;2. J&amp;rsquo;ai longtemps souffert d&amp;rsquo;anxiété sociale, que j&amp;rsquo;ai &amp;ldquo;soignée&amp;rdquo; à grand coup de conférences tech
&lt;/h2&gt;&lt;h3 id="une-timidité-handicapante-cétait-pas-ça-le-bon-terme"&gt;Une &lt;em&gt;timidité&lt;/em&gt;* handicapante (*c&amp;rsquo;était pas ça le bon terme)
&lt;/h3&gt;&lt;p&gt;Voilà quelque chose que beaucoup de mes collègues ou amis ont du mal à croire quand je le raconte : je suis quelqu&amp;rsquo;un qui était, petit, très timide et réservé. Aller parler à des gens &lt;strong&gt;que je connais&lt;/strong&gt; est parfois difficile, et alors les gens que je ne connais pas, n&amp;rsquo;en parlons pas. J&amp;rsquo;avais peu d&amp;rsquo;amis, en partie parce que garçon dans la cours de récrée dans les années 90, il fallait parler fort, faire le chef, n&amp;rsquo;avoir peur de rien. Des &lt;em&gt;trucs de garçon&lt;/em&gt;, quoi. Tout l&amp;rsquo;inverse de moi.&lt;/p&gt;
&lt;p&gt;Pendant longtemps, je me suis donc catégorisé comme &amp;ldquo;timide&amp;rdquo; faute de mieux. Je ne savais pas décrire ce que je ressentais réellement. Plus tard, j&amp;rsquo;ai aussi cru que j&amp;rsquo;étais &amp;ldquo;introverti&amp;rdquo;, mais ce n&amp;rsquo;était toujours pas ça (je le suis un peu aussi, mais ce n&amp;rsquo;est pas le fond du problème). En réalité, le vrai &amp;ldquo;mal&amp;rdquo; qui m&amp;rsquo;handicapait, c&amp;rsquo;était probablement plutôt de l&amp;rsquo;anxiété sociale, mais j&amp;rsquo;ai découvert ce terme très tard.&lt;/p&gt;
&lt;p&gt;Devenu adulte, cette &amp;ldquo;timidité&amp;rdquo; est devenue franchement handicapante. Dans mon premier travail, il fallait parfois que j&amp;rsquo;appelle des clients et l&amp;rsquo;idée de décrocher le téléphone pour les appeler (et peut-être les déranger, ou pire, me tromper de numéro !) m&amp;rsquo;angoissait, au point de ne pas traiter les tickets dans les SLA fixés.&lt;/p&gt;
&lt;p&gt;Une autre anecdote qui illustre bien mes difficultés : un soir, mes amis m&amp;rsquo;ont demandé de commander des pizzas. Je n&amp;rsquo;ai pas pu le faire en ligne, donc il fallait téléphoner. J&amp;rsquo;étais tellement mal à l&amp;rsquo;aise que j&amp;rsquo;ai appelé d&amp;rsquo;une toute petite voix, comme un enfant qui aurait fait une bêtise.&lt;/p&gt;
&lt;p&gt;Mes copains m&amp;rsquo;ont charrié (ne sachant pas que c&amp;rsquo;était un obstacle difficile pour moi) pendant des années en mimant régulièrement cette scène et en déformant mes paroles : &amp;ldquo;Bonjour, je m&amp;rsquo;appelle Denis et je voudrais commander des pizzas&amp;rdquo;, avec une petite voix de gamin penaud.&lt;/p&gt;
&lt;p&gt;Ça ne m&amp;rsquo;a jamais fait rire.&lt;/p&gt;
&lt;h3 id="le-déclic-des-conférences-tech"&gt;Le déclic des conférences tech
&lt;/h3&gt;&lt;p&gt;Le truc qui m&amp;rsquo;a vraiment aidé, c&amp;rsquo;est de commencer à faire des conférences tech. J&amp;rsquo;étais en admiration pour certains speakers, en particulier ceux se qualifiant ouvertement d&amp;rsquo;introvertis. Si eux pouvaient le faire, moi aussi, non ?&lt;/p&gt;
&lt;p&gt;Et je me suis rapidement rendu compte que c&amp;rsquo;était une excellente idée. Quand on monte sur scène, une fois le trac initial passé, la foule n&amp;rsquo;est pas vraiment effrayante, c&amp;rsquo;est une masse informe et silencieuse. Mais surtout, être speaker et avoir des difficultés sociales &lt;strong&gt;confère un super pouvoir&lt;/strong&gt; : celui de ne pas avoir à aller VERS les gens. C&amp;rsquo;est eux qui viennent vers vous, et pour vous parler du sujet dont vous venez de parler et que &lt;strong&gt;vous&lt;/strong&gt; maîtrisez.&lt;/p&gt;
&lt;p&gt;Cette technique m&amp;rsquo;a permis de parler avec une quantité incroyable d&amp;rsquo;inconnus (et même de me faire quelques copains de conf) et aujourd&amp;rsquo;hui, j&amp;rsquo;ai beaucoup moins de mal à aller voir un groupe de personnes en pleine discussion et à entamer la conversation. Même si ça me demande toujours un effort et que je me sens toujours un peu idiot, avec cette peur de déranger.&lt;/p&gt;
&lt;p&gt;L&amp;rsquo;ironie, c&amp;rsquo;est que celui qui n&amp;rsquo;arrivait pas à commander des pizzas au téléphone donne des confs devant 800 personnes ou tape la discute avec le PDG de l&amp;rsquo;entreprise qui l&amp;rsquo;emploie !&lt;/p&gt;
&lt;p&gt;Alors ok, ça fait un peu &amp;ldquo;bullshit LinkedIn&amp;rdquo; mais cette anecdote est 100% vraie, comme les deux autres.&lt;/p&gt;
&lt;h2 id="3-doù-vient-mon-pseudo-zwindler"&gt;3. D&amp;rsquo;où vient mon pseudo &amp;ldquo;zwindler&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;Last but not least, l&amp;rsquo;origine de mon pseudo ! J&amp;rsquo;ai eu cette question tellement de fois que j&amp;rsquo;ai décidé de l&amp;rsquo;immortaliser ici. Certaines personnes que j&amp;rsquo;ai rencontrées en vrai après qu&amp;rsquo;on a échangé sur les réseaux sociaux pensaient même que &amp;ldquo;zwindler&amp;rdquo; était mon nom de famille !&lt;/p&gt;
&lt;p&gt;En fait, ça vient tout simplement d&amp;rsquo;un jeu vidéo, mais l&amp;rsquo;histoire est plus complexe qu&amp;rsquo;un ado qui a choisi un handle au hasard, avec des Z et des R pour faire cool.&lt;/p&gt;
&lt;p&gt;Ça date de l&amp;rsquo;époque où j&amp;rsquo;étais étudiant et que je jouais beaucoup à Counter-Strike Source. Malgré des centaines d&amp;rsquo;heures de jeu (j&amp;rsquo;avais beaucoup trop de temps libre lol), j&amp;rsquo;étais assez mauvais, même sur des serveurs pas particulièrement compétitifs.&lt;/p&gt;
&lt;p&gt;Pour sortir mon épingle du jeu et éviter la frustration de toujours me faire &amp;ldquo;headshot&amp;rdquo; en suivant les tactiques classiques comme tout le monde, je me suis mis à expérimenter : me déplacer dans la map de façon non conventionnelle, me cacher dans des coins inhabituels, utiliser des armes boudées par les autres joueurs (pas de Deagle + M4 + HE + casque pour moi).&lt;/p&gt;
&lt;p&gt;Je ne vais pas mentir, souvent ça ne marchait pas, en particulier contre des &amp;ldquo;pros&amp;rdquo;. Mais de temps en temps, ça me permettait quelques coups d&amp;rsquo;éclat où j&amp;rsquo;arrivais à renverser le cours de la partie quasiment à moi tout seul, grâce à l&amp;rsquo;effet de surprise et un peu de créativité (genre, balayer 5 ou 6 membres de l&amp;rsquo;équipe adverse à moi tout seul en 12v12).&lt;/p&gt;
&lt;p&gt;Mes amis ont pris l&amp;rsquo;habitude de m&amp;rsquo;appeler &amp;ldquo;l&amp;rsquo;escroc&amp;rdquo; car ils étaient agacés de perdre dans ces conditions. Le surnom est resté et j&amp;rsquo;ai voulu l&amp;rsquo;angliciser en &amp;ldquo;swindler&amp;rdquo;. Le pseudo étant déjà pris sur de nombreux réseaux sociaux, j&amp;rsquo;ai donc simplement changé &amp;ldquo;swindler&amp;rdquo; en &amp;ldquo;zwindler&amp;rdquo; (et parfois zwindl3r quand zwindler est pris, comme sur discord ou twitch par exemple).&lt;/p&gt;
&lt;p&gt;Et puis les années ont passé, le pseudo a suivi mes pérégrinations numériques : ce blog, Twitter, GitHub&amp;hellip; Et aujourd&amp;rsquo;hui, il n&amp;rsquo;est pas rare qu&amp;rsquo;on me connaisse mieux sous le pseudo &amp;ldquo;zwindler&amp;rdquo; que sous mon vrai prénom ! C&amp;rsquo;est d&amp;rsquo;ailleurs pour cette raison que j&amp;rsquo;avais fait imprimer des stickers avec écrit &lt;strong&gt;@zwindler&lt;/strong&gt; pour les coller sur les badges de confs. Je ne compte plus le nombre de &amp;ldquo;Aaah mais c&amp;rsquo;est TOI zwindler !&amp;rdquo; que j&amp;rsquo;ai entendu.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/06/zwindler.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Fun fact : en 2015, mes collègues blaguaient que j&amp;rsquo;étais un &amp;ldquo;expert en malinerie&amp;rdquo;, à la fois en référence à Denis la Malice (ça m&amp;rsquo;a suivi un moment), mais aussi et surtout pour ma capacité à trouver des solutions créatives à des problèmes complexes. Trait que j&amp;rsquo;ai su conserver en milieu pro :)&lt;/p&gt;
&lt;p&gt;Voilà, maintenant vous savez tout !&lt;/p&gt;
&lt;p&gt;Enfin, presque. Il faut bien que je garde quelques mystères pour les prochains articles&amp;hellip;&lt;/p&gt;</description></item><item><title>15 ans que je partage mes bidules sur ce blog</title><link>https://blog.zwindler.fr/2025/04/28/blog-zwindler-fr-a-15-ans/</link><pubDate>Mon, 28 Apr 2025 18:00:00 +0000</pubDate><guid>https://blog.zwindler.fr/2025/04/28/blog-zwindler-fr-a-15-ans/</guid><description>&lt;img src="https://blog.zwindler.fr/2019/12/nyanonimous_rond_850x550.webp" alt="Featured image of post 15 ans que je partage mes bidules sur ce blog" /&gt;&lt;h2 id="ce-blog-à-15-ans-et-une-semaine-parce-que-je-suis-à-la-bourre"&gt;Ce blog à 15 ans (et une semaine&amp;hellip; parce que je suis à la bourre)
&lt;/h2&gt;&lt;p&gt;Ca me parait assez incroyable de me dire que j&amp;rsquo;ai un projet perso et hobby depuis 15 ans&amp;hellip;&lt;/p&gt;
&lt;p&gt;Je pense que je n&amp;rsquo;ai jamais fait quelque chose aussi longtemps de ma vie. Je ne fais de la course à pied &amp;ldquo;que&amp;rdquo; depuis 10 ans, par exemple. Je suis resté dans une entreprise au maximum 6 ans (pour l&amp;rsquo;instant).&lt;/p&gt;
&lt;p&gt;Et en vrai, je n&amp;rsquo;imagine même pas qu&amp;rsquo;un jour, j&amp;rsquo;arrêterai.&lt;/p&gt;
&lt;h2 id="le-temps-passe"&gt;Le temps passe
&lt;/h2&gt;&lt;p&gt;Je ne regarde même plus vraiment les stats. Un peu, par curiosité, surtout depuis que je teste &lt;a class="link" href="https://hakanai.io/" target="_blank" rel="noopener"
&gt;https://hakanai.io/&lt;/a&gt; (clin d&amp;rsquo;oeil pas très discret ;P).&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/04/hakanai.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;J&amp;rsquo;ai beaucoup moins de retours depuis que j&amp;rsquo;ai coupé les commentaires en passant en full statique et je ne suis pas spécialement triste. Bien sûr, je suis toujours hyper content quand quelqu&amp;rsquo;un me dit qu&amp;rsquo;il/elle aime bien me lire, ou que j&amp;rsquo;ai pu aider avec un article.&lt;/p&gt;
&lt;p&gt;Mais ne pas en avoir ne me démotive pas.&lt;/p&gt;
&lt;p&gt;La course à être le premier dans la page Google ne m&amp;rsquo;intéresse plus (pas sûr que ça m&amp;rsquo;ait jamais importé cela dit ?). J&amp;rsquo;ai plus envie qu&amp;rsquo;on me donne de l&amp;rsquo;argent &lt;strong&gt;pour me récompenser&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Et c&amp;rsquo;est tant mieux :)&lt;/p&gt;
&lt;h2 id="les-blogs-disparaissent"&gt;Les blogs disparaissent
&lt;/h2&gt;&lt;p&gt;C&amp;rsquo;est le constat que je faisais dans cet article plus tôt en début d&amp;rsquo;année (&lt;a class="link" href="https://blog.zwindler.fr/2025/01/15/ca-bouge-encore-sur-le-blog/" &gt;Ça bouge encore sur le blog&lt;/a&gt;), suite à un gros nettoyage des liens morts sur ces pages.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/04/liensmorts.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Alors, oui, OK. Dans le tas, il y avait &lt;em&gt;surtout&lt;/em&gt; des pages de KB VMware, Dell ou HP complètement obsolètes.&lt;/p&gt;
&lt;p&gt;Mais dans le tas, il y a aussi tout un tas de blogs de gens que j&amp;rsquo;admirais depuis des années. Je pense même que certains admins de ces blogs ne sont plus parmi nous, pas juste leurs blogs.&lt;/p&gt;
&lt;p&gt;Désolé pour le coup de blues.&lt;/p&gt;
&lt;h2 id="au-secours-mon-manager-me-demande-des-kpis"&gt;&amp;ldquo;Au secours, mon manager me demande des KPIs&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;(Ce n&amp;rsquo;est pas un message pour toi Raph, c&amp;rsquo;est un clin d&amp;rsquo;oeil à un talk :-P)&lt;/p&gt;
&lt;p&gt;Même si j&amp;rsquo;ai plus les yeux rivés dessus, j&amp;rsquo;aime quand même un peu les chiffres.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;En 15 ans de blog, j&amp;rsquo;ai atteint les &lt;strong&gt;500&lt;/strong&gt; articles (quasiment&amp;hellip; 497, et 3 brouillons). Ces dernières années, j&amp;rsquo;en écris environ 40 par an. Chaque article prend entre 2 et 10 heures à écrire, veille techno et tests inclus.&lt;/li&gt;
&lt;li&gt;Ca représente plus de 580000 mots, presque 49 heures de lecture non-stop (à 200 mots par minutes).&lt;/li&gt;
&lt;li&gt;Dans le top3 des articles les plus consultés, il y a ma fameuse (world famous!) &lt;a class="link" href="https://blog.zwindler.fr/2024/01/14/pancakes-banane-faciles/" &gt;recette de pancakes sans PLV&lt;/a&gt; (le trafic vient surtout de Google) et je pense que cette info absurde est représentative de ma personnalité.&lt;/li&gt;
&lt;li&gt;Il y a environ 20k vues sur ce blog tous les mois, ce qui est stable depuis 2019, année où le blog &amp;ldquo;a arrêté de croitre&amp;rdquo; en popularité.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="autres-supports--formats-"&gt;Autres supports / formats ?
&lt;/h2&gt;&lt;p&gt;De temps en temps, je me demande si je ne devrais pas essayer d&amp;rsquo;autres formats (plus populaires ?).&lt;/p&gt;
&lt;p&gt;Mais :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Je suis trop perfectionniste pour faire des lives casses-gueules comme les copains de Cuistops par exemple.&lt;/li&gt;
&lt;li&gt;Je n&amp;rsquo;aime pas trop ma voix, et ce n&amp;rsquo;est pas terrible pour un podcast, moyen pour youtube.&lt;/li&gt;
&lt;li&gt;J&amp;rsquo;aime bien les confs, surtout pour les discussions qui suivent, mais je veux pas en faire mon métier.&lt;/li&gt;
&lt;li&gt;J&amp;rsquo;ai écrit un livre, c&amp;rsquo;est long XD.&lt;/li&gt;
&lt;li&gt;Je n&amp;rsquo;ai pas encore testé de donner des cours, mais c&amp;rsquo;est sur la &amp;ldquo;bucket list&amp;rdquo;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Donc, je pense que mon blog a encore de beaux jours devant lui.&lt;/p&gt;
&lt;p&gt;De toute façon, le plus grand lecteur de mon propre blog, c&amp;rsquo;est probablement moi ;-).&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;15 ans, ça mérite bien un gros cadeau pour mes lecteurs / lectrices, non ?&lt;/p&gt;
&lt;p&gt;J&amp;rsquo;avais déjà théoriquement libéré tout mon travail en lui appliquant une licence Creative Commons BY-SA 4.0. Pour aller jusqu&amp;rsquo;au bout de la démarche, j&amp;rsquo;ouvre maintenant les sources de mon blog :)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/zwindler/blog.zwindler.fr" target="_blank" rel="noopener"
&gt;github.com/zwindler/blog.zwindler.fr&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Vous pouvez tout copier, même pour des projets commerciaux*. Have fun!&lt;/p&gt;
&lt;p&gt;*dans la limite de la CC BY-SA, c&amp;rsquo;est-à-dire que dans vos modifications, il faut me citer comme auteur de l&amp;rsquo;oeuvre d&amp;rsquo;origine et partager vos modifications dans les mêmes conditions. Plus d&amp;rsquo;infos sur l&amp;rsquo;article &amp;ldquo;&lt;a class="link" href="https://blog.zwindler.fr/2024/10/03/blog-creative-commons" &gt;Le contenu du blog passe en Creative Commons CC BY-SA 4.0&lt;/a&gt;&amp;rdquo;.&lt;/p&gt;</description></item><item><title>Ça bouge encore sur le blog</title><link>https://blog.zwindler.fr/2025/01/15/ca-bouge-encore-sur-le-blog/</link><pubDate>Wed, 15 Jan 2025 20:00:00 +0200</pubDate><guid>https://blog.zwindler.fr/2025/01/15/ca-bouge-encore-sur-le-blog/</guid><description>&lt;img src="https://blog.zwindler.fr/2019/12/nyanonimous_rond_850x550.webp" alt="Featured image of post Ça bouge encore sur le blog" /&gt;&lt;p&gt;À la toute fin 2019 (il y a cinq ans à 2 semaines près donc, c&amp;rsquo;est marrant comme j&amp;rsquo;ai une façon cyclique dans mon écriture), j&amp;rsquo;écrivais un long article pour détailler les nombreux (à l&amp;rsquo;époque) changements opérés sur le blog.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2019/12/24/ca-bouge-pas-mal-sur-le-blog/" &gt;Ça bouge pas mal sur le blog !&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Les plus gros d&amp;rsquo;entre eux étant la suppression de toute forme de tracking intrusif, en particulier ceux des boites comme Google (AMP, Analytics, Adwords), Amazon (liens sponsorisés) et le début d&amp;rsquo;un virage vers l&amp;rsquo;abandon de Wordpress (qui m&amp;rsquo;aura finalement nécessité quelques années de plus pour être totalement terminé, on en reparlera).&lt;/p&gt;
&lt;h2 id="creative-commons-cc-by-sa-40"&gt;Creative Commons (CC BY-SA 4.0)
&lt;/h2&gt;&lt;blockquote&gt;
&lt;p&gt;Je passe tout le contenu qui m&amp;rsquo;appartient sur ce blog sous licence Creative Commons&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Voilà la décision aussi militante que radicale que j&amp;rsquo;ai prise le 3 octobre dernier, suite à un énième vol de propriété intellectuelle entre blogueurs et le drama qui en a suivi :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2024/10/03/blog-creative-commons" &gt;Le contenu du blog passe en Creative Commons CC BY-SA 4.0&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Rétrospectivement, je me demande vraiment pourquoi j&amp;rsquo;ai attendu aussi longtemps pour le faire, car cette décision colle parfaitement à ma philosophie de vie depuis plusieurs années.&lt;/p&gt;
&lt;p&gt;Ce n&amp;rsquo;est peut-être pas le cas de tout le monde (c&amp;rsquo;est leur droit), mais ce site n&amp;rsquo;existe pas pour me faire gagner de l&amp;rsquo;argent.&lt;/p&gt;
&lt;p&gt;Je n&amp;rsquo;ai aucune raison de le monétiser, quelle qu&amp;rsquo;en soit la forme.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Son but unique est le partage de connaissance&lt;/strong&gt;. Et je n&amp;rsquo;ai pas la prétention d&amp;rsquo;être le meilleur ou de connaître la meilleure façon de diffuser cette connaissance.&lt;/p&gt;
&lt;p&gt;TL;DR : vous pouvez donc &lt;strong&gt;copier, améliorer, redistribuer tout ou partie de ce blog, et même tirer directement ou indirectement des revenus&lt;/strong&gt;, tant que je suis cité quelque part comme l&amp;rsquo;auteur initial. Ça me fait plaisir si vous en profitez. Enjoy.&lt;/p&gt;
&lt;h2 id="pas-dia"&gt;Pas d&amp;rsquo;IA
&lt;/h2&gt;&lt;p&gt;J&amp;rsquo;ai adhéré au AI manifesto, popularisé par &lt;a class="link" href="https://bsky.app/profile/cassidoo.co" target="_blank" rel="noopener"
&gt;Cassidy Williams / cassidoo&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Je pars du principe que si je ne prends pas la peine d&amp;rsquo;écrire moi-même le contenu de ce blog, vous ne devriez pas prendre la peine de le lire.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Je sais que &lt;strong&gt;cette décision est plus clivante&lt;/strong&gt; que le passage du blog en CC BY-SA 4.0, et que plusieurs personnes dans ma sphère tech sont en opposition avec ce principe. Là aussi, c&amp;rsquo;est leur droit.&lt;/p&gt;
&lt;p&gt;Moi, j&amp;rsquo;adhère totalement à cette vision.&lt;/p&gt;
&lt;p&gt;Aucun de mes articles n&amp;rsquo;est écrit par l&amp;rsquo;IA. Ils ne sont même pas relus par des IA pour les &amp;ldquo;améliorer&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Note pour plus tard, je vais aussi essayer également de ne plus générer de visuels avec l&amp;rsquo;IA pour des problématiques de propriété intellectuelle.&lt;/p&gt;
&lt;h2 id="fin-de-la-migration-wordpress-vers-hugo-et-ménage-dans-les-liens-morts"&gt;Fin de la migration Wordpress vers Hugo et ménage dans les liens morts
&lt;/h2&gt;&lt;p&gt;Lorsque j&amp;rsquo;avais entamé la migration Wordpress vers Hugo en 2020, je n&amp;rsquo;avais pas nettoyé tout le code qui avait été importé de Wordpress. Pas mal d&amp;rsquo;images restaient dans un format bâtard non-markdown, avec des balises HTML chelou.&lt;/p&gt;
&lt;p&gt;De la même manière, ça doit faire plus de 10 ans que je n&amp;rsquo;ai pas fait une passe sur mon site pour nettoyer les liens morts derrière moi.&lt;/p&gt;
&lt;p&gt;Comme je suis quelqu&amp;rsquo;un d&amp;rsquo;un peu têtu/bourrin, je me suis mis en tête qu&amp;rsquo;il était temps de finaliser cette migration, que j&amp;rsquo;ai quasiment faite d&amp;rsquo;une traite (je n&amp;rsquo;ai écrit que 2 articles entre le début et la fin). Quelques exemples :&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/01/html-to-markdown.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/01/lien-mort.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Même à coup de regex et de tooling python pour automatiser le plus possible, cette énorme passe de nettoyage a duré plusieurs sessions de blogging, étalées sur plusieurs semaines (probablement 10 heures de travail). Quelques stats pour vous donner une idée :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;360 fichiers modifiés (sur un peu moins de 500 articles)&lt;/li&gt;
&lt;li&gt;3600 lignes modifiées&lt;/li&gt;
&lt;li&gt;plus de 200 liens morts&lt;/li&gt;
&lt;li&gt;plus de 120 contenus retrouvés (souvent manuellement) sur Internet Archive&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Je ne remercie pas du tout HPE, VMware, Dell EMC, Microsoft, d&amp;rsquo;avoir supprimé la totalité des ressources sur lesquelles j&amp;rsquo;ai bloggé dans les années 2015 et +. Toutes les docs, les forums, les KBs, les fichiers de configuration ont été annihilés. Si une partie de ces contenus étaient effectivement obsolètes, ce n&amp;rsquo;était pas le cas de tous.&lt;/p&gt;
&lt;p&gt;Je ne remercie pas non plus les boites comme Oracle, SAP ou autre qui ont racheté de plus petites entités et détruit des billets de blogs corporate pourtant toujours utiles aujourd&amp;rsquo;hui et que je n&amp;rsquo;ai pas toujours pu retrouver sur Internet Archive, malheureusement.&lt;/p&gt;
&lt;p&gt;Je ne remercie toujours pas les entreprises ou les logiciels open source tels que Ansible, pfSense ou encore XWiki (que j&amp;rsquo;apprécie par ailleurs) qui ont décidé un jour de changer l&amp;rsquo;arborescence de leur site / leur documentation officielle sans mettre en place de redirection correcte&amp;hellip;&lt;/p&gt;
&lt;p&gt;Enfin, j&amp;rsquo;ai pu constater avec tristesse que de très nombreux blogs de petites entreprises ou de particuliers, dont certains que j&amp;rsquo;aimais beaucoup, avaient disparu d&amp;rsquo;Internet sans laisser de traces&amp;hellip; Souvent autour de 2020&amp;hellip; J&amp;rsquo;espère ne pas y voir un signe lugubre 😔.&lt;/p&gt;
&lt;h2 id="au-revoir-clever-cloud"&gt;Au revoir, Clever Cloud
&lt;/h2&gt;&lt;p&gt;Depuis que j&amp;rsquo;ai migré de Wordpress à Hugo (changement que je bénis chaque jour&amp;hellip; que c&amp;rsquo;était horrible ce CMS de la mort), j&amp;rsquo;ai un peu hésité sur le provider pour héberger le blog statique (et l&amp;rsquo;infra pour le construire).&lt;/p&gt;
&lt;p&gt;J&amp;rsquo;ai fait de l&amp;rsquo;auto-hébergé, &lt;a class="link" href="https://vercel.com/" target="_blank" rel="noopener"
&gt;vercel&lt;/a&gt;, &lt;a class="link" href="https://froggit.fr/" target="_blank" rel="noopener"
&gt;froggit (gitlab runners + gitlab pages)&lt;/a&gt;, de l&amp;rsquo;auto-hébergé encore. Puis finalement, pour tester le produit des copains, j&amp;rsquo;ai essayé &lt;a class="link" href="https://www.clever-cloud.com/fr/" target="_blank" rel="noopener"
&gt;Clever Cloud&lt;/a&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2023/12/30/its-migration-day-again" &gt;Migration du blog sur Clever Cloud&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Au début, j&amp;rsquo;étais plutôt content, et &lt;a class="link" href="https://blog.zwindler.fr/recherche/?keyword=clever&amp;#43;cloud" target="_blank" rel="noopener"
&gt;j&amp;rsquo;ai même fait quelques articles&lt;/a&gt; de plus pour améliorer mon expérience (sur les conseils avisés de David). Mais au fil des mois, je me suis rendu compte que l&amp;rsquo;expérience utilisateur n&amp;rsquo;y était pas.&lt;/p&gt;
&lt;p&gt;Pousser un commit sur un remote git spécial pour mettre à jour mon blog, ou alors déclencher une github action au commit, qui lance la CLI avec un token, qui relance un build, ce n&amp;rsquo;est pas dingue.&lt;/p&gt;
&lt;p&gt;Et avoir une facture entre 7 et 8€ par mois (que j&amp;rsquo;aurais pu optimiser un peu, certes) pour ne pas avoir de compatibilité IPv6 (&lt;a class="link" href="https://blog.zwindler.fr/2021/03/01/le-blog-en-ipv6-ca-aurait-du-etre-simple-et-pourtant/" target="_blank" rel="noopener"
&gt;une régression par rapport à mon site auto-hébergé en 2021&lt;/a&gt;), et autant de configuration manuelle et de &amp;ldquo;moving parts&amp;rdquo; pouvant planter à chaque commit, ce n&amp;rsquo;était vraiment pas pour moi.&lt;/p&gt;
&lt;p&gt;Le dernier mois, j&amp;rsquo;ai eu de nombreux plantages, que ce soit la partie Github action, la partie build qui ne rendait pas la main, ou la partie Run qui ne se lançait pas correctement, ce qui laissait mes commits errer dans les limbes (et le compteur de temps d&amp;rsquo;instance M qui va avec).&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/01/clever.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Je ne pense pas qu&amp;rsquo;admettre que le produit n&amp;rsquo;était pas pour moi ne remette d&amp;rsquo;aucune façon en question la qualité de ce qui est fait chez Clever en tant que PaaS. Mais il était grand temps de repartir sur un setup plus simple à base d&amp;rsquo;auto-hébergement, que j&amp;rsquo;avais heureusement documenté (sur ce blog, ahahah) et que j&amp;rsquo;ai remonté en moins d&amp;rsquo;une soirée :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2023/08/01/automatiser-hugo-sans-github-action/" target="_blank" rel="noopener"
&gt;Automatiser son site Hugo sans Github Action ou Vercel&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ce blog est donc hébergé sur une VM ultra cheap à 1,20€ par mois chez IONOS, se rebuild en moins d&amp;rsquo;une minute à chaque commit via un webhook et est compatible IPv6 sans effort.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/01/ipv6.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;À ce prix-là, je ne m&amp;rsquo;attends pas à un très bon support utilisateur en cas de problème. Mais en réalité, je n&amp;rsquo;ai pas de SLO à tenir et je peux migrer n&amp;rsquo;importe où en très peu de temps (la preuve avec les multiples migrations faites ces cinq dernières années).&lt;/p&gt;
&lt;h2 id="analytics"&gt;Analytics
&lt;/h2&gt;&lt;p&gt;Pour ne pas m&amp;rsquo;embêter avec le RGPD (et ne pas avoir de bandeau à mettre), et n&amp;rsquo;utilisant même pas 1% des capacités de Matomo, j&amp;rsquo;avais retiré toute forme d&amp;rsquo;analytics sur un coup de tête.&lt;/p&gt;
&lt;p&gt;Sur les conseils de Pierre, j&amp;rsquo;ai quand même remis le minimum l&amp;rsquo;an dernier (j&amp;rsquo;ai un an tout pile d&amp;rsquo;historique maintenant) &lt;a class="link" href="https://www.goatcounter.com/" target="_blank" rel="noopener"
&gt;goatcounter.com&lt;/a&gt;, un site d&amp;rsquo;analytics gratuit qu&amp;rsquo;on peut auto-héberger ou utiliser gratuitement en SaaS.&lt;/p&gt;
&lt;p&gt;C&amp;rsquo;est moche, mais ça marche. Et ça me permet de savoir les articles qui marchent le mieux, &lt;strong&gt;comme ma fameuse recette des pancakes à la banane sans PLV&lt;/strong&gt; (j&amp;rsquo;en parlais dans l&amp;rsquo;article &lt;a class="link" href="https://blog.zwindler.fr/2024/12/31/dans-le-retro-2024" &gt;Dans le rétro de 2024 : trop de side projects&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/01/goat.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Et comme c&amp;rsquo;est un peu moche et que je suis &lt;a class="link" href="https://eventuallycoding.com/" target="_blank" rel="noopener"
&gt;les aventures d&amp;rsquo;Hugo Lassiège&lt;/a&gt; avec attention, je teste aussi en parallèle son tout dernier produit d&amp;rsquo;analytics spécial blogueurs &lt;a class="link" href="https://blogtally.com/" target="_blank" rel="noopener"
&gt;BlogTally&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;L&amp;rsquo;objectif étant de faire un outil de web analytics mais dédié aux bloggers&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Je vous en dirais plus quand j&amp;rsquo;aurai de quoi comparer par rapport à toutes les solutions que j&amp;rsquo;ai pu tester jusqu&amp;rsquo;à présent.&lt;/p&gt;</description></item><item><title>Être speaker en conférence ne triplera pas votre salaire (sauf exceptions)</title><link>https://blog.zwindler.fr/2024/04/28/conference-tripler-son-salaire/</link><pubDate>Sun, 28 Apr 2024 12:00:00 +0200</pubDate><guid>https://blog.zwindler.fr/2024/04/28/conference-tripler-son-salaire/</guid><description>&lt;img src="https://blog.zwindler.fr/2024/04/riche.webp" alt="Featured image of post Être speaker en conférence ne triplera pas votre salaire (sauf exceptions)" /&gt;&lt;p&gt;Note : cet article est la version longue / version litteraire, d&amp;rsquo;un long thread Twitter (lien mort, j&amp;rsquo;ai quitté Twitter), et du débat qui a suivi. Il s&amp;rsquo;agit de mon expérience personnelle et tout le monde n&amp;rsquo;est pas d&amp;rsquo;accord avec mon point de vue. YMMV comme disent les anglosaxons.&lt;/p&gt;
&lt;h2 id="marronnier-et-contexte"&gt;Marronnier et contexte
&lt;/h2&gt;&lt;p&gt;Début avril, j&amp;rsquo;ai lu un message qui m&amp;rsquo;a fait tiquer sur Twitter.&lt;/p&gt;
&lt;p&gt;Grosso modo, une personne (j&amp;rsquo;irai même jusqu&amp;rsquo;à dire &amp;ldquo;un habitué&amp;rdquo;) raconte que &amp;ldquo;les gens&amp;rdquo; qui vont faire des talks en conférence, le font pour l&amp;rsquo;argent, allant même jusqu&amp;rsquo;à affirmer qu&amp;rsquo;un speaker lui aurait secrètement avoué que depuis qu&amp;rsquo;il était speaker, son salaire avait été multiplié par 3.&lt;/p&gt;
&lt;p&gt;Argent, théorie du complot, société secrète. Tout ce qu&amp;rsquo;il faut pour buzzer sur X.&lt;/p&gt;
&lt;p&gt;Alors bien sûr, quand d&amp;rsquo;autres personnes viennent expliquer que non, &lt;strong&gt;eux le font par passion&lt;/strong&gt;, l&amp;rsquo;énergumène leur répond par un GIF d&amp;rsquo;une personne qui nage dans des billets de banque avec comme légende &amp;ldquo;la passion en question&amp;rdquo; (ça vous donne une idée du niveau de l&amp;rsquo;argumentaire du bonhomme).&lt;/p&gt;
&lt;p&gt;Donc allons-y : qu&amp;rsquo;est-ce que j&amp;rsquo;en pense, moi ?&lt;/p&gt;
&lt;h2 id="me-myself-and-i"&gt;Me, myself and I
&lt;/h2&gt;&lt;p&gt;Je suis speaker depuis 2018, avec une trentaine de talks, quelques podcasts, quelques interviews à mon actif. J&amp;rsquo;imagine que je rentre donc dans la catégorie des &amp;ldquo;habitués&amp;rdquo; des confs en France dont on parle ici.&lt;/p&gt;
&lt;p&gt;Plus récemment, je suis aussi organisateur de conférences (KCD France 2023, BDX I/O 2024).&lt;/p&gt;
&lt;p&gt;Et avant ça, je blogue ici depuis 2010, avec plus de 400 articles techniques, des milliers d&amp;rsquo;heures (sur mon temps personnel) à faire de la veille, tester des trucs, rédiger, poster, améliorer ce blog. Un investissement en temps et en énergie considérable.&lt;/p&gt;
&lt;p&gt;Pour rappel (ou info ???), &lt;strong&gt;tout ceci est entièrement bénévole&lt;/strong&gt;. L&amp;rsquo;écosystème tech français ne rémunère pas les speakers ou les orgas en conférence (sauf exceptions très rares). &lt;strong&gt;Je ne tire pas un centime de cet engagement personnel&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Et la question est donc : est-ce qu&amp;rsquo;en échange de tout cet engagement personnel pour la communauté, j&amp;rsquo;ai multiplié mon salaire par 3 depuis 2018 ?&lt;/p&gt;
&lt;p&gt;TL;DR : non. J&amp;rsquo;ai un très (très) bon salaire, mais clairement, je n&amp;rsquo;ai pas triplé mon salaire depuis 2018. Ni même doublé. Et les confs n&amp;rsquo;y sont pour rien dans les augmentations que j&amp;rsquo;ai eues (le full remote a eu bien plus d&amp;rsquo;impact, par exemple).&lt;/p&gt;
&lt;h2 id="back-to-reality"&gt;Back to reality
&lt;/h2&gt;&lt;p&gt;On passe notre temps à nous dire qu&amp;rsquo;il faudrait avoir des side projects pour se différencier au moment du tri des CVs ou pendant les entretiens.&lt;/p&gt;
&lt;p&gt;La réalité que &lt;strong&gt;moi&lt;/strong&gt;, j&amp;rsquo;ai expérimenté est tout autre.&lt;/p&gt;
&lt;p&gt;La plupart du temps, lors des entretiens que j&amp;rsquo;ai pu passer ces cinq dernières années, la mention (pourtant bien visible sur le CV et j&amp;rsquo;insiste oralement) de mon blog est &lt;strong&gt;ignorée&lt;/strong&gt;. Ça me met un peu en rogne, car j&amp;rsquo;en retire une grande fierté.&lt;/p&gt;
&lt;p&gt;Pour ce qui est de l&amp;rsquo;expérience en tant qu&amp;rsquo;organisateur de conférences ou de mon &amp;ldquo;habitude&amp;rdquo; d&amp;rsquo;aller en conférence partager des sujets, dans la grande majorité des cas, c&amp;rsquo;est aussi ignoré. Au pire, c&amp;rsquo;est vu comme une contrainte plus qu&amp;rsquo;un atout. Pour illustrer ça, on m&amp;rsquo;a dit lors d&amp;rsquo;un processus de recrutement :&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Denis, c&amp;rsquo;est bon ! J&amp;rsquo;ai négocié avec le CTO, tu pourras &lt;strong&gt;aller&lt;/strong&gt; en conférence de temps en temps.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Euh&amp;hellip; Merci ?&lt;/p&gt;
&lt;p&gt;Note : pour être parfaitement honnête, il y a eu deux exceptions, où c&amp;rsquo;était vu comme quelque chose de chouette, mais avec un peu de méfiance quand même, notamment sur la fréquence ou sur le hype driven development.&lt;/p&gt;
&lt;p&gt;Pour beaucoup d&amp;rsquo;entreprises, sortir du cadre de ce qui est prévu pour les salariés est impensable. Et comme beaucoup n&amp;rsquo;ont pas ou plus (post COVID) de stratégie concernant la marque employeur ou les contributions externes à l&amp;rsquo;écosystème tech, aller en conférence présenter le travail de l&amp;rsquo;entreprise n&amp;rsquo;est pas vu comme une partie de mon travail en tant que &amp;ldquo;staff engineer&amp;rdquo;, mais comme un &amp;ldquo;privilège&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Mon actuel employeur accepte que j&amp;rsquo;aille en conférence et que je prépare &lt;em&gt;en partie&lt;/em&gt; mes sujets sur mon temps de travail, et me paie le transport et l&amp;rsquo;hébergement pour aller à ces conférences.&lt;/p&gt;
&lt;p&gt;C&amp;rsquo;est déjà &lt;strong&gt;mieux que beaucoup de speakers&lt;/strong&gt; qui doivent tout faire sur leur temps perso, et parfois doivent poser des congés, voire se payer les transports eux même quand ils sont pris comme speaker. Certains speakers dépensent beaucoup d&amp;rsquo;argent pour partager leur passion et ça me fait ch**r qu&amp;rsquo;on fasse croire qu&amp;rsquo;ils le font pour s&amp;rsquo;enrichir, alors que c&amp;rsquo;est l&amp;rsquo;inverse.&lt;/p&gt;
&lt;h2 id="tout-nest-pas-à-jeter-non-plus"&gt;Tout n&amp;rsquo;est pas à jeter non plus
&lt;/h2&gt;&lt;p&gt;Il y a cependant des avantages réels à participer à des conférences en tant que speaker.&lt;/p&gt;
&lt;p&gt;D&amp;rsquo;abord, ça force à bosser et se documenter sur des sujets que vous pensez maîtriser. Le risque à ne pas le faire, c&amp;rsquo;est que vous disiez une énormité et qu&amp;rsquo;une personne vous le fasse bruyamment remarquer. Pas super agréable. Donc, on se documente et on apprend beaucoup.&lt;/p&gt;
&lt;p&gt;Autre avantage indéniable, ça entraîne à la prise de parole.&lt;/p&gt;
&lt;p&gt;Ça peut paraître difficile à croire, car j&amp;rsquo;ai l&amp;rsquo;air à l&amp;rsquo;aise en public aujourd&amp;rsquo;hui, mais j&amp;rsquo;ai longtemps été paralysé en groupe par une &lt;strong&gt;phobie sociale&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Aller en conférence est un moyen (certes un peu brutal) de dépasser ce genre d&amp;rsquo;angoisses : après le talk, les gens viennent vers vous d&amp;rsquo;eux-mêmes, vous n&amp;rsquo;avez pas à combattre l&amp;rsquo;angoisse de leur parler, et en plus, ça sera un sujet que vous maîtriserez.&lt;/p&gt;
&lt;h2 id="le-vrai-game-changer-le-réseau"&gt;Le vrai &amp;ldquo;game changer&amp;rdquo;, le réseau
&lt;/h2&gt;&lt;p&gt;Dernier point, qui a été évoqué sur Twitter à un moment : ça agrandira considérablement votre réseau.&lt;/p&gt;
&lt;p&gt;Toutes les opportunités intéressantes que j&amp;rsquo;ai eu récemment, j&amp;rsquo;ai su qu&amp;rsquo;elles existaient grâce à mon réseau.&lt;/p&gt;
&lt;p&gt;Attention, ça ne veut pas dire que j&amp;rsquo;ai directement eu le poste ! J&amp;rsquo;ai passé les entretiens comme les autres et j&amp;rsquo;ai ensuite eu le poste, ou pas !&lt;/p&gt;
&lt;p&gt;J&amp;rsquo;ai &lt;strong&gt;juste eu connaissance&lt;/strong&gt; de ces opportunités GRÂCE à mon réseau, sans passes droits.&lt;/p&gt;
&lt;p&gt;Cependant, il y a 1000 autres façons de se faire un réseau pro. Les conférences ne sont qu&amp;rsquo;une manière parmi d&amp;rsquo;autres et je ne vois personne s&amp;rsquo;insurger contre ça. Pourquoi se faire un réseau en conférence serait une mauvaise chose (spoiler, ça n&amp;rsquo;en est pas) ?&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;On en a beaucoup discuté sur Twitter et j&amp;rsquo;ai échangé avec des gens pour qui faire des conférences avaient eu un impact significatif sur leur carrière. Tant mieux pour elles/eux, c&amp;rsquo;est mérité !&lt;/p&gt;
&lt;p&gt;Mais je pense qu&amp;rsquo;il ne s&amp;rsquo;agit pas de la mentalité majoritaire des entreprises, en particulier en France.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sauf exceptions, faire un amphi bleu DevoxxFR ne vous rendra pas riche&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Tripler votre salaire (ou même juste l&amp;rsquo;augmenter) n&amp;rsquo;est pas une fin en soi. À vous de savoir si vous avez besoin de cet argent ou pas. Avoir un salaire à six chiffres peut quand même faire de vous un individu triste comme les pierres.&lt;/p&gt;
&lt;p&gt;Bref : ne soyez pas speaker (ou écrire un blog, ou écrire un livre tech) parce que vous pensez que vous allez en retirer de l&amp;rsquo;argent. Faites-le parce que partager sa connaissance, c&amp;rsquo;est hyper gratifiant et ça vaut tous les dramas Twitter.&lt;/p&gt;</description></item></channel></rss>