docs(33-11): WebP decision note and STACK row
github.com/gen2brain/webp pinned at v0.6.4 (approved 2026-11-04), with how it runs (purego or the wasm2go-transpiled libwebp), the linking consequence, the benchmark and where the code lives.
This commit is contained in:
50
.planning/notes/2026-10-28-webp-gen2brain.md
Normal file
50
.planning/notes/2026-10-28-webp-gen2brain.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Decision: WebP variants of attachment thumbnails through github.com/gen2brain/webp
|
||||
|
||||
**Date:** 2026-10-28 (dependency approved 2026-11-04)
|
||||
**Status:** Accepted
|
||||
**Context:** Phase 33 (SEO output, audit and content, D-85), 33-CONTEXT.md "Image formats (Jakub, 2026-10-28): WebP now, in Go", 33-RESEARCH.md Open Question 3 and Package Legitimacy Audit, plan 33-06 Task 4 (approval), plan 33-11 (P33-42, P33-43)
|
||||
|
||||
## Decision
|
||||
|
||||
Sites serve their featured images as `<picture>` with a WebP source and the original format as the `srcset` fallback. The WebP files are encoded in Go, once per thumbnail size, in a background job, never in the upload request. AVIF is deferred.
|
||||
|
||||
- **`github.com/gen2brain/webp` pinned at exactly v0.6.4.** Published 2026-07-01 (proxy.golang.org, origin `github.com/gen2brain/webp` at tag `v0.6.4`, commit `3dcf5268`), MIT (the bundled libwebp under its BSD licence, `lib/LICENSE.libwebp`). Approved by Jakub on 2026-11-04 (33-06 Task 4). Its go.mod requires `github.com/ebitengine/purego` v0.10.1 and nothing else.
|
||||
- **Why this library.** `golang.org/x/image/webp`, which `attach` already uses, decodes only; the standard library has no WebP encoder. cgo bindings to libwebp would break the cgo-free build of the single binary. gen2brain/webp encodes in Go without cgo.
|
||||
- **What is encoded.** Each thumbnail a caller asks for (`attach.File.ThumbKey` at a width, height and mode) gets one WebP variant next to it, at quality 80, under `attach.VariantKey(thumbKey, "webp")`. The original upload is never re-encoded, and images an editor inserts into a body keep their original file.
|
||||
- **Where it runs.** Journal's conga job `golem15.journal.image_variants` is dispatched after a featured image upload commits, and `journal:image-variants --all` backfills existing images. A slow or failing encode never fails the upload.
|
||||
|
||||
## How it runs (checked in the v0.6.4 module source, 2026-11-04)
|
||||
|
||||
- **First choice: the system libwebp through purego.** At init the package opens `libwebp.so` and `libwebpdemux.so` with `purego.Dlopen` (no cgo). On Linux this only works in a dynamically linked binary.
|
||||
- **Fallback: libwebp transpiled to Go.** When the libraries are missing (or with the build tag `nodynamic`), it uses libwebp compiled to WASM and then transpiled to plain Go source by wasm2go (`libwebp.go`, about 2.7 MB of generated Go). There is no WASM runtime at run time and no wazero; 33-CONTEXT's "WASM fallback" means this transpiled code. `webp.Dynamic()` reports which path is active.
|
||||
- **Linking consequence.** purego's no-cgo runtime support makes a binary that imports the package dynamically linked, even with `CGO_ENABLED=0`: the probe binary needed the ELF interpreter `/lib64/ld-linux-x86-64.so.2` and `libc.so.6`, `libdl.so.2` and `libpthread.so.0`. It runs on glibc hosts such as the Debian servers. It does not run in a `scratch` image, a distroless static image or on musl (Alpine). Building with `-tags nodynamic` keeps the binary statically linked and uses only the transpiled code (checked: `statically linked`, `Dynamic()` reports "dynamic disabled").
|
||||
- **Side effect.** The package registers a `webp` decoder with `image.RegisterFormat`, next to the one `golang.org/x/image/webp` registers. Both decode WebP, so `image.Decode` keeps working whichever one it picks.
|
||||
|
||||
## Benchmark
|
||||
|
||||
- **33-CONTEXT, 2026-10-28.** A 1200 px variant of a 2592×1754 photo: WebP 78 KB against JPEG q80 at 133 KB. Encoding took about 90 ms with the system libwebp and about 510 ms in the fallback. AVIF came out only about 6 % smaller than WebP, at 1.3× to 20× the CPU.
|
||||
- **Probe of v0.6.4 on 2026-11-04** (this workstation, a synthetic 1200×812 gradient, quality 80): about 17 ms through the system libwebp and about 90 ms in the transpiled fallback. WebP was 29.8 KB against 58.4 KB for JPEG q80. The two runs used different images and machines, so compare the paths with each other, not the numbers across runs.
|
||||
|
||||
## Where the code lives (P33-42)
|
||||
|
||||
- **Framework, generic media.** `modules/lagoon/attach/webp` (package `webp`): `Encode(w, img)` at quality 80 and `Variant(ctx, bucket, f, w, h, mode)`. `Variant` writes the variant next to the thumbnail, returns early when it exists, and deletes the key when an encode or close fails. In `attach` itself there is one generic helper, `attach.VariantKey(thumbKey, format)`.
|
||||
- **Why it is not SEO code.** A WebP variant of a thumbnail is a media format. It has nothing to do with SEO, and the framework gets no SEO wording (D-90). The addition is additive at patch level (D-106).
|
||||
- **Only binaries that import it link it.** `attach` does not import the sub-package, so a binary links gen2brain/webp, purego and the linking consequence above only when it imports `modules/lagoon/attach/webp`. In practice that means through sm-journal-plugin.
|
||||
- **The job lives in Journal.** Only plugins register conga jobs, and Journal owns the featured images and their variants table (`golem15_journal_image_variants`, P33-43).
|
||||
|
||||
## Rejected alternatives
|
||||
|
||||
- **AVIF through `github.com/gen2brain/avif`.** About 6 % smaller than WebP at 1.3× to 20× the CPU. Deferred.
|
||||
- **A Nuxt image pipeline (sharp).** New npm dependencies with a native module, and the encoding would move into the frontend server.
|
||||
- **cgo bindings to libwebp.** They break the cgo-free cross-compiled builds the applications use.
|
||||
- **Widths only, no new format.** This needs no dependency, but D-85 asks for a modern format.
|
||||
|
||||
## Package legitimacy
|
||||
|
||||
The package-legitimacy seam cannot check Go modules, so gen2brain/webp was gated by the blocking human check of plan 33-06 Task 4 and approved by Jakub at v0.6.4 before any `go get`. v0.6.4 resolves through proxy.golang.org with the origin above (`h1:SUDdmxADOAiPQ+5ylNmuHhuYf2dOi0KgKZHL5vpVCNU=`).
|
||||
|
||||
## Consequences
|
||||
|
||||
- The framework go.mod requires `github.com/gen2brain/webp v0.6.4` (and, through it, `github.com/ebitengine/purego v0.10.1`). Only packages under `modules/lagoon/attach/webp` import it.
|
||||
- An application that mounts sm-journal-plugin links the encoder. Its binary is dynamically linked against glibc unless it is built with `-tags nodynamic`. Installing libwebp on the server makes encoding about five times faster, and nothing else changes. The package opens the unversioned names `libwebp.so` and `libwebpdemux.so`, which Debian ships in `libwebp-dev`.
|
||||
- If the library ever needs replacing, only `modules/lagoon/attach/webp/webp.go` changes. Callers use `Encode` and `Variant`.
|
||||
@@ -48,6 +48,7 @@ This extends `.planning/research/go-ecosystem.md` (2026-09-16 GitHub-API-verifie
|
||||
| golangci-lint | v2.13.2 (Aug 27, 2026) | Linting | v2's config schema (`version: "2"` in `.golangci.yml`) is a breaking change from v1 — do not copy a v1 config from an older Go project without migrating it. |
|
||||
| `github.com/markbates/goth` (core plus providers google, facebook, github, azureadv2; never gothic) | v1.82.0 | OAuth 2.0 client for social sign-in in sm-sociallogin-plugin | Decided in .planning/notes/2026-10-20-sociallogin-goth.md (Phase 19.5 D-02); plugin-only, the framework go.mod does not require it. |
|
||||
| `github.com/chromedp/chromedp` | v0.21.0 (Oct 11, 2026; pinned exactly, newer tags deliberately not used) | Optional raw-versus-rendered comparison of the SEO site audit in sm-seo-plugin, driving an operator-installed Chrome or Chromium (`golem15.seo.crawl_chrome_path`) | Decided in .planning/notes/2026-10-28-chromedp-rendered-crawl.md (Phase 33 D-70, P33-55); pure Go, no cgo or Node; plugin-only, the framework go.mod does not require it. |
|
||||
| `github.com/gen2brain/webp` | v0.6.4 (Jul 1, 2026; pinned exactly, with `github.com/ebitengine/purego` v0.10.1) | WebP variants of attachment thumbnails (`modules/lagoon/attach/webp`, quality 80), encoded in a conga job of sm-journal-plugin | Decided in .planning/notes/2026-10-28-webp-gen2brain.md (Phase 33 D-85, P33-42); no cgo: the system libwebp through purego, else libwebp transpiled to Go (wasm2go). Only binaries that import the sub-package link it, and they are dynamically linked against glibc unless built with `-tags nodynamic`. |
|
||||
| `github.com/jimsmart/grobotstxt` | v1.0.3 | Google-faithful robots.txt matching (longest match, allow wins ties, `*` and `$`, matching line) for the robots.txt tester and the noindex-blocked warning in sm-seo-plugin | Decided in .planning/notes/2026-10-27-robots-txt-grobotstxt.md (Phase 32 D-99); plugin-only, the framework go.mod does not require it. |
|
||||
| `charm.land/lipgloss/v2` | v2.0.6 | Console styles, layout and tables (bonfire palette, diary console handler) | Decided in .planning/notes/2026-10-21-console-charm-stack.md (Phase 19.8 D-02); imported only by modules/bonfire and modules/diary |
|
||||
| `charm.land/bubbletea/v2` | v2.1.0 | Runtime for the live spinner, process and progress views | Decided in .planning/notes/2026-10-21-console-charm-stack.md (Phase 19.8 D-02); imported only by modules/bonfire and modules/diary |
|
||||
|
||||
Reference in New Issue
Block a user