Takumi

ImageResponse

Serve rendered images over HTTP with a next/og-compatible Response.

ImageResponse extends the web Response, so any runtime with Request and Response returns it from a route handler. The API matches next/og.

import { ImageResponse } from "takumi-js/response";

export function GET() {
  return new ImageResponse(<OgImage />, { width: 1200, height: 630 });
}

Options

ImageResponseOptions is RenderOptions plus the standard ResponseInit fields and an error hook.

Prop

Type

Description

widthnumber

Canvas width in pixels.

heightnumber

Canvas height in pixels.

format?"png" | "jpeg" | "webp" | "ico" | "raw"

Output encoder. Sets the default content-type.

quality?number

Encoder quality, 0–100. Applies to JPEG and lossy WebP.

fonts?Font[]

Per-response fonts; identical fonts are decoded once.

images?ImageLoader[]

Pre-fetched images keyed by src.

renderer?Renderer

Bring your own renderer instance to reuse caches.

jsx?FromJsxOptions

JSX conversion options, e.g. { defaultStyles: false }.

headers?HeadersInit

Response headers. content-type defaults to format.

status?number

Response status code.

onError?(error: unknown) => void | Promise<void>

Runs after a render failure, for side effects like logging.

Custom headers

content-type defaults to the chosen format. Set cache headers or override anything via headers.

new ImageResponse(<OgImage />, {
  format: "webp",
  headers: {
    "Cache-Control": "public, immutable, max-age=31536000",
  },
});

Awaiting the render, and ETag

The constructor hands back a streaming Response while the render is still running, which is why its headers can carry no ETag. ImageResponse.render takes the same options and awaits instead, so its body is the finished bytes: the runtime knows the length and can skip chunked encoding, and the headers carry a strong ETag hashed from those bytes.

import { ImageResponse } from "takumi-js/response";

export function GET() {
  return ImageResponse.render(<OgImage />, {
    width: 1200,
    height: 630,
    headers: { "Cache-Control": "public, max-age=0, must-revalidate" },
  });
}

Takumi never sees the request, so it cannot answer one with 304 Not Modified. The ETag is what lets your server, framework adapter, or CDN compare If-None-Match and skip resending the image.

An etag of your own in headers is left alone. There is no ready promise here: the returned promise rejects with the render error, and onError still runs.

The digest comes from global Web Crypto. Node exposes it from version 19 onwards; on Node 18 you need --experimental-global-webcrypto.

Error handling

ImageResponse exposes a ready promise: it resolves when the render succeeds and rejects when it fails. Await it to serve a fallback.

const response = new ImageResponse(<OgImage />);

try {
  await response.ready;
  return response;
} catch {
  return new Response("Failed to generate image", { status: 500 });
}

onError is a notification hook for side effects like logging. Its return value is ignored and the response stream still errors, so it cannot substitute a fallback image; use ready for that.

new ImageResponse(<OgImage />, {
  onError: (error) => console.error(error),
});

Bring your own renderer

Pass a renderer to reuse a configured instance across responses. A reused renderer decodes each font and image once.

import { Renderer } from "@takumi-rs/core";

const renderer = new Renderer();

new ImageResponse(<OgImage />, { renderer });

Last updated on

On this page