Skip to content

feat: on-demand image optimization with sharp#95

Merged
rsbh merged 13 commits into
mainfrom
feat/image-optimization
May 21, 2026
Merged

feat: on-demand image optimization with sharp#95
rsbh merged 13 commits into
mainfrom
feat/image-optimization

Conversation

@rsbh

@rsbh rsbh commented May 20, 2026

Copy link
Copy Markdown
Member

Summary

  • Add /api/image endpoint that resizes + converts content images to WebP via sharp on first request
  • Remark plugin rewrites all image URLs (markdown, HTML, JSX) to route through the optimization endpoint
  • SSR preload links and client-side prefetch use optimized URLs
  • 1.8MB PNG → 6KB WebP at 640w quality 75

Changes

  • src/server/api/image.ts — new optimization endpoint with disk caching, allowlisted widths, SVG passthrough
  • src/lib/image-utils.ts — shared helpers (buildOptimizedUrl, isLocalImage, isSvg)
  • src/lib/remark-resolve-images.ts — rewrite resolved image URLs through /api/image
  • src/components/mdx/image.tsx — route local images through optimization endpoint
  • src/server/vite-config.ts — externalize sharp for Nitro bundling
  • src/server/entry-server.tsx — preload optimized URLs, deduplicate
  • src/lib/page-context.tsx — client preload uses optimized URLs

Test plan

  • /api/image?url=/_content/...&w=640&q=75 returns 200 with WebP
  • SVG URLs return 307 redirect to original
  • Missing/invalid params return 400
  • SSR preload links point to optimized URLs
  • Build and tests pass

🤖 Generated with Claude Code

Add /api/image endpoint that resizes and converts content images to
WebP on first request, caching results to disk. Images go from ~1.8MB
originals to ~6KB optimized WebP at 640w.

- New /api/image endpoint: accepts url, w (width), q (quality) params
- Remark plugin rewrites image URLs to point through optimization endpoint
- Image component and SSR preload use optimized URLs
- SVGs pass through unchanged, sharp externalized for Nitro bundling
- Allowlisted widths prevent cache flooding

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@vercel

vercel Bot commented May 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
chronicle Ready Ready Preview, Comment May 21, 2026 8:28am

@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@rsbh has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 48 minutes and 18 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 31a19394-2b35-45dd-b6c5-765e348000ff

📥 Commits

Reviewing files that changed from the base of the PR and between 9acd409 and 647b564.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • packages/chronicle/package.json
  • packages/chronicle/src/components/mdx/image.tsx
  • packages/chronicle/src/components/mdx/index.tsx
  • packages/chronicle/src/lib/image-utils.test.ts
  • packages/chronicle/src/lib/image-utils.ts
  • packages/chronicle/src/lib/page-context.tsx
  • packages/chronicle/src/lib/remark-resolve-images.ts
  • packages/chronicle/src/server/api/image.test.ts
  • packages/chronicle/src/server/api/image.ts
  • packages/chronicle/src/server/entry-server.tsx
  • packages/chronicle/src/server/vite-config.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/image-optimization

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Serve AVIF, WebP, or original format based on browser Accept header.
AVIF preferred > WebP > original (resized only). Adds Vary: Accept
for proper CDN caching.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Portable across deployment targets (filesystem, KV, Redis, etc.).

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Persists optimized images to .cache/images/ on disk. Survives
server restarts.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@rsbh rsbh marked this pull request as ready for review May 21, 2026 04:04
rsbh and others added 2 commits May 21, 2026 09:36
Move useStorage before early returns to satisfy Biome hook ordering
rule. Replace magic status numbers with StatusCodes constants.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Replace magic numbers 1024 and 75 with named constants from
image-utils.ts, used across all image optimization call sites.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
24 tests covering negotiateFormat, cacheKey, MIME mapping,
isLocalImage, isSvg, buildOptimizedUrl, and constants.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

@rohanchkrabrty rohanchkrabrty left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. .cache/images/ has no size cap or eviction — disk usage grows forever as content changes.
  2. q accepts any 1–100; restrict to a few steps like [60, 75, 90], otherwise each image can produce 2,100 cache entries (100 × 3 formats × 7
    widths).
  3. Cache stampede in image.ts:86-99 — two concurrent requests for the same uncached variant both run sharp. Coalesce in-flight work with a
    Map<key, Promise>.
  4. cacheKey in image.ts:29-32 ignores mtime, so replacing an image at the same path keeps serving the old bytes. Mix fs.stat(filePath).mtimeMs
    into the hash.
  5. The remark plugin rewrites every to /api/image?…, so builds deployed to S3, Cloudflare Pages static, or GitHub Pages 404 on images.
    Either pre-generate variants at build time, or skip the rewrite when the preset has no Node runtime.

- Restrict quality to allowed steps [60, 75, 90, 100] with snap-to-nearest
- Add cache stampede protection via in-flight promise map
- Add LRU cache eviction with 500 entry cap
- Skip image optimization rewrite for static build presets
- Accept optimize option in remark-resolve-images plugin

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@rsbh

rsbh commented May 21, 2026

Copy link
Copy Markdown
Member Author

Review fixes applied

  • Quality restricted to allowed steps [60, 75, 90, 100] with snap-to-nearest (was any 1-100)
  • Cache stampede protection via in-flight promise map — concurrent requests for same variant coalesce
  • Cache eviction with 500 entry cap (oldest entries removed on insert)
  • Remark plugin accepts optimize option — disabled for static build presets (vercel-static, cloudflare-pages, github-pages) so images don't 404
  • Removed mtime from cache key — cache is ephemeral (pod resets on deploy)

@rsbh rsbh merged commit a38e999 into main May 21, 2026
4 checks passed
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