Assets & downloads#
Images, video, audio, PDFs, fonts, zips — anything that isn't a Markdown page. There are two places to put them, depending on whether the file belongs to one page or should live at a stable site-wide URL.
1. Next to your content#
Any non-Markdown file anywhere in your content folder is copied to the output verbatim, keeping its path. This is the natural home for an asset tied to a specific page or section:
content/
guides/
install.md
architecture.svg → /guides/architecture.svg
setup.zip → /guides/setup.zip
Reference it with a root-absolute path (a leading /):

[Download the starter kit (4 MB)](/guides/setup.zip)
Use root-absolute paths, not relative ones. Pages get pretty URLs (
guides/install.md→/guides/install/), so a relativearchitecture.svgwould resolve against/guides/install/rather than the folder the file is in./guides/architecture.svgalways points where you mean.
2. public/ folder → site root#
The reserved publicDir (default public/) is
copied to the site root — the same convention as Next.js, Astro, Vite, and
Hugo (static/). Use it for files that must live at the root, and for shared
downloads you want at clean, permanent URLs:
content/
public/
favicon.ico → /favicon.ico
robots.txt → /robots.txt
report.pdf → /report.pdf
media/intro.mp4 → /media/intro.mp4
downloads/app.zip → /downloads/app.zip
Nothing inside public/ is processed — even a .md is copied as-is, never
turned into a page. Rename it with site.publicDir if you like.
Rule of thumb: a page-specific image → put it beside the page; a download or
shared asset that wants a tidy permanent URL (or a root-required file like
favicon.ico / robots.txt) → public/.
Serving public/ from a CDN#
By default public/ ships with your site. To serve it from a CDN or object
store instead, set site.assetBaseUrl — the same
idea as Vite's base or Next's assetPrefix:
export default {
site: {
assetBaseUrl: 'https://cdn.example.com/site',
},
} satisfies OvellumUserConfig;
You keep authoring the same root-absolute paths (/report.pdf,
/media/intro.mp4). At build time Ovellum:
- stops copying
public/locally — you upload its contents to the CDN yourself (one-time, or in your deploy step), and - rewrites every reference to a
public/file in the rendered HTML to the CDN, so/report.pdfbecomeshttps://cdn.example.com/site/report.pdf.
Assets that live next to your content (section 1) are part of the HTML site
and are left untouched — only public/ moves to the CDN. URLs that already
carry a query string or live in a srcset aren't rewritten; reference those
files by their final CDN URL directly.
By file type#
Images#

