Image Resizing
On-the-fly image resizing on Bun.Image, served from an encrypted, stale-while-revalidate disk cache. Every URL's payload is AES-256-SIV encrypted, so the source and params stay hidden and can't be tampered with.
Component
A plain <Image> renders a single resized <img> with no client JS:

With a blur placeholder
Add placeholder to show a ThumbHash blur until the image loads — it's the <img>'s own background-image, so no client JS is needed.
The placeholder shows first, then the loaded image that paints over it:

Inside a hydrated island
<Image> also works inside a mochi:hydrate island: the server-minted URL and placeholder are serialized into the page (via Svelte's hydratable) and reused during hydration, so the browser never needs the encryption secret. The button is live client-side state:
Caveat: props passed to a hydrated island — like this card's src — are serialized in plain text into the page for hydration, so the source URL is visible to the
client here. If your origin must stay secret, keep <Image> in server-rendered markup or a server island, whose props are encrypted.
Programmatic
getResizedImage() returns an encrypted URL you can use anywhere:
/_mochi/image/mochi-1-400x400.jpg?p=pUArWpgSeWhFq2TFxScA2IxEMajH3wPYdxVEF9KzsFDhhGqLxgLko3kL220VTuc-57X-YVQNMqqNv6_CDZNcItKYH_OWf0zpeySlGYr4_yIfyv1w_rgJyJRmlJI

This uses the default fit: 'inside', which preserves aspect ratio and fits within the 400×400 box — so this 3:2 photo becomes 400×267. Pass fit: 'fill' to force an exact square (stretching); Bun.Image has no crop/cover mode.
Full-size original
getImage() returns an encrypted URL for the un-resized original — fetched once and shared, so every resized variant above reuses this one cached download:
/_mochi/image/mochi-1-original.jpg?p=Sk1c81Dx9mRI6k16Gy3h-F4IMIetm-eAPwukkeBNfaL1m8ss62ZanmJtfRHm5X2wX-THk8dLRCa82_-Dzjs5bgSIAZ4x7G2h-uCEqZgnguJbUSyTY_AGWw

Gallery
Fourteen source photos, each resized to a square <img> on the fly with a placeholder blur-up — all server-rendered, zero client JS:














Photos by Minh Anh Nguyen, Andreas Haubold, blackieshoot, Amanda Lim, Negar Mz, Vi Tran, Hamada, Lea Ren and Yuliia Kucherenko on Unsplash.
<script>
import ImageCredits from '../../components/ImageCredits.svelte';
import { files } from './files.ts';
import ImageIslandCard from './ImageIslandCard.svelte';
import { Image } from 'mochi-framework/image';
import { getResizedImage, getImage, getImagePlaceholder } from 'mochi-framework';
import ArrowDown from '@lucide/svelte/icons/arrow-down';
const remote = 'https://sta-public.fra1.cdn.digitaloceanspaces.com/mochi/mochi-1.jpg';
const gallery = Array.from({ length: 14 }, (_, i) => `https://sta-public.fra1.cdn.digitaloceanspaces.com/mochi/mochi-${i + 1}.jpg`);
const directUrl = getResizedImage(remote, { width: 400, height: 400, fit: 'inside', format: 'jpeg', quality: 60 });
const originalUrl = getImage(remote);
const blur = await getImagePlaceholder(remote);
const sources = await loadSources(files);
</script>
<h3>Component</h3>
<p>A plain <code><Image></code> renders a single resized <code><img></code> with no client JS:</p>
<div class="frame">
<Image src={remote} width={640} height={400} alt="A resized random photo" />
</div>
<h3>With a blur placeholder</h3>
<p>
Add <code>placeholder</code> to show a ThumbHash blur until the image loads — it's the <code><img></code>'s own <code>background-image</code>, so no client JS is needed.
The placeholder shows first, then the loaded image that paints over it:
</p>
{#if blur}
<div class="frame blur-compare">
<span class="blur-compare__placeholder" style:background-image="url({blur})" role="img" aria-label="ThumbHash blur placeholder"></span>
<span class="blur-compare__arrow"><ArrowDown size={28} aria-hidden="true" /></span>
<Image src={remote} width={600} height={400} placeholder alt="A resized random photo with blur-up" />
</div>
{:else}
<div class="frame">
<Image src={remote} width={600} height={400} alt="A resized random photo with blur-up" placeholder />
</div>
{/if}
<h3>Inside a hydrated island</h3>
<p>
<code><Image></code> also works inside a <code>mochi:hydrate</code> island: the server-minted URL and placeholder are serialized into the page (via Svelte's
<code>hydratable</code>) and reused during hydration, so the browser never needs the encryption secret. The button is live client-side state:
</p>
<div class="frame">
<ImageIslandCard mochi:hydrate src={remote} />
</div>
<p class="note">
Caveat: props passed to a hydrated island — like this card's <code>src</code> — are serialized in plain text into the page for hydration, so the source URL is visible to the
client here. If your origin must stay secret, keep <code><Image></code> in server-rendered markup or a server island, whose props are encrypted.
</p>
<h3>Programmatic</h3>
<p><code>getResizedImage()</code> returns an encrypted URL you can use anywhere:</p>
<pre class="url">{directUrl}</pre>
<div class="frame">
<img src={directUrl} width={400} alt="Resized via getResizedImage()" />
</div>
<p class="note">
This uses the default <code>fit: 'inside'</code>, which preserves aspect ratio and fits <em>within</em> the 400×400 box — so this 3:2 photo becomes 400×267. Pass
<code>fit: 'fill'</code>
to force an exact square (stretching); <code>Bun.Image</code> has no crop/cover mode.
</p>
<h3>Full-size original</h3>
<p>
<code>getImage()</code> returns an encrypted URL for the un-resized original — fetched once and shared, so every resized variant above reuses this one cached download:
</p>
<pre class="url">{originalUrl}</pre>
<div class="frame">
<img src={originalUrl} width={400} alt="Full-size original via getImage()" />
</div>
<h3>Gallery</h3>
<p>
Fourteen source photos, each resized to a square <code><img></code> on the fly with a <code>placeholder</code> blur-up — all server-rendered, zero client JS:
</p>
<div class="grid">
{#each gallery as src, i (src)}
<Image {src} width={400} height={400} fit="inside" placeholder alt="Gallery photo {i + 1}" class="grid__img" />
{/each}
</div>
<ImageCredits />
Styles
<style>
h3 {
margin-top: 1.5rem;
}
.frame {
display: flex;
justify-content: center;
margin: 1rem 0;
}
.frame :global(img) {
max-width: 100%;
height: auto;
border-radius: var(--radius-md);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 0.5rem;
margin: 1rem 0;
}
.grid :global(.grid__img) {
width: 100%;
aspect-ratio: 1 / 1;
height: auto;
object-fit: cover;
display: block;
border-radius: var(--radius-md);
}
.blur-compare {
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.blur-compare__placeholder {
width: 600px;
max-width: 100%;
aspect-ratio: 3 / 2;
background-size: cover;
background-position: center;
border-radius: var(--radius-md);
}
.blur-compare__arrow {
color: var(--text-muted, #888);
line-height: 0;
}
/* The global `pre` style supplies the dark code background/text; only the
size differs here — overriding the background alone would strand the
light code text on a light surface. */
.url {
font-size: 0.8rem;
}
.note {
margin-top: 0.5rem;
font-size: 0.85rem;
color: var(--text-muted, #888);
}
</style>
More demos