🍡 mochi
← All demos

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:

A resized random photo

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:

A resized random photo with blur-up

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:

A photo rendered inside a hydrated island

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
Resized via getResizedImage()

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
Full-size original via getImage()

Gallery

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

Gallery photo 1Gallery photo 2Gallery photo 3Gallery photo 4Gallery photo 5Gallery photo 6Gallery photo 7Gallery photo 8Gallery photo 9Gallery photo 10Gallery photo 11Gallery photo 12Gallery photo 13Gallery photo 14

Photos by Minh Anh Nguyen, Andreas Haubold, blackieshoot, Amanda Lim, Negar Mz, Vi Tran, Hamada, Lea Ren and Yuliia Kucherenko on Unsplash.

View source on GitHub
<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>&lt;Image&gt;</code> renders a single resized <code>&lt;img&gt;</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>&lt;img&gt;</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>&lt;Image&gt;</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>&lt;Image&gt;</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&times;400 box — so this 3:2 photo becomes 400&times;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>&lt;img&gt;</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>