Skip to content

fix(images): drop Unpic, return the scaffold to Astro's sharp service - #57

Merged
dz0ny merged 5 commits into
mainfrom
fix/drop-unpic-native-image-service
Aug 4, 2026
Merged

fix(images): drop Unpic, return the scaffold to Astro's sharp service#57
dz0ny merged 5 commits into
mainfrom
fix/drop-unpic-native-image-service

Conversation

@dmurko

@dmurko dmurko commented Aug 4, 2026

Copy link
Copy Markdown
Member

Propagates the findings from site-paretosecurity#33 back into the scaffold and skills.

The scaffold shipped @unpic/astro as Astro's image service, and both CLAUDE.md and the code-review skill had been written around its quirks — to the point that its bugs were codified as doctrine:

.img-uncapThis is the sanctioned escape hatch, not a hack
width is what controls the emitted file. widths does NOT.

Removing unpic inverts all of that.

What unpic was actually doing

Verified against a production build before removal:

# Defect Impact
1 Ignored the widths prop Generated its own 12-step ladder (640…3840w) regardless of what the markup declared.
2 Turned width/height into inline max-width/max-height/width:100% Inline style outranks every Tailwind class — this is the only reason .img-uncap existed.
3 Emitted style="[object Object]" + stray url=/format= 41 images across 15 pages, 4 of which rendered at 2×2px (invisible). unpic returns style as an object; Astro's content layer stringifies it with String(value).

Removing it cut that site's built asset footprint from 173MB → 47MB.

⚠️ imageService: "compile" was wrong everywhere — now flagged

website-builder, code-review and scaffold CLAUDE.md all recommended "compile". It does not keep sharp:

// adapter v13 — unconditional
case "compile": return { ...config, service: WORKERD_IMAGE_SERVICE, ... };

// adapter v14 — and hasUserImageService() explicitly excludes the sharp entrypoint
case "compile": return { ...config, service: hasUserImageService(config) ? config.service : WORKERD_IMAGE_SERVICE, ... };

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 in code-review.

Worth noting: no adapter upgrade is needed for this. v13 is the more absolute of the two, and @astrojs/cloudflare@14 has peer astro: ^7.0.0 while the scaffold is on Astro 6.4.8 — so upgrading would force an Astro major bump, which is separate work.

Scaffold

  • Drop @unpic/astro and the image.service key (Astro's schema default is already sharp).
  • Delete the .img-uncap utility from src/index.css.
  • Comment imageService: "custom" as load-bearing, in the config itself, so nobody "fixes" it back.
  • Replace the unpic right-sizing rules with sharp's real semantics:
    • sizes without widths/densitiesgetSrcSet returns [], so no srcset is emitted and sizes is inert.
    • widths above the source's intrinsic width are clamped, never upscaled — a 2× candidate needs a big enough source.
    • width + height disagreeing with the source ratio → sharp resizes with fit: cover, a silent crop.
    • width sizes the file; w-full sizes the element.
    • Markdown rehype plugins need a widths ladder too — user rehype plugins run before rehypeImages, which folds node.properties into the __ASTRO_IMAGE_ payload, so those props are real getImage() options.

Skills

  • code-review — right-sizing section rewritten against sharp's getSrcSet/getTargetDimensions. New Critical for sizes without widths; new warnings for width+height on a CSS-controlled box and for max-width: none !important image utilities. Removes the "do NOT flag .img-uncap" pass rule.
  • seo-audit — new build-output check that measures real pixel dimensions against the slot sizes declares, instead of using file size in MB as a proxy. Catches the "5000px file in a 200px slot" case, inert sizes, 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 without w-full, and tags that silently lose their srcset without widths).

Plugin version bumped to 0.3.1 to match the migration entry.

Verification

  • Scaffold bun install + bun run build succeed.
  • bun run check is unchanged from main — 6 pre-existing errors (schema-dts typing on 404.astro, missing @types/bun), confirmed by stashing and re-running on main rather than assumed.
  • Built markup carries zero unpic artifacts (style="[object Object]", url=, format=) and emits a correct 2-candidate srcset.

🤖 Generated with Claude Code

dmurko and others added 5 commits August 4, 2026 12:08
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants