fix(images): drop Unpic, return the scaffold to Astro's sharp service - #57
Merged
Conversation
The scaffold shipped `@unpic/astro` as Astro's image service, and both the
CLAUDE.md guidance and the code-review skill had been written around its
quirks — to the point that its bugs were documented as doctrine
(`.img-uncap` as "the sanctioned escape hatch", "`widths` does NOT
constrain output"). Removing unpic inverts all of it.
What unpic was doing, verified on a production site before removal:
- Ignored the `widths` prop, generating its own 12-step ladder.
- Turned `width`/`height` into inline `max-width`/`max-height`/`width:100%`
styles that outranked every Tailwind class, which is what forced the
`.img-uncap { max-width: none !important }` workaround.
- Emitted `style="[object Object]"` plus stray `url=`/`format=` attributes
on every content-collection image — 41 images across 15 pages on that
site, 4 of which rendered at 2x2px.
Removing it cut that site's built asset footprint from 173MB to 47MB.
Scaffold:
- Drop `@unpic/astro` and the `image.service` key. Astro's schema default
is already `astro/assets/services/sharp`.
- Delete the `.img-uncap` utility from `src/index.css`.
- Document why `imageService: "custom"` is load-bearing, in the config
itself and in CLAUDE.md.
- Replace the unpic-specific right-sizing rules with the sharp ones:
`sizes` needs `widths` or no srcset is emitted; `widths` above the
intrinsic width are clamped, never upscaled; `width` + `height` that
disagree with the source ratio crop via `fit: cover`; `width` sizes the
file, `w-full` sizes the element.
`imageService: "compile"` is now flagged as wrong rather than recommended.
It does not keep sharp: on adapter v13 `case "compile"` returns the workerd
service unconditionally, and on v14 `hasUserImageService()` explicitly
excludes the sharp entrypoint. This corrects website-builder, code-review
and CLAUDE.md, which all recommended it.
Skills:
- code-review: rewrite the right-sizing section against sharp's actual
`getSrcSet`/`getTargetDimensions` behaviour. New critical for `sizes`
without `widths`; new warnings for `width`+`height` on a CSS-controlled
box and for `max-width: none !important` image utilities. `"compile"`
moves from Pass to Critical.
- seo-audit: add a build-output right-sizing check that measures real pixel
dimensions against the slot `sizes` declares, rather than relying on file
size in MB as a proxy. Also catches inert `sizes`, aspect-ratio drift and
malformed image markup.
- scaffold-sync: mark the 0.1.2 Unpic entry superseded and add a 0.3.1
migration entry with the manual steps for existing sites.
Scaffold builds clean; `bun run check` is unchanged from main (6
pre-existing errors: schema-dts typing on 404.astro and missing Bun types).
Built markup carries no unpic artifacts and a correct 2-candidate srcset.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two unrelated causes, both long-standing on main. schema-dts (2 errors, src/pages/404.astro): `satisfies WebPage` is the wrong annotation for a top-level JSON-LD object. A bare schema-dts type has no `@context` property, so the literal failed with "Object literal may only specify known properties, and '\"@context\"' does not exist in type 'WebPageLeaf'", and the result wasn't assignable to the `WithContext<Thing>` that Layout's `schema` prop expects. Annotate with `WithContext<WebPage>` instead. CLAUDE.md taught this same broken pattern in its SEO & Structured Data example, which is where the page copied it from — fixed there too, with a note explaining why `satisfies T` fails so it doesn't get reintroduced. Bun types (4 errors, test/dev-smoke.test.ts): `tsconfig.json` set `"types": ["node"]`, which restricts global type inclusion to that one package, and `@types/bun` wasn't installed. So `bun:test` and the `Bun` global both went unresolved. Add the dev dependency and widen to `["node", "bun"]`. `bun run check` now reports 0 errors, 0 warnings. Build and `bun test` (4 pass) unaffected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The plugin version was bumped to 0.4.0 on this branch after the entry was written, so the registry pointed at a 0.3.1 that never shipped — which would have stopped scaffold-sync matching the note to the release. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Editorial pass over the four skills touched by the Unpic removal.
- Drop the site-paretosecurity#28 link from code-review. A skill shouldn't
cite one repo's PR as authority; the sharp semantics above it are the
reason the checks exist.
- Cut self-assessment ("the single highest-impact check") and lines that
restated the rule they followed ("this is the 5000px into a 200px slot
case", "runs on the built HTML" in a skill whose description already says
it audits built HTML).
- Generalise away from the vendor. The malformed-markup check no longer
greps for unpic's `url=`/`format=` attributes — `style="[object Object]"`
is the generic symptom worth catching. The `max-width: none !important`
warning drops the "workaround for a service we no longer use" history and
just states why the pattern is wrong.
- Compress the scaffold-sync 0.4.0 entry: a migration registry is read for
actions, not for the war story that produced them.
- Compress the website-builder adapter bullet. That file states its rules
are "enforced by CLAUDE.md — see CLAUDE.md for full details", so v13/v14
`setImageConfig` mechanics don't belong in its always-on constraints.
- Shorten seo-audit's description; a trigger description should say what the
skill covers, not enumerate each sub-check.
Restored one clarification that was over-trimmed: a `widths` array does not
satisfy the missing-`width` check, since `getTargetDimensions` then falls
back to the intrinsic size and the `<img>` fallback still ships full-res.
Net +103/-21 across the four skills, down from +108/-21, with the remaining
lines carrying more per token.
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.
Propagates the findings from site-paretosecurity#33 back into the scaffold and skills.
The scaffold shipped
@unpic/astroas Astro's image service, and both CLAUDE.md and thecode-reviewskill had been written around its quirks — to the point that its bugs were codified as doctrine:Removing unpic inverts all of that.
What unpic was actually doing
Verified against a production build before removal:
widthsprop640…3840w) regardless of what the markup declared.width/heightinto inlinemax-width/max-height/width:100%.img-uncapexisted.style="[object Object]"+ strayurl=/format=styleas an object; Astro's content layer stringifies it withString(value).Removing it cut that site's built asset footprint from 173MB → 47MB.
imageService: "compile"was wrong everywhere — now flaggedwebsite-builder,code-reviewand scaffoldCLAUDE.mdall recommended"compile". It does not keep sharp:Either version replaces sharp.
case "custom": return { ...config }is the only branch that passes the config through untouched. The scaffold already used"custom"— the docs were wrong, not the config."compile"moves from Pass to Critical incode-review.Scaffold
@unpic/astroand theimage.servicekey (Astro's schema default is already sharp)..img-uncaputility fromsrc/index.css.imageService: "custom"as load-bearing, in the config itself, so nobody "fixes" it back.sizeswithoutwidths/densities→getSrcSetreturns[], so no srcset is emitted andsizesis inert.widthsabove the source's intrinsic width are clamped, never upscaled — a 2× candidate needs a big enough source.width+heightdisagreeing with the source ratio → sharp resizes withfit: cover, a silent crop.widthsizes the file;w-fullsizes the element.widthsladder too — user rehype plugins run beforerehypeImages, which foldsnode.propertiesinto the__ASTRO_IMAGE_payload, so those props are realgetImage()options.Skills
code-review— right-sizing section rewritten against sharp'sgetSrcSet/getTargetDimensions. New Critical forsizeswithoutwidths; new warnings forwidth+heighton a CSS-controlled box and formax-width: none !importantimage utilities. Removes the "do NOT flag.img-uncap" pass rule.seo-audit— new build-output check that measures real pixel dimensions against the slotsizesdeclares, instead of using file size in MB as a proxy. Catches the "5000px file in a 200px slot" case, inertsizes, aspect-ratio drift, and malformed image markup. Skips SVG (vector — measuring it produces false positives on every logo).website-builder— corrected adapter guidance plus a right-sizing rule in the always-on constraints.scaffold-sync— 0.1.2 Unpic entry marked superseded; new 0.3.1 migration entry with the manual steps for existing sites (including the two failure modes to watch for: images that shrink withoutw-full, and tags that silently lose their srcset withoutwidths).Plugin version bumped to 0.3.1 to match the migration entry.
Verification
bun install+bun run buildsucceed.bun run checkis unchanged from main — 6 pre-existing errors (schema-dts typing on404.astro, missing@types/bun), confirmed by stashing and re-running onmainrather than assumed.style="[object Object]",url=,format=) and emits a correct 2-candidate srcset.🤖 Generated with Claude Code