Only generate responsive image variants for images that use them - #3281
Merged
Conversation
|
Pull request environment is available at https://stoctodocspr3281.z22.web.core.windows.net. You can view the ephemeral environment status in Octopus Deploy. This environment will be automatically deprovisioned when the pull request is closed, or after 7 days of inactivity. |
borland
force-pushed
the
orion/responsive-images
branch
3 times, most recently
from
August 5, 2026 21:19
0ad0b5f to
08432a7
Compare
`public/docs/i/` holds 600/1000/2000px WebP variants plus an `x/` fallback
for every image in `public/docs/img/` - 7,905 files, 335 MB, all committed
and all uploaded to the CDN on every deploy.
Almost none of it is ever requested. The only code that emits `/docs/i/`
URLs is `getImageInfo` in custom-markdown.mjs, and it only runs for the
`:img{}` remark directive and `bannerImage` frontmatter. Across 1,267
content pages, exactly one page (/docs/projects/badges) uses `:img{}`;
everything else writes plain `` markdown, which serves the
original straight out of `/docs/img/`. So 7,905 binaries exist to serve two
images.
The tree has also gone stale, because `img.mjs` only ever added files:
`i/x/blueprint.png` is still present long after its source image was
deleted, and the site's own index page still points at it.
Rewrite `img.mjs` to scan `src/**/*.{md,mdx,astro}` for the paths
`getImageInfo` actually receives and generate variants only for those, then
prune anything that no longer belongs. That makes the tree self-maintaining
- removing the last `:img{}` reference to an image now removes its generated
output on the next `npm run dev` - and takes `public/docs/i/` from 7,905
files to 8.
This also drops 1,971 `.json` metadata sidecars. They are build-time inputs
that `getImageInfo` reads out of `public/` to emit width/height, but because
they live under `public/` they were also copied into the deployed package and
uploaded to blob storage, where nothing ever fetches them.
The sidecars no longer carry an `updated` timestamp. Its only reader is
dist-thinner.mjs, which is not wired into the build and never runs; without
the field it simply finds nothing to do. (dist-thinner is removed on the
housekeeping branch, which is independent of this change.)
Finally, drop the dead `bannerImage` from src/pages/index.md. It points at
`/docs/img/blueprint.png`, which no longer exists, so it produces a broken
`itemprop="image"` and is the sole reason a stale variant survives.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
borland
force-pushed
the
orion/responsive-images
branch
from
August 5, 2026 22:08
08432a7 to
f663a69
Compare
enf0rc3
approved these changes
Aug 7, 2026
enf0rc3
left a comment
Contributor
There was a problem hiding this comment.
LGTM will be great for the lightbox component! https://github.com/OctopusDeploy/docs/pull/3313/changes#diff-9f9745786a41273d05896d7981bd762c8c33b441c55b0ddd97aa901ac6f9a464
enf0rc3
added a commit
that referenced
this pull request
Aug 7, 2026
#3281 scoped variant generation to what getImageInfo actually consumed, which was :img{} and bannerImage - four images. The figure plugin makes markdown images inside a :::figure consume them too, so the generator now scans for those, along with the Image component, and the tree covers 1,372 sources instead of 4. Two things keep that from undoing the win #3281 banked. The x fallback is gone. It re-encoded every source at full size purely to fill src= for browsers without srcset support, duplicating an image tree that already ships under /img/. getImageInfo now points src at the source itself: same pixels, same fidelity, one fewer copy. That alone is 115 MB, and it retires the .jpeg/.jpg naming mismatch, since the generator wrote .jpg where getDestination asked for .jpeg. The generator also stops writing candidates the srcset will never name. getImageInfo caps the list once the source runs out of pixels, so a 500px image only ever names the 600w entry; generating 1000w and 2000w for it produced files no markup references. That skips 406 of 4,116 variants. Package size lands at 542 MB against 483 MB after #3281 and 758.9 MB before it. The 59 MB buys webp at the right width for 1,372 images rather than 4: the pages those images sit on drop from 158 MB of source PNG to 43 MB at the 1000w candidate most desktop readers get. Reading headers for 1,372 images needs batching - opening them all at once exhausts the descriptor limit, which sharp reports as a missing input file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.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.
How to review this PR
Go commit by commit and skip commit 2; there's too many files otherwise
:img{andbannerImage:frontmatter:img{as there is significant benefit for the index page that everyone starts with - it contains two images, both of which are larger than they should beBefore
After
Reducing risk
I tested this both locally and on the staging site. All you need to do is browse to pages with images. The main
/docspage now uses responsive images to prove that path, and basically any other page has a raw PNG in itClaude's notes
The problem
public/docs/i/holds 600/1000/2000px WebP variants plus anx/fallback for every image inpublic/docs/img/— 7,905 files, 335 MB, all committed to the repo and all uploaded to blob storage on every deploy.Almost none of it is ever requested. The only code that emits
/docs/i/URLs isgetImageInfoincustom-markdown.mjs, and it only runs for two things: the:img{}remark directive andbannerImagefrontmatter. Across 1,267 content pages, exactly one page (/docs/projects/badges) uses:img{}. Everything else writes plainmarkdown, which serves the original straight out of/docs/img/.So 7,905 committed binaries exist to serve two images.
I found this by grepping the built package: of 2,697 HTML files, 2 reference
/docs/i/at all.The tree had also gone stale, because
img.mjsonly ever added files, never removed them.i/x/blueprint.pngwas still there long after its source image was deleted — and the site's own index page still pointed at it.The fix
Rewrite
src/themes/octopus/utilities/img.mjsto scansrc/**/*.{md,mdx,astro}for the pathsgetImageInfoactually receives, generate variants only for those, and prune anything that no longer belongs.That makes the tree self-maintaining: removing the last
:img{}reference to an image now removes its generated output on the nextnpm run dev, and adding one generates just what that image needs.public/docs/i/goes from 7,905 files to 8.Three smaller things fall out of it:
.jsonmetadata sidecars dropped. These are build-time inputs —getImageInforeads them out ofpublic/to emitwidth/heightand cap the srcset widths — but because they live underpublic/they were also copied into the deployed package and uploaded, where nothing ever fetches them.updatedtimestamp is gone from the sidecars. Its only reader isdist-thinner.mjs, which is not wired into the build and never runs; without the field it simply finds nothing to do. (dist-thinneris deleted on the housekeeping branch, which is independent of this one.)bannerImageinsrc/pages/index.mdis removed. It pointed at/docs/img/blueprint.png, which no longer exists, so it produced a brokenitemprop="image"— and it was the sole reason a stale variant survived.I also fixed a latent bug in the generator while rewriting it: the original never awaited its
sharp(...).toFile()calls, so writes raced against the process exiting.Why remove rather than expand
Responsive images are worth having, and the honest alternative is to make every markdown image go through the pipeline — the win there would be large, since
/docs/img/is 214 MB of unresized PNGs. But that is a content-and-rendering change affecting 1,267 pages, not a build cleanup, and it should be its own decision.What is not defensible is the status quo: paying 335 MB of repo weight and deploy time to serve two images, with no mechanism to keep it in sync. This PR makes the cost match the usage. If we later decide to apply
:img{}broadly, the generator now scales to it correctly instead of brute-forcing every file in the tree.Note that the variants must stay committed:
astro builddoes not runimg.mjs(onlynpm run devdoes), so CI copies whatever is inpublic/. At 8 files that is fine.Verification
Package size on this branch alone: 758.9 MB / 15,887 files → 483 MB / 6,020 files.
npm run crawl(linkinator)npx playwright testastro buildThe one missing reference is
/docs/img/icon-example.svgfromcomponents.mdx. It is absent frommainand from the previously built package too, so it is pre-existing and unrelated — andcomponents.mdxis an internal-only page that the housekeeping branch stops publishing anyway.I confirmed
/docs/projects/badgesstill renders correctly: the<img>keeps itssrcsetwith all three widths, itsx/fallback, and thewidth/heightattributes derived from the metadata sidecar.Review notes
src/themes/octopus/utilities/img.mjsplus three frontmatter lines insrc/pages/index.md.orion/housekeeping) and can merge in either order. The two overlap only in that housekeeping deletes 367 orphaned source images whose sidecars this branch also deletes; git resolves identical deletions without conflict, andimg.mjsre-prunes on the next dev run regardless.🤖 Generated with Claude Code