Skip to Content
FeaturesAssets

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/image endpoint
  • 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

  1. User selects file in admin ImagePicker
  2. Client sends file to /api/assets/upload (our server route)
  3. Server uploads to Vercel Blob via put()
  4. Server returns the Blob URL
  5. Client submits asset record to save in database
  6. 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

ContextWidthUsage
Thumbnails100Cart, orders, wishlist
Admin grid200Product list
Product cards400Category/collection pages
Product detail600Main product image
Content pages768Page hero images
OG images1200Social 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: timestamp

Products reference assets:

products.featuredAssetId → assets.id product_variants.featuredAssetId → assets.id

Deleting 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