Ovellum v0.25.0
English
Open in

Edited

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 /):

![Architecture](/guides/architecture.svg)
[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 relative architecture.svg would resolve against /guides/install/ rather than the folder the file is in. /guides/architecture.svg always 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.pdf becomes https://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#

![A diagram of the build pipeline](/guides/pipeline.svg)

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 ![…](/img/hero.png) 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.images is 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 ![](/img/hero.png) 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: format rewrites image paths, so it isn't compatible with site.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 / .avif path 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.minify is true — install it with npm 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

frame, so the fixed 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.

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.

Edit this page