<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Hugo on Zwindler's Reflection</title><link>https://blog.zwindler.fr/tags/hugo/</link><description>Recent content in Hugo 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/hugo/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>J'ai donné 1 heure à des agents Copilot pour migrer un site de Bloggrify à Hugo</title><link>https://blog.zwindler.fr/2025/12/02/jai-donn%C3%A9-1-heure-%C3%A0-des-agents-copilot-pour-migrer-un-site-de-bloggrify-%C3%A0-hugo/</link><pubDate>Tue, 02 Dec 2025 18:00:00 +0200</pubDate><guid>https://blog.zwindler.fr/2025/12/02/jai-donn%C3%A9-1-heure-%C3%A0-des-agents-copilot-pour-migrer-un-site-de-bloggrify-%C3%A0-hugo/</guid><description>&lt;img src="https://blog.zwindler.fr/2025/12/playwright1.webp" alt="Featured image of post J'ai donné 1 heure à des agents Copilot pour migrer un site de Bloggrify à Hugo" /&gt;&lt;h2 id="tldr-pour-les-pressés"&gt;TL;DR pour les pressés
&lt;/h2&gt;&lt;p&gt;J&amp;rsquo;ai fait migrer un site statique de Bloggrify vers Hugo par GitHub Copilot Agent.&lt;/p&gt;
&lt;p&gt;Les règles ? Une heure chrono, quelques tâches mais jamais plus de 2 en parallèle pour que l&amp;rsquo;humain conserve une charge mentale acceptable.&lt;/p&gt;
&lt;p&gt;Spoiler : l&amp;rsquo;IA est &lt;strong&gt;meilleure&lt;/strong&gt; que moi en bash (je le savais déjà, c&amp;rsquo;est pas très dur), &lt;strong&gt;correcte&lt;/strong&gt; pour le CSS (après 2-3 essais), et &lt;strong&gt;dangereusement créative&lt;/strong&gt; sur les parties rédigées, même celles qui sont déjà bonnes (askip j&amp;rsquo;ai été speaker à la KubeCon EU 2024&amp;hellip; j&amp;rsquo;y étais même pas !).&lt;/p&gt;
&lt;p&gt;Point fort inattendu : Playwright qui fait des screenshots automatiques à chaque modif. &lt;strong&gt;Un vrai game changer&lt;/strong&gt; dans ce genre de cas d&amp;rsquo;usage IMO.&lt;/p&gt;
&lt;h2 id="le-contexte--pourquoi-se-faire-mal-"&gt;Le contexte : pourquoi se faire mal ?
&lt;/h2&gt;&lt;p&gt;Si vous suivez le blog, vous savez que j&amp;rsquo;ai publié un livre (Kubernetes : 50 solutions pour les postes de développement et les clusters de production), et vous savez aussi qu&amp;rsquo;il y a un site séparé pour la promotion du livre : &lt;a class="link" href="https://50ndk.zwindler.fr/" target="_blank" rel="noopener"
&gt;50ndk.zwindler.fr&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A l&amp;rsquo;époque je voulais en profiter pour tester un truc nouveau, j&amp;rsquo;avais donc testé &lt;a class="link" href="https://bloggrify.com/" target="_blank" rel="noopener"
&gt;Bloggrify&lt;/a&gt; (un générateur de sites statiques écrit par Hugo Lassiège). Sauf qu&amp;rsquo;à l&amp;rsquo;usage, je suis perdu dans l&amp;rsquo;écosystème JS, et je me suis dit que j&amp;rsquo;allais le migrer vers Hugo avec le thème &lt;a class="link" href="https://github.com/CaiJimmy/hugo-theme-stack" target="_blank" rel="noopener"
&gt;Stack&lt;/a&gt; (exactement le même que ce blog).&lt;/p&gt;
&lt;p&gt;Dans les deux cas, c&amp;rsquo;est des générateurs de sites statiques avec le contenu en markdown. Théoriquement, c&amp;rsquo;est pas compliqué à faire.&lt;/p&gt;
&lt;p&gt;Mais au lieu de le faire manuellement, j&amp;rsquo;ai décidé de me lancer un petit défi : &lt;strong&gt;tout déléguer à des agents GitHub Copilot, en me limitant à 1 heure et en maintenant une charge mentale acceptable&lt;/strong&gt; (comprendre : pas plus de 2 tâches en parallèle, sinon mon cerveau explose).&lt;/p&gt;
&lt;p&gt;J&amp;rsquo;ai donc ouvert Copilot, choisi Claude Sonnet 4.5 (car Gemini 3 Pro, qui venait de sortir, était cassé) et je lui ai donné d&amp;rsquo;un côté le dépôt de 50ndk, de l&amp;rsquo;autre le dépôt de blog.zwindler.fr et je lui ai donné un court prompt du type :&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Ce site a été créé à partir d&amp;rsquo;un outil appelé bloggrify.
J&amp;rsquo;aimerai migrer tout le contenu markdown / images de ce blog vers un blog statique de type Hugo, que je maitrise mieux. Bases toi sur @zwindler/blog.zwindler.fr pour la structure et le thème à utiliser&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="phase-1--la-migration-technique-15-minutes"&gt;Phase 1 : La migration technique (15 minutes)
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Ce qui a marché tout seul&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Première bonne impression : la migration de base a pris 15 minutes. Copilot Agent a :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Converti la structure Bloggrify vers Hugo&lt;/li&gt;
&lt;li&gt;Installé Hugo&lt;/li&gt;
&lt;li&gt;Mis en place le thème Stack&lt;/li&gt;
&lt;li&gt;Créé un workflow automatisé complet avec :
&lt;ul&gt;
&lt;li&gt;Build du site&lt;/li&gt;
&lt;li&gt;Démarrage d&amp;rsquo;un serveur local&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Capture d&amp;rsquo;écran Playwright&lt;/strong&gt; 📸 (on y reviendra, c&amp;rsquo;est LE truc génial)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Nettoyer tout ce qui avait un rapport avec Bloggrify / Javascript&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Résultat : un site fonctionnel du premier coup. Mais pas parfait !&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/12/playwright1.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Les premiers soucis (détectés par l&amp;rsquo;humain&amp;hellip; aka moi)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;En regardant le screenshot généré, j&amp;rsquo;ai repéré deux trucs bofs :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Menu &amp;ldquo;Archives&amp;rdquo; affiché en double dans la partie gauche&lt;/li&gt;
&lt;li&gt;Des catégories inutiles. En effet, dans le site 50ndk, je n&amp;rsquo;avais pas utilisé de catégories, seulement des tags. Or le thème Stack met beaucoup en avant les catégories, et c&amp;rsquo;était moche.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;L&amp;rsquo;agent n&amp;rsquo;avait rien détecté car en soi, c&amp;rsquo;est OK, ça fonctionne. Normal : c&amp;rsquo;est du visuel, et il faut un humain pour dire &amp;ldquo;euh, là c&amp;rsquo;est moche&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Les corrections (7 min + 4 min)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Une fois que je lui ai signalé les problèmes, l&amp;rsquo;agent a :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;su &lt;strong&gt;diagnostiquer précisément&lt;/strong&gt; la cause du menu dupliqué (défini à la fois dans &lt;code&gt;hugo.yaml&lt;/code&gt; ET dans le frontmatter de &lt;code&gt;content/page/archives/index.md&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;corrigé en ~7 minutes&lt;/li&gt;
&lt;li&gt;réglé le problème des catégories en ~4 minutes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;L&amp;rsquo;agent est bon pour débugger&amp;hellip; une fois que l&amp;rsquo;humain a détecté le bug :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Agent génère&lt;/li&gt;
&lt;li&gt;Screenshot Playwright&lt;/li&gt;
&lt;li&gt;Humain vérifie puis formule un feedback&lt;/li&gt;
&lt;li&gt;Agent corrige&lt;/li&gt;
&lt;li&gt;et on itère comme ça&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sans Playwright, j&amp;rsquo;aurais dû manuellement builder, lancer le serveur, rafraîchir le navigateur à chaque itération. &lt;strong&gt;L&amp;rsquo;automatisation de cette boucle, c&amp;rsquo;est un game changer absolu&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id="phase-2--la-page-à-propos-20-25-min-et-des-hallucinations"&gt;Phase 2 : La page &amp;ldquo;À propos&amp;rdquo; (20-25 min, et des &amp;ldquo;&amp;ldquo;&amp;ldquo;hallucinations&amp;rdquo;&amp;rdquo;&amp;rdquo;)
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Ce qui a bien commencé&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Deuxième tâche : améliorer la page &amp;ldquo;À propos&amp;rdquo;. L&amp;rsquo;agent a :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Analysé le contexte (mes articles, ma page whoami sur blog.zwindler.fr)&lt;/li&gt;
&lt;li&gt;Pondu une première version correcte, bien structurée&lt;/li&gt;
&lt;li&gt;Su ajouter des images quand je lui ai demandé&lt;/li&gt;
&lt;li&gt;Été capable de faire des ajustements rapides sur demande (&amp;ldquo;double la longueur&amp;rdquo;) ou seul, &amp;ldquo;j&amp;rsquo;au corrigé la faute de frappe &amp;lsquo;20240&amp;rsquo; → &amp;lsquo;2024&amp;rsquo;&amp;rdquo;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Jusque-là, RAS. Puis&amp;hellip;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Quand l&amp;rsquo;IA devient romancier&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Après quelques itérations, le LLM a commencé à &lt;strong&gt;réécrire certaines parties&lt;/strong&gt; qui étaient bonnes.&lt;/p&gt;
&lt;p&gt;Genre, carrément :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;ldquo;Participation à la KubeCon EU 2024 en tant qu&amp;rsquo;organisateur/speaker&amp;rdquo;&lt;/li&gt;
&lt;li&gt;Changé le nom du livre et l&amp;rsquo;éditeur&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;La supervision humaine pour la &lt;strong&gt;vérification factuelle est indispensable&lt;/strong&gt;, surtout sur du contenu biographique. J&amp;rsquo;ai dû corriger plusieurs fois en mode &amp;ldquo;non, cette partie est fausse&amp;rdquo;.&lt;/p&gt;
&lt;h2 id="phase-3--les-tâches-techniques-et-là-ça-roule"&gt;Phase 3 : Les tâches techniques (et là, ça roule)
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Ajout de scripts de tracking&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Tâche : intégrer &lt;a class="link" href="https://hakanai.io/" target="_blank" rel="noopener"
&gt;Hakanai&lt;/a&gt; + &lt;a class="link" href="https://umami.is/" target="_blank" rel="noopener"
&gt;Umami&lt;/a&gt; sur toutes les pages.&lt;/p&gt;
&lt;p&gt;L&amp;rsquo;agent a :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Créé un partial template dans &lt;code&gt;layouts/partials/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Inclus automatiquement dans le layout principal&lt;/li&gt;
&lt;li&gt;Placé les scripts au bon endroit (avant &lt;code&gt;&amp;lt;/body&amp;gt;&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;C&amp;rsquo;est ce que j&amp;rsquo;aurais fait aussi. Nickel, RAS.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Optimisation d&amp;rsquo;image (succès&amp;hellip; mais&amp;hellip; pourquoi ???)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Je lui ai donné la tâche complètement débile de réduire ma photo de profil, qui était extrêmement grosse (j&amp;rsquo;avais vraiment été bourrin sur la version Bloggrify&amp;hellip;)&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Tâche : réduire &lt;code&gt;avatar.jpg&lt;/code&gt; de 1.3 Mo à ~250 Ko.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;L&amp;rsquo;agent l&amp;rsquo;a fait. Techniquement, ✅. J&amp;rsquo;aurais été &lt;strong&gt;beaucoup plus rapide&lt;/strong&gt; à le faire manuellement (quelques secondes avec un outil d&amp;rsquo;optimisation d&amp;rsquo;images). Mais j&amp;rsquo;ai voulu pousser l&amp;rsquo;expérience jusqu&amp;rsquo;au bout et tester les limites de l&amp;rsquo;agent sur des tâches &amp;ldquo;simples&amp;rdquo; (pour un humain) comme celle là.&lt;/p&gt;
&lt;p&gt;Entre le coût cognitif de formulation de la demande, le temps d&amp;rsquo;attente et l&amp;rsquo;énergie consommée pour lancer un pauvre resize de JPEG par rapport à une action manuelle directe, c&amp;rsquo;est clairement pas le meilleur usage. Mais ça fonctionne.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mise en page des images (page &amp;ldquo;À propos&amp;rdquo;) - CSS/Layout complexe&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Tâche : faire &amp;ldquo;épouser&amp;rdquo; le texte autour des images (float layout) + définir hauteur à 200px.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/12/playwright2.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;L&amp;rsquo;agent a proposé plusieurs itérations avant d&amp;rsquo;arriver à une version qui fonctionnait. Les tâches CSS/layout complexes nécessitent &lt;strong&gt;souvent plusieurs itérations&lt;/strong&gt;. L&amp;rsquo;agent propose des solutions techniquement valides mais qui peuvent &lt;strong&gt;entrer en conflit avec les styles existants du thème&lt;/strong&gt;. La validation visuelle via screenshot est indispensable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Widget &amp;ldquo;Commander le livre&amp;rdquo; dans la barre de droite&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Tâche : ajouter un widget dans la sidebar avec liens vers vendeurs (Eyrolles, Cultura, Amazon, Fnac).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Là encore, il a fallu plusieurs tentatives pour avoir un truc qui fonctionne sur Hugo / mon thème. La première itération était KO. Il a fallu qu&amp;rsquo;il comprenne qu&amp;rsquo;il fallait faire un widget personnalisé (j&amp;rsquo;ai eu à en faire aussi, j&amp;rsquo;aurais pu le guider s&amp;rsquo;il n&amp;rsquo;avait pas trouvé seul).&lt;/p&gt;
&lt;p&gt;L&amp;rsquo;agent a créé :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;layouts/partials/widget/book-links.html&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;HTML + CSS inline cohérent avec le thème Stack&lt;/li&gt;
&lt;li&gt;Modification de &lt;code&gt;hugo.yaml&lt;/code&gt; pour référencer &lt;code&gt;book-links&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Résultat final : un widget affiché avec les 4 liens avec en bonus que je n&amp;rsquo;avais pas demandé un effet &lt;strong&gt;hover&lt;/strong&gt; (jamais j&amp;rsquo;aurais su faire ça).&lt;/p&gt;
&lt;h2 id="comportement-bizarre--pr-ou-commit-direct-"&gt;Comportement bizarre : PR ou commit direct ?
&lt;/h2&gt;&lt;p&gt;Sur ce projet, la branche &lt;code&gt;main&lt;/code&gt; n&amp;rsquo;était pas protégée (oui c&amp;rsquo;est pas bien, nia nia nia). Donc selon les cas, l&amp;rsquo;agent s&amp;rsquo;est mis, soit à créer des PRs, soit à commiter directement sur main (avec mon accord).&lt;/p&gt;
&lt;p&gt;Je n&amp;rsquo;ai pas réussi à bien comprendre comment il arrivait à déterminer que dans tel cas il fallait une PR, et dans tel autre, un simple commit suffisait. Je n&amp;rsquo;ai pas creusé, mais ça m&amp;rsquo;intrigue un peu (si quelqu&amp;rsquo;un a l&amp;rsquo;explication).&lt;/p&gt;
&lt;p&gt;Dans tous les cas, cette question est probablement un peu bêbette, le mieux, c&amp;rsquo;est de protéger &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="un-paas-ça-aide"&gt;Un PaaS, ça aide
&lt;/h2&gt;&lt;p&gt;Petit point &lt;strong&gt;bonus&lt;/strong&gt; qui a bien aidé pour cette migration, le fait que le site ait été hébergé sur Clever Cloud (sur un PaaS plus généralement, car ça aurait probablement été vrai sur d&amp;rsquo;autres PaaS).&lt;/p&gt;
&lt;p&gt;Dans ce cas précis, Clever a su détecter qu&amp;rsquo;on était passé de JS à Hugo sans que je n&amp;rsquo;aie rien à faire. Ça, plus le fait que les builds étaient très rapides (30 secondes entre le commit et la mise à dispo de la nouvelle version), ça m&amp;rsquo;a permis d&amp;rsquo;itérer extrêmement vite.&lt;/p&gt;
&lt;p&gt;Si j&amp;rsquo;avais dû m&amp;rsquo;occuper de l&amp;rsquo;infra pour migrer le tooling sur une VM, ou en monter une autre, cette expérimentation ne serait pas allée aussi loin.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;La découverte majeure de l&amp;rsquo;expérimentation, c&amp;rsquo;est sans aucun doute possible le fait que Copilot utilise &lt;strong&gt;Playwright&lt;/strong&gt; pour tester, et quand il est content de son résultat, pouvoir afficher à l&amp;rsquo;humain une capture d&amp;rsquo;écran de ce qu&amp;rsquo;il pense être valide.&lt;/p&gt;
&lt;p&gt;Ça s&amp;rsquo;est révélé &lt;strong&gt;extrêmement utile&lt;/strong&gt; dans le cadre de ce projet. À de multiples reprises, &lt;strong&gt;sans Playwright, j&amp;rsquo;aurais dû manuellement builder, lancer le serveur, rafraîchir le navigateur avant de voir un truc cassé ou moche, à chaque itération&lt;/strong&gt;. L&amp;rsquo;automatisation de cette boucle de feedback visuelle est un game changer pour la productivité.&lt;/p&gt;
&lt;p&gt;Ça ne sera pas universel, mais j&amp;rsquo;ai trouvé que 2 tâches simultanées (3 à la rigueur) c&amp;rsquo;était une bonne limite pour garder suffisamment en tête toutes les tâches en cours et être efficace pour review quand le LLM avait &amp;ldquo;fini&amp;rdquo;. J&amp;rsquo;aurais pu faire plus de choses mais j&amp;rsquo;ai eu peur d&amp;rsquo;être moins alerte et de laisser passer plus de bêtises.&lt;/p&gt;
&lt;p&gt;Cette expérimentation d&amp;rsquo;environ &lt;strong&gt;1 heure&lt;/strong&gt; a démontré que GitHub Copilot Agent est particulièrement efficace pour :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Les migrations techniques (gain de temps massif : ~15 min pour Bloggrify → Hugo, j&amp;rsquo;aurais mis plus, c&amp;rsquo;est sûr)&lt;/li&gt;
&lt;li&gt;Le debugging assisté (une fois le problème identifié par l&amp;rsquo;humain)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Cependant, il nécessite toujours une supervision humaine attentive, voire très attentive pour :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Les faits (ça sortait tellement de nulle part, cette réécriture du contenu alors que je lui demandais de rajouter des images ?!?)&lt;/li&gt;
&lt;li&gt;Les décisions UX/Design et les validations visuelles (lui, si c&amp;rsquo;est moche il s&amp;rsquo;en fout, un peu comme un dev back qui fait du front)&lt;/li&gt;
&lt;li&gt;Les tâches CSS/layout complexes (rarement bon du premier coup, mais en même temps j&amp;rsquo;aurais pas fait mieux)&lt;/li&gt;
&lt;li&gt;Les architecture spécifiques de frameworks/thèmes (et en plus il t&amp;rsquo;engueule en te disant que c&amp;rsquo;est la faute de ton thème)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;J&amp;rsquo;insiste une dernière fois, mais l&amp;rsquo;intégration de Playwright pour les screenshots automatiques crée une boucle de feedback visuel qui transforme radicalement l&amp;rsquo;expérience de développement avec les agents. Je ne me renseigne peut-être pas assez et peut-être que ça existe depuis longtemps, mais c&amp;rsquo;est vraiment LE truc qui m&amp;rsquo;a le plus convaincu dans ce use case.&lt;/p&gt;
&lt;p&gt;Globalement satisfait, ça fait réfléchir. &lt;a class="link" href="https://50ndk.zwindler.fr/" target="_blank" rel="noopener"
&gt;Je vous laisse admirer le résultat&lt;/a&gt; :&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/12/resultat.avif"
loading="lazy"
&gt;&lt;/p&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>Je réessaye les sites statiques (Bloggrify) chez Clever Cloud</title><link>https://blog.zwindler.fr/2025/07/07/je-reessaye-les-sites-statiques-chez-clever/</link><pubDate>Mon, 07 Jul 2025 09:00:00 +0200</pubDate><guid>https://blog.zwindler.fr/2025/07/07/je-reessaye-les-sites-statiques-chez-clever/</guid><description>&lt;img src="https://blog.zwindler.fr/2025/07/9zdbra.webp" alt="Featured image of post Je réessaye les sites statiques (Bloggrify) chez Clever Cloud" /&gt;&lt;h2 id="introduction"&gt;Introduction
&lt;/h2&gt;&lt;p&gt;Pour celles et ceux qui suivent mes péripéties d&amp;rsquo;hébergement de blog, vous savez que j&amp;rsquo;ai une relation compliquée (le fameux &amp;ldquo;it&amp;rsquo;s complicated&amp;rdquo; sur Facebook) avec &lt;a class="link" href="https://www.clever-cloud.com/fr/" target="_blank" rel="noopener"
&gt;Clever Cloud&lt;/a&gt;. J&amp;rsquo;ai déjà écrit à ce sujet dans deux articles précédents :&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;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2024/01/29/planifier-les-posts-clever-cloud/" &gt;Planifier les posts avec Clever Cloud&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pour finalement abandonner Clever Cloud et revenir sur une VM IONOS que je gère moi-même (voir &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;).&lt;/p&gt;
&lt;p&gt;Mais depuis, Clever Cloud a ajouté les &lt;strong&gt;apps statiques&lt;/strong&gt; à son catalogue. Plus besoin de passer par un runtime Apache + PHP, ce qui était effectivement un peu overkill pour un site Hugo statique (et pas compatible avec l&amp;rsquo;offre Pico, donc fallait prendre au minimum Nano à ~7€ par mois).&lt;/p&gt;
&lt;p&gt;Du coup, je me suis dit : pourquoi ne pas retenter l&amp;rsquo;expérience ?&lt;/p&gt;
&lt;p&gt;Comme Julien Wittouck m&amp;rsquo;a devancé de 3 semaines et a fait un article sur l&amp;rsquo;hébergement de sites statiques avec Hugo via ce nouveau type d&amp;rsquo;app chez Clever Cloud, &lt;a class="link" href="https://codeka.io/2025/06/05/d%C3%A9ployer-des-applications-statiques-sur-clever-cloud/" target="_blank" rel="noopener"
&gt;vous pouvez aller lire son post ici&lt;/a&gt;, je n&amp;rsquo;ai pas envie de faire la même chose.&lt;/p&gt;
&lt;p&gt;Pour ce test, j&amp;rsquo;ai choisi d&amp;rsquo;utiliser un autre site que celui-ci : &lt;strong&gt;50ndk.zwindler.fr&lt;/strong&gt;, qui me sert pour faire la promotion de mon livre.&lt;/p&gt;
&lt;p&gt;Il est actuellement hébergé sur GitHub Pages et utilise le moteur &lt;a class="link" href="https://bloggrify.io/" target="_blank" rel="noopener"
&gt;Bloggrify&lt;/a&gt; (un des projets d&amp;rsquo;&lt;a class="link" href="https://eventuallycoding.com/" target="_blank" rel="noopener"
&gt;Hugo Lassiège&lt;/a&gt;, quelqu&amp;rsquo;un que j&amp;rsquo;apprécie énormément dans l&amp;rsquo;écosystème tech français).&lt;/p&gt;
&lt;p&gt;Le principe est globalement le même qu&amp;rsquo;avec Hugo, à quelques petites différences près. C&amp;rsquo;est un site statique généré tout pareil, mais c&amp;rsquo;est Nuxt sous le capot et ça va nous être utile. Il y a quelques petites différences aussi depuis mon dernier test de 2023 que je ne manquerai pas de souligner.&lt;/p&gt;
&lt;p&gt;Les sources du site sont disponibles ici si vous êtes curieux / curieuse :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/zwindler/50ndk" target="_blank" rel="noopener"
&gt;https://github.com/zwindler/50ndk&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="prérequis"&gt;Prérequis
&lt;/h2&gt;&lt;p&gt;On pourrait aller créer l&amp;rsquo;application dans l&amp;rsquo;UI. Normalement, les &amp;ldquo;tuiles&amp;rdquo; Static, Linux et VLang sont disponibles à tout le monde depuis le 4 juillet. J&amp;rsquo;ai un petit accès anticipé (merci David) mais je n&amp;rsquo;en ai pas beaucoup profité 🙃 (occupé avec le livre). Grosso modo c&amp;rsquo;est comme les autres types d&amp;rsquo;Apps chez Clever, vous ne serez pas perdus.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/06/cleverl-nouvelles-tuiles.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Bon, en WebUI c&amp;rsquo;est bien mais c&amp;rsquo;est plus rigolo de le faire en CLI.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;clever login
Opening https://console.clever-cloud.com/cli-oauth?cli_version=3.0.2&amp;amp;cli_token=xxxxxxxxxxxxx in your browser to log you in…
Login successful as Denis GERMAIN &amp;lt;denis@domain.org&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/06/clever-login.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Euh, je vois 3.0.2 dans l&amp;rsquo;URL ??? Je ne suis pas à jour. Visiblement j&amp;rsquo;avais installé la CLI clever avec npm (je ne m&amp;rsquo;en souviens pas). Plus simple, &lt;a class="link" href="https://www.clever-cloud.com/developers/doc/cli/install/" target="_blank" rel="noopener"
&gt;il y a aussi des dépôts .deb (ou brew, autre selon votre distribution)&lt;/a&gt;.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;clever version
3.0.2
╭─────────────────────────────────────────╮
│ │
│ Update available 3.0.2 → 3.13.1 │
│ Run npm i -g clever-tools to update │
│ │
╰─────────────────────────────────────────╯
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Un petit &lt;code&gt;npm i -g clever-tools&lt;/code&gt; et ça va mieux :)&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ clever version
3.13.1
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="configuration-de-lapp"&gt;Configuration de l&amp;rsquo;app
&lt;/h2&gt;&lt;p&gt;Un petit &lt;code&gt;clever create&lt;/code&gt; avec le type de l&amp;rsquo;app et c&amp;rsquo;est parti !&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;denis@coucou % clever create --type static
✓ Application 50ndk successfully created!
Type ⬢ Static
ID app_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Org ID user_yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy
Name 50ndk
Zone par
Next steps:
! Commit your changes first:
$ git add .
$ git commit -m &amp;#34;My changes&amp;#34;
→ Run clever deploy to deploy your application
→ Manage your application at: https://console.clever-cloud.com/goto/app_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Note : si votre app est hébergée sur GitHub, vous pouvez rajouter aussi &lt;code&gt;--github OWNER/REPO&lt;/code&gt; à la ligne de commande précédente. J&amp;rsquo;en parle plus tard, mais lisez tout avant de le faire.&lt;/p&gt;
&lt;p&gt;Comme fin 2023, je vais avoir besoin de 2 machines différentes. Une qui va construire mon site statique, une qui va le servir. Et du coup, là comme je n&amp;rsquo;ai pas Apache, je peux avoir accès à une pico et économiser quelques euros à la fin de l&amp;rsquo;année :&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ clever scale --build-flavor M
App rescaled successfully
$ clever scale --flavor pico
App rescaled successfully
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Dans mon test précédent avec Hugo+Clever, j&amp;rsquo;avais besoin de spécifier dans quel dossier le contenu statique doit être servi (et aussi quel dossier la machine qui construit doit partager avec la machine qui sert).&lt;/p&gt;
&lt;p&gt;Dans le cas de Bloggrify, tout est dans .output/public. MAIS comme c&amp;rsquo;est du Nuxt, David m&amp;rsquo;a dit que je n&amp;rsquo;en avais pas besoin parce que Clever détecte automatiquement que c&amp;rsquo;est du Nuxt grâce au fichier &lt;code&gt;nuxt.config.ts&lt;/code&gt; à la racine du projet.&lt;/p&gt;
&lt;p&gt;Inutile donc de spécifier les variables CC_WEBROOT, CC_OVERRIDE_BUILDCACHE (pour pointer sur &lt;code&gt;.output/public&lt;/code&gt;) CC_PRE_BUILD_HOOK et CC_BUILD_COMMAND (les commandes &lt;code&gt;npm&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Plutôt une bonne surprise :).&lt;/p&gt;
&lt;h2 id="déploiement-de-lapplication"&gt;Déploiement de l&amp;rsquo;application
&lt;/h2&gt;&lt;p&gt;À partir de là, on peut essayer de faire un premier déploiement à la main. Pour rappel, soit on envoie un commit sur le remote spécial créé par Clever (cf le message lors de la commande &lt;code&gt;clever create&lt;/code&gt;), mais je vais préférer utiliser la commande &lt;code&gt;clever deploy&lt;/code&gt;, déjà disponible en 2023 lors de mon premier test, car je l&amp;rsquo;utiliserai pour automatiser le workflow plus tard.&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;dgermain@dgermain-mac 50ndk % clever deploy
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;🚀 Deploying 50ndk
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Application ID app_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Org ID user_yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy
&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;🔀 Git information
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ! App is brand new, no commits on remote yet
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Local commit aaaaaa &lt;span class="o"&gt;[&lt;/span&gt;will be deployed&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;La première partie du processus va donc lancer une machine de taille M dans le but d&amp;rsquo;accélérer un peu le temps de construction :&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;🔄 Deployment progress
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → Pushing &lt;span class="nb"&gt;source&lt;/span&gt; code to Clever Cloud…
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ✓ Code pushed to Clever Cloud
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → Waiting &lt;span class="k"&gt;for&lt;/span&gt; deployment to start…
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ✓ Deployment started &lt;span class="o"&gt;(&lt;/span&gt;deployment_zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → Waiting &lt;span class="k"&gt;for&lt;/span&gt; application logs…
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ...
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Clever va détecter tout seul que j&amp;rsquo;ai du Nuxt, lancer un &lt;code&gt;npm install&lt;/code&gt; pour installer les prérequis pour Bloggrify, puis le &lt;code&gt;npm run generate&lt;/code&gt; pour générer le code HTML statique.&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="o"&gt;[&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;2025-07-07T10:23:47.847Z Deploying commit ID defc0be02d44ace246127e11a17d4f359262ccfb
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2025-07-07T10:23:47.847Z Nuxt.js configuration file detected
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2025-07-07T10:23:47.847Z Running build command: npm i &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run generate &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; mv .output/public cc_static_autobuilt
&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="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2025-07-07T10:24:48.809Z &lt;span class="o"&gt;[&lt;/span&gt;nitro&lt;span class="o"&gt;]&lt;/span&gt; ℹ Initializing prerenderer
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2025-07-07T10:24:52.533Z &lt;span class="o"&gt;[&lt;/span&gt;nitro&lt;span class="o"&gt;]&lt;/span&gt; ℹ Prerendering &lt;span class="m"&gt;8&lt;/span&gt; initial routes with crawler
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2025-07-07T10:24:52.598Z &lt;span class="o"&gt;[&lt;/span&gt;nitro&lt;span class="o"&gt;]&lt;/span&gt; ├─ /robots.txt &lt;span class="o"&gt;(&lt;/span&gt;17ms&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2025-07-07T10:24:52.841Z &lt;span class="o"&gt;[&lt;/span&gt;nitro&lt;span class="o"&gt;]&lt;/span&gt; ├─ /200.html &lt;span class="o"&gt;(&lt;/span&gt;296ms&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2025-07-07T10:24:52.842Z &lt;span class="o"&gt;[&lt;/span&gt;nitro&lt;span class="o"&gt;]&lt;/span&gt; ├─ /404.html &lt;span class="o"&gt;(&lt;/span&gt;297ms&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2025-07-07T10:24:52.964Z &lt;span class="o"&gt;[&lt;/span&gt;nitro&lt;span class="o"&gt;]&lt;/span&gt; ├─ /api/search &lt;span class="o"&gt;(&lt;/span&gt;415ms&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2025-07-07T10:24:52.965Z &lt;span class="o"&gt;[&lt;/span&gt;nitro&lt;span class="o"&gt;]&lt;/span&gt; ├─ /sitemap.xml &lt;span class="o"&gt;(&lt;/span&gt;417ms&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2025-07-07T10:24:52.965Z &lt;span class="o"&gt;[&lt;/span&gt;nitro&lt;span class="o"&gt;]&lt;/span&gt; ├─ /rss.xml &lt;span class="o"&gt;(&lt;/span&gt;416ms&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2025-07-07T10:24:53.105Z &lt;span class="o"&gt;[&lt;/span&gt;nitro&lt;span class="o"&gt;]&lt;/span&gt; ├─ / &lt;span class="o"&gt;(&lt;/span&gt;564ms&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="o"&gt;[&lt;/span&gt;...&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Une fois le generate terminé, la machine de construction génère un artefact contenant notre site, et passe la main à la machine qui va servir le trafic :&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;2025-07-07T10:24:55.843Z ✔ You can now deploy .output/public to any static hosting!
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2025-07-07T10:24:56.137Z Creating build cache archive…
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2025-07-07T10:24:56.197Z build cache archive successfully created
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2025-07-07T10:24:56.197Z No cron to setup
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2025-07-07T10:24:56.213Z Uploading application build cache archive… file is 21M before compression.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2025-07-07T10:24:56.899Z 2025-07-07T10:24:56.899385Z INFO multipart_upload_lib::uploader: Uploaded &lt;span class="nv"&gt;part&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2025-07-07T10:24:57.041Z 2025-07-07T10:24:57.041879Z INFO multipart_upload_lib::uploader: Completed the multipart upload
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2025-07-07T10:24:57.350Z Done uploading build cache archive
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2025-07-07T10:24:57.350Z Build succeeded in &lt;span class="m"&gt;1&lt;/span&gt; minute and &lt;span class="m"&gt;14&lt;/span&gt; seconds
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;11 secondes plus tard, le site de promotion de mon livre est déployé sur Clever Cloud&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;2025-07-07T10:25:28.425Z Serving static website from /cc_static_autobuilt
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2025-07-07T10:25:28.425Z Launching &lt;span class="s1"&gt;&amp;#39;static-web-server&amp;#39;&lt;/span&gt; on port &lt;span class="m"&gt;8080&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2025-07-07T10:25:28.425Z No cron to setup
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2025-07-07T10:25:28.425Z Successfully deployed in &lt;span class="m"&gt;0&lt;/span&gt; minutes and &lt;span class="m"&gt;10&lt;/span&gt; seconds
&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;✓ Access your application: https://app-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.cleverapps.io
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ Manage your application: https://console.clever-cloud.com/goto/app_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/07/preview.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;h2 id="ajouter-un-domaine"&gt;Ajouter un domaine
&lt;/h2&gt;&lt;p&gt;Comme chez tous les PaaS, il est évidemment possible d&amp;rsquo;ajouter un domaine de manière à ce que les requêtes entrantes n&amp;rsquo;aient pas besoin de se faire sur l&amp;rsquo;URL en cleverapps.io&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;% clever domain add 50ndk.zwindler.fr
Your domain has been successfully saved
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Une fois le FQDN associé, il est possible d&amp;rsquo;ajouter un CNAME chez votre gestionnaire de nom de domaine :&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/07/cname.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;h2 id="automatiser-le-clever-deploy"&gt;Automatiser le &lt;code&gt;clever deploy&lt;/code&gt;
&lt;/h2&gt;&lt;p&gt;Imaginons que je sois un fainéant ou que je n&amp;rsquo;aie pas envie d&amp;rsquo;installer clever CLI sur tous les postes où je travaille. Admettons que j&amp;rsquo;aie envie qu&amp;rsquo;une nouvelle version de mon site soit automatiquement déployée dès que je pousse un commit sur GitHub.&lt;/p&gt;
&lt;h3 id="deploy-to-clever-cloud"&gt;deploy-to-clever-cloud
&lt;/h3&gt;&lt;p&gt;Dans l&amp;rsquo;article &lt;a class="link" href="https://blog.zwindler.fr/2024/01/29/planifier-les-posts-clever-cloud" &gt;Planifier les posts de mon blog Hugo sur Clever Cloud&lt;/a&gt;, j&amp;rsquo;avais exploré la piste d&amp;rsquo;un Cron pour redéployer régulièrement mon site, notamment pour que les posts publiés dans le futur soient postés &amp;ldquo;au bon moment&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;J&amp;rsquo;avais aussi trouvé une GitHub Action tierce de quelqu&amp;rsquo;un (&lt;a class="link" href="https://github.com/47ng" target="_blank" rel="noopener"
&gt;47ng&lt;/a&gt;) qui a l&amp;rsquo;air très bien mais que je ne connais pas :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/marketplace/actions/deploy-to-clever-cloud" target="_blank" rel="noopener"
&gt;https://github.com/marketplace/actions/deploy-to-clever-cloud&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;J&amp;rsquo;ai testé, elle fonctionne, mais j&amp;rsquo;étais moyen chaud à l&amp;rsquo;époque de déléguer mes creds à Github (les fameuses variables CLEVER_TOKEN et CLEVER_SECRET affichées lors du &lt;code&gt;clever login&lt;/code&gt;), qui en retour les aurait injecté à cette &amp;ldquo;action&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Et ben ça tombe bien parce qu&amp;rsquo;il y a deux nouvelles pour répondre à ces problématiques.&lt;/p&gt;
&lt;h3 id="preview-par-pr"&gt;preview par PR
&lt;/h3&gt;&lt;p&gt;La première, c&amp;rsquo;est qu&amp;rsquo;il existe plusieurs façons d&amp;rsquo;automatiser les déploiements avec Clever Cloud. Je ne sais pas si elle existait à l&amp;rsquo;époque, mais en tout cas je l&amp;rsquo;ai trouvée aujourd&amp;rsquo;hui ;-P.&lt;/p&gt;
&lt;p&gt;On peut faire des environnements de preview par PR (depuis 2024) :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://www.clever-cloud.com/developers/doc/ci-cd/github/" target="_blank" rel="noopener"
&gt;https://www.clever-cloud.com/developers/doc/ci-cd/github/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/marketplace/actions/clever-cloud-review-app-on-prs" target="_blank" rel="noopener"
&gt;https://github.com/marketplace/actions/clever-cloud-review-app-on-prs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Je ne vais pas dans cet article aborder cette action qui permet de déployer des apps en préview &lt;strong&gt;par PR&lt;/strong&gt; car je n&amp;rsquo;en ai pas du tout le besoin pour ce site secondaire. Cela dit, si jamais j&amp;rsquo;ai une grosse mise à jour avec breaking changes, ça pourra valoir le coup de jeter un œil.&lt;/p&gt;
&lt;h3 id="créer-des-tokens-de-ci"&gt;Créer des tokens de CI
&lt;/h3&gt;&lt;p&gt;La seconde, c&amp;rsquo;est qu&amp;rsquo;il est possible de créer des tokens (là encore, je ne sais plus si c&amp;rsquo;était possible en 2023), qu&amp;rsquo;il sera possible de révoquer en cas de leak.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://www.clever-cloud.com/developers/api/howto/#request-the-api" target="_blank" rel="noopener"
&gt;https://www.clever-cloud.com/developers/api/howto/#request-the-api&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Et avec ces tokens, on peut se faire sa propre CI :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://www.clever-cloud.com/developers/doc/ci-cd/custom-scripts/" target="_blank" rel="noopener"
&gt;https://www.clever-cloud.com/developers/doc/ci-cd/custom-scripts/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note : j&amp;rsquo;ai cependant l&amp;rsquo;impression qu&amp;rsquo;il n&amp;rsquo;y a pas encore de scope sur le token. Donc en termes de sécu, c&amp;rsquo;est mieux car je peux les révoquer, mais s&amp;rsquo;il y a un leak, on a accès à tout mon compte, pour l&amp;rsquo;instant (sauf si j&amp;rsquo;ai mal compris).&lt;/p&gt;
&lt;h3 id="déploiements-depuis-github-ou-gitlab"&gt;Déploiements depuis GitHub (ou GitLab)
&lt;/h3&gt;&lt;p&gt;Mais le plus simple ici, c&amp;rsquo;est d&amp;rsquo;utiliser une fonctionnalité de Clever qui existe depuis &amp;ldquo;toujours&amp;rdquo; mais dont j&amp;rsquo;ignorais l&amp;rsquo;existance : les déploiements via Github.&lt;/p&gt;
&lt;p&gt;Si vous voulez utiliser cette fonctionnalité, il faudra par contre autoriser Clever Cloud à lister vos dépôts préalablement (ce qui peut être un problème pour vous), et vous ne pourrez plus pousser des commits avec &lt;code&gt;clever deploy&lt;/code&gt; ou &lt;code&gt;git push&lt;/code&gt; sur le git remote de clever (vous prendrez une erreur 401).&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Clever Cloud provides a GitHub integration to deploy any repository hosted on GitHub to Clever Cloud&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;On configure ça grâce au &lt;code&gt;--github&lt;/code&gt; de tout à l&amp;rsquo;heure !&lt;/p&gt;
&lt;p&gt;A partir du moment où c&amp;rsquo;est fait, il n&amp;rsquo;y a rien à faire&amp;hellip; le rebuild sera déclenché dès que je pousserai un nouveau commit sur ma branche Github :)&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;commit 788465fb980c09e597872a3b510ac44fdaa27d48 (HEAD -&amp;gt; main, origin/main, origin/HEAD)
Author: Denis GERMAIN &amp;lt;denis@example.com&amp;gt;
Date: Fri Jul 4 16:31:11 2025 +0200
test commit
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/07/test-commit.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/07/link.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/07/authorize.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Note : on peut spécifier quelle branche on veut envoyer en prod dans les paramètres de l&amp;rsquo;application :&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/07/github-main.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;Clever Cloud a énormément bougé ces derniers mois, avec beaucoup de nouvelles apps, de nouvelles fonctionnalités (la plus grosse étant Materia, ainsi que tout le travail autour de l&amp;rsquo;IA).&lt;/p&gt;
&lt;p&gt;Les static apps (et Linux, et V) pourraient presque paraître anecdotiques, mais couplées aux autres améliorations (tokens, Grafana managé avec les stats), ce n&amp;rsquo;est pas le même produit qu&amp;rsquo;en 2023.&lt;/p&gt;
&lt;p&gt;C&amp;rsquo;est cool à voir :)&lt;/p&gt;</description></item><item><title>Planifier les posts de mon blog Hugo sur Clever Cloud</title><link>https://blog.zwindler.fr/2024/01/29/planifier-les-posts-clever-cloud/</link><pubDate>Mon, 29 Jan 2024 06:00:00 +0200</pubDate><guid>https://blog.zwindler.fr/2024/01/29/planifier-les-posts-clever-cloud/</guid><description>&lt;img src="https://blog.zwindler.fr/2022/05/clever-trott.webp" alt="Featured image of post Planifier les posts de mon blog Hugo sur Clever Cloud" /&gt;&lt;h2 id="suite-de-la-migration"&gt;Suite de la migration
&lt;/h2&gt;&lt;p&gt;Fin décembre, &lt;a class="link" href="https://blog.zwindler.fr/2023/12/30/its-migration-day-again" &gt;j&amp;rsquo;ai migré ce blog sur Clever Cloud&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Cependant, David Legrand m&amp;rsquo;a fait remarquer que je n&amp;rsquo;avais plus de méthode pour planifier mes posts.&lt;/p&gt;
&lt;p&gt;Depuis quelque temps, je poste à flux tendu donc ça ne m&amp;rsquo;a pas encore gêné (j&amp;rsquo;écris un post, je le rends public dans la foulée) mais c&amp;rsquo;est vrai qu&amp;rsquo;il me manque cette possibilité par rapport à précédent mon hébergement (une VM).&lt;/p&gt;
&lt;p&gt;Cependant, il existe une solution sur Clever Cloud pour le faire. David en a d&amp;rsquo;ailleurs écrit un petit article sur son blog perso, adapté pour &amp;ldquo;Astro&amp;rdquo; son générateur de site statique.&lt;/p&gt;
&lt;p&gt;Je vais donc faire pareil pour mon propre blog avec Hugo.&lt;/p&gt;
&lt;h2 id="adaptations"&gt;Adaptations
&lt;/h2&gt;&lt;p&gt;Repartons donc d&amp;rsquo;où j&amp;rsquo;en étais au post précédent. Nous avons un blog avec Hugo qui se déploie quand on fait des &lt;code&gt;clever deploy&lt;/code&gt; (ou des commits sur le &lt;em&gt;git remote&lt;/em&gt; de clever).&lt;/p&gt;
&lt;p&gt;La première chose qu&amp;rsquo;on va devoir faire, c&amp;rsquo;est récupérer un token. En effet, c&amp;rsquo;est notre blog qui va déclencher de lui même les redéploiements, on va donc devoir lui donner de quoi s&amp;rsquo;authentifier.&lt;/p&gt;
&lt;p&gt;Si vous n&amp;rsquo;avez pas de token sous la main, on peut en retrouver un en faisant &lt;code&gt;clever login&lt;/code&gt; depuis un terminal, comme je l&amp;rsquo;expliquais dans l&amp;rsquo;article précédent.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2023/12/clever-login.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Et on va rajouter le TOKEN dans la liste des variables d&amp;rsquo;environnement de notre app :&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;$ clever env &lt;span class="nb"&gt;set&lt;/span&gt; CC_SECRET &lt;span class="s2"&gt;&amp;#34;aaaaaaaaaaaaaaaaaaaaaaaaaa&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ clever env &lt;span class="nb"&gt;set&lt;/span&gt; CC_TOKEN &lt;span class="s2"&gt;&amp;#34;aaaaaaaaaaaaaaaaaaaaaaaaaa&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A partir de là on va créer plusieurs fichiers. D&amp;rsquo;abord, un script qui va déclencher ce fameux redéploiement :&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; clever_rebuild.sh &lt;span class="s"&gt;&amp;lt;&amp;lt; EOF
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;#!/bin/bash -l
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;clever link ${APP_ID}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;clever restart --quiet --without-cache
&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;span class="line"&gt;&lt;span class="cl"&gt;chmod +x clever_rebuild.sh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ensuite, je vais créer un fichier qui sera lu par clever-cloud et qui va déclencher ce redéploiement aux heures où je publie mes posts planifiés (le matin, un peu avant 9h) :&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;mkdir clevercloud
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;[&amp;#34;00 08 * * * $ROOT/clever_rebuild.sh&amp;#34;]&amp;#39;&lt;/span&gt; &amp;gt; clevercloud/cron.json
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Attention aux petites blaguounettes de timezone. Comme tous sysadmins qui se respectent, les gens de clever utilisent le temps UTC sur leurs serveurs (moi aussi). Cependant, ça nécessite une petite gymnastique intellectuelle si jamais vous n&amp;rsquo;utilisez pas aussi le temps UTC dans votre tête pour planifier les posts ;-P.&lt;/p&gt;
&lt;p&gt;Si tout se passe bien, vous verrez qu&amp;rsquo;il remarque qu&amp;rsquo;il y a un cron dans les logs :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-console" data-lang="console"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;[...]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;2024-01-25T14:05:21+01:00 Importing cronjobs
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;2024-01-25T14:05:21+01:00 Starting crons setup
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;[...]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;2024-01-25T14:06:05+01:00 (CRON) INFO (running with inotify support)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;2024-01-25T14:06:05+01:00 (CRON) INFO (RANDOM_DELAY will be scaled with factor 89% if used.)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;2024-01-25T14:06:05+01:00 (CRON) STARTUP (1.7.0)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;[...]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Tout dernier point que j&amp;rsquo;ai éludé jusqu&amp;rsquo;à présent, il est possible de redémarrer une application à partir d&amp;rsquo;un cache (pour que ça aille plus vite). Cependant, il est nécessaire d&amp;rsquo;indiquer à clever cloud quoi mettre dans ce cache. On va donc ajouter &amp;ldquo;/clevercloud/cron.json:/clever_rebuild.sh&amp;rdquo; à la variable existante :&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;# précédemment CC_OVERRIDE_BUILDCACHE &amp;#34;/public&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ clever env &lt;span class="nb"&gt;set&lt;/span&gt; CC_OVERRIDE_BUILDCACHE &lt;span class="s2"&gt;&amp;#34;/public:/clevercloud/cron.json:/clever_rebuild.sh&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="fail-to-hack-the-template"&gt;(Fail to) hack the template
&lt;/h2&gt;&lt;p&gt;Si Hugo a bien un flag pour permettre de &amp;ldquo;builder&amp;rdquo; les posts dont la date de sortie est postérieure à la date actuelle (&lt;code&gt;buildFuture&lt;/code&gt;), mon thème &amp;ldquo;Stack&amp;rdquo; les affiche par défaut dans la liste des derniers posts ainsi que dans le flux RSS.&lt;/p&gt;
&lt;p&gt;La solution la plus simple à ce problème est de laisser les paramètres par défaut, et d&amp;rsquo;attendre que la date de publication soit dépassée (idéalement juste avant le passage d&amp;rsquo;un cron) et le tour est joué.&lt;/p&gt;
&lt;p&gt;Mais David a eu une autre approche qui m&amp;rsquo;a bien plu : ajouter une condition dans le template pour que le post ne soit pas visible dans la liste des posts, mais quand même publié.&lt;/p&gt;
&lt;p&gt;Avantage : on peut consulter l&amp;rsquo;article à paraître, si on a le lien. Il se rajoute à la liste des articles de la homepage de lui-même lors du prochain rebuild, une fois la date dépassée.&lt;/p&gt;
&lt;p&gt;Dans mon thème, la page d&amp;rsquo;accueil qui liste les posts &lt;a class="link" href="https://github.com/CaiJimmy/hugo-theme-stack/blob/master/layouts/index.html" target="_blank" rel="noopener"
&gt;est définie ici dans le code&lt;/a&gt; :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-diff" data-lang="diff"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; {{ define &amp;#34;main&amp;#34; }}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; {{ $pages := where .Site.RegularPages &amp;#34;Type&amp;#34; &amp;#34;in&amp;#34; .Site.Params.mainSections }}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; {{ $notHidden := where .Site.RegularPages &amp;#34;Params.hidden&amp;#34; &amp;#34;!=&amp;#34; true }}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ {{ $notFuture := where .Site.RegularPages &amp;#34;.Date&amp;#34; &amp;#34;le&amp;#34; now }}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ {{ $filtered := ($pages | intersect $notHidden | intersect $notFuture) }}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- {{ $filtered := ($pages | intersect $notHidden) }}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; {{ $pag := .Paginate ($filtered) }}
&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; &amp;lt;section class=&amp;#34;article-list&amp;#34;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; {{ range $index, $element := $pag.Pages }}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; {{ partial &amp;#34;article-list/default&amp;#34; . }}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; {{ end }}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;/section&amp;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; {{- partial &amp;#34;pagination.html&amp;#34; . -}}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; {{- partial &amp;#34;footer/footer&amp;#34; . -}}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; {{ end }}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[...]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;En théorie, ça fonctionne. Sauf que je n&amp;rsquo;ai pas réussi à le faire marcher avec le thème &amp;ldquo;Stack&amp;rdquo; que j&amp;rsquo;utilise sur le site (j&amp;rsquo;ai réussi avec un thème &amp;ldquo;blank&amp;rdquo;).&lt;/p&gt;
&lt;p&gt;C&amp;rsquo;est peut-être lié à &lt;a class="link" href="https://github.com/gohugoio/hugo/issues/11916#issuecomment-1910462590" target="_blank" rel="noopener"
&gt;un problème d&amp;rsquo;appel multiple à la fonction &lt;code&gt;.Paginate()&lt;/code&gt;&lt;/a&gt;, qui &amp;ldquo;cache&amp;rdquo; la pagination la première fois qu&amp;rsquo;elle est lancée.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you call .Paginator or .Paginate multiple times on the same page, you should ensure all the calls are identical. Once either .Paginator or .Paginate is called while generating a page, its result is cached, and any subsequent similar call will reuse the cached result. This means that any such calls which do not match the first one will not behave as written.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://gohugo.io/templates/pagination/" target="_blank" rel="noopener"
&gt;https://gohugo.io/templates/pagination/&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Même en partant du principe que j&amp;rsquo;arrive à le faire marcher, reste encore la problématique du flux RSS, comme j&amp;rsquo;ai pu le remarquer avec Seboss666 ;-)&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2024/01/seboss666.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;J&amp;rsquo;ai donc laissé tomber cette idée. De toute façon, je n&amp;rsquo;avais jamais vraiment eu besoin jusqu&amp;rsquo;à présent puisque je n&amp;rsquo;y avais pas pensé ;-P.&lt;/p&gt;
&lt;p&gt;J&amp;rsquo;ai donc, comme avant, mes posts &amp;ldquo;planifiés&amp;rdquo; qui sont visible le matin de leur publication et c&amp;rsquo;est déjà bien comme ça (pour l&amp;rsquo;instant).&lt;/p&gt;
&lt;p&gt;Have fun !&lt;/p&gt;</description></item><item><title>Migration du blog sur Clever Cloud</title><link>https://blog.zwindler.fr/2023/12/30/its-migration-day-again/</link><pubDate>Sat, 30 Dec 2023 16:00:00 +0200</pubDate><guid>https://blog.zwindler.fr/2023/12/30/its-migration-day-again/</guid><description>&lt;img src="https://blog.zwindler.fr/2022/05/clever-trott.webp" alt="Featured image of post Migration du blog sur Clever Cloud" /&gt;&lt;h2 id="its-groundhog-migration-day-again"&gt;It’s &lt;del&gt;groundhog&lt;/del&gt; migration day, again
&lt;/h2&gt;&lt;p&gt;Tous les 6 mois environ, une mouche me pique.&lt;/p&gt;
&lt;p&gt;Après avoir migré un nombre incalculable de fois mon blog wordpress à droite / à gauche, puis &lt;a class="link" href="https://blog.zwindler.fr/2019/06/10/comment-migrer-de-wordpress-a-hugo/" &gt;migré sur Hugo&lt;/a&gt;, autohébergé, utilisé du managé chez &lt;a class="link" href="https://vercel.com/" target="_blank" rel="noopener"
&gt;vercel&lt;/a&gt;, chez &lt;a class="link" href="https://froggit.fr/" target="_blank" rel="noopener"
&gt;froggit&lt;/a&gt;, autohébergé encore&amp;hellip;&lt;/p&gt;
&lt;p&gt;J&amp;rsquo;ai décidé (un peu sur un coup de tête) de migrer le blog chez les copain⋅es de chez Clever Cloud.&lt;/p&gt;
&lt;p&gt;Et je vous montre comment j&amp;rsquo;ai fait.&lt;/p&gt;
&lt;h2 id="prérequis"&gt;Prérequis
&lt;/h2&gt;&lt;p&gt;Je pars du principe que vous avez comme moi un dépôt git / site hugo déjà fonctionnel. Et je ne vais pas non plus détailler la procédure pour activer un compte chez &lt;a class="link" href="https://www.clever-cloud.com/" target="_blank" rel="noopener"
&gt;Clever Cloud&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Je commence par installer la CLI, les &amp;ldquo;clever tools&amp;rdquo;. Pour les installer, il nous faut &lt;code&gt;npm&lt;/code&gt;. J&amp;rsquo;utilise rarement &lt;code&gt;npm&lt;/code&gt;, mais quand je le fais, j&amp;rsquo;utilise un autre tool, &lt;code&gt;volta&lt;/code&gt; (&lt;a class="link" href="https://volta.sh/" target="_blank" rel="noopener"
&gt;volta.sh&lt;/a&gt;), qui va me permettre de choisir la version dont j&amp;rsquo;ai besoin&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2023/12/idontalways.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ curl https://get.volta.sh | bash
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Une fois installé, je peux télécharger la version qui m&amp;rsquo;intéresse de npm (18 par exemple, la version minimum pour installer les clever tools)&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ volta install node@20
success: installed and set node@20.10.0 (with npm@10.2.3) as default
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Je peux donc maintenant installer les clever tools :&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;npm i -g clever-tools
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Puis me loguer depuis mon terminal&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ clever login
Opening https://console.clever-cloud.com/cli-oauth?cli_version=3.0.2&amp;amp;cli_token=xxxxxxxxxxxxxxxxx in your browser to log you in…
Login successful as [unspecified name] &amp;lt;xxx.yyy@example.org&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2023/12/clever-login.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Si jamais comme indiqué sur l&amp;rsquo;écran de login, vous avez besoin d&amp;rsquo;un token + secret (pour de la CI par exemple), sachez qu&amp;rsquo;ils sont stockés dans un fichier &lt;code&gt;~/.config/clever-cloud/clever-tools.json&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2024/01/clever-token.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Denier prérequis, je vais ajouter une &lt;a class="link" href="https://console.clever-cloud.com/users/me/ssh-keys" target="_blank" rel="noopener"
&gt;clé SSH dans ma console clever&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2023/12/add-ssh.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;h2 id="création-de-lapplication"&gt;Création de l&amp;rsquo;application
&lt;/h2&gt;&lt;p&gt;A partir de maintenant, je suis capable d’interagir avec clever cloud en CLI. Je vais commencer par créer l&amp;rsquo;application qui va héberger mon site statique :&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ clever create -t static-apache blog-zwindler
Your application has been successfully created!
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Clever Cloud offre la possibilité de dissocier la taille des instances pour la partie run et la partie build. Je vais donc créer l&amp;rsquo;instance la plus petite possible pour le run (une nano), et une plus pêchue (une M) pour le build, histoire d&amp;rsquo;être plus réactif sur les commits de modifications :&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ clever scale --build-flavor M
App rescaled successfully
$ clever scale --flavor nano
App rescaled successfully
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Je peux ensuite configurer quelques variables d&amp;rsquo;environnement (trouvées sur la doc de clever cloud) pour générer mon site statique avec l&amp;rsquo;image &lt;strong&gt;static-apache&lt;/strong&gt; de clever.&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;$ clever env &lt;span class="nb"&gt;set&lt;/span&gt; CC_WEBROOT &lt;span class="s2"&gt;&amp;#34;/public&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ clever env &lt;span class="nb"&gt;set&lt;/span&gt; CC_OVERRIDE_BUILDCACHE &lt;span class="s2"&gt;&amp;#34;/public&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ clever env &lt;span class="nb"&gt;set&lt;/span&gt; CC_PRE_BUILD_HOOK &lt;span class="s2"&gt;&amp;#34;bash setup_hugo.sh&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ clever env &lt;span class="nb"&gt;set&lt;/span&gt; CC_POST_BUILD_HOOK &lt;span class="s2"&gt;&amp;#34;hugo --minify --gc&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Pour finir, on crée le script &lt;code&gt;setup_hugo.sh&lt;/code&gt; à la racine de notre site statique Hugo :&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; setup_hugo.sh &lt;span class="s"&gt;&amp;lt;&amp;lt; EOF
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;HUGO_VERSION=&amp;#34;0.121.1&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;HUGO_URL=&amp;#34;https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;DEST_BIN=&amp;#34;${HOME}/.local/bin&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;FILENAME=&amp;#34;hugo.tar.gz&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;# Download Hugo Extended and place it in a folder in the $PATH
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;curl --create-dirs -s -L -o ${DEST_BIN}/${FILENAME} ${HUGO_URL}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;cd ${DEST_BIN}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;tar xvf ${FILENAME} -C ${DEST_BIN}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;rm ${FILENAME}
&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;On commit les fichiers créés :&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;git add .
git commit -m &amp;#34;Add clever&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="déployer-notre-code"&gt;Déployer notre code
&lt;/h2&gt;&lt;p&gt;A partir de là, on a notre instance qui est prête à démarrer, dès qu&amp;rsquo;elle aura reçu du code.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2023/12/app-created.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Ici, clever cloud attend un push sur un dépôt git.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2023/12/blog-zwindler-clever.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;La méthode la plus &amp;ldquo;simple&amp;rdquo; pour pousser du code sur clever à ce moment-là est donc de créer un dépôt distance (remote) :&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;git remote add clever git+ssh://git@push-n3-par-clevercloud-customers.services.clever-cloud.com/app_xxxxxxxxxxxxxxxxxxxx.git
git push clever master
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Le &amp;ldquo;problème&amp;rdquo; dans mon cas est que (pour l&amp;rsquo;instant), le nom de la branche n&amp;rsquo;est pas configurable et est forcément &amp;ldquo;master&amp;rdquo;, ce qui ne m&amp;rsquo;arrange pas puisque je bosse habituellement sur &amp;ldquo;main&amp;rdquo;.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;remote: Error: You tried to push to a custom branch. This is not allowed.
remote: error: hook declined to update refs/heads/main
To git+ssh://push-n3-par-clevercloud-customers.services.clever-cloud.com/app_xxxxxxxxxxxxxxxxxxxx.git
! [remote rejected] main -&amp;gt; main (hook declined)
error: impossible de pousser des références vers &amp;#39;git+ssh://push-n3-par-clevercloud-customers.services.clever-cloud.com/app_xxxxxxxxxxxxxxxxxxxx.git&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;2 manières de contourner ce problème :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;soit je force la branche master sur le remote clever dans git&lt;/li&gt;
&lt;li&gt;soit j&amp;rsquo;utilise la CLI &lt;code&gt;clever deploy&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Dans un premier temps, je me contente de feinter git :&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;git push clever main:master
Énumération des objets: 30, fait.
Décompte des objets: 100% (24/24), fait.
Compression par delta en utilisant jusqu&amp;#39;à 16 fils d&amp;#39;exécution
Compression des objets: 100% (16/16), fait.
Écriture des objets: 100% (16/16), 287.70 Kio | 31.97 Mio/s, fait.
Total 16 (delta 8), réutilisés 0 (delta 0), réutilisés du pack 0
remote: [SUCCESS] The application has successfully been queued for redeploy.
To git+ssh://push-n3-par-clevercloud-customers.services.clever-cloud.com/app_xxxxxxxxxxxxxxxxxxxx.git
54ff42f..960ce62 main -&amp;gt; master
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;On termine par la configuration du nom de domaine :&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ clever domain add blog.zwindler.fr
Your domain has been successfully saved
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Il ne nous reste plus qu&amp;rsquo;à mettre à jour le CNAME dans notre DNS (&lt;em&gt;domain.par.clever-cloud.com.&lt;/em&gt; dans mon cas, mais la bonne configuration est visible dans l&amp;rsquo;onglet &lt;strong&gt;Domain names&lt;/strong&gt; de l&amp;rsquo;application)&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2023/12/dns.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;h2 id="et-avec-clever-deploy-"&gt;Et avec &lt;code&gt;clever deploy&lt;/code&gt; ?
&lt;/h2&gt;&lt;p&gt;Eh bien en fait, c&amp;rsquo;est &amp;ldquo;encore plus simple&amp;rdquo;. Il me suffit juste de faire une modif, de commit&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;git add .
git commit -m &amp;#34;test clever deploy&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Puis de simplement lancer la commande &lt;code&gt;clever deploy&lt;/code&gt;.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ clever deploy
Remote git head commit is c4e3c283585cafff44f07c7d78d860065318cd68
Current deployed commit is c4e3c283585cafff44f07c7d78d860065318cd68
New local commit to push is 0b3536a9f1d61178a99f6238831ccb4197989ddf (from refs/heads/main)
Pushing source code to Clever Cloud…
Your source code has been pushed to Clever Cloud.
Waiting for deployment to start…
Deployment started (deployment_b5f841ff-a233-4bb4-8a16-f618ee69ef28)
Waiting for application logs…
[...]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Quelle est donc cette diablerie ?&lt;/p&gt;
&lt;p&gt;En réalité, les informations nécessaires pour pousser le code sont en réalité simplement dans un fichier &lt;code&gt;.clever.json&lt;/code&gt; qui a été créé à la racine lorsqu&amp;rsquo;on a lancé la commande &lt;code&gt;clever create -t static-apache blog-zwindler&lt;/code&gt; au tout début du tuto.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&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="nt"&gt;&amp;#34;apps&amp;#34;&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;app_id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;app_xxxxxxxxxxxxxxxxxxx&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="nt"&gt;&amp;#34;org_id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;user_yyyyyyyyyyyyyyyyyyyyy&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="nt"&gt;&amp;#34;deploy_url&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;https://push-n3-par-clevercloud-customers.services.clever-cloud.com/xxxxxxxxxxxxxxxxxxxx.git&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="nt"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;blog-zwindler&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="nt"&gt;&amp;#34;alias&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;blog-zwindler&amp;#34;&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;Évidemment, je ne vais pas m&amp;rsquo;amuser à faire un &lt;code&gt;clever login&lt;/code&gt; / &lt;code&gt;clever deploy&lt;/code&gt; à chaque fois que je veux pousser une modif sur mon blog. Et c&amp;rsquo;est là où nos CLEVER_TOKEN / CLEVER_SECRET seront utiles !&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;Après quelques heures d&amp;rsquo;usage, je peux dire que &lt;strong&gt;dans le cadre de l&amp;rsquo;hébergement de mon blog&lt;/strong&gt;, j&amp;rsquo;aime bien utiliser Clever Cloud.&lt;/p&gt;
&lt;p&gt;Qu&amp;rsquo;on soit bien clair, c&amp;rsquo;est pour mon cas d&amp;rsquo;usage et comparé à :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Vercel : l&amp;rsquo;expérience utilisateur est incroyable sur Vercel, mais j&amp;rsquo;ai toujours été un peu gêné par le côté &amp;ldquo;boite noire&amp;rdquo; (on a pas accès à beaucoup plus que des variables d&amp;rsquo;environnement). Et je ne parle pas des problématiques de privacy&amp;hellip;&lt;/li&gt;
&lt;li&gt;Deux serveurs physiques avec Proxmox VE en cluster étendu chez One-provider : à l&amp;rsquo;inverse héberger moi-même mon blog sur une machine (en vrai, 2) louée résout les problèmes de privacy et j&amp;rsquo;ai la main sur toute la stack, mais ça me demande de la maintenance pour des perfs minables. Ok, c&amp;rsquo;est pas cher, mais j&amp;rsquo;ai 2 à 3 minutes de rebuild avec le CPU à 100% sur mon Atom de 2010 chez Online&amp;hellip;&lt;/li&gt;
&lt;li&gt;Froggit : c&amp;rsquo;était du Gitlab + Gitlab pages et c&amp;rsquo;était très cool car c&amp;rsquo;est plus flexible que Vercel, mais j&amp;rsquo;avais dû passer un peu de temps à configurer la pipeline et leur faire pousser les murs pour que mon site rentre (les 400 Mo de médias n&amp;rsquo;étaient pas supportés au début et les transferts Gitlab =&amp;gt; Gitlab pages étaient un peu longs).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Avec Clever, mes 450 articles (3600 pages générées, 400 Mo de média) sont très rapidement générés, transférés, et enfin mis à disposition (&amp;lt; 1 minute tout compris, plus rapide que les 3 solutions précédentes).&lt;/p&gt;
&lt;p&gt;Le tout avec une disponibilité/fiabilité sans aucun doute bien meilleure que ce que j&amp;rsquo;aurais pu faire moi-même en auto hébergé avec un coût similaire, et en très peu de temps.&lt;/p&gt;
&lt;p&gt;J&amp;rsquo;ai passé beaucoup plus de temps à écrire cet article qu&amp;rsquo;à migrer le blog, puisque la doc est bien écrite, le site est intuitif, et je n&amp;rsquo;ai été bloqué à aucun moment.&lt;/p&gt;
&lt;p&gt;Il me reste encore à optimiser l&amp;rsquo;étape de déploiement, pour qu&amp;rsquo;un commit/push sur mon dépôt git déclenche automatiquement un déploiement côté clever. Objectivement, ça ne devrait pas prendre trop longtemps (github action / gitlab CI)&amp;hellip;&lt;/p&gt;
&lt;p&gt;Et par contre, ça manque d&amp;rsquo;IPv6 natif, screugneugneu !&lt;/p&gt;</description></item><item><title>Automatiser son site Hugo sans Github Action ou Vercel</title><link>https://blog.zwindler.fr/2023/08/01/automatiser-hugo-sans-github-action/</link><pubDate>Tue, 01 Aug 2023 06:00:00 +0200</pubDate><guid>https://blog.zwindler.fr/2023/08/01/automatiser-hugo-sans-github-action/</guid><description>&lt;img src="https://blog.zwindler.fr/2021/11/wordpress-to-hugo.webp" alt="Featured image of post Automatiser son site Hugo sans Github Action ou Vercel" /&gt;&lt;h2 id="contexte"&gt;Contexte
&lt;/h2&gt;&lt;p&gt;Fin 2021, après plus de 10 ans à écrire des articles de blog tech sur Wordpress, je prenais la décision radicale d&amp;rsquo;arrêter de maintenir cette bouse infâme (je mâche mes mots) et de partir sur des articles rédigés en markdown et un site statique avec Hugo.&lt;/p&gt;
&lt;p&gt;Au tout début, je gérais le blog moi-même mais assez vite j&amp;rsquo;en ai eu assez de devoir aller rebuild le blog à chaque modification. J&amp;rsquo;ai rapidement mis un cron qui faisait des &lt;code&gt;git pull&lt;/code&gt; régulièrement, mais on ne va pas se mentir, c&amp;rsquo;est assez crado&amp;hellip;&lt;/p&gt;
&lt;p&gt;Finalement, on m&amp;rsquo;a rapidement pointé que je devrais arrêter de m&amp;rsquo;embêter et utiliser &lt;a class="link" href="https://www.netlify.com/" target="_blank" rel="noopener"
&gt;Netlify&lt;/a&gt; ou &lt;a class="link" href="https://vercel.com/" target="_blank" rel="noopener"
&gt;Vercel&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;J&amp;rsquo;étais pas super chaud, car ça allait à l&amp;rsquo;encontre de ma volonté d&amp;rsquo;auto héberger mon contenu et de limiter l&amp;rsquo;impact sur la vie privée de mes lecteurs, mais finalement, l&amp;rsquo;expérience utilisateur sur Vercel était tellement bonne que j&amp;rsquo;avais craqué. Je ferai peut-être un article là-dessus d&amp;rsquo;ailleurs, c&amp;rsquo;est la première fois que j&amp;rsquo;ai compris (ressenti) l&amp;rsquo;intérêt d&amp;rsquo;un PaaS.&lt;/p&gt;
&lt;p&gt;Mais aujourd&amp;rsquo;hui, je reviens sur cette décision et j&amp;rsquo;essaye de voir ce qu&amp;rsquo;il est nécessaire de mettre en place pour disposer soi-même d&amp;rsquo;un site hugo qui se refresh tout seul dès qu&amp;rsquo;un commit arrive sur le dépôt git, sans utiliser de plateforme type Vercel ni d&amp;rsquo;outils types Github action+pages.&lt;/p&gt;
&lt;h2 id="prérequis"&gt;Prérequis
&lt;/h2&gt;&lt;p&gt;Je pars du principe que vous avez déjà un site statique généré avec Hugo. Si ce n&amp;rsquo;est pas le cas je vous invite à aller lire mes articles précédents sur le sujet (&lt;a class="link" href="https://blog.zwindler.fr/2019/06/10/comment-migrer-de-wordpress-a-hugo/" &gt;premiers pas&lt;/a&gt;, &lt;a class="link" href="https://blog.zwindler.fr/2019/06/18/mes-stats-de-visites-hugo-sans-google-analytics-avec-matomo/" &gt;stats avec matomo&lt;/a&gt;, &lt;a class="link" href="https://blog.zwindler.fr/2021/11/22/jai-enfin-migre-de-wordpress-a-hugo-partie-1/" &gt;migration wordpress vers hugo&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Vous avez donc un serveur Linux sur lequel vous avez la main pour installer des choses et un dépôt git (sous Github dans mon cas, mais on peut probablement adapter l&amp;rsquo;article pour autre chose).&lt;/p&gt;
&lt;p&gt;Hugo étant d&amp;rsquo;abord un générateur de site statique, je vais utiliser nginx en frontal pour servir les fichiers qu&amp;rsquo;on va générer. Jusqu&amp;rsquo;à il y a peu, l&amp;rsquo;usage du mode &amp;ldquo;server&amp;rdquo; de hugo n&amp;rsquo;était d&amp;rsquo;ailleurs pas conseillé en production (mais ça ne semble plus être le cas, je n&amp;rsquo;ai pas vu de warning dernièrement ?).&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;apt install nginx git
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="déposer-les-sources-sur-le-serveur"&gt;Déposer les sources sur le serveur
&lt;/h2&gt;&lt;p&gt;Dans mon cas, les sources de mon blog ne sont pas disponibles publiquement (parce que j&amp;rsquo;ai pas envie, c&amp;rsquo;est comme ça ¯\_(ツ)_/¯).&lt;/p&gt;
&lt;p&gt;Il faut donc préalablement se loguer en SSH avec git avant de pouvoir &lt;em&gt;pull&lt;/em&gt; le code. Et comme je n&amp;rsquo;ai pas envie de laisser ma clé privée sur le serveur qui va héberger mon blog, il faut que j&amp;rsquo;utilise une autre clé.&lt;/p&gt;
&lt;p&gt;Dans mon cas, Github, qui héberge le code markdown de mon blog, a une notion de &amp;ldquo;deploy keys&amp;rdquo;, qui servent justement pour ça :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://docs.github.com/en/authentication/connecting-to-github-with-ssh/managing-deploy-keys" target="_blank" rel="noopener"
&gt;docs.github.com/en/authentication/connecting-to-github-with-ssh/managing-deploy-keys&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Je vais donc créer une clé sur le serveur qui héberge le blog :&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;ssh-keygen -t ed25519 -C &lt;span class="s2"&gt;&amp;#34;xxx@xxx.tld&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Generating public/private ed25519 key pair.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Enter file in which to save the key &lt;span class="o"&gt;(&lt;/span&gt;/home/toto/.ssh/id_ed25519&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="o"&gt;[&lt;/span&gt;...&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Et je la copie dans le dossier de &lt;em&gt;www-data&lt;/em&gt; (l&amp;rsquo;utilisateur nginx sous Ubuntu)&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;chmod &lt;span class="m"&gt;600&lt;/span&gt; /root/.ssh/id_ed25519
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkdir /var/www/.ssh/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo cp ~/.ssh/id_ed25519* /var/www/.ssh/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chown www-data: /var/www/.ssh/*
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Une fois la clé créée, on doit l&amp;rsquo;ajouter dans Github.com. Je ne lui donne que les droits &amp;ldquo;read only&amp;rdquo; puisque le but c&amp;rsquo;est seulement de récupérer le code et générer le site statique, pas qu&amp;rsquo;on puisse éditer le code (&lt;em&gt;cé pour la sécuritay&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2023/07/github-deploy-keys2.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Je retourne ensuite sur mon serveur et j&amp;rsquo;essaye de télécharger les sources et le thème:&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="nb"&gt;cd&lt;/span&gt; /usr/share/nginx/html
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;GIT_SSH_COMMAND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;ssh -i /var/www/.ssh/id_ed25519 -o IdentitiesOnly=yes&amp;#39;&lt;/span&gt; git clone git@github.com:zwindler/blog.example.org.git
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; blog.example.org/themes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git submodule add -f https://github.com/CaiJimmy/hugo-theme-stack.git
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chown -R www-data: /usr/share/nginx/html/blog.example.org
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="webhook"&gt;Webhook
&lt;/h2&gt;&lt;p&gt;Pour réagir à l&amp;rsquo;arrivée d&amp;rsquo;un nouveau commit sur notre dépôt source, on va utiliser 2 choses :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;un projet qui s&amp;rsquo;appelle sobrement &amp;ldquo;Webhook&amp;rdquo;.&lt;/li&gt;
&lt;li&gt;configurer un webhook dans github, qui va envoyer l&amp;rsquo;info que quelque chose est arrivé sur le dépôt (mais on verra ça plus tard)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Revenons à &amp;ldquo;Webhook&amp;rdquo; dans un premier temps :&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;webhook is a lightweight configurable tool written in Go, that allows you to easily create HTTP endpoints (hooks) on your server, which you can use to execute configured commands. You can also pass data from the HTTP request (such as headers, payload or query variables) to your commands. webhook also allows you to specify rules which have to be satisfied in order for the hook to be triggered.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;On récupère la dernière release et déposer le binaire sur notre serveur :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/adnanh/webhook/releases/tag/2.8.1" target="_blank" rel="noopener"
&gt;github.com/adnanh/webhook/releases/tag/2.8.1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&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="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;2.8.1&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;wget https://github.com/adnanh/webhook/releases/download/&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VERSION&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/webhook-linux-amd64.tar.gz
&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;tar -xvf webhook*.tar.gz
&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;sudo mv webhook-linux-amd64/webhook /usr/local/bin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm -rf webhook-linux-amd64*
&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;webhook --version
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Une fois que c&amp;rsquo;est fait, on va générer une chaîne aléatoire qui va nous servir de &amp;ldquo;secret&amp;rdquo; qui sera partagé entre le binaire &lt;em&gt;webhook&lt;/em&gt; qui tourne sur notre serveur et un &lt;em&gt;webhook&lt;/em&gt; sur Github.com :&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="nv"&gt;WHSECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;uuidgen &lt;span class="p"&gt;|&lt;/span&gt; base64&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On crée un fichier de configuration &lt;code&gt;hook.json&lt;/code&gt; pour déclarer nos webhooks&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;mkdir&lt;/span&gt; &lt;span class="err"&gt;/etc/webhook&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;cat&lt;/span&gt; &lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;/etc/webhook/hook.json&lt;/span&gt; &lt;span class="err"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="err"&gt;EOF&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="nt"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;redeploy&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="nt"&gt;&amp;#34;execute-command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/usr/share/nginx/html/blog.example.org/blog_refresh.sh&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="nt"&gt;&amp;#34;command-working-directory&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/usr/share/nginx/html/blog.example.org&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="nt"&gt;&amp;#34;trigger-rule&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="nt"&gt;&amp;#34;match&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="nt"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;payload-hmac-sha1&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="nt"&gt;&amp;#34;secret&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;$WHSECRET&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="nt"&gt;&amp;#34;parameter&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="nt"&gt;&amp;#34;source&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;header&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="nt"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;X-Hub-Signature&amp;#34;&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;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="err"&gt;EOF&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ici, quand le serveur va recevoir l&amp;rsquo;url /hooks/redeploy, on va lancer le script &lt;code&gt;/usr/share/nginx/html/blog.example.org/blog_refresh.sh&lt;/code&gt; depuis le répertoire /usr/share/nginx/html/blog.example.org.&lt;/p&gt;
&lt;p&gt;Charge à nous de créer un script qui va soit :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;récupérer les sources (git pull)&lt;/li&gt;
&lt;li&gt;relancer un build&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Voilà à quoi pourrait ressembler le script &lt;code&gt;blog_refresh.sh&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="cp"&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /usr/share/nginx/html/blog.example.org
&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;#pull latest version&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;GIT_SSH_COMMAND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;ssh -i /var/www/.ssh/id_ed25519 -o IdentitiesOnly=yes&amp;#39;&lt;/span&gt; git clone git@github.com:zwindler/blog.example.org.git
&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;#refresh theme submodule&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git submodule init
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git submodule update
&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;#rebuild (fire and forget)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hugo --minify --gc &lt;span class="p"&gt;&amp;amp;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On va ensuite créer un service systemd qui va lancer le binaire &lt;em&gt;webhook&lt;/em&gt; en tâche de fond&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/systemd/system/webhook.service &lt;span class="s"&gt;&amp;lt;&amp;lt; EOF
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[Unit]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;Description=Simple Golang webhook server
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;ConditionPathExists=/usr/local/bin/webhook
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;After=network.target
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[Service]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;User=www-data
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;Group=www-data
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;Type=simple
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;WorkingDirectory=/usr/share/nginx/html/blog.example.org
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;ExecStart=/usr/local/bin/webhook -ip 127.0.0.1 -hooks /etc/webhook/hook.json -verbose
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;Restart=on-failure
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[Install]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;WantedBy=default.target
&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;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;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; webhook
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl start webhook
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Côté Github.com, on crée le webhook qui va contacter le serveur webhook de la façon suivante :&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2023/07/github-webhook.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;On a maintenant un serveur web qui est en attente de certains événements webhooks et qui va lancer un script si jamais l&amp;rsquo;URL de webhook est appelée par Github.com.&lt;/p&gt;
&lt;h2 id="configuration-nginx"&gt;Configuration nginx
&lt;/h2&gt;&lt;p&gt;Il reste maintenant toute la configuration de notre nginx en frontal à faire. En partant du principe qu&amp;rsquo;on vient juste de faire l&amp;rsquo;installation du package, on commence par supprimer le fichier &lt;code&gt;/etc/nginx/sites-enabled/default&lt;/code&gt;, et on en crée un nouveau :&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;rm /etc/nginx/sites-enabled/default
&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;cat &amp;gt; /etc/nginx/sites-available/blog.example.org.conf &lt;span class="s"&gt;&amp;lt;&amp;lt; EOF
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;# Root configuration
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;server {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; listen 80;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; server_name blog.example.org;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; location /hooks/ {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; proxy_pass http://127.0.0.1:9000/hooks/;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; error_page 404 /404.html;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; location / {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; root /usr/share/nginx/html/blog.example.org/public;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; index index.html;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;}
&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;span class="line"&gt;&lt;span class="cl"&gt;ln -s /etc/nginx/sites-&lt;span class="o"&gt;{&lt;/span&gt;available,enabled&lt;span class="o"&gt;}&lt;/span&gt;/blog.example.org.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On vérifie que la configuration est valide et si oui on redémarre nginx&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;nginx -t
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl reload nginx
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A partir de maintenant, toutes les URLs qui arriveront jusqu&amp;rsquo;à votre serveur en /hooks seront redirigées sur le logiciel &amp;ldquo;webhook&amp;rdquo; et le reste ira sur votre site statique (/usr/share/nginx/html/blog.example.org/public).&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-console" data-lang="console"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;curl https://blog.example.org/hooks/redeploy -L
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Hook rules were not satisfied.%
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ici le hook renvoie une erreur, car on a pas envoyé le token secret mais c&amp;rsquo;est probable que ça fonctionne. Toutes les autres pages renvoient bien une page du blog :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-console" data-lang="console"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;curl https://blog.example.org/
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;&amp;lt;!DOCTYPE html&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;&amp;lt;html lang=&amp;#34;fr-fr&amp;#34; dir=&amp;#34;ltr&amp;#34;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; &amp;lt;head&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; &amp;lt;meta name=&amp;#34;generator&amp;#34; content=&amp;#34;Hugo 0.115.1&amp;#34;&amp;gt;&amp;lt;meta charset=&amp;#39;utf-8&amp;#39;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;[...]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On peut maintenant envoyer des commits et voir si ça déclenche des rebuilds de notre blog. Ou alors, on peut récupérer un push précédent et cliquer sur &amp;ldquo;Redeliver&amp;rdquo; si on veut trigger un rebuild du blog.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2023/07/github-redelivery.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-console" data-lang="console"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;root@hugo:~# systemctl status webhook
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;* webhook.service - Simple Golang webhook server
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; Loaded: loaded (/etc/systemd/system/webhook.service; enabled; vendor preset: enabled)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; Active: active (running) since Mon 2023-07-31 07:34:35 UTC; 33s ago
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; Main PID: 900109 (webhook)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; Tasks: 15 (limit: 4574)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; Memory: 240.4M
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; CPU: 45.504s
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; CGroup: /system.slice/webhook.service
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; |-900109 /usr/local/bin/webhook -ip 127.0.0.1 -hooks /etc/webhook/hook.json -verbose
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; `-900161 hugo --minify --gc
&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;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Jul 31 07:34:35 hugo webhook[900109]: [webhook] 2023/07/31 07:34:35 attempting to load hooks from /etc/webhook/hook.json
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Jul 31 07:34:35 hugo webhook[900109]: [webhook] 2023/07/31 07:34:35 found 1 hook(s) in file
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Jul 31 07:34:35 hugo webhook[900109]: [webhook] 2023/07/31 07:34:35 loaded: redeploy
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Jul 31 07:34:35 hugo webhook[900109]: [webhook] 2023/07/31 07:34:35 serving hooks on http://127.0.0.1:9000/hooks/{id}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Jul 31 07:34:35 hugo webhook[900109]: [webhook] 2023/07/31 07:34:35 os signal watcher ready
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Jul 31 07:34:41 hugo webhook[900109]: [webhook] 2023/07/31 07:34:41 [f089a0] incoming HTTP POST request from 127.0.0.1:43258
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Jul 31 07:34:41 hugo webhook[900109]: [webhook] 2023/07/31 07:34:41 [f089a0] redeploy got matched
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Jul 31 07:34:41 hugo webhook[900109]: [webhook] 2023/07/31 07:34:41 [f089a0] redeploy hook triggered successfully
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Jul 31 07:34:41 hugo webhook[900109]: [webhook] 2023/07/31 07:34:41 [f089a0] 200 | 0 B | 1.151688ms | 127.0.0.1:9000 | POST /hooks/redeploy
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Jul 31 07:34:41 hugo webhook[900109]: [webhook] 2023/07/31 07:34:41 [f089a0] executing /usr/share/nginx/html/blog.example.org/blog_refresh.sh
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;[...]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Jul 31 07:37:28 hugo webhook[900109]: Pages | 3572
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Jul 31 07:37:28 hugo webhook[900109]: Paginator pages | 516
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Jul 31 07:37:28 hugo webhook[900109]: Non-page files | 14
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Jul 31 07:37:28 hugo webhook[900109]: Static files | 2282
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Jul 31 07:37:28 hugo webhook[900109]: Processed images | 2
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Jul 31 07:37:28 hugo webhook[900109]: Aliases | 1563
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Jul 31 07:37:28 hugo webhook[900109]: Sitemaps | 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Jul 31 07:37:28 hugo webhook[900109]: Cleaned | 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Jul 31 07:37:28 hugo webhook[900109]: Total in 166131 ms
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Jul 31 07:37:28 hugo webhook[900109]: [webhook] 2023/07/31 07:37:28 [f089a0] finished handling redeploy
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;Si vous êtes à l&amp;rsquo;aise avec Linux et nginx, ces quelques manipulations sont assez rapides à faire, il n&amp;rsquo;y a rien de vraiment complexe.&lt;/p&gt;
&lt;p&gt;Cependant, ça demande à l&amp;rsquo;usage un peu d&amp;rsquo;administration et de vouloir mettre les mains dans le cambouis (avoir une VM à gérer). Je ne peux reprocher à personne de préférer l&amp;rsquo;utilisation de Github/Gitlab pages, couplé à de Github actions/Gitlab runners, voire de tout déléguer à Vercel/Netlify&amp;hellip;&lt;/p&gt;
&lt;p&gt;Mais j&amp;rsquo;avais envie de reprendre la main sur le hosting de mon blog (avec tous les inconvénients que ça va avoir en termes de maintien en condition opérationnelle), notamment pour disposer à nouveau de l&amp;rsquo;IPv6 (perdue lors du passage à Vercel) et une navigation sur mon blog plus respectueuse de votre vie privée (pas que j&amp;rsquo;aie pas confiance en Vercel, mais bon&amp;hellip;).&lt;/p&gt;
&lt;p&gt;Et c&amp;rsquo;est en revanche beaucoup plus simple que d&amp;rsquo;héberger son propre serveur Gitlab + Gitlab runners + Gitlab pages ;-).&lt;/p&gt;
&lt;p&gt;Donc, pour mon usage, ça fait le taf :)&lt;/p&gt;
&lt;h2 id="ressources-additionnelles"&gt;Ressources additionnelles
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://ansonvandoren.com/posts/deploy-hugo-from-github/" target="_blank" rel="noopener"
&gt;ansonvandoren.com/posts/deploy-hugo-from-github/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://magefan.com/blog/configure-webhooks-in-github" target="_blank" rel="noopener"
&gt;magefan.com/blog/configure-webhooks-in-github&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>