Media Guide
This guide covers how to use @verevoir/assets and @verevoir/media together to manage, display, and resize images and video in a Verevoir application.
Overview
Two packages handle media:
@verevoir/assets— persistence concern. Stores binary data + metadata. Extracts image dimensions on upload. Supports mutable hotspot and filename metadata.@verevoir/media— display concern. Deterministic imgproxy URL builder,AssetSourceabstraction, image/video block definitions, React editor components.
@verevoir/media has no runtime dependency on @verevoir/assets. It defines an AssetSource interface; you provide an implementation. The package ships a prebuilt adapter that uses structural typing (duck typing).
Uploading Assets
import { AssetManager, MemoryBlobStore } from '@verevoir/assets';
import { MemoryAdapter } from '@verevoir/storage';
const storage = new MemoryAdapter();
const blobStore = new MemoryBlobStore();
await storage.connect();
const manager = new AssetManager({ storage, blobStore });
// Upload — dimensions extracted automatically for bitmap images
const asset = await manager.upload({
data: imageBuffer, // Uint8Array
filename: 'hero.jpg',
contentType: 'image/jpeg',
createdBy: 'user-1',
});
console.log(asset.width); // 1920
console.log(asset.height); // 1080
console.log(asset.hotspot); // null (not set yet)
Dimension Extraction
On upload, Sharp reads width and height from bitmap images. SVG and video assets get null dimensions — SVG is vector (no inherent pixel size), and video would require ffprobe (out of scope).
Hotspot
A hotspot marks the focal point of an image (e.g. a person's face). It's a property of the image itself, stored as normalised coordinates (0–1 range).
// Set a hotspot
await manager.updateMetadata(asset.id, {
hotspot: { x: 0.5, y: 0.3 }, // centre-top
});
// Clear a hotspot
await manager.updateMetadata(asset.id, {
hotspot: null,
});
// Rename the file
await manager.updateMetadata(asset.id, {
filename: 'updated-hero.jpg',
});
updateMetadata() can change hotspot and filename. Blob-derived fields (size, contentType, width, height, type, format, createdBy) are immutable.
Bridging Assets to Media
The AssetSource interface decouples media display from asset storage:
import { createAssetSource } from '@verevoir/media';
const source = createAssetSource({
manager, // AssetManager instance (duck typed)
blobUrl: (blobKey) => `https://cdn.example.com/blobs/${blobKey}`,
});
The blobUrl function maps blob keys to HTTP URLs. This is where you configure your storage backend — S3 presigned URLs, local dev server, CDN paths, etc.
Building Resize URLs
Verevoir generates deterministic imgproxy URLs. No resize logic runs in Verevoir itself — imgproxy handles resizing at serve time.
import { buildImageUrl, buildSrcSet, imageProps } from '@verevoir/media';
const config = { baseUrl: 'https://imgproxy.example.com' };
// Single URL
const asset = await source.getAsset(assetId);
const url = buildImageUrl(asset, { width: 400, height: 300 }, config);
// → https://imgproxy.example.com/resize:fill:400:300/plain/https://cdn.example.com/blobs/...
// With hotspot (gravity focal point)
const urlWithHotspot = buildImageUrl(
{ ...asset, hotspot: { x: 0.5, y: 0.3 } },
{ width: 400, height: 300 },
config,
);
// → https://imgproxy.example.com/resize:fill:400:300/gravity:fp:0.5:0.3/plain/...
// srcSet for responsive images
const srcSet = buildSrcSet(asset, 400, 300, config);
// → ...resize:fill:400:300/plain/... 1x, ...resize:fill:800:600/plain/... 2x
Fit Modes
fill(default) — crop to exact dimensions. Hotspot gravity applied.fit— scale to fit within dimensions, no cropping. Hotspot ignored.auto— imgproxy decides based on image aspect ratio.
Rendering Helper
For output/rendering, use imageProps() to get ready-made <img> attributes:
const props = imageProps(asset, 400, 300, config);
// { src: '...', srcSet: '...', width: 400, height: 300 }
<img {...props} alt="Hero image" />
Image and Video Blocks
The media package provides pre-defined block definitions for embedding media in content:
import { imageBlock, videoBlock } from '@verevoir/media';
// Use in your content model
const data = {
asset: '550e8400-e29b-41d4-a716-446655440000',
alt: 'Team photo',
displayWidth: 800,
displayHeight: 600,
};
imageBlock.validate(data); // throws on invalid
// Video has autoplay and loop (default false)
const videoData = {
asset: '550e8400-e29b-41d4-a716-446655440000',
displayWidth: 1280,
displayHeight: 720,
autoplay: true,
loop: false,
};
videoBlock.validate(videoData);
React Components
Providers
Wrap your app with both providers:
import {
AssetSourceProvider,
ImgproxyConfigProvider,
} from '@verevoir/media';
function App() {
return (
<AssetSourceProvider source={source}>
<ImgproxyConfigProvider config={{ baseUrl: 'https://imgproxy.example.com' }}>
<Editor />
</ImgproxyConfigProvider>
</AssetSourceProvider>
);
}
ImageField
Asset picker with thumbnail preview:
import { ImageField } from '@verevoir/media';
<ImageField
value={selectedAssetId}
onChange={(id) => setSelectedAssetId(id)}
label="Hero Image"
/>
HotspotOverlay
Click-to-set focal point on an image:
import { HotspotOverlay } from '@verevoir/media';
<HotspotOverlay
src={imageUrl}
hotspot={currentHotspot}
onChange={(hotspot) => updateHotspot(hotspot)}
/>
imgproxy Setup
Verevoir generates unsigned imgproxy URLs for v1. The ImgproxyConfig type reserves key and salt fields for future HMAC signing.
For local development, run imgproxy via Docker:
docker run -p 8080:8080 darthsim/imgproxy
Then configure:
const config = { baseUrl: 'http://localhost:8080' };