Skip to content

feat: @pracht/image with responsive images and pluggable optimization loaders - #192

Open
JoviDeCroock wants to merge 6 commits into
mainfrom
claude/pracht-image
Open

feat: @pracht/image with responsive images and pluggable optimization loaders#192
JoviDeCroock wants to merge 6 commits into
mainfrom
claude/pracht-image

Conversation

@JoviDeCroock

Copy link
Copy Markdown
Owner

Summary

  • New package @pracht/image bringing next/image-quality image handling to pracht apps, mirroring next/image's loader architecture:
    • <Image> Preact component — requires width+height (or fill) so space is always reserved (dev-mode console.error when missing), generates srcset across a configurable device-size breakpoint list (w descriptors with sizes/fill, cache-friendly snapped 1x/2x otherwise), loading="lazy" + decoding="async" by default with a priority prop switching to eager + fetchpriority="high", quality (default 75), and next/image-style absolute-inset styling for fill. Renders a plain <img> — SSR-safe, zero client runtime.
    • Pluggable loaders (({ src, width, quality }) => string): defaultLoader (pracht endpoint), cloudflareLoader (/cdn-cgi/image/width=,quality=,format=auto/<src>), vercelLoader (/_vercel/image?url=&w=&q=), passthroughLoader (static hosts; srcset omitted). Configured globally via configureImage() and overridable per component via the loader prop.
    • Node optimization endpointcreateImageHandler() from @pracht/image/node, mounted as the src/api/_pracht/image.ts API route (the least invasive wiring: it rides the existing API-route auto-discovery, so it works identically in pracht dev and on every adapter with a Node-compatible runtime, no vite-plugin or adapter changes). Uses sharp as an optional peer dependency with a clear install hint when missing. Security: same-origin (relative) sources only unless remotePatterns allowlists hosts, redirect re-validation, width allowlist + maxWidth cap so caches can't be filled with arbitrary variants, source size cap, image/* content-type enforcement, SVG served with Content-Disposition: attachment. Negotiates WebP (AVIF opt-in) via Accept and answers with Cache-Control: public, max-age=31536000, immutable + Vary: Accept.
  • Example usage in examples/basic: /gallery route (priority, fixed, and fill images), the mounted endpoint, and public/banner.jpg; the node-build e2e now exercises the endpoint against the built server (webp output, immutable headers, 403 for non-allowlisted remote sources).
  • Docs: new docs/IMAGES.md (linked from the README repo map), package README, docs/WORKSPACE.md + VISION_MVP.md package enumerations updated, root build filter + tsconfig paths wired.

Follow-ups intentionally out of scope for v1:

  • Build-time image optimization of SSG pages.
  • Blur placeholders (placeholder="blur").

Testing

  • pnpm e2e (60 passed, including new node-build assertions for the image endpoint)
  • pnpm format
  • pnpm lint
  • pnpm test (411 passed; 41 new tests for srcset generation, priority, fill, loader selection, and the endpoint's validation/allowlist/format negotiation)
  • pnpm typecheck
  • Manual pracht dev smoke test in examples/basic: /gallery renders, /api/_pracht/image?url=%2Fbanner.jpg&w=640&q=75 returns webp with immutable cache headers, remote sources are denied with 403

Checklist

  • Docs updated if needed (docs/IMAGES.md, README repo map, docs/WORKSPACE.md, VISION_MVP.md)
  • Skills updated if needed — N/A (skills don't enumerate packages; an /add-images skill is a possible follow-up)
  • Changeset added if published packages changed (.changeset/pracht-image-package.md, minor for @pracht/image)

🤖 Generated with Claude Code

@JoviDeCroock
JoviDeCroock force-pushed the claude/pracht-image branch 2 times, most recently from 8127f01 to df1a0a3 Compare July 9, 2026 05:19
JoviDeCroock and others added 2 commits July 9, 2026 07:37
… loaders

New package providing next/image-quality image handling for pracht apps:

- <Image> Preact component: required width/height (or fill) for CLS
  safety with dev warnings, srcset across configurable device-size
  breakpoints (w descriptors with sizes/fill, snapped 1x/2x otherwise),
  lazy + async decoding by default, priority -> eager + fetchpriority,
  next/image-style fill styling. Renders plain <img>; zero client runtime.
- Pluggable loaders ({ src, width, quality }) => string: defaultLoader
  (pracht endpoint), cloudflareLoader (/cdn-cgi/image/...), vercelLoader
  (/_vercel/image), passthroughLoader. Global configureImage() plus a
  per-component loader prop.
- @pracht/image/node: createImageHandler(), a sharp-backed (optional peer
  dependency with an install hint) optimization endpoint mounted as the
  src/api/_pracht/image.ts API route so it works in pracht dev and on
  every adapter. Same-origin sources only unless remotePatterns allows,
  width allowlist + max cap, redirect re-validation, Accept-negotiated
  WebP/AVIF, immutable cache headers, SVG/GIF passthrough.
- Example usage in examples/basic (gallery route + endpoint + banner),
  e2e coverage in node-build, 41 unit tests, docs/IMAGES.md, changeset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@JoviDeCroock
JoviDeCroock force-pushed the claude/pracht-image branch from df1a0a3 to 5d6fe8f Compare July 9, 2026 05:37
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.

1 participant