Scale and convert images in Docly
What you'll see
A developer — or an AI coding agent — needs a smaller or reformatted copy of an image that already sits in the site's asset folder, and concludes that Docly cannot do it. The usual path to that wrong conclusion:
- Query parameters are tried first:
/img/photo.webp?w=1200,?width=1200. They return the original, unscaled, at 200 — so they look like they silently do nothing. - The documented URL form is tried next:
/img/photo.jpg/1200x675x0/photo.webp. It returns 404. docly.scaleImage()is tried as a fallback and throws'scaleImage' is not a functionon sites without the package that provides it.
The conclusion drawn is "no image tooling available, re-export the file by hand". All three observations are real; the conclusion is wrong.
What's actually happening
Every uploaded file in Docly is a document, not a loose file on disk. A JPEG dropped in an asset folder is stored with schema Uploaded Image, and the actual bytes live as an embedded file inside that document. The public path (/img/photo.jpg) resolves to the document; the bytes are addressed by an embedded id.
That is why the scaling URL needs four segments, not three:
/<public path>/<embeddedId>/<WxHxM>/<display name>.<format>Leaving out the embeddedId does not produce a helpful error. The router simply finds no route and answers 404 — indistinguishable from "this feature does not exist". That single missing segment is what sends people down the wrong path.
Reading the status code
| 404 | The URL shape is wrong — almost always the missing embeddedId segment. |
| 400 | The shape is right. The size is rejected by images.sizePolicy. Under auto with both allowedSizes and discoveredSizes empty there is nothing to snap to, so every size is refused. |
| 200 | Working. The extension on the display name decided the output format. |
Why linkImage does not cover this
linkImage() builds exactly this URL for you, but its reference states it "only works from site display templates for documents". It is available when rendering a document that has an image field — not when you are writing plain CSS, a static <img>, or a master page that just needs a smaller background. For those cases the URL has to be assembled by hand, which means the embeddedId has to be looked up first.
Scaling up costs more than it saves
The URL form exposes no quality parameter. Requesting a size larger than needed re-encodes from the full-resolution source at default quality and can produce a file heavier than the original. Measured on a 199 kB JPEG whose existing WebP was 155 kB: 1600x900x0 → 246 kB, 1100x620x0 → 151 kB, 800x450x0 → 96 kB. Always verify the byte count of the derivative instead of assuming a smaller box means a smaller file.
What to do
1. Find the embeddedId with a probe file
Write a throwaway .hash that dumps the file object. This is the fastest way to see what you are actually working with, and it costs nothing to delete afterwards.
#setmime("text/plain")#
#{
let fi = docly.getFile("#/Root/img/photo.jpg");
write(JSON.stringify(fi, null, 2));
}#Note the path form: filesystem functions take a single #/, not the ##/ used by #include() in bundle files. The response contains:
{
"File": "7ba189d5-b124-4eb6-b30c-82a817adb909.jpg", <-- embeddedId
"Title": "photo.jpg",
"filepath": "/#/Root/img/photo.jpg",
"fileschema": "Uploaded Image",
"Filesize": 199170
}File is the embeddedId. Delete the probe when you are done — it is served publicly like any other page.
2. Register the sizes you intend to use
In #/site.json. With auto and empty lists every scaled request returns 400:
"images": {
"sizePolicy": "auto",
"defaultFormat": "webp",
"onDisallowed": "nearest",
"maxDimension": 2000,
"allowedSizes": ["1600x900x0", "1100x620x0", "800x450x0"],
"discoveredSizes": []
}WebP and AVIF also require sizePolicy to be auto or strict — under the default open a .webp display name is served as JPEG regardless of the extension.
3. Assemble the URL
/img/photo.jpg/7ba189d5-b124-4eb6-b30c-82a817adb909.jpg/800x450x0/photo.webp
\_________/ \_________________________________________/ \________/ \_______/
public path embeddedId WxHxM name.formatUsable anywhere a URL is: a CSS background-image, a static <img src>, an Open Graph tag. The extension on the last segment picks the format — .jpg, .png, .webp, .avif. .svg and .gif are never converted.
In template code, do not write this URL out. The breakdown above is here so you can read and debug a scaled URL — not as a construction recipe. From #JS use docly.linkImage(Image1, 800, 450, 0, 'photo.webp'), or docly.linkImage(f, f.Image1, …) when the image lives on another document; docly.linkFile takes the same two forms for non-image files. Assembling the path by hand skips the encoding the functions do and hides the size from the Image sizes → Scan, so it will not be registered in allowedSizes. See Do not build file and image URLs by hand. Hand-assembly is for the places a function cannot run — client-side JavaScript and references from outside the site.
4. Measure the result
Request the derivative and compare bytes against the original before committing to it. If the derivative is heavier, pick a smaller box — there is no quality knob on this URL.
When to reach for scaleImage instead
docly.scaleImage(bytes, w, h, mode, quality, format) runs server-side, takes and returns bytes, exposes quality, and is not subject to the size policy. Use it when you need a one-off derivative written to disk with docly.saveFile(), or quality control the URL form cannot give.
Verify it is callable before designing around it. On sites without the package that provides it, every call form fails identically:
docly.scaleImage(b, 1600, 900) -> 'scaleImage' is not a function
docly.scaleImage(b, 1600, 900, opts) -> 'scaleImage' is not a function
scaleImage(b, 1600, 900) -> 'scaleImage' is not a functionNote that typeof docly.scaleImage returns "object" even when it is callable, so typeof is not a usable availability check — call it inside a try instead. If it is missing, compare Packages in the workspace .docly against a workspace where it works, and install the missing package rather than coding around the gap.
Rule of thumb
Docly scales and converts images on serving. The URL is filepath / embeddedId / WxHxM / name.format — and the embeddedId is the
Fileproperty fromdocly.getFile(). A 404 means you left it out; a 400 means the size is not registered inimages.allowedSizes. Never conclude from a 404 that scaling is unavailable.