## Demo: image-invalidation ### ImageInvalidationDemo.svelte ```svelte

The images below all derive from one shared cached original, fetched from our own /demos/image-invalidation/source.jpg route — a Mochi.file() endpoint that serves a random bundled photo on every request. Hit the button to invalidateImage(src, { hard: true }): it hard-deletes the cached original and cascades to every named size, so the next request re-fetches the source and all sizes update to the same new photo in lockstep.

{#if generation > 0} Invalidated {generation} time{generation === 1 ? '' : 's'} {/if}

Shared original

The shared cached original at full size

Named sizes off that original

Shown at their relative sizes on one row, so the difference is visible at a glance:

{#each variants as v (v.size)}
{v.size} variant of the cached original
{v.label}
{/each}

invalidateImage() clears the cache on the server. A browser already holding a copy (via Cache-Control in production) keeps showing it until that lapses — so this demo appends a small &g= nonce to force an immediate re-request. Pass { hard: false } (the default) instead to mark the original stale: the next request serves the cached bytes right away and re-fetches in the background.

``` ### routes.ts ```ts import { Mochi, invalidateImage, redirect, getRequestContext } from 'mochi-framework'; import type { MochiRouteValue } from 'mochi-framework'; // Our own source endpoint serves a *random* one of the bundled photos on each // request, so an invalidated cache is visibly different once it's re-fetched. const IMAGE_COUNT = 14; const pickImage = () => `./images/mochi-${1 + Math.floor(Math.random() * IMAGE_COUNT)}.jpg`; // The image source is our own endpoint, absolute so the image endpoint can fetch // it. `.jpg` keeps it clear of the site's trailingSlash normalization. const sourceUrl = () => `${getRequestContext().url.origin}/demos/image-invalidation/source.jpg`; let generation = 0; export const routes: Record = { // Random-image source: a fresh random bundled photo on every request. '/demos/image-invalidation/source.jpg': Mochi.file(pickImage), '/demos/image-invalidation': Mochi.page('./src/demos/image-invalidation/ImageInvalidationDemo.svelte', { serverProps: () => ({ src: sourceUrl(), generation }), actions: { // Hard delete: drop the shared original and cascade to its variants. // Post/Redirect/Get so a refresh doesn't re-submit; the bumped // `generation` rides through serverProps on the GET. default: async () => { await invalidateImage(sourceUrl(), { hard: true }); generation++; return redirect(303, '/demos/image-invalidation/'); }, }, }), }; ``` ### index.ts ```ts import { Mochi, logger } from 'mochi-framework'; import { routes } from './routes'; await Mochi.serve({ port: 3333, development: process.env.MODE === 'development', // Named image sizes — referenced by name from , // getImageUrl(src, 'name') and getImage(src, 'name'). The URL only carries the // src + size name; the endpoint runs the transform lazily on request. image: { sizes: { hero: { width: 600, height: 400, fit: 'inside' }, square: { width: 400, height: 400, fit: 'inside' }, card: { width: 400, height: 267, fit: 'inside' }, thumb: { width: 240, height: 240, fit: 'inside' }, 'fit-fill': { width: 240, height: 240, fit: 'fill' }, 'fit-inside': { width: 240, height: 240, fit: 'inside' }, rotate90: { width: 200, height: 200, fit: 'inside', rotate: 90 }, rotate180: { width: 200, height: 200, fit: 'inside', rotate: 180 }, rotate270: { width: 200, height: 200, fit: 'inside', rotate: 270 }, flip: { width: 200, height: 200, fit: 'inside', flip: true }, flop: { width: 200, height: 200, fit: 'inside', flop: true }, grayscale: { width: 200, height: 200, fit: 'inside', modulate: { saturation: 0 } }, brighten: { width: 200, height: 200, fit: 'inside', modulate: { brightness: 1.5 } }, saturate: { width: 200, height: 200, fit: 'inside', modulate: { saturation: 2 } }, 'fmt-jpeg': { width: 300, height: 300, fit: 'inside', format: 'jpeg', quality: 85 }, 'fmt-png': { width: 300, height: 300, fit: 'inside', format: 'png' }, 'fmt-webp': { width: 300, height: 300, fit: 'inside', format: 'webp', quality: 80 }, }, }, routes, }); logger.info('Server running at http://localhost:3333'); ```