One API. Any CDN. Full control.
A lightweight, flexible, CDN-agnostic image URL builder,
designed with SSR and hydration in mind.
TL;DR: Define image transformations once and apply them across any CDN with a single shared API.
Install this core package along with the CDN adapters you need:
npm install @yoot/yoot @yoot/shopify @yoot/cloudinary
Build predictable image URLs with a modular, chainable API — designed for reusable, CDN-agnostic image transformations.
- 🧠 Predictable API — chainable, composable, no surprises.
- 🧵 Presets — define once, reuse everywhere.
- 🔄 Portable — safely serialize and hydrate in any SSR framework.
- ⚙️ Lightweight — zero runtime deps, framework-agnostic (Astro, SvelteKit, etc.).
- 💅 Layout and delivery only — leave visual effects to CSS.
- 🧱 Small, modular adapters — no global config or hidden logic.
- 🔌 Pluggable architecture — choose an adapter or write your own.
Adapters translate yoot
directives into CDN-specific URLs — handling each provider's syntax and features.
Install this core library, plus CDN adapters needed for your project:
Replace
<adapter-name>
with the specific adapter you want to use, e.g.shopify
,cloudinary
.
npm install @yoot/yoot @yoot/<adapter-name>
import {yoot} from 'jsr:@yoot/yoot';
import adapter from 'jsr:@yoot/<adapter-name>';
<script type="importmap">
{
"imports": {
"@yoot/yoot": "https://cdn.jsdelivr.net/npm/@yoot/yoot/+esm",
"@yoot/<adapter-name>": "https://cdn.jsdelivr.net/npm/@yoot/<adapter-name>/+esm"
}
}
</script>
<script type="module">
import {yoot} from '@yoot/yoot';
import adapter from '@yoot/<adapter-name>';
</script>
Do this once per runtime (server/client). Use a bootstrap file:
import {registerAdapters} from '@yoot/yoot';
import adapter1 from '@yoot/<adapter-name-1>';
import adapter2 from '@yoot/<adapter-name-2>';
registerAdapters(adapter1, adapter2);
import '@yoot/<adapter-name>/register';
The yoot
function returns a chainable builder. You can optionally initialize it with an image URL or an object.
import {yoot} from '@yoot/yoot';
// Without arguments
const preset = yoot();
// With image URL
const preset = yoot('https://...');
// With an object
const preset = yoot({
src: 'https://...',
alt: 'Alt text',
width: 1024, // Optional: intrinsic width
height: 1024, // Optional: intrinsic height
});
const imgPreset = yoot('https://...').width(1024).aspectRatio(1).format('webp');
// Shortform: yoot('https://...').w(1024).ar(1).fm('webp');
const url = imgPreset.url; // Returns generated URL
const attrs = getImgAttrs(imgPreset); // Attributes for `<img>`
// yoot-presets.ts
import {yoot} from '@yoot/yoot';
import {defineSrcSetBuilder, withImgAttrs, withSourceAttrs} from '@yoot/yoot/jsx'; // Or @yoot/yoot/html
// Hero presets
export const heroPreset = yoot()
.width(1024)
.aspectRatio(16 / 9)
.fit('cover');
export const getHeroImgAttrs = withImgAttrs({loading: 'eager'});
export const getHeroSourceAttrs = withSourceAttrs({
srcSetBuilder: defineSrcSetBuilder({densities: [1, 2, 3]}),
});
// Thumbnail presets
export const thumbnailPreset = yoot().width(100).aspectRatio(1).fit('cover');
export const getThumbnailImgAttrs = withImgAttrs({loading: 'lazy'});
export const getThumbnailSourceAttrs = withSourceAttrs({
srcSetBuilder: defineSrcSetBuilder({widths: [100, 200, 300]}),
});
See the API docs for all transformation options.
import {thumbnailPreset, getThumbnailImgAttrs, getThumbnailSourceAttrs} from './yoot-presets.ts';
// With a URL string
const thumbnail = thumbnailPreset('https://cdn.example.com/image.jpg');
// Alternatively: thumbnailPreset.src('https://cdn.example.com/image.jpg');
// With an object
const thumbnail = thumbnailPreset({
src: 'https://cdn.example.com/image.jpg',
alt: 'Alt text',
width: 2048, // Intrinsic width
height: 2048, // Intrinsic height
});
const thumbnailAttrs = getThumbnailImgAttrs(thumbnail);
const webpSourceAttrs = getThumbnailSourceAttrs(thumbnail, {
type: 'image/webp', // this helper modifies the format to webp
});
const jpegSourceAttrs = getThumbnailSourceAttrs(thumbnail, {
type: 'image/jpeg', // this helper modifies the format to jpeg
});
Note: Use environment-specific imports:
- Use
@yoot/yoot/jsx
for React, Preact, Solid- Use
@yoot/yoot/html
for Astro, Svelte, plain HTML
import {yoot} from '@yoot/yoot';
import {defineSrcSetBuilder, getImgAttrs, getSourceAttrs} from '@yoot/yoot/jsx'; // Or '@yoot/yoot/html'
const imgPreset = yoot('https://...').format('png').width(800);
const imgAttrs = getImgAttrs(imgPreset);
// Example demonstrating that format can be overridden via `type`
// and different `srcset` strategies can be used per <source>.
const webpSourceAttrs = getSourceAttrs(imgPreset, {
type: 'image/webp', // `type` overrides format 'png'
media: '(min-width: 800px)',
sizes: '(min-width: 800px) 800px, 100vw',
srcSetBuilder: defineSrcSetBuilder({widths: [600, 800, 1200]}),
});
const jpegSourceAttrs = getSourceAttrs(imgPreset, {
type: 'image/jpeg', // `type` overrides format 'png'
media: '(max-width: 799px)',
sizes: '(max-width: 799px) 100vw',
srcSetBuilder: defineSrcSetBuilder({densities: [1, 2, 3]}),
});
return (
<picture>
<source {...webpSourceAttrs} />
<source {...jpegSourceAttrs} />
<img {...imgAttrs} />
</picture>
);
Try it live — zero setup:
Found a bug or wish to contribute? Open an issue or send a PR.
Licensed under the ISC License.