Generating image URLs
A Rendorix image URL is a normal GET: the path identifies the original in storage, and the query string carries the transform, expiry (exp), and signature (s). The response is the transformed image bytes, cacheable at the CDN if allowed.
Two layers:
| Layer | What you work with | Typical content |
|---|---|---|
| App / “frontend” (server code) | @rendorix/client: preset: "hero", optional w/h/f/q | Semantic names + small option objects—no HMAC in your head. |
| Wire (what CloudFront and the browser see) | Sorted query: transform fields, exp, then s | Concrete parameters the edge verifies (see below). |
Use @rendorix/client in Node for the resolved flow; use manual steps only if you are not on Node or you are debugging parity.
What the URL does
Section titled “What the URL does”- Client (browser) requests
GETwith the full URL. - CloudFront may return a cached 200 for that exact URL (path + query as keyed).
- Otherwise the viewer code verifies
sandexp, may strip or normalize params for the origin/cache key, then the image worker reads the original and applies the transform.
So the URL is both address (which file) and authorized recipe (how to resize / encode)—plus proof it was minted by someone with the secret before exp.
Wire format: @rendorix/client and the official stack
Section titled “Wire format: @rendorix/client and the official stack”The Node client (Client library) does not emit a p=… “preset id” in the query. It merges your preset and overrides into a concrete transform, then signs a query containing only defined transform fields plus exp:
- Transform —
w,h,f(format),q(quality)—only present if set; keys sorted for signing. exp— Unix seconds,floor(now/1000) + ttl.s— hex HMAC-SHA256 of the signing stringpath + "?" + queryWithoutS(nosin the string being signed).
Example shape (values illustrative):
GET https://img.example.com/photos/hero.jpg?exp=1715000000&f=webp&h=630&q=85&w=1200&s=abc…Presets exist in your createRendorix({ presets: { hero: { w, h, f, q } } }) config—they are not a separate key on the wire unless you build a custom signer that adds p=.
With @rendorix/client (recommended in Node)
Section titled “With @rendorix/client (recommended in Node)”import { createRendorix } from "@rendorix/client";
const rx = createRendorix({ baseUrl: "https://img.example.com", secret: process.env.RENDORIX_HMAC_SECRET!, presets: { hero: { w: 1200, h: 630, f: "webp", q: 85 }, thumb: { w: 400, h: 400, f: "webp", q: 80 }, },});
// “Tailwind for images”: name the role, not the pixels in every fileconst src = rx.img("photos/team.jpg", { preset: "hero" });
// Optional inline overrides (merged on top of the preset)const tight = rx.img("photos/team.jpg", { preset: "thumb", w: 200 });That is the intended selling point: configure transforms like design tokens, call img from server templates or loaders, and ship without thinking through query order or HMAC. See Client library for errors, ttl, and types.
Manual URL generation (without the library)
Section titled “Manual URL generation (without the library)”Use this when you are not using Node, or for test / debug parity checks.
- Normalize the path — Same leading-slash rules as your signer and edge (e.g.
/+ key without duplicate slashes). - Build the transform map —
w,h,f,q(only keys you allow). If you use named presets in your app, resolve the name to these fields before signing—same asresolvein the client. - Set
exp— Unix seconds; policy for TTL is yours (TTL, Expiration). - Build
queryWithoutS— Include transform params andexponly; sort keys lexicographically (the client’scanonicalizestep must match the CloudFront Function in the Rendorix infra repo). - Signing string —
`${path}?${queryWithoutS}`(no&s=yet). - Compute
s—createHmac("sha256", secret).update(signingString).digest("hex")(lowercase hex, 64 chars) if you match the official stack—confirm against HMAC signing and the infra repo. - Final URL — Append
&s=(or your separator rules) to the full query. URL-encode for HTML if needed.
Illustrative Node snippet (for parity testing—field order in the string must follow your sorter, not the order below):
import { createHmac } from "node:crypto";
const path = "/photos/hero.jpg";const exp = String(Math.floor(Date.now() / 1000) + 3600);// Official client sorts keys: e.g. exp, f, h, q, wconst queryWithoutS = `exp=${exp}&f=webp&h=630&q=85&w=1200`;const signingString = `${path}?${queryWithoutS}`;const s = createHmac("sha256", process.env.RENDORIX_HMAC_SECRET) .update(signingString) .digest("hex");const url = `https://img.example.com${path}?${queryWithoutS}&s=${s}`;If your product uses a literal p=hero on the wire, your canonical string must include that—do not copy the snippet above without matching the deployed validator.
Cache and security notes
Section titled “Cache and security notes”- Any input that changes output bytes (including which transform fields are present) should participate in the signed string so tampering is detected (Signed URLs).
- Cache keys at CloudFront should line up with how unique each variant is—see Caching.
Related reading
Section titled “Related reading”- Client library —
createRendorix, presets,img(), tests for parity - Using presets — names in app code vs fields on the wire
- Overrides —
w/h/f/qin the client API - HMAC signing
- Quick start —
curlsmoke test