Placement
An output path says where the bytes go and nothing about how anyone reaches them, so every site ends up rebuilding that mapping in its own templates, from information Colophon had while generating and then threw away.
A placement says both:
export default defineConfig({ // Astro, Eleventy, Vite: one directory, served under one prefix. placement: { strategy: "public-dir", dir: "public/og", urlBase: "/og" },});wrote public/og/my-post-og.png -> /og/my-post-og.pngStrategies
Section titled “Strategies”| Strategy | Writes | Suits |
|---|---|---|
beside-content |
Next to the post, as it always has | Hugo-style page bundles |
public-dir |
Into dir, one directory for the lot |
Astro, Eleventy, Vite |
custom |
Wherever path says |
Anything else |
For beside-content and public-dir, one relative path makes both the disk
path and the URL, so the two cannot drift apart. It is held in URL form, with
/ rather than the platform separator, so a Windows build does not serve
posts\my-post\a.png. A custom placement names its own path and URL
separately, so it can map them however it needs to.
The URL comes from urlBase, prefixed to the image’s path under whatever root
placed it. It can be site-relative, such as /og, or absolute for images served
from a CDN.
No urlBase, no URL. A directory on disk does not say how, or whether, it
is served, and a URL Colophon invented would be worse than the gap it fills. The
point of writing a URL down is that a site can trust it.
Each result carries the URL as result.url, which is undefined where nothing
says. That covers three cases: no urlBase, an image placed by generate’s
outputPath callback, and an extra image that named its own
path. outputPath still takes precedence over a placement, and once it does the
placement no longer describes where the file went.
Custom placements
Section titled “Custom placements”custom works both halves out itself, for a mapping that is nobody else’s.
Images under a dated directory, say:
placement: { strategy: "custom", path: (file, size) => `public/og/2026/${file.slug}-${size.name}.png`, url: (file, size) => `/og/2026/${file.slug}-${size.name}.png`,}Content hashed filenames
Section titled “Content hashed filenames”Social platforms cache share images hard, and they cache by URL. Correct a post’s image and the old one can keep turning up in feeds for a long time afterwards. A hash in the filename is the reliable way round it:
placement: { strategy: "public-dir", dir: "public/og", urlBase: "/og", hash: true }wrote public/og/my-post-og.ecd0aab2.png -> /og/my-post-og.ecd0aab2.pngCorrect the post and the name moves with it, so the URL is one nothing has cached:
wrote public/og/my-post-og.2e7bd5a9.png -> /og/my-post-og.2e7bd5a9.pngThe hash is the image’s rebuild stamp, covering its props, config and size. Hashing the rendered bytes would be a truer name, but they are not known until the image has been rendered, and not rendering the unchanged ones is the point of the stamp.
It follows that anything the stamp covers moves the name, including a Colophon upgrade. Those images are re-rendered by the upgrade anyway, and a fresh URL is the right answer for an image that may have changed.
Hashing is opt-in for two reasons. The filename then moves whenever the image does, which not every setup wants. And it leaves the old files behind.
Nothing deletes them. That is the point under a public-dir you can rebuild
from scratch, since a crawler holding the old URL still gets an image. It does
mean a beside-content tree slowly accumulates them in your content directory.
The manifest always names the current one, so a site never has to
work out which is which.
custom has no hash option. A placement naming its own paths is the one that
can hash them itself.
Filename collisions
Section titled “Filename collisions”A flat placement makes collisions much easier to hit. Two posts named intro.md
in different sections both want public/og/intro-og.png.
Colophon refuses the build and names both posts rather than letting one overwrite the other. Overwriting would also leave the pair re-rendering on every build, since each would stamp the same file and neither stamp would ever match again.
Pair public-dir with slugStrategy: "route" and
each post keeps its section:
export default defineConfig({ content: { slugStrategy: "route" }, placement: { strategy: "public-dir", dir: "public/og", urlBase: "/og" },});// public/og/blog/intro-og.png -> /og/blog/intro-og.pngPaths are compared case-insensitively on macOS and Windows, where Card.png and
card.png are one file.
