From 231831ebd7412215f7ae7beb187c344f2a74cfd5 Mon Sep 17 00:00:00 2001 From: Nicolas MASSE Date: Sun, 4 Oct 2026 12:43:25 +0200 Subject: [PATCH] serve the images as WebP in several widths The image render hook converts the JPEG, PNG and WebP images to WebP in several widths (480, 800, 1200, 1600 px, no wider than the original) with srcset and sizes matching the layout. All images get their dimensions (no layout shift) and are lazy loaded, except the first one of the page. GIF and SVG images are served as-is. The images downloaded by the pages go from 99 MB to 13 MB (desktop) or 8 MB (mobile), and the site from 168 MB to 101 MB. The social image partial no longer reads the .RelPermalink of the page images, which published all of them, and compares the images by name since the content now refers to the processed variants. Refs #11 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01YNHsWYTXPThURz6TbVS1j3 --- layouts/_markup/render-image.html | 64 +++++++++++++++++++++++++++-- layouts/_partials/social-image.html | 9 +++- 2 files changed, 67 insertions(+), 6 deletions(-) diff --git a/layouts/_markup/render-image.html b/layouts/_markup/render-image.html index 981b18e..eb9a418 100644 --- a/layouts/_markup/render-image.html +++ b/layouts/_markup/render-image.html @@ -6,21 +6,77 @@ -- figure, with its title as caption (Markdown allowed): -- -- ![Alternative text](image.png "Caption") + -- + -- Raster images (JPEG, PNG, WebP) are converted to WebP in several widths + -- (srcset), so that the browser downloads the most appropriate one. + -- GIF (possibly animated) and SVG images are served as-is. + -- + -- All images get their dimensions (to avoid layout shifts) and are lazy + -- loaded, except the first one of the page. */ -}} {{- $u := urls.Parse .Destination -}} -{{- $src := .Destination -}} +{{- $attrs := dict "src" .Destination "alt" .PlainText -}} +{{- $image := false -}} {{- if not $u.IsAbs -}} {{- with or (.Page.Resources.Get $u.Path) (resources.Get (strings.TrimPrefix "/" $u.Path)) -}} - {{- $src = .RelPermalink -}} + {{- $image = . -}} {{- else -}} {{- if not (strings.HasPrefix $u.Path "/") -}} {{- errorf "%s: image %q not found" .Page.String .Destination -}} {{- end -}} {{- end -}} {{- end -}} + +{{- with $image -}} + {{- /* Note: .RelPermalink publishes the original image, only done when it is served as-is */ -}} + {{- if in (slice "jpeg" "png" "webp") .MediaType.SubType -}} + {{- /* WebP variants, no wider than the original image */ -}} + {{- $hint := cond (eq .MediaType.SubType "png") "drawing" "photo" -}} + {{- $variants := slice -}} + {{- range $width := slice 480 800 1200 1600 -}} + {{- if lt $width $image.Width -}} + {{- $variants = $variants | append ($image.Resize (printf "%dx webp q80 %s" $width $hint)) -}} + {{- end -}} + {{- end -}} + {{- if lt (len $variants) 4 -}} + {{- /* The original width, when smaller than the largest variant */ -}} + {{- $variants = $variants | append ($image.Process (printf "webp q80 %s" $hint)) -}} + {{- end -}} + {{- $srcset := slice -}} + {{- range $variants -}} + {{- $srcset = $srcset | append (printf "%s %dw" .RelPermalink .Width) -}} + {{- end -}} + {{- $largest := index $variants (sub (len $variants) 1) -}} + {{- $attrs = merge $attrs (dict + "src" $largest.RelPermalink + "srcset" (delimit $srcset ", ") + "sizes" "(max-width: 1140px) 100vw, (max-width: 1280px) 75vw, (max-width: 1440px) 55vw, 50vw" + "width" $largest.Width + "height" $largest.Height + ) -}} + {{- else if eq .MediaType.SubType "gif" -}} + {{- $attrs = merge $attrs (dict "src" .RelPermalink "width" .Width "height" .Height) -}} + {{- else -}} + {{- $attrs = merge $attrs (dict "src" .RelPermalink) -}} + {{- end -}} +{{- end -}} + +{{- /* The first image of the page is often the largest visible element */ -}} +{{- if eq .Ordinal 0 -}} + {{- $attrs = merge $attrs (dict "fetchpriority" "high") -}} +{{- else -}} + {{- $attrs = merge $attrs (dict "loading" "lazy" "decoding" "async") -}} +{{- end -}} + +{{- $img := "" -}} +{{- range $k, $v := $attrs -}} + {{- $img = printf `%s %s="%s"` $img $k (string $v | htmlEscape) -}} +{{- end -}} +{{- $img = printf "" $img | safeHTML -}} + {{- if .IsBlock -}}
- {{ .PlainText }} + {{ $img }} {{- with .Title }}
{{- . | markdownify -}} @@ -28,5 +84,5 @@ {{- end }}
{{- else -}} -{{ .PlainText }} +{{ $img }} {{- end -}} diff --git a/layouts/_partials/social-image.html b/layouts/_partials/social-image.html index 945bb1f..31af322 100644 --- a/layouts/_partials/social-image.html +++ b/layouts/_partials/social-image.html @@ -43,11 +43,16 @@ {{- /* 3. The first image displayed in the page content */ -}} {{- if and (not $source) $images -}} + {{- /* + -- The images are compared by name (without extension) since the image + -- render hook serves processed variants (name_hu_.webp). Do not use + -- .RelPermalink here: it would publish all the images of the page. + */ -}} {{- range findRESubmatch `]*\ssrc="([^"]+)"` .Content -}} - {{- $src := index . 1 -}} + {{- $name := index . 1 | path.Base | replaceRE `_hu_[0-9a-f]+\.\w+$` "" | replaceRE `\.\w+$` "" -}} {{- if not $source -}} {{- range $images -}} - {{- if and (not $source) (or (eq .RelPermalink $src) (eq .Permalink $src)) -}}{{- $source = . -}}{{- end -}} + {{- if and (not $source) (eq (replaceRE `\.\w+$` "" (path.Base .Name)) $name) -}}{{- $source = . -}}{{- end -}} {{- end -}} {{- end -}} {{- end -}}