Page resources and image processing#
When resources is enabled, each Markdown page directory is a bundle.
Images that sit next to the page (or in a subdirectory of that directory)
become addressable with relative URLs. Optional query-string transforms
resize, crop, or convert those files at build time.
The feature is off unless you turn it on. Existing sites stay unchanged.
images (figures, captions, lazy-loading) is a separate option and stays
compatible.
import { oxContent } from "@ox-content/vite-plugin";
export default {
plugins: [
oxContent({
resources: true,
}),
],
};
false or omitted keeps colocated assets and transforms off. true or {}
enables the defaults. An object enables the feature and overrides only the
fields you set:
oxContent({
resources: {
formats: ["png", "jpeg", "webp"],
widths: [400, 800],
missing: "error",
dedupe: false,
},
});
| Option | Type | Default |
|---|---|---|
resources |
boolean / ResourcesOptions |
false |
formats |
string[] |
["png", "jpeg", "webp"] |
widths |
number[] |
[] (any positive width) |
missing |
"error" / "warn" |
"error" |
dedupe |
boolean |
false |
Page bundle#
The bundle root is the directory that contains the Markdown file.
content/
guide.md
hero.png
posts/
hello.md
cover.png
From guide.md,  and  resolve to
content/hero.png. From posts/hello.md, ./cover.png resolves to
content/posts/cover.png. Nested files under the same page directory are
also inside the bundle.
SSG copies the file next to the generated HTML so the relative URL keeps working in the output tree.
Transforms#
Append a query string to request a build-time derivative:



Use a <destination> when the query string contains &, so Markdown does
not treat the rest of the URL as text.
| Param | Meaning |
|---|---|
width / w |
Target width in pixels |
height / h |
Target height in pixels |
crop=center |
Scale to cover width × height, then center-crop |
crop=x,y,w,h |
Crop that rectangle from the source |
format |
Output container: png, jpeg / jpg, or webp |
When only one of width or height is set, the other side follows the
source aspect ratio. crop=center requires both width and height.
Pixel transforms encode PNG and JPEG. webp is copied when the
source is already WebP and no pixel transform is requested. If widths is
non-empty, ?width= must be one of those values. ?format= must be in
formats.
Cache#
Each derivative is cached under .cache/ox-content-resources/ in the
project root. The cache key is SHA-256 of:
- the absolute source path
- the source file mtime
- the normalized transform (
width,height,crop,format)
A later build with the same key copies the cached bytes instead of re-encoding. Changing the source file or any transform param produces a new key and a new output filename.
Content deduplication#
resources.dedupe is off by default. true and {} do not turn it on.
Set dedupe: true to write identical emitted bytes once:
oxContent({
resources: { dedupe: true },
});
The digest is SHA-256 of the final emitted bytes, a NUL, and the serving
extension (jpg for JPEG). The canonical file is:
/assets/content/<sha256>.<ext>
base is prefixed (/docs/ → /docs/assets/content/...). HTML src,
poster, and relevant href values that point at the resource become that
URL. Leftover query strings (after consumed transform params) and hash
fragments stay on the rewritten URL. Remote, data:, and javascript:
values are not rewritten.
The first page that produces a digest writes the canonical file. Later pages reuse that path and hash. Deduping does not decode images. Large files are hashed incrementally.
The original page-output path is kept as an alias: a hard link when the filesystem allows, otherwise a copy. A failed link never overwrites a shared inode. Names are deterministic across builds.
Same bytes with a different extension stay separate. Different bytes stay separate.
Missing sources#
When a relative image is missing, the default missing: "error" fails
the build (PageResourceError). Set missing: "warn" to keep the page
and record the issue on the SSG result instead.
oxContent({
resources: { missing: "warn" },
});
Path escape#
After resolve, the source must stay inside both the page bundle and
srcDir. ../ that leaves the page directory is rejected and fails the
build. Absolute filesystem paths, javascript:, data:, vbscript:,
protocol-relative //, and site-root /... URLs are not processed as
page resources.
Image syntax inside fenced code, indented code, and inline code is not an
<img> and is left untouched.
Rewritten src values are HTML-escaped. Output names come from the
resource basename plus a cache-key suffix, never from raw author input.