Optimizing images#
By default images are copied verbatim. To re-compress raster images
(.jpg / .jpeg / .png / .webp / .avif) during the build, set
site.images:
site: {
images: { quality: 80 }, // quality is optional (default 80)
}
Each image is re-encoded in place — same path and format, smaller bytes — so
your  references never change. Lossy formats use quality;
PNG is recompressed losslessly. If a re-encode would be larger (the image is
already optimized), Ovellum keeps the original, so optimization never makes a
file bigger. SVG and GIF pass through untouched. The build reports how many
images it optimized and the bytes saved.
Optimization uses sharp, an optional dependency that's only loaded when
site.imagesis set — install it alongside Ovellum:npm i sharp. (It's left out of the default install so a docs site that doesn't optimize images stays lean.)
Converting to WebP or AVIF#
To go further than re-compression, set format to convert raster images to
WebP
(much smaller than PNG/JPEG, ~97% browser support) or
AVIF
(smaller still, ~95% support):
site: {
images: { format: 'webp' }, // or 'avif'
}
.png / .jpg / .jpeg assets are written as a sibling .webp / .avif, and
Ovellum rewrites the matching Markdown <img src> references to point at the new
files — so  resolves to /img/hero.webp with no edits on
your part. Other formats (.webp, .avif, .svg, .gif) and external /
data: image URLs are left alone.
Note:
formatrewrites image paths, so it isn't compatible withsite.assetBaseUrl(a CDN serves the originals). It also rewrites references in Markdown body content only — if you point a landing hero or a raw-HTML<img>at an image, reference it at its final.webp/.avifpath yourself.
Capping image width#
Screenshots from a retina display are routinely 3000+ px wide — far more than
any docs layout renders. maxWidth downscales any raster wider than the cap
(aspect ratio kept; smaller images are untouched, never enlarged):
site: {
images: { maxWidth: 1600, quality: 80, format: 'webp' }, // each part optional
}
It composes with re-compression and format — resize first, then encode.
Minifying CSS and JS#
If you ship your own .css / .js — files in your content folder, or a custom
templateDir's
style.css / script.js — set site.minify to
minify them during the build:
site: {
minify: true,
}
It only touches your assets: the bundled default theme already ships minified, and HTML pages aren't minified. A minified output that would be larger than the original is discarded (the original is kept), and a file that fails to minify is copied as-is with a warning. The build reports how many assets it minified and the bytes saved.
Minification uses esbuild, an optional dependency loaded only when
site.minifyistrue— install it withnpm i esbuild. Like image optimization, it's left out of the default install so a docs site that doesn't need it stays lean.
PDFs, zips, and other downloads#
A plain link — the browser opens or downloads it:
[Read the spec (PDF, 1.2 MB)](/report.pdf)
[Download v1.0 (zip)](/downloads/app-1.0.zip)
Video and audio#
Embed a native player with raw HTML in your Markdown (it's allowed through the sanitizer):
<video src="/media/demo.mp4" controls width="720" poster="/media/cover.jpg"></video>
<audio controls>
<source src="/media/talk.mp3" type="audio/mpeg" />
</audio>
Allowed attributes are presentational/playback only — controls, width,
height, poster, preload, loop, muted, autoplay, playsinline, plus
<source>/<track>. src/poster URLs are scheme-checked (http(s) or
relative), and event handlers (onerror, …) are stripped, so an embed can't
carry script. Prefer a small, web-optimized .mp4/.webm/.mp3.
YouTube and Vimeo#
Open the video on YouTube or Vimeo, hit Share → Embed, and paste the
<iframe> it gives you verbatim — no editing required:
<iframe
width="560"
height="315"
src="https://www.youtube.com/embed/VIDEO_ID"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen
></iframe>
Ovellum allows <iframe> only from known video hosts (youtube.com,
youtube-nocookie.com, vimeo.com), so an iframe pointing anywhere else (or at
a relative path) is removed during sanitization — you can't accidentally embed
an untrusted page. Survivors are hardened automatically (loading="lazy", a
strict referrer policy) and wrapped in a responsive 16
width/height in the pasted snippet don't matter. Prefer
youtube-nocookie.com if you want YouTube's privacy-preserving embed. See the
styleguide for a live example.
Social share images (OpenGraph)#
When a page is shared on social platforms or chat apps, a preview card is pulled
from its OpenGraph meta. Ovellum can generate a card per page for you — set
site.ogImage:
site: {
baseUrl: 'https://docs.example.com', // required — social tags are absolute URLs
ogImage: true,
}
Each page gets a 1200×630 image (its title + your site name on a flat
background) written to og/<slug>.png, and the page <head> gains og:image,
twitter:image, og:title, og:url, and twitter:card meta. The
landing page gets a card too (its hero
title); drafts and the 404 page are skipped. To tune the colors, pass an
object:
site: {
ogImage: { background: '#101418', foreground: '#fafafa' },
}
site.baseUrl is required — without it the build warns and generates
nothing (a relative og:image won't resolve for a scraper). Generation uses the
optional sharp peer dependency (npm i sharp), lazy-loaded only when ogImage is set.
The card text renders with the build machine's default sans-serif font.
Checking links#
ovellum check validates internal page links. Asset URLs (images,
downloads) point at files rather than pages, so keep their paths correct
yourself — a quick local ovellum serve (or ovellum dev) is the fastest way
to confirm an image renders and a download resolves.