Assets & Images
Hoikka uses Vercel Blob for image storage and Vercel Image Optimization for on-the-fly transforms.
Why Vercel Blob + Image Optimization?
- Zero config - Auto-provisioned when connecting Blob store in Vercel dashboard
- CDN delivery - Fast global delivery via Vercel’s edge network
- On-the-fly transforms - Resize and optimize via
/_vercel/imageendpoint - Automatic format - WebP/AVIF served based on browser support
- No external accounts - Everything lives in your Vercel project
Configuration
Environment Variables
# Auto-provisioned by Vercel Blob integration
BLOB_READ_WRITE_TOKEN=Image Optimization Config
In vercel.json:
{
"images": {
"sizes": [64, 100, 128, 150, 200, 400, 600, 768, 1200],
"remotePatterns": [{ "hostname": "*.public.blob.vercel-storage.com" }]
}
}Uploading Images
Server-side Upload
import { put } from "@vercel/blob";
const blob = await put(`products/${file.name}`, file, {
access: "public",
addRandomSuffix: true
});
// blob.url = "https://xxx.public.blob.vercel-storage.com/products/image-abc123.jpg"Admin Upload Flow
- User selects file in admin ImagePicker
- Client sends file to
/api/assets/upload(our server route) - Server uploads to Vercel Blob via
put() - Server returns the Blob URL
- Client submits asset record to save in database
- Asset linked to product/collection
// Upload route (src/routes/api/assets/upload/+server.ts)
const blob = await put(`${folder}/${file.name}`, file, {
access: "public",
addRandomSuffix: true
});
return json({ url: blob.url, name: file.name });Image Optimization
Vercel Image Optimization resizes and optimizes images on the fly:
/_vercel/image?url={encoded_blob_url}&w={width}&q={quality}Helper Function
src/lib/image.ts
import { dev } from "$app/environment";
export function imageUrl(source: string, width: number, quality = 75): string {
if (dev) return source;
return `/_vercel/image?url=${encodeURIComponent(source)}&w=${width}&q=${quality}`;
}The /_vercel/image endpoint only exists on Vercel’s infrastructure. In local development, the helper returns the raw Vercel Blob URL directly. Images will be unoptimized (full-size) locally — this is expected.
Usage in Components
<script>
import { imageUrl } from "$lib/image";
let { product } = $props();
</script>
<img
src={imageUrl(product.featuredAsset.source, 400)}
alt={product.name}
loading="lazy"
/>Common Sizes
| Context | Width | Usage |
|---|---|---|
| Thumbnails | 100 | Cart, orders, wishlist |
| Admin grid | 200 | Product list |
| Product cards | 400 | Category/collection pages |
| Product detail | 600 | Main product image |
| Content pages | 768 | Page hero images |
| OG images | 1200 | Social sharing meta tags |
Database Schema
Assets are stored in the assets table:
assets
├── id: serial primary key
├── name: varchar // File name
├── type: text // image, video, document, other
├── mimeType: varchar // image/jpeg, image/png, etc.
├── source: varchar // Vercel Blob URL
├── width: integer
├── height: integer
├── fileSize: integer
├── alt: text // Alt text for accessibility
├── focalX: numeric // 0–1 horizontal focal point
├── focalY: numeric // 0–1 vertical focal point
├── createdAt: timestampProducts reference assets:
products.featuredAssetId → assets.id
product_variants.featuredAssetId → assets.idDeleting Images
When an asset is deleted from the admin, the Vercel Blob file is also removed:
import { del } from "@vercel/blob";
// Delete from Blob storage
await del(asset.source);
// Delete from database
await db.delete(assets).where(eq(assets.id, assetId));Last updated on