Source code of my website
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

📝 : update draft post on static site optimization with hugo and caddy

+288 -5
+288 -5
content/posts/drafts/2026-02-06-optimiser-site-statique/index.md
··· 5 5 --- 6 6 7 7 [//]: # (TODO link vers le blog d'antoine) 8 - Sur les bons conseils du pote Antoine Caron, j'ai pris le temps d'optimiser un peu mon site. 8 + Sur les bons conseils du pote Antoine Caron, j'ai pris temps cette semaine d'optimiser un peu mon site. 9 9 10 10 Ce site que vous êtes en train de lire est un site statique, buildé avec Hugo. 11 11 12 12 J'ai déjà un peu travaillé la compression des différentes ressources, principalement les illustrations, mais je m'étais arrêté à ça. 13 + Dans cet article, je détaille comment j'ai optimisé le build de ce site, pour minimiser les temps de chargement, et comment j'ai amélioré sa sécurité en suivant les bonnes pratiques poussées par MDN. 13 14 14 15 ## Le score Lighthouse 15 16 16 17 Pour faire un premier travail sur les performances de ce site, j'ai utilisé [une analyse LightHouse](https://pagespeed.web.dev/analysis/https-codeka-io/we5dukzmku?form_factor=desktop). 17 18 18 19 Lighthouse permet en quelques minutes d'avoir une vue des performances d'une application ou d'un site web, à la fois pour une cible _Desktop_ et _Mobile_. 19 - Il permet aussi de valider certains propriétés d'accessibilité, comme des contrastes, la présence de texte alternatif pour les lecteurs d'écran, etc. 20 + Il permet aussi de valider certaines propriétés d'accessibilité, comme des contrastes, la présence de texte alternatif pour les lecteurs d'écran, etc. 20 21 21 22 C'est, je pense, un bon point de départ. 22 23 ··· 29 30 30 31 > J'ai clairement une marge d'amélioration sur l'accessibilité et les performances. 31 32 32 - ## Compression des images en webp 33 + ## Conversion des images en webp et redimensionnement 33 34 34 - Une des actions que j'ai mis en place il y a un moment, est l'utilisation du format _webp_ pour compression les illustrations que j'utilise dans mes articles. 35 + Une des actions que j'ai mis en place il y a un moment, est l'utilisation du format _webp_ pour compresser les illustrations que j'utilise dans mes articles. 35 36 36 - J'utilise souvent des photos que j'ai capturées avec mon smartphone (pour les articles de conférence), des schémas que je produits sur draw.io le plus souvent et que j'exporte en _png_, ou des photos _stock_ que je vais chercher pour illustrer mes articles de veille. 37 + J'utilise souvent des photos que j'ai capturées avec mon smartphone (pour les articles de conférence), des captures d'écran ou des schémas (produit sur draw.io le plus souvent), ou des photos _stock_ que je vais chercher pour illustrer mes articles de veille. 37 38 38 39 Ces photos sont souvent lourdes (plusieurs mégaoctets) et en haute résolution, et la première action simple consiste à redimensionner ces photo et les recompresser au format _webp_. 39 40 41 + Hugo supporte la recompression des images dans différents formats à la volée, mais pas leur redimensionnement automatique. 42 + Pour pouvoir redimensionner les images à la volée, la meilleure solution semble d'utiliser un hook "img" Hugo, qui permet de surcharger la traduction du markdown et d'y mettre le code qu'on souhaite. 43 + 44 + Le hook utilisé par défaut est le suivant : 45 + 46 + ```go 47 + <img src="{{ .Destination | safeURL }}" 48 + {{- with .PlainText }} alt="{{ . }}"{{ end -}} 49 + {{- with .Title }} title="{{ . }}"{{ end -}} 50 + > 51 + {{- /* chomp trailing newline */ -}} 52 + ``` 53 + 54 + Pour redimensionner les images à une taille maximale de 820px (la taille utilisée sur la colonne de contenu de ce site), j'utilise le hook suivant : 55 + 56 + ```go 57 + {{- $image := .Page.Resources.GetMatch .Destination -}} 58 + {{- $width := math.Min 820 $image.Width -}} 59 + {{- $resizeOpts := printf "%dx webp lossless q100 lanczos" (int $width) -}} 60 + {{- with $image.Resize $resizeOpts -}} 61 + <img src="{{ .RelPermalink }}" width="{{ .Width }}" height="{{ .Height }}" 62 + {{- with $.PlainText }} alt="{{ . }}"{{ end -}} 63 + {{ with $.Title }}title="{{ . }}"{{ end }}> 64 + {{- end -}} 65 + {{- /* chomp trailing newline */ -}} 66 + ``` 67 + 68 + ## Pré-compression des ressources statiques 69 + 70 + Mon site est hébergé chez Clever Cloud, dans une instance de type _static_. 71 + J'avais écrit un article à ce sujet l'année dernière : [Déployer des applications statiques sur Clever Cloud](/2025/06//2025-06-05-static-apps-clever). 72 + 73 + Clever Cloud permet d'utiliser Caddy pour servir les fichiers statiques en surcharge de `static-web-server`, simplement en ajoutant un `Caddyfile` à la racine du projet. 74 + 75 + Cette option va me permettre de pouvoir configurer Caddy pour servir le répertoire `public` du site : 76 + 77 + ```Caddyfile 78 + # Clever Cloud needs us to listen on port 8080 79 + :8080 80 + 81 + file_server { 82 + # Clever Cloud serves the public directory in a cc_static_autobuilt directory 83 + root public 84 + } 85 + 86 + # Ask Caddy to compress static files 87 + encode 88 + ``` 89 + 90 + Lors de l'exécution d'une requête, Caddy va servir les fichiers statiques, et potentiellement compresser les réponses HTTP en alimentant le headers `Content-Encoding`. 91 + 92 + Cette compression permet d'économiser de la bande passante et accélère le temps de chargement des pages. 93 + 94 + Cependant, la compression se fait en utilisant un peu de CPU à la volée. 95 + Il est alors intéressant de pré-compresser les ressources statiques à la phase de build pour économiser un peu de CPU. 96 + 97 + Un directive Caddy permet de servir des fichiers statiques pré-compressés : `precompressed`. 98 + Caddy va alors rechercher des variantes compressées des fichiers, sous la forme de fichiers sidecar. 99 + À côté de chaque fichier statique, il faut donc générer les variantes compressées et les nommer en utilisant les extensions `.br` et `.gz` par exemple. 100 + 101 + Hugo ne permet pas de générer ces variantes compressées, donc je dois utiliser un petit script qui s'exécutera en fin de la phase de build. 102 + 103 + J'ai donc créé ce script dans mon fichier `mise.toml`: 104 + 105 + ```toml 106 + [tasks.build] 107 + description = "Build le site avec Hugo" 108 + run = "hugo --gc --minify --destination public" 109 + 110 + [tasks.post-build] 111 + description = "Post build hooks" 112 + depends_post = ["precompress"] 113 + 114 + [tasks.precompress] 115 + description = "Precompress static resources" 116 + run = ''' 117 + COMPRESSREGEX=".*(html|css|js|xml|ico|svg|md|png|webp|pdf)$" 118 + find public/ -type f -regextype egrep -regex $COMPRESSREGEX | xargs zstd -15 -f 119 + find public/ -type f -regextype egrep -regex $COMPRESSREGEX | xargs zstd --format=gzip -15 -f 120 + ''' 121 + ``` 122 + 123 + Par défaut, Clever Cloud exécute une tâche `mise run build` si elle existe, donc l'ajouter dans mon fichier permet de pouvoir préciser mes options de build. 124 + 125 + Pour la phase de compression, il suffit d'indiquer à Clever Cloud d'exécuter `mise run post-build`, cela se fait avec un hook sur 126 + 127 + Le script `precompress` est inspiré d'un [article de blog de Scott Laird](https://scottstuff.net/posts/2025/03/09/precompressing-content-with-hugo-and-caddy/) sur lequel je suis tombé en faisant quelques recherches. 128 + Il recherche l'ensemble des fichiers matchant la regex donnée, et utilise `zstd` pour compresser ces fichiers. 129 + 130 + L'exécution de ces scripts produit la sortie suivante : 131 + 132 + ```bash 133 + [build] $ hugo build hugo --gc --minify --destination public 134 + Start building sites … 135 + hugo v0.155.2-d8c0dfccf72ab43db2b2bca1483a61c8660021d9+extended linux/amd64 BuildDate=2026-02-02T10:04:51Z VendorInfo=gohugoio 136 + 137 + │ EN │ FR 138 + ──────────────────┼────┼───── 139 + Pages │ 75 │ 139 140 + Paginator pages │ 0 │ 4 141 + Non-page files │ 14 │ 222 142 + Static files │ 36 │ 36 143 + Processed images │ 3 │ 275 144 + Aliases │ 1 │ 8 145 + Cleaned │ 0 │ 0 146 + 147 + Total in 272 ms 148 + [precompress] $ COMPRESSREGEX=".*(html|css|js|xml|ico|svg|md|png|webp|pdf)$" 149 + 593 files compressed : 90.10% ( 165 MiB => 149 MiB) B ==> 98%% 150 + 593 files compressed : 93.31% ( 165 MiB => 154 MiB) 151 + Finished in 7.77s 152 + ``` 153 + 154 + On peut valider que les fichiers buildés sont précompressés comme souhaité, avec les extensions `.gz` et `.zst` : 155 + 156 + ```bash 157 + $ ls public/ 158 + 2020 ai-manifesto icons logo_blue.png.zst pp_ekite_itvw_hu_41404e93ad715bdf.webp.gz 159 + 2021 books images logo_transparent_background.png pp_ekite_itvw_hu_41404e93ad715bdf.webp.zst 160 + 2022 credentials index.html logo_transparent_background.png.gz projects 161 + 2023 css index.html.gz logo_transparent_background.png.zst robots.txt 162 + 2024 ekite index.html.zst now series 163 + 2025 en index.xml page sitemap.xml 164 + 2026 favicon.png index.xml.gz posts sitemap.xml.gz 165 + 404.html favicon.png.gz index.xml.zst pp_ekite_itvw.png sitemap.xml.zst 166 + 404.html.gz favicon.png.zst js pp_ekite_itvw.png.gz stats 167 + 404.html.zst fonts logo_blue.png pp_ekite_itvw.png.zst tags 168 + ai fr logo_blue.png.gz pp_ekite_itvw_hu_41404e93ad715bdf.webp talks 169 + ``` 170 + 171 + Pour ensuite servir les fichiers précompressés, il faut ajouter la directive `precompressed` dans le `Caddyfile` : 172 + 173 + ```Caddyfile 174 + # Clever Cloud needs us to listen on port 8080 175 + :8080 176 + 177 + file_server { 178 + # Clever Cloud serves the public directory 179 + root public 180 + precompressed 181 + } 182 + 183 + # Ask Caddy to compress static files 184 + encode 185 + ``` 186 + 187 + On peut ensuite simplement vérifier que les fichiers compressés sont servis avec une commande `curl`. 188 + 189 + Voici ce qui était renvoyé _avant_ la compression : 190 + 191 + ```bash 192 + $ curl --compressed --head https://codeka.io 193 + 194 + HTTP/1.1 200 OK 195 + content-length: 81156 196 + content-type: text/html 197 + accept-ranges: bytes 198 + last-modified: Tue, 03 Feb 2026 12:10:53 GMT 199 + vary: accept-encoding 200 + cache-control: max-age=86400 201 + date: Fri, 06 Feb 2026 16:42:58 GMT 202 + Sozu-Id: 01KGSXBPJWK3D8CZEF8FSPD1Y5 203 + ``` 204 + 205 + et la même commande après la compression : 206 + 207 + ```bash 208 + $ curl --compressed --head https://codeka.io 209 + 210 + HTTP/1.1 206 Partial Content 211 + Accept-Ranges: bytes 212 + Content-Encoding: zstd 213 + Content-Length: 9487 214 + Content-Range: bytes 0-9486/9487 215 + Content-Type: text/html; charset=utf-8 216 + Etag: "dg80yb3ycgay7bj" 217 + Last-Modified: Fri, 06 Feb 2026 16:37:33 GMT 218 + Server: Caddy 219 + Vary: Accept-Encoding 220 + Date: Fri, 06 Feb 2026 16:43:06 GMT 221 + Sozu-Id: 01KGSXBY5TS81HK0BCRSYZSM4S 222 + ``` 223 + 224 + On passe d'une page HTML de 81ko à une donnée compressée de 13ko, sans impacter le CPU du serveur puisque la compression se fait au build ! 225 + 226 + ## Headers de sécurité 227 + 228 + La dernière étape de cette configuration consiste à moderniser les headers servis pour impléments un peu de sécurité supplémentaire. 229 + 230 + Maintenant que Caddy sert le site et que j'ai un Caddyfile sur lequel j'ai la main, je peux contrôler les headers HTTP renvoyés. 231 + 232 + Pour savoir quoi faire, sur les conseils d'Antoine, j'ai utilisé l'analyseur de MDN : 233 + 234 + https://developer.mozilla.org/en-US/observatory/analyze?host=codeka.io#scoring 235 + 236 + ![Résultat de l'analyse de MDN](mdn-analysis.png "Résultat de l'analyse de MDN") 237 + 238 + ### HSTS 239 + 240 + Le premier header intéressant à utiliser est le `Strict-Transport-Security`. 241 + 242 + Ce header a pour effet de forcer les navigateurs à utiliser HTTPS. 243 + Bien que j'ai déjà configuré une redirection HTTP vers HTTPS sur mon domaine avec Clever Cloud, c'est une mesure de sécurité supplémentaire. 244 + 245 + La recommandation de MDN est de positionner cette valeur : 246 + 247 + ```HTTP 248 + Strict-Transport-Security: max-age=63072000 249 + ``` 250 + 251 + Dans mon Caddyfile, rien de plus simple, j'ajoute le header `Strict-Transport-Security` : 252 + 253 + ```Caddyfile 254 + # Clever Cloud needs us to listen on port 8080 255 + :8080 256 + 257 + file_server { 258 + # Clever Cloud serves the public directory 259 + root public 260 + precompressed 261 + } 262 + 263 + # Custom headers for security 264 + header { 265 + Strict-Transport-Security "max-age=63072000" 266 + } 267 + 268 + # Ask Caddy to compress static files 269 + encode 270 + ``` 271 + 272 + ### Content-Security-Policy 273 + 274 + Le premier header intéressant à utiliser est le `Content-Security-Policy`. 275 + Ce header indique au navigateur quelle politique de sécurité appliquer à l'exécution des scripts provenant de sources externes au site web. 276 + C'est une mesure de sécurité permettant de se prémunir des injections de type XSS (Cross-Site Scripting). 277 + 278 + Le header doit déclarer l'ensemble des sources (domaines) acceptés pour le chargement des scripts, styles, images et autres ressources. 279 + Utiliser ce header a aussi pour effet de désactiver le CSS et le JS "inline", ce qui est plutôt une bonne pratique. 280 + 281 + Après avoir supprimé tous les styles inlines de mon site, j'ai configuré le header dans mon Caddyfile : 282 + 283 + ```Caddyfile 284 + # Clever Cloud needs us to listen on port 8080 285 + :8080 286 + 287 + file_server { 288 + # Clever Cloud serves the public directory 289 + root public 290 + precompressed 291 + } 292 + 293 + # Custom headers for security 294 + header { 295 + Strict-Transport-Security "max-age=63072000" 296 + 297 + Content-Security-Policy " 298 + script-src 'self' codeka.io plausible.io; 299 + frame-src 'self' plausible.io www.youtube-nocookie.com openfeedback.io; 300 + img-src 'self' img.shields.io; 301 + default-src 'self'; 302 + " 303 + } 304 + 305 + # Ask Caddy to compress static files 306 + encode 307 + ``` 308 + 309 + J'utilise plausible.io pour suivre les visites de mes articles, donc son script doit pouvoir être chargé. De la même manière, j'ai des iframes (bouh) sur les pages de mes talks qui référencent les videos Youtube ainsi que les feedbacks OpenFeedback.io. Je dois donc aussi autoriser ces ressources. 310 + 311 + La directive `default-src` sert de fallback pour toutes les directives possible, et indique que seul mon site est autorisé. 312 + 313 + ## Liens et références 314 + 315 + * Configuration de l'optimisation des images avec Hugo : https://gohugo.io/configuration/imaging/#quality 316 + * La méthode [Resize de Hugo](https://gohugo.io/methods/resource/resize/) 317 + * [Le hookimage de Hugo](https://gohugo.io/render-hooks/images/#article) 318 + 319 + * Documentation MDN : 320 + * [Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy) 321 + 322 + * [Precompressing Content With Hugo and Caddy](https://scottstuff.net/posts/2025/03/09/precompressing-content-with-hugo-and-caddy/)
content/posts/drafts/2026-02-06-optimiser-site-statique/mdn-analysis.png

This is a binary file and will not be displayed.