Skip to content

fix(parser): rendered images are missing alt, width, height and loading attributes #97

Description

@harrydayexe

Problem

Images added in #89 render with only src and (sometimes) alt. Three attributes that browsers and Core Web Vitals care about are never emitted, and alt is dropped entirely for bare wikilink embeds.

Current output, verified against pkg/parser with BlogRoot /:

<!-- ![A cat](cat.png)     --> <p><img src="/images/cat.png" alt="A cat" /></p>
<!-- ![[dog.png]]          --> <p><img src="/images/dog.png"></p>
<!-- ![[dog.png|A dog]]    --> <p><img src="/images/dog.png" alt="A dog"></p>

Consequences:

  1. No width/height — the browser cannot reserve space before the image loads, so content reflows as each image arrives. That is a Cumulative Layout Shift (CLS) penalty in Core Web Vitals, and it affects every post with an image.
  2. No loading="lazy" — every image in a post is fetched during initial page load, even ones far below the fold.
  3. alt missing on ![[dog.png]] — no attribute at all, not even alt="". Screen readers fall back to announcing the file name or URL. The README currently documents this as a caveat ("A bare ![[pipeline.png]] has no alt text, so prefer the |label form"); it should be a guarantee instead.

Expected output

For the same three inputs, with posts/images/cat.png a 1200×800 PNG and posts/images/dog.png a 640×480 PNG:

<p><img src="/images/cat.png" alt="A cat" width="1200" height="800" loading="lazy" decoding="async" /></p>
<p><img src="/images/dog.png" alt="" width="640" height="480" loading="lazy" decoding="async"></p>
<p><img src="/images/dog.png" alt="A dog" width="640" height="480" loading="lazy" decoding="async"></p>

Rules:

  • width/height are the image's intrinsic pixel dimensions, read from the asset file at parse time. Both are emitted, or neither.
  • loading="lazy" and decoding="async" on every image, both syntaxes. (Deliberate choice: uniform and simple. The first-image-eager/fetchpriority refinement for LCP is out of scope here.)
  • alt is always present. A bare ![[foo.png]] emits alt="", which is the correct signal for "no description available" and stops assistive tech reading out the URL.
  • Attributes are only added to images the parser rewrote to an assets URL. Images left as written — absolute URLs, //host/x, /foo.png, data: — keep loading/decoding but get no dimensions, since there is no local file to measure. alt handling is unchanged for those.
  • The existing .prose img { max-width: 100%; height: auto; } rule in pkg/templates/default/partials/head.tmpl must stay. With the attributes present it is what keeps images responsive while still letting the browser derive the aspect ratio from width/height.

How dimensions get read

pkg/parser has no access to the assets filesystem today — only outputter and server hold a config.AssetsDir. The decision taken is to probe the asset file:

  • Add a config.AssetsDir embedded field to the parser, following the config pattern in CLAUDE.md: the type already exists in pkg/config, so this needs a parser.WithAssetsDir option (or reuse of the existing BaseOption, whichever matches how WithBlogRoot and WithLogger are wired into parser.Config) and plumbing from the CLI's --assets-dir through to the parser in both generate and serve.
  • Read dimensions with image.DecodeConfig, which reads only the header rather than decoding pixels. Register the decoders for the formats the assets pipeline accepts (image/png, image/jpeg, image/gif via blank imports; golang.org/x/image/webp if WebP is wanted — flag it if you would rather not take the dependency, and leave WebP undimensioned).
  • Graceful degradation is required, never a parse error: no assets FS configured, file missing, unreadable, an unsupported format, or SVG/AVIF (which image.DecodeConfig cannot measure) all mean "emit no width/height" and carry on. Log at debug or warn level, consistent with the existing ".. was not rewritten" warning in pkg/parser/images.go.
  • Cache measurements per path within a parse run — the same image referenced from several posts should be read once.

Implementation notes

  • Standard markdown images are straightforward: set node attributes on the *ast.Image in imageTransformer.Transform (pkg/parser/images.go). goldmark's html.ImageAttributeFilter already allows width, height, loading and decoding, so the default image renderer will emit them.
  • Wikilink embeds are the harder half. go.abhg.dev/goldmark/wikilink renders <img> itself in its own Renderer and does not consult node attributes, so setting attributes on a *wikilink.Node will have no effect. Two options, please pick whichever you find cleaner after checking the behaviour:
    1. In an AST transformer, replace image-target embed nodes (n.Embed && isImageTarget(n.Target), per pkg/parser/wikilink.go) with equivalent *ast.Image nodes. This unifies both syntaxes onto one code path and makes the alt="" default fall out naturally.
    2. Register a GoBlog node renderer for wikilink.Kind at a priority that beats the extension's, and emit the <img> ourselves.
  • assetURL returns the URL path; measuring needs the corresponding path within the assets FS (URL /images/screenshots/a.png → FS path screenshots/a.png). Derive it from the same normalisation rather than string-trimming the final URL, and keep the existing .. containment guarantee intact — an asset read must never escape the assets root.

Tests

  • pkg/parser: table tests over both syntaxes covering a measurable PNG, a bare embed (alt=""), a labelled embed, a subdirectory path, a missing file, an unsupported/undecodable format, an absolute URL, a /-rooted path, and no assets FS configured. Use fstest.MapFS with small real encoded images so image.DecodeConfig has genuine headers to read.
  • Integration: extend the existing image coverage (ba4a5cc) to assert the served HTML carries width, height, loading="lazy" and a present alt for a real asset, under both generate and serve.

Documentation

Per CLAUDE.md, any new exported symbol needs godoc plus README coverage:

  • godoc on the new option and config type, and an entry in pkg/parser/doc.go alongside the other options.
  • pkg/config/doc.go if a new option function lands there.
  • README: update the images section (around line 232) to show the full rendered <img> tag, state that dimensions are read from the asset file and that unmeasurable files simply omit them, and delete the "bare ![[pipeline.png]] has no alt text" caveat once alt="" is guaranteed.
  • --assets-dir in the CLI flag table now also affects parsing, not just serving and copying — worth a word.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions