Skip to content

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:

Text
<Image src="3f2a9c1e-8b4d-4e7a-a1c2-5d6e7f809a1b" width={1200} ... />
  -> https://cdn.steadylink.io/a/3f2a9c1e-8b4d-4e7a-a1c2-5d6e7f809a1b?w=1200&fm=webp&q=75

Browsers 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#

  1. Install the SDK

    Terminal
    npm install @steadylink/sdk

    The loader has no runtime dependencies and needs no API key: it only builds URLs.

  2. Create a loader file

    steadylink-loader.js
    export { default } from "@steadylink/sdk/next-loader";
  3. Point next.config at it

    next.config.js
    module.exports = {
      images: { loader: "custom", loaderFile: "./steadylink-loader.js" },
    };

    The loader now applies to every next/image in the app. Sources that are not SteadyLink references pass through unchanged (see What counts as a SteadyLink source).

  4. 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 assetId of 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.

The loader recognizes three forms of src and leaves everything else alone:

srcResult
An asset ID, such as 3f2a9c1e-8b4d-4e7a-a1c2-5d6e7f809a1bhttps://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=3Same 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 domainReturned 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:

steadylink-loader.js
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

originstringDefault NEXT_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 in src keeps its own host.
format"webp" | "jpg" | "png" | falseDefault "webp"
Output format sent as fm, unless the src URL already has fm. false omits fm, which makes SteadyLink return JPEG for every resized image, not the source format. Use "png" for images that need lossless output.
qualitynumberDefault 75
Quality used when the Image has no quality prop. Clamped to 30–95.
fit"cover" | "contain" | "inside" | "outside"
Sent as fit unless the src URL already has one. Because the loader only sets a width, fit changes nothing unless the src URL also carries h.

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:

next.config.js
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:

TSX
<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 example src="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) as src; 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:

TSX
<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#

  1. Someone replaces the file in the dashboard, with steadylink.replace(), or with steadylink replace.
  2. 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.
  3. Each srcset URL 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.

Next steps#