Source code of my website
1
fork

Configure Feed

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

🚧 : update draft

+113 -29
content/posts/drafts/2026-02-06-optimiser-site-statique/index-html-minified.webp

This is a binary file and will not be displayed.

+113 -29
content/posts/drafts/2026-02-06-optimiser-site-statique/index.md
··· 1 1 --- 2 - date: 2026-02-06 3 - title: Optimiser un site Hugo 2 + date: 2026-02-20 3 + title: Optimiser les perfs et la sécurité d'un site Hugo 4 + tags: 5 + - clevercloud 6 + - security 7 + - devops 8 + - tools 4 9 draft: true 5 10 --- 6 11 7 - [//]: # (TODO link vers le blog d'antoine) 8 - Sur les bons conseils du pote Antoine Caron, j'ai pris temps cette semaine d'optimiser un peu mon site. 12 + Sur les bons conseils du pote [Antoine Caron](https://blog.slashgear.dev/), j'ai pris temps cette semaine d'optimiser un peu mon site. 9 13 10 14 Ce site que vous êtes en train de lire est un site statique, buildé avec Hugo. 11 15 12 16 J'ai déjà un peu travaillé la compression des différentes ressources, principalement les illustrations, mais je m'étais arrêté à ça. 13 17 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. 18 + 19 + <!-- more --> 14 20 15 21 ## Le score Lighthouse 16 22 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). 23 + 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) (assez classique). 18 24 19 25 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_. 20 26 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. ··· 23 29 24 30 Voici les scores de mon site à l'heure actuelle : 25 31 26 - 27 32 ![Score Lighthouse pour un mobile](lighthouse-mobile.png) 28 33 ![Score Lighthouse pour un desktop](lighthouse-desktop.png) 34 + { class="images-grid-2" } 29 35 30 36 37 + Ces scores peuvent sembler intéressants sur la page d'accueil, mais ils se dégradent fortement sur certaines pages. 38 + Voici les scores pour la page de mon talk sur Factorio : 39 + 40 + ![Score Lighthouse sur mobile pour une autre page](lighthouse-talk-mobile.png) 41 + ![Score Lighthouse sur desktop pour une autre page](lighthouse-talk-desktop.png) 42 + { class="images-grid-2" } 43 + 31 44 > J'ai clairement une marge d'amélioration sur l'accessibilité et les performances. 45 + 46 + Sans rentrer dans le détail et l'analyse de ce qui est remonté par cet outil, on va tout de suite s'attaquer au vif du sujet. 32 47 33 48 ## Minification 34 49 ··· 47 62 48 63 Ce qui produit des fichiers HTML minifiés de ce type : 49 64 50 - ```html 51 - <!doctype html><html xmlns=http://www.w3.org/1999/xhtml xml:lang=fr-FR lang=fr-FR><head><script defer language=javascript type=text/javascript src=/js/bundle.min.39a1898ad60dcb3b845d8dc359b7c996c10aa0da902f0d461da32348b1bc5f02.js></script><script defer data-domain=codeka.io src=https://plausible.io/js/script.js></script><script type=text/javascript src=https://app.affilizz.com/affilizz.js async></script><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><link rel=icon href=/favicon.png><meta property="og:image" content="/pp_ekite_itvw.png"><meta name=twitter:image content="/pp_ekite_itvw.png"><meta name=twitter:card content="summary_large_image"><meta property="og:image:width" content="639"><meta property="og:image:height" content="708"><meta property="og:image:type" content="image/png"><title itemprop=name>Julien Wittouck</title><meta property="og:title" content="Julien Wittouck"><meta name=twitter:title content="Julien Wittouck"><meta itemprop=name content="Julien Wittouck"><meta name=application-name content="Julien Wittouck"><meta property="og:site_name" content="Julien Wittouck"> 52 - ``` 65 + ![Mon fichier index.html minifié](index-html-minified.png "Mon fichier index.html minifié") 53 66 54 - Hop, on peut passer rapidement à autre chose 🚶 67 + Pas de surprise ni de difficulté sur cette première partie, hop, on peut passer rapidement à autre chose 🚶 55 68 56 69 ## Conversion des images en webp et redimensionnement 57 70 58 - 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. 59 - 60 71 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. 61 72 62 - 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_. 73 + Ces photos sont souvent lourdes (plusieurs mégaoctets) et en haute résolution, et une action simple consiste à redimensionner ces photo et les recompresser au format _webp_. 63 74 64 75 Hugo supporte la recompression des images dans différents formats à la volée, mais pas leur redimensionnement automatique, il faut implémenter soi-même la mécanique. 65 76 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. ··· 74 85 {{- /* chomp trailing newline */ -}} 75 86 ``` 76 87 88 + Une image déclarée en Markdown de cette manière : 89 + 90 + ```markdown 91 + ![Une image](photo.jpg) 92 + ``` 93 + 94 + Aura pour équivalent HTML le code suivant : 95 + 96 + ```html 97 + <img src="/photo.jpg" alt="Une image"> 98 + ``` 99 + 77 100 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 : 78 101 79 102 ```go 80 103 {{- $image := .Page.Resources.GetMatch .Destination -}} 81 104 {{- $width := math.Min 820 $image.Width -}} 82 - {{- $resizeOpts := printf "%dx webp lossless q100 lanczos" (int $width) -}} 105 + {{- $resizeOpts := printf "%dx webp q75 lanczos" (int $width) -}} 83 106 {{- with $image.Resize $resizeOpts -}} 84 107 <img src="{{ .RelPermalink }}" width="{{ .Width }}" height="{{ .Height }}" 85 108 {{- with $.PlainText }} alt="{{ . }}"{{ end -}} ··· 88 111 {{- /* chomp trailing newline */ -}} 89 112 ``` 90 113 91 - Je force l'utilisation de `lossless` avec la qualité maximale `q100` pour éviter une perte de données qui rendrait les illustations peu lisible, ce qui serait surtout problématique pour les schémas. 114 + La magie a lieu sur les premières lignes. 115 + Je redimensionne l'image à la taille maximale de 820px (ou moins si l'image est plus petite), et j'applique une conversion en `webp`. 116 + 117 + Je force l'utilisation de `lossless` avec la qualité maximale `q100` pour éviter une perte de données qui rendrait les illustrations peu lisibles, ce qui serait surtout problématique pour les schémas. 118 + 119 + Le HTML généré par Hugo pour mes images est maintenant le suivant : 120 + 121 + ```html 122 + <img src="/photo_hu_ed495de5ae801a42.webp" width="820" height="540" alt="Une image"> 123 + ``` 124 + 125 + Avec le redimensionnement et la conversion en webp, j'optimise les images pour leur affichage sur le format de mon site. 126 + 127 + Je peux même aller encore un peu plus loin en travaillant avec un `srcset` pour proposer au navigateur des images de différentes tailles en fonction de la taille d'affichage de la vue, ce qui permet de ne pas télécharger une image de 820 pixels de large pour un affichage qui n'en comporte que 480. 128 + 129 + En retravaillant le hook pour générer plusieurs images de dimensions différentes, j'obtiens le code suivant : 130 + 131 + ```go 132 + {{- $image := .Page.Resources.GetMatch .Destination -}} 133 + 134 + {{- $width820 := math.Min 820 $image.Width -}} 135 + {{- $resizeOpts := printf "%dx webp q75 lanczos" (int $width820) -}} 136 + {{- $img820 := $image.Resize $resizeOpts -}} 137 + 138 + {{- $width480 := math.Min 480 $image.Width -}} 139 + {{- $resizeOpts := printf "%dx webp q75 lanczos" (int $width480) -}} 140 + {{- $img480 := $image.Resize $resizeOpts -}} 141 + 142 + <img srcset="{{ $img820.RelPermalink }} 820w, 143 + {{ $img480.RelPermalink }} 480w" 144 + sizes="(max-width: 480px) 480px, 145 + 820px" 146 + src="{{ $img820.RelPermalink }}" 147 + {{- with $.PlainText }} alt="{{ . }}"{{ end -}} 148 + {{ with $.Title }}title="{{ . }}"{{ end }}> 149 + {{- /* chomp trailing newline */ -}} 150 + ``` 151 + 152 + Le code HTML généré ressemble donc à ça : 153 + 154 + ```html 155 + <img srcset="/photo_hu_ed495de5ae801a42.webp 820w, 156 + /photo_hu_21e26f92cb8d445a.webp 480w" 157 + sizes="(max-width: 480px) 480px, 158 + 820px" 159 + src="/photo_hu_ed495de5ae801a42.webp" 160 + alt="Une image"> 161 + ``` 162 + 163 + Très basiquement, je redimensionne les images en 2 tailles, `820px` et `480px`, et je demande au navigateur d'utiliser la version de `480px` pour toutes les tailles d'écran inférieures à `480px` et la version de `820px` pour toutes les autres tailles. 164 + 165 + On peut encore aller un peu plus loin, mais on a déjà fait un bon travail sur les images, il est tant de passer à une étape suivante. 92 166 93 167 ## Pré-compression des ressources statiques 94 168 ··· 108 182 :8080 109 183 110 184 file_server { 111 - # Clever Cloud serves the public directory in a cc_static_autobuilt directory 185 + # Clever Cloud serves the public directory 112 186 root public 113 187 } 114 188 ··· 116 190 encode 117 191 ``` 118 192 119 - 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`. Les formats utilisés par défaut par Caddy sont `zstd` et `gzip`, et seules les ressources pertinentes sont compressées (les formats déjà compressés comme `jpg` ne sont pas re-compressés). 193 + 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`, grâce à la directive `encode`. Les formats utilisés par défaut par Caddy sont `zstd` et `gzip`, et seules les ressources pertinentes sont compressées (les formats déjà compressés comme `jpg` ne sont pas re-compressés). 120 194 121 195 Cette compression permet d'économiser de la bande passante et accélère le temps de chargement des pages. 122 196 ··· 143 217 [tasks.precompress] 144 218 description = "Precompress static resources" 145 219 run = ''' 146 - COMPRESSREGEX=".*(html|css|js|xml|ico|svg|md|pdf|woff2)$" 220 + COMPRESSREGEX=".*(html|css|js|xml|ico|svg|md|pdf)$" 147 221 find public/ -type f -regextype egrep -regex $COMPRESSREGEX | xargs zstd --keep --force -19 148 222 find public/ -type f -regextype egrep -regex $COMPRESSREGEX | xargs gzip --keep --force --best 149 223 ''' ··· 177 251 Cleaned │ 0 │ 0 178 252 179 253 Total in 272 ms 180 - [precompress] $ COMPRESSREGEX=".*(html|css|js|xml|ico|svg|md|pdf|woff2)$" 254 + [precompress] $ COMPRESSREGEX=".*(html|css|js|xml|ico|svg|md|pdf)$" 181 255 245 files compressed : 80.99% ( 83.3 MiB => 67.4 MiB) B ==> 98%^T 182 256 Finished in 7.77s 183 257 ``` ··· 235 309 ```bash 236 310 $ curl --head https://codeka.io 237 311 238 - Content-Length: 81157 312 + Content-Length: 34963 239 313 Content-Type: text/html; charset=utf-8 240 314 Server: Caddy 241 315 ``` ··· 252 326 Content-Length: 9 253 327 ``` 254 328 255 - [//]: # (TODO) revérifier après le déploiement 256 - 257 - 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 ! 329 + On passe d'une page HTML de 34ko à une donnée compressée de 13ko, sans impacter le CPU du serveur puisque la compression se fait au build ! 258 330 259 331 ## Headers de sécurité 260 332 261 - La dernière étape de cette configuration consiste à moderniser les headers servis pour impléments un peu de sécurité supplémentaire. 333 + La dernière étape de cette configuration consiste à moderniser les headers servis pour implémenter un peu de sécurité supplémentaire. 262 334 263 - 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. 335 + 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 facilement. 264 336 265 - Pour savoir quoi faire, sur les conseils d'Antoine, j'ai utilisé l'analyseur de MDN : 337 + Pour savoir quoi-faire, sur les conseils d'Antoine, j'ai utilisé l'analyseur du [MDN Observatory](https://developer.mozilla.org/en-US/observatory) : 266 338 267 339 https://developer.mozilla.org/en-US/observatory/analyze?host=codeka.io#scoring 268 340 269 341 ![Résultat de l'analyse de MDN](mdn-analysis.png "Résultat de l'analyse de MDN") 270 342 271 - ### HSTS 343 + > Encore une fois, le résultat de l'analyse est médiocre, puisqu'aucune optimisation n'avait été faite. Il y a du travail sur cette partie ! 344 + 345 + ### HSTS et headers faciles 272 346 273 347 Le premier header intéressant à utiliser est le `Strict-Transport-Security`. 274 348 ··· 277 351 278 352 La recommandation de MDN est de positionner cette valeur : 279 353 280 - ```HTTP 354 + ```text 281 355 Strict-Transport-Security: max-age=63072000 282 356 ``` 283 357 ··· 301 375 # Ask Caddy to compress static files 302 376 encode 303 377 ``` 378 + 379 + Je fais la même chose pour quelques headers recommandés supplémentaires, comme `X-Content-Type-Optionsfire` et `X-XSS-Protection`. 304 380 305 381 ### Content-Security-Policy 306 382 ··· 345 421 346 422 ## Conclusion 347 423 348 - Ça m'a pris une bonne demi-journée pour mettre en place tous ces mécanismes, mais j'en ressort avec une meilleure compréhension de la sécurité et de la compression en HTTP. 424 + Ça m'a pris une bonne demi-journée pour mettre en place tous ces mécanismes, mais j'en ressors avec une meilleure compréhension de la sécurité et de la compression en HTTP. 349 425 J'ai aussi découvert Caddy, et amélioré mon fichier `mise.toml`. 350 426 427 + Et le résultat n'est pas des moindres. 428 + Voici l'analyse issue de Lighthouse : 429 + 430 + 431 + Et le nouveau score MDN Observatory : 432 + 351 433 Pour la plupart de mes lecteurs, l'impact de la compression sera probablement minime, car sur des réseaux performants, la différence de temps de chargement ne se ressentira peut-être pas beaucoup. 352 434 Mais avec une compression effectuée uniquement au build, c'est aussi une du CPU de moins de consommé, ce qui devrait pouvoir m'assurer de rester sur des instances les plus petites pour mon site le plus longtemps possible. 353 435 ··· 362 444 * [La directive `precompressed`](https://caddyserver.com/docs/caddyfile/directives/file_server#precompressed) 363 445 364 446 * Documentation MDN : 447 + * [Responsive Images](https://developer.mozilla.org/en-US/docs/Web/HTML/Guides/Responsive_images) 448 + * [MDN HTTP Observatory](https://developer.mozilla.org/en-US/observatory) 365 449 * [Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy) 366 450 367 451 * [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/lighthouse-desktop.webp

This is a binary file and will not be displayed.

content/posts/drafts/2026-02-06-optimiser-site-statique/lighthouse-mobile.webp

This is a binary file and will not be displayed.

content/posts/drafts/2026-02-06-optimiser-site-statique/lighthouse-talk-desktop.webp

This is a binary file and will not be displayed.

content/posts/drafts/2026-02-06-optimiser-site-statique/lighthouse-talk-mobile.webp

This is a binary file and will not be displayed.

content/posts/drafts/2026-02-06-optimiser-site-statique/mdn-analysis.webp

This is a binary file and will not be displayed.