Skip to main content
There are two ways to put an image on a page.

Plain <img>

No component, no service, no extra syntax. src and alt interpolate like any other attribute, and the tag comes out as written:
A site already serving images at the required sizes and formats needs nothing further on this page.
Quote or brace attribute values. Unquoted, width=600 lexes the value as a second attribute NAME, so <img src="..." width=600 height=400 /> renders as <img 400 600 src=https://x.test/a.jpg>. Both dimensions are gone and nothing is reported.

The <Image> component

<Image> is built into the compiler. It renders a <picture> offering AVIF and WebP with a JPEG fallback, all three pointing at an image service:
That is wrapped for reading. The real output is one minified line. <Image> is a URL generator. It does no work at build time and touches no pixels. Those URLs 404 until something is serving /_slurp/image.

Props

Brace the dimensions. width={600} works; width=600 lexes as a boolean attribute plus a second one named 600, the value is lost, and the default of 800 is used instead. The page renders, the image is the wrong size, and nothing is reported.There is no lazy prop either. Lazy is already the default, and an unrecognised prop is dropped silently.

The URL is fixed and root-relative

/_slurp/image is hardcoded. There is no base-path option and no way for a theme to point at another host, so the service must answer on the same origin as the page, behind a reverse proxy.
src has to be an absolute http or https URL. A theme-local path passes through unchanged and produces /_slurp/image?src=/static/logo.png&w=800&f=jpeg, which the service rejects:
Use a plain <img> for self-served assets and <Image> for remote ones. A src containing reserved characters is percent-encoded.

Running the service

One route, GET /_slurp/image. Proxy /_slurp/image from the web server to port 3001 so the page and the images share an origin.
The dev server does not serve this route. slurp-dev returns 404 for /_slurp/image, so images render as broken until you run slurp-image alongside it and proxy the path yourself.
The defaults are closed. This service fetches URLs an attacker may have chosen, so it binds 127.0.0.1 and sends no CORS headers. Widen only as needed.
There is no authentication, so put it behind a reverse proxy rather than on a public port.

Flags

RUST_LOG is the only environment variable it reads.

Requesting a transform

There is no h and no q, and the format parameter is f, not fmt. Height is not a transform input at all: the <Image> component uses it for the <img> attribute so the browser can reserve space. The only transform is a width-only Lanczos3 resize. It preserves aspect ratio and never upscales, so asking for a width larger than the source returns the source size. Quality is fixed.

Formats in and out

A PNG or GIF source is transcoded to one of the three output formats, never re-emitted as itself. An AVIF or HEIC source is recognised only so it can be reported as unsupported rather than as corrupt data.

Errors

Every error is JSON, shaped {"error": "..."}. A successful response carries the output MIME type and cache-control: public, max-age=<cache-ttl>.

Request guards

The service takes a URL from a query string and fetches it, so it is a server-side request forgery primitive unless written not to be. The guards:
  • Scheme allowlist. Only http and https. Anything else is invalid source URL, including a URL carrying userinfo.
  • DNS resolved up front, every returned address required to be globally routable unicast, and those addresses pinned into the HTTP client so a second lookup cannot rebind to a private one. A request for http://127.0.0.1:8080/a.jpg comes back 403 request blocked.
  • No redirects. The redirect policy is none(), so a public URL cannot bounce the fetcher inward.
  • Magic-byte validation, backed by a decoder-format allowlist, so a polyglot file cannot be routed to a decoder other than the one its header declares.
  • Source size capped at 20 MiB, checked against Content-Length and again while streaming, because the header can be absent or can lie.
  • Decompression bomb protection. Declared width times height times the decoder’s real bytes per pixel must fit under 50 MiB, checked before any pixel buffer is allocated.
  • Transforms run off the async workers behind a concurrency limit, so a burst of large images cannot starve the runtime.
Fixed limits, not configurable by flag or environment variable: source 20 MiB, decoded 50 MiB, width 8192, fetch timeout 10 s, and a rate limit of 100 requests per 60 s per IP.
The rate limiter is an in-memory fixed-window counter. It resets on restart and is not shared between instances, so treat it as a backstop and do real rate limiting at the proxy.

Caching

Two levels, both keyed on the exact (src, w, f) triple:
  1. an in-memory LRU, 512 entries by default,
  2. a disk cache under --cache-dir, with a background sweeper that evicts expired entries and then the oldest ones to stay under --max-disk-cache-bytes (2 GiB by default).
An attacker requesting many distinct variants therefore cannot fill the disk.

Next

Components

Props, scope, slots, and imports.

Image service reference

Every parameter, limit and status code.

Security model

The order the guards run in, and what they do not cover.

Dev server

What the dev server serves.