Perf: inject width/height on inline images to prevent CLS - #74
Merged
Conversation
added 2 commits
September 11, 2026 20:12
Extend img_hygiene to add real pixel width/height attributes to inline
content images, so the browser reserves layout space before image bytes
arrive and cumulative layout shift (CLS) is eliminated.
The plugin maps each <img src> back to its source file under the content
tree (handling {static} markers, relative ./images paths, and absolute
site URLs alike), reads the dimensions with Pillow, and emits them as
plain width/height HTML attributes. Remote hosts and data:/blob: URIs are
skipped; imgs that already carry width or height are left untouched, as
are existing loading/decoding attributes.
Dimensions are plain attributes, never a style="" attribute, so the
strict-CSP audit stays green. custom.css sets height:auto on content
images, so injected dimensions drive aspect-ratio reservation without
distorting the rendered image.
Pillow's DecompressionBombError subclasses Exception, not OSError, so a future oversized inline image would break the build instead of degrading to no-dimensions. Dimension injection is best-effort; never fatal. (Critic follow-up on the CLS PR.)
rivassec
added a commit
that referenced
this pull request
Sep 12, 2026
…ks in prod (#76) PR #74's width/height injection needs Pillow, but it was only a dev dep; deploy installs requirements.txt with --no-deps so 'from PIL import Image' failed on the runner -> dims silently skipped -> #74 inert in production. Pillow has no required runtime deps, so --no-deps stays safe. Compiled with --no-emit-index-url to keep private index URLs out of the lockfile. Co-authored-by: rivassec <rivassec@rivassec.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Extends the existing
plugins/img_hygiene.pyplugin to inject real pixelwidth/heightattributes on inline content images, eliminating cumulative layout shift (CLS). No new plugin, nopelicanconf.pychange (img_hygiene is already registered).Why (CLS rationale)
Inline body images had
loading/decodingbut no dimensions, so the browser could not reserve layout space until the image bytes arrived, shifting the text below every image. Supplying intrinsicwidth/heightlets the browser compute the aspect ratio up front and reserve the box, which is what removes the shift.content/static/custom.cssalready declaresmain article p img { aspect-ratio: auto; height: auto; max-width: 100% }, so it was primed to honor these attributes and had no effect until they existed.Signal / approach
Keeps the existing
content_object_initsignal. Rather than depending on link resolution timing, the plugin maps each<img src>back to its source file on disk and reads the dimensions with Pillow. The resolver is form-agnostic: it strips a leading{static}/{filename}/{attach}marker (the form present atcontent_object_init), or a scheme+host for a resolved absolute URL, or handles a document-relative./images/...path, then maps the/images/...tail (anySTATIC_PATHSdir) onto<PATH>/images/.... Because it resolves against the file on disk, it works identically whether the src is still a marker or already a resolved URL.Verified working in both a normal local build (
pelican content -o output -s pelicanconf.py, relative srcs like./images/x.webp) and the publish build (-s publishconf.py, absolutehttps://rivassec.com/...srcs) - not deploy-only.Guardrails:
width="800" height="450"), neverstyle="", soscripts/csp_audit.pystays green.data:/blob:URIs (no local file to measure).widthorheight(author sizing preserved).loading/decodingbehavior; degrades gracefully if Pillow is ever absent.width="72" height="72", cover/OG meta) are untouched - they are not in article_content.Injected dimensions (the 3 posts with inline images)
Spot-checked against
PIL.Image.open(...).size- injected attrs match the real files.Verification
scripts/csp_audit.py output- OK (no inline styles introduced)scripts/check_image_alt.py,check_link_graph.py,check_em_dashes.py- all passpython3 -m unittest discover tests- 38 tests OK