Overview of IPX
mainsharp and svgo. It allows you to serve images from a specific directory or a list of allowed domains, providing on-the-fly transformations (size, format, quality) via URL modifiers.repository·main·Indexed 25 days ago
https://github.com/unjs/ipxA high-performance, secure image optimizer powered by sharp and svgo. IPX allows developers to serve transformed images (resized, re-formatted, etc.) on-the-fly via URL modifiers. It supports local filesystem and HTTP(s) storage, provides programmatic integration for H3 and Express, and includes built-in SVG sanitization and optimization.
sharp and svgo. It allows you to serve images from a specific directory or a list of allowed domains, providing on-the-fly transformations (size, format, quality) via URL modifiers.When using ipxHttpStorage, setting blockPrivateIPs: true provides a second line of defense against SSRF. It ensures that the host of the requested URL and every redirect hop resolves to a public IP address. It rejects loopback, RFC1918, CGNAT, link-local, and other reserved/private ranges.
Note: This is off by default to allow fetching from internal cluster origins (like sidecars or local object storage) in development or specific deployments.
ipxHttpStorage({ domains: ["cdn.example.com"], blockPrivateIPs: true });To prevent XSS, IPX always sanitizes SVG documents. Sanitization is independent of optimization. IPX removes:
<script> elements and event handler attributes (on*).<foreignObject>, <iframe>, etc.).javascript:).<!DOCTYPE> declarations and processing instructions.External references like <image href="..."> or <use> are preserved.
Warning: Sanitization can be disabled with unsafeSkipSanitize: true, but this should only be done if the source is fully trusted, as it allows XSS payloads to pass through unchanged.
createIPX({
storage,
svg: {
// Serve SVG images unsanitized. Only for fully trusted sources!
unsafeSkipSanitize: false,
},
});IPX uses modifiers in the URL path to transform images. Modifier arguments are separated by underscores (_).
400 IPX_INVALID_MODIFIER_ARG: An argument was provided but is invalid.400 IPX_MISSING_MODIFIER_ARG: A required argument for a modifier is missing.400 IPX_INVALID_MODIFIER: The modifier could only be validated by libvips after the pipeline was set up.f00, ff0000, ff000080) or CSS colour names (e.g., red). Note that the leading # cannot be used in a URL path and should be omitted.true/false or the shorthand 1/0.IPX uses a specific URL pattern to apply transformations to images. The structure is:
/<modifiers>/<id>
_)._ alone to request the original image.| URL | Result |
|---|---|
/_/static/buffalo.png | The original image. |
/w_200/static/buffalo.png | Width set to 200, original format (png) kept. |
/f_webp/static/buffalo.png | Format changed to webp, everything else kept as in the source. |
/f_auto/static/buffalo.png | Best format for the client (avif/webp/jpeg), negotiated from the browser's accept header. |
/s_200x200,fit_contain,f_webp/static/buffalo.png | Resized to fit inside 200x200px on a background canvas and converted to webp. |
/<modifiers>/<id>If you are using the Bun runtime, you can start the IPX server using bunx.
bunx ipx serve --dir ./To start an IPX server for images located in your current directory, use the ipx serve command. You can run this via npx or bunx.
npx ipx serve --dir ./IPX_* environment variables. Explicit options passed to the programmatic API will take precedence over environment variables.To use IPX within an h3 application, use the createIPXFetchHandler function to create a handler from your IPX instance.
import { H3, serve } from "h3";
import {
createIPX,
ipxFSStorage,
ipxHttpStorage,
createIPXFetchHandler,
} from "ipx";
const ipx = createIPX({
storage: ipxFSStorage({ dir: "./public" }),
httpStorage: ipxHttpStorage({ domains: ["picsum.photos"] }),
alias: { "/picsum": "https://picsum.photos" },
});
const app = new H3();
app.mount("/ipx", createIPXFetchHandler(ipx));
serve(app);IPX processes SVG images by sanitizing them and optionally optimizing them with svgo.
Optimization: By default, SVGO's preset-default is applied. This can break SVGs that rely on specific IDs for external references (like sprite sheets) or internal CSS selectors. To sanitize without optimizing, use optimize: false. To keep optimization but prevent ID renaming or element removal, configure the plugins manually.
// Sanitize without optimizing
createIPX({
storage,
svg: {
optimize: false,
},
});
// Optimize with specific plugin overrides to preserve IDs/elements
createIPX({
storage,
svg: {
optimize: {
plugins: [
{
name: "preset-default",
params: {
overrides: { cleanupIds: false, removeHiddenElems: false },
},
},
],
},
},
});You can define a custom URL structure by providing a parseURL function to the createIPXFetchHandler options. This function is responsible for extracting the resource id and the modifiers object from the incoming request URL.
Key details for implementing a custom parser:
async function.HTTPError (re-exported from ipx) to reject a request with a specific status code.id and modifiers are automatically escaped by IPX; you do not need to manually escape them.id is still subject to the constraints of your storage layer (e.g., ipxFSStorage directory boundaries or ipxHttpStorage domain allowlists).This is useful for styles like /<id>@@<modifiers>.<format>, which is often preferred for static hosting and prerendering.
import { createIPXFetchHandler, parseIPXURL } from "ipx";
const handler = createIPXFetchHandler(ipx, {
parseURL(url) {
const path = decodeURIComponent(new URL(url).pathname.slice(1));
const match = path.match(/^(.+)@@(.+)\.([^.]+)$/);
if (!match) {
// Not our style, fall back to the default `/<modifiers>/<id>`
return parseIPXURL(url);
}
const [, id = "", modifiersString = "", format = ""] = match;
const modifiers = Object.fromEntries(
modifiersString.split(",").map((m) => {
const [key = "", ...values] = m.split("_");
return [key, values.join("_")];
}),
);
return { id, modifiers: { ...modifiers, format } };
},
});
// http://localhost:3000/static/buffalo.png@@s_200x200.webp
// http://localhost:3000/static/buffalo.png@@grayscale,w_200.webpTo use IPX within an express application, use the createIPXNodeHandler function. Note that you may need to cast the result to a RequestHandler for TypeScript compatibility.
import Express from "express";
import {
createIPX,
ipxFSStorage,
ipxHttpStorage,
createIPXNodeHandler,
} from "ipx";
import type { RequestHandler } from "express";
const ipx = createIPX({
storage: ipxFSStorage({ dir: "./public" }),
httpStorage: ipxHttpStorage({ domains: ["picsum.photos"] }),
alias: { "/picsum": "https://picsum.photos" },
});
const app = Express();
app.use("/ipx", createIPXNodeHandler(ipx) as RequestHandler);
app.listen(3000);