Images in Next.js
Serve responsive images through next/image from SteadyLink links, so replacing a file updates every page without a redeploy.
On this page
This guide is for Next.js developers who want next/image to load images from SteadyLink. You keep an asset ID in your code or CMS, next/image asks SteadyLink for each width it needs, and when someone replaces the image in SteadyLink, every page shows the new version within minutes. No rebuild, no new URL, and no image optimization running on your own servers.
How it works#
next/image normally sends every image through the Next.js image optimizer. A custom loader replaces that step with a function that returns a URL. The SteadyLink loader in @steadylink/sdk turns an asset ID into a delivery URL with the width Next.js asks for:
<Image src="3f2a9c1e-8b4d-4e7a-a1c2-5d6e7f809a1b" width={1200} ... />
-> https://cdn.steadylink.io/a/3f2a9c1e-8b4d-4e7a-a1c2-5d6e7f809a1b?w=1200&fm=webp&q=75Browsers fetch those URLs straight from SteadyLink, which resizes the current revision and caches each variant. Because the URL follows the file's current revision, a replacement reaches every srcset entry once the five-minute delivery cache expires.
Set up the loader#
Install the SDK
Terminal npm install @steadylink/sdkThe loader has no runtime dependencies and needs no API key: it only builds URLs.
Create a loader file
steadylink-loader.js export { default } from "@steadylink/sdk/next-loader";Point next.config at it
next.config.js module.exports = { images: { loader: "custom", loaderFile: "./steadylink-loader.js" }, };The loader now applies to every
next/imagein the app. Sources that are not SteadyLink references pass through unchanged (see What counts as a SteadyLink source).Use an asset ID as the src
app/page.tsx import Image from "next/image"; export default function Page() { return ( <Image src="3f2a9c1e-8b4d-4e7a-a1c2-5d6e7f809a1b" alt="Launch hero" width={1200} height={630} sizes="100vw" priority /> ); }Find the asset ID in the file's details in the dashboard (File ID), in the link itself, or in the
assetIdof an SDK upload result.
width and height here describe the image's intrinsic aspect ratio for layout, as with any next/image. They do not crop: SteadyLink resizes to the width in each srcset entry and keeps the original aspect ratio. With fill, give the parent a size and set sizes as usual.
What counts as a SteadyLink source#
The loader recognizes three forms of src and leaves everything else alone:
src | Result |
|---|---|
An asset ID, such as 3f2a9c1e-8b4d-4e7a-a1c2-5d6e7f809a1b | https://cdn.steadylink.io/a/{id}?w=...&fm=webp&q=... |
A path, such as /a/3f2a9c1e-... | Same, on the configured origin. |
A full delivery URL, such as https://cdn.steadylink.io/a/3f2a9c1e-...?v=3 | Same host, and the existing query (for example v=3 or a signed token) is kept. w, q, and fm are added or overwritten. |
Anything else: /images/logo.png, a static import, another domain | Returned unchanged, so local images keep loading. |
A plain value made only of letters, digits, -, and _ is treated as an asset ID. A local src="logo" (with no slash or extension) would therefore be sent to SteadyLink; give local files a path such as /logo.png. Images that pass through unchanged are not resized by anyone, because the custom loader replaces the built-in optimizer for the whole app.
Customize the loader#
Use createSteadyLinkLoader when you need a different origin or defaults:
import { createSteadyLinkLoader } from "@steadylink/sdk/next-loader";
export default createSteadyLinkLoader({
origin: process.env.NEXT_PUBLIC_STEADYLINK_CDN_URL, // e.g. https://media.example.com
format: "webp",
quality: 75,
});createSteadyLinkLoader options
originstringDefaultNEXT_PUBLIC_STEADYLINK_CDN_URL, then https://cdn.steadylink.io- Delivery origin for asset IDs and
/a/paths. Use it for a verified custom delivery domain. A full URL insrckeeps its own host. format"webp" | "jpg" | "png" | falseDefault"webp"- Output format sent as
fm, unless thesrcURL already hasfm.falseomitsfm, which makes SteadyLink return JPEG for every resized image, not the source format. Use"png"for images that need lossless output. qualitynumberDefault75- Quality used when the
Imagehas noqualityprop. Clamped to 30–95. fit"cover" | "contain" | "inside" | "outside"- Sent as
fitunless thesrcURL already has one. Because the loader only sets a width,fitchanges nothing unless thesrcURL also carriesh.
Reference process.env.NEXT_PUBLIC_STEADYLINK_CDN_URL in your own loader file, as above, rather than relying on the default loader to find it. Next.js inlines NEXT_PUBLIC_ variables only where your code names them directly, so this keeps the origin identical in server-rendered HTML and in the browser. A custom domain must be added and verified for your workspace before it serves files; see the Platform API.
Control how many variants you create#
Each distinct combination of width, quality, and format is a stored variant. By default SteadyLink allows 100 variants per file and 10,000 per workspace; a request for a new variant beyond that returns 429 (existing variants keep working). next/image generates one URL per configured width, which is usually well within the per-file limit, but a large catalog can approach the workspace limit. Keep the set small and fixed:
module.exports = {
images: {
loader: "custom",
loaderFile: "./steadylink-loader.js",
deviceSizes: [640, 828, 1200, 1920],
imageSizes: [64, 128, 256],
},
};Avoid passing many different quality values for the same image; each one multiplies the variants. Asking for a width larger than the original is harmless: SteadyLink's default fit never enlarges, so it returns the original size.
Crops and fixed aspect ratios#
The loader only controls width. For a crop to an exact shape, such as square avatars or 1200×630 social cards, put the crop in the src URL and render a single size:
<Image
src="https://cdn.steadylink.io/a/3f2a9c1e-8b4d-4e7a-a1c2-5d6e7f809a1b?h=400&fit=cover&focus=auto"
alt="Ana Ruiz"
width={400}
height={400}
sizes="400px"
/>The loader adds w for each srcset entry while h=400 stays fixed, so only the entry at 400 pixels is square. Set sizes and the device sizes so that entry is the one browsers pick, or save a focal point and pre-crop the image when you need several sizes of the same shape.
Pinned versions and private images#
- Pin a version by using a full URL with
v, for examplesrc="https://cdn.steadylink.io/a/3f2a...?v=3". The image then never changes, and the browser can cache it for a year. - Private images need a signed link. Pass the signed URL (with its
token) assrc; the loader keeps the token. Private responses are never stored by shared caches, and the image breaks when the token expires, so avoid embedding short-lived signed links in pages that are statically generated or cached for longer than the link lives. See Private links.
Files that cannot be resized#
SVG, PDF, and other non-raster files return 415 when asked for a width. Render them without the loader by passing the full link and unoptimized:
<Image unoptimized src="https://cdn.steadylink.io/a/9d4e2b7a-1c3f-4e5d-8a6b-7c8d9e0f1a2b" alt="Logo" width={160} height={40} />What happens when the image is replaced#
- Someone replaces the file in the dashboard, with
steadylink.replace(), or withsteadylink replace. - SteadyLink scans the new revision. During the scan, requests that reach SteadyLink get
423, which a browser shows as a broken image for that moment. - Each
srcsetURL is cached for five minutes. As copies expire, browsers and edges fetch the new revision, and SteadyLink generates new variants on first request.
No Next.js rebuild or revalidation is involved, because the HTML never contained the image bytes or a version-specific URL. To roll back, restore the previous revision; the same URLs serve it again.