fix(parser): rendered images are missing alt, width, height and loading attributes (#97) - #100
Conversation
Images rendered by the parser carried only src and, for markdown images, alt. Browsers could not reserve space for them before they loaded (a CLS penalty), every image was fetched during the initial page load, and a bare ![[foo.png]] embed had no alt attribute at all, so assistive tech fell back to announcing the file name. Every image now renders with loading="lazy", decoding="async" and an alt attribute, and images rewritten to an assets URL also carry their intrinsic width and height, read from the asset file with image.DecodeConfig. Measuring is best-effort: a missing, unreadable or undecodable file (SVG, AVIF and WebP among them) simply renders without dimensions. Measurements are cached per path, so an image referenced from several posts is read once. Wikilink image embeds are replaced with markdown image nodes during AST transformation, because the wikilink extension renders its own <img> tag and ignores node attributes. Both syntaxes now share one rendering path, which is where the alt="" default for bare embeds falls out. Refs #97 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds table tests over both image syntaxes in pkg/parser using real encoded PNG and JPEG headers, plus coverage of the plumbing that carries the assets filesystem from the generate and serve entry points down to the parser. Refs #97 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Covers the full rendered <img> tag in the README and pkg/parser, states that dimensions come from the asset file and that unmeasurable files omit them, and drops the caveat about bare wikilink embeds having no alt text now that they render alt="". Notes that --assets-dir also affects parsing, not just serving and copying. Refs #97 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Refs #97 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Embedding config.AssetsDir alongside config.Logger made the promoted AsOption method ambiguous at depth 1, silently removing p.AsOption() from Parser's exported API and leaking Parser.FS and Parser.Enabled() into the public surface. The embed bought nothing: the filesystem is already carried by Config.AssetsDir and was read once to build the measurer, and parser does not use the config.BaseOption application chain. Build the measurer straight from the config instead, and add a regression test pinning the promoted method. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Automated reviewI read the full diff, the surrounding parser/server/generator code, the goldmark and wikilink library sources it depends on, and ran One problem:
Agent responsePushed to Review points1. I confirmed the break independently before changing anything: built an external module against this worktree calling The fix, as suggested:
Beyond the minimal change, I added a regression test ( No documentation needed updating: Verification
Posted by rain. Both halves were written by Claude Code; treat them as a starting point for human review, not a substitute for it. |
Resolve conflicts between the image-attribute work and the sitemap/robots feature from main: both sets of generator config fields and both README flag rows are kept. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes #97
Note
This pull request was written by rain running Claude Code. It has not been reviewed by a human. CI and an automated review pass run before it is handed over.
Issue
#97: fix(parser): rendered images are missing alt, width, height and loading attributes
What the agent reports
Done — branch
rain/issue-97is pushed with four commits.What changed
pkg/parser(the fix)images.go:imageTransformernow setsloading="lazy"anddecoding="async"on every image, andwidth/heighton images it rewrote to an assets URL and could measure.assetURLbecameresolveAsset, which returns both the served URL and the matching path inside the assets FS (/images/screenshots/a.png→screenshots/a.png), derived from the same normalisation, so the existing..containment guarantee still governs what can be read.replaceImageEmbeds(option 1 from the issue): wikilink image embeds are swapped for*ast.Imagenodes during AST transformation, so both syntaxes share one rendering path.alt=""for a bare![[foo.png]]falls out of the wikilink renderer's own "label == target means no alt" rule.wikilinkResolverlost its now-dead image branch and its unusedblogRoot.dimensions.go(new):imageMeasurerreads headers viaimage.DecodeConfig(png/jpeg/gif registered), caches per path including failures, and is mutex-guarded sinceParseris documented concurrency-safe. Every failure path is a debug log and no dimensions — never a parse error.parser.WithAssetsDiroption +Config.AssetsDir, matching howWithBlogRoot/WithLoggerare already wired intoparser.Config;Parserembedsconfig.AssetsDir.Plumbing:
Generatorembedsconfig.AssetsDir, appliesWithAssetsDirFunc, and forwards it to the parser config inGenerate.pkg/server.Newforwards its resolvedAssetsDirinto the generator options when set. The CLI needed no change —generatealready passedconfig.WithAssetsDir(...).AsGeneratorOption()andservepasses it as a server option.Docs: README images section (full rendered tag, dimensions sourced from the file, unmeasurable files omit them, bare-embed alt caveat deleted),
--assets-dirflag row,pkg/parser/doc.go,pkg/config/doc.go,pkg/config/baseOption.go,pkg/server/doc.go. The.prose imgrule inhead.tmplis untouched.Tests: parser table tests over both syntaxes with real encoded PNG/JPEG headers (measurable, bare embed, labelled embed, subdirectory, missing, corrupt, SVG, absolute/protocol-relative/data/
/-rooted,.., no assets FS), a cache-hit-count test, a concurrency test, plus plumbing tests inpkg/generator,pkg/server,internal/generator(writes real output HTML) andinternal/server.Verified with
gofmt -l .,go vet ./...,go test ./...,go test -race ./...,go mod tidy(no dependency change), and a real run:go run ./cmd/goblog generate docs/example-posts /tmp/out97renders<img src="/images/github.png" alt="The GoBlog README on GitHub" width="943" height="581" loading="lazy" decoding="async" />.For the reviewer
golang.org/x/image/webp— the issue offered that choice, so WebP joins SVG and AVIF as undimensioned (documented in README and godoc). And because wikilink embeds are now realast.Imagenodes, they render in the XHTML self-closing form… />rather than the…>shown in the issue's expected-output snippet; the attribute set matches the spec exactly, only the closing token differs from the pasted-from-current-behaviour example.replaceImageEmbedsnode surgery inpkg/parser/images.go— I collect the embeds in a full walk, then replace, and copy the label as a freshast.NewTextSegmentrather than re-parenting the original child, to avoid touching sibling pointers mid-walk.integration/container_test.go'sTestServe_Imageswas extended (real 640×480 PNG, asserts both<img>tags includingalt=""for the bare embed); it compiles and vets, but has not executed. Thegeneratehalf of that request is covered byTestRunGenerate_ImageDimensionsininternal/generatorinstead, since the container's ENTRYPOINT isgoblog serveandgenerateisn't reachable through it.Parser, so withservea resized image needs a reload forwidth/heightto catch up (eachGeneratebuilds a fresh parser). Noted inpkg/server/doc.goand the README.Changelog (#100)
✨ New Features
🐛 Bug Fixes
📚 Documentation
🧪 Tests
🧹 Chores