ValGuide Docs
EngineeringEngineering Reference

Image Handling

For the full system architecture (R2 → Worker Guard → edge caching → browser), see Image Delivery Architecture.

Image Delivery Provider

The active image delivery provider is controlled by VITE_IMAGE_DELIVERY_PROVIDER when set, with fallback to the legacy VITE_IMAGE_PROVIDER env var:

ValueBehavior
imagekitImages served via ImageKit CDN (ik.imagekit.io/valguide/...)
cloudflare (legacy fallback default)Images served via Image Guard Worker (/i/w-640/...) with Cloudflare edge transforms
originImages served directly from the asset origin with no transform layer

Switching providers requires only an env var change + redeploy. The origin provider exists as a portability fallback for environments without Cloudflare or ImageKit.

<Image> Component

The custom Image component provides automatic responsive images, lazy loading, and content layout shift prevention. It wraps @unpic/react/base and selects a transformer through the shared image delivery provider contract:

  • Cloudflare mode: Custom transformer generating /i/ URLs for the Image Guard Worker. Breakpoints are restricted to 320, 640, 960, 1280, 1920 to match the Worker's allowed widths.
  • ImageKit mode: Uses unpic/providers/imagekit transformer.
  • Origin mode: Uses the direct asset origin without transform URLs.
// In apps (studio, app, admin):
import { Image } from '@valguide/ui/components/image'

// In packages/core:
import { Image } from '@valguide/core/ui/components/image'

Layout Modes

ModeWhen to UseExample
layout="fullWidth"Image fills its container width (most common)Cover images, cards, thumbnails, galleries
layout="constrained"Image has a max size but can shrinkFullscreen image viewer with width/height

Common Patterns

Cover image in a fixed-height container:

<div className="relative h-44 w-full overflow-hidden">
  <Image
    src={imageUrl}
    alt={title}
    layout="fullWidth"
    className="h-full w-full object-cover"
  />
</div>

Thumbnail in a square container:

<div className="relative h-12 w-12 rounded-md overflow-hidden bg-muted">
  <Image
    src={imageUrl}
    alt={title}
    layout="fullWidth"
    className="w-full h-full object-cover"
  />
</div>

Constrained image (e.g., zoomable viewer):

<Image
  src={src}
  alt={alt}
  layout="constrained"
  width={1200}
  height={1200}
  className="max-w-full max-h-full object-contain select-none pointer-events-none"
/>

Image with object-contain (gallery, preserving aspect ratio):

<div className="relative aspect-[4/3] w-full rounded-lg overflow-hidden bg-muted">
  <Image
    src={imageUrl}
    alt={fileName}
    layout="fullWidth"
    className="absolute inset-0 w-full h-full object-contain"
  />
</div>

Fallback / Empty State

Always provide a fallback when an image may be missing. Use a relevant Lucide icon inside a styled container:

import { ImageIcon, Music } from 'lucide-react'

{imageUrl ? (
  <Image src={imageUrl} alt={title} layout="fullWidth" className="h-full w-full object-cover" />
) : (
  <div className="flex h-full w-full items-center justify-center bg-muted/30">
    <ImageIcon className="h-7 w-7 text-muted-foreground" />
  </div>
)}

Common fallback icons by context:

  • Tour/stop cover: ImageIcon from lucide-react
  • Audio content: Music from lucide-react
  • Video content: Video from lucide-react

URL Helpers

All image URL construction goes through @valguide/core/features/assets/image-url:

FunctionUse CaseOutput
getAssetUrl(storagePath)Direct asset URL (audio/video, or image source for <Image>)`${VITE_ASSET_BASE_URL
getImageKitUrl(storagePath)ImageKit-optimized URL (used internally when provider is imagekit)${VITE_IMAGEKIT_URL}/${path}
getAssetImageUrl(asset)Optimized image URL for <Image> component (provider-aware)Cloudflare → direct asset URL transformed to /i/..., ImageKit → ImageKit URL, Origin → direct asset URL
getAssetDisplayUrl(asset)Auto-selects by asset typeImages → getAssetImageUrl, audio/video → direct asset origin
getAssetVideoThumbnailUrl(storagePath, options)Provider-managed video thumbnail URL when supportedCloudflare → /v/..., ImageKit/Origin → null
import { getAssetImageUrl } from '@valguide/core/features/assets/image-url'

const url = getAssetImageUrl({ storagePath: 'orgs/abc/cover.jpg' })
// → https://assets.valguide.com/orgs/abc/cover.jpg
// The <Image> component transforms this to:
// → https://assets.valguide.com/i/w-640/orgs/abc/cover.jpg (in srcset)

Dependencies

The image component lives in @valguide/core and wraps:

PackagePurpose
@unpic/react (^1.0.2)Base <Image> component with responsive rendering (uses @unpic/react/base)
unpicImageKit transformer (unpic/providers/imagekit) — used when VITE_IMAGE_DELIVERY_PROVIDER=imagekit

The Cloudflare transformer is a custom function in image.tsx — no additional dependency needed.

Apps do not need @unpic/react or unpic as direct dependencies — they import the pre-configured component from @valguide/ui/components/image.

Usage Inventory

packages/core/ (5 files)

FileContextLayout
features/tours/preview-card.tsxTour card cover imagefullWidth
features/player/components/cover-image.tsxPlayer cover artfullWidth
features/player/components/fullscreen-image.tsxZoomable fullscreen viewerconstrained
features/player/components/mini-player.tsxMini player thumbnailfullWidth
features/player/components/stops-list-item.tsxStop list thumbnailfullWidth

apps/app/ (2 files)

FileContextLayout
src/components/tours/tour-hero.tsxTour hero bannerfullWidth
src/components/tours/image-gallery.tsxGallery main image + thumbnailsfullWidth

apps/studio/ (5 files)

FileContextLayout
src/features/assets/components/asset-card.tsxAsset grid cardfullWidth
src/features/assets/components/asset-picker-modal.tsxAsset picker thumbnailfullWidth
src/features/assets/components/media-picker/media-picker-preview.tsxSelected media previewfullWidth
src/features/assets/components/media-picker/media-picker-gallery.tsxMulti-asset gallery thumbnailsfullWidth
src/features/tours/components/tour-detail-view.tsxTour detail hero imagefullWidth

apps/admin/ (1 file)

FileContextLayout
src/features/admin/components/tours-columns.tsxTour table cover thumbnailfullWidth

Conventions

  1. Always import Image from @valguide/ui/components/image (or @valguide/core/ui/components/image inside core) — never from @unpic/react directly
  2. Always use getAssetImageUrl() to build image URLs — never construct URLs manually
  3. Always use layout="fullWidth" unless the image needs a max size constraint
  4. Always provide alt text — use the entity title or file name
  5. Always wrap in a sized container — the <Image> component fills its parent
  6. Always include a fallback for optional images (cover images, thumbnails)
  7. Use object-cover for thumbnails/cards, object-contain for galleries/viewers
  8. Never use raw <img> tags for stored assets — always use the <Image> component
  9. Never add @unpic/react or unpic to app-level package.json — the dependency is centralized in @valguide/core

On this page