Image optimization
On this page
Drop a full-size image into src/ or public/, point an <img> at it, and sitelo resizes it, converts it to a modern format, and rewrites the tag with a srcset. Nothing to import, no component to learn.
Enable it
export default {
images: true,
}Encoding is done by sharp, an optional peer dependency — install it alongside sitelo when you turn images on:
npm install -D sharpSites that skip image optimization never need to install it.
Write a plain <img>
export default () => `
<html lang="en">
<body>
<img src="/images/hero.png" alt="Sunrise over the harbour">
</body>
</html>
`import { html, body, img } from 'javascript-to-html'
export default () =>
html({ lang: 'en' },
body(
img({ src: '/images/hero.png', alt: 'Sunrise over the harbour' }),
),
)export default function Home() {
return (
<html lang="en">
<body>
<img src="/images/hero.png" alt="Sunrise over the harbour" />
</body>
</html>
)
}A 3000×2000 source comes out the other side like this:
<img src="/assets/img/hero.a1b2c3d4-1200.webp"
alt="Sunrise over the harbour"
sizes="(max-width: 1200px) 100vw, 1200px"
width="1200" height="800"
loading="lazy" decoding="async"
srcset="/assets/img/hero.9f8e7d6c-400.webp 400w,
/assets/img/hero.5b4a3c2d-800.webp 800w,
/assets/img/hero.a1b2c3d4-1200.webp 1200w">What you get
- Resizing that never upscales — a 600px source with
widths: [400, 800, 1200]emits 400 and 600, and stops there. - Modern formats — one format gives a plain
<img srcset>; two or more wrap it in<picture>with a fallback in the original format. - No layout shift —
widthandheightare filled in from the real image, plusloading="lazy"anddecoding="async". - The same markup in dev —
sitelo(dev) rewrites pages and serves variants on demand, so you preview what you ship. - A shared cache — variants are keyed by content hash in
node_modules/.sitelo/images, so dev and rebuilds never encode the same image twice.
The rewrite runs over the built HTML, so it covers images from src/ and public/ alike.
Options
export default {
images: {
widths: [400, 800, 1200],
formats: ['avif', 'webp'],
quality: { avif: 55, webp: 78, jpeg: 82 },
exclude: ['**/og/**'],
},
}widths— default[400, 800, 1200]; the largest is also the capformats— default['webp'];avif,webp,jpeg,pngquality— default{ avif: 55, webp: 78, jpeg: 82 }; per-format encoder quality (png uses compression, not quality)sizes— thesizesattribute; asizeson the tag always winsdimensions— defaulttrue; addswidth/heightlazy— defaulttrue; addsloadinganddecodingexclude— glob(s) or RegExp(s) of image URLs to leave aloneassetsDir— default'assets/img'cacheDir— default'node_modules/.sitelo/images'remote— defaultfalse; optimizehttps://images at build timeprune— defaultfalse; delete originals nothing references any moredev— defaulttrue; setfalseto serve untouched originals in devconcurrency— parallel encodes, default CPUs − 1 (max 8); 1 when remote is on
Two formats give you
AVIF is smaller but younger than WebP, so listing both lets the browser pick and keeps a fallback for old ones:
<picture>
<source type="image/avif" srcset="/assets/img/hero.*-400.avif 400w, ..." sizes="...">
<source type="image/webp" srcset="/assets/img/hero.*-400.webp 400w, ..." sizes="...">
<img src="/assets/img/hero.*-1200.png" alt="..." srcset="..." width="1200" height="800">
</picture>Opting out
Tags that already have a srcset, sit inside a <picture>, or point at an SVG, an animated GIF, or a remote URL are left untouched. For anything else, say so:
<img src="/images/exact.png" alt="Pixel art" data-no-optimize>Social-card and favicon images live in <meta> and <link>, which this never rewrites — they keep their fixed URL.
Remote images
Content imported from a CMS often points at someone else’s server. Turn on remote and those images are downloaded, optimized, and served from your own domain:
export default {
images: {
remote: true,
prune: true,
},
}A fetch that fails leaves the tag exactly as it was, with a warning — a flaky origin never fails your build. prune then removes local originals that nothing references any more.