Skip to content

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:

LayerWhat you work withTypical content
App / “frontend” (server code)@rendorix/client: preset: "hero", optional w/h/f/qSemantic names + small option objects—no HMAC in your head.
Wire (what CloudFront and the browser see)Sorted query: transform fields, exp, then sConcrete 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.


  1. Client (browser) requests GET with the full URL.
  2. CloudFront may return a cached 200 for that exact URL (path + query as keyed).
  3. Otherwise the viewer code verifies s and exp, 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:

  • Transformw, 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 string path + "?" + queryWithoutS (no s in 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=.


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 file
const 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.

  1. Normalize the path — Same leading-slash rules as your signer and edge (e.g. / + key without duplicate slashes).
  2. Build the transform mapw, h, f, q (only keys you allow). If you use named presets in your app, resolve the name to these fields before signing—same as resolve in the client.
  3. Set exp — Unix seconds; policy for TTL is yours (TTL, Expiration).
  4. Build queryWithoutS — Include transform params and exp only; sort keys lexicographically (the client’s canonicalize step must match the CloudFront Function in the Rendorix infra repo).
  5. Signing string`${path}?${queryWithoutS}` (no &s= yet).
  6. Compute screateHmac("sha256", secret).update(signingString).digest("hex") (lowercase hex, 64 chars) if you match the official stack—confirm against HMAC signing and the infra repo.
  7. 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, w
const 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.


  • 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.