Skip to main content

Use your own photos

Show a folder from public/ with usePhotoFolder, map CMS or API records to PhotoItem, get real image width and height at upload or from localImages, keep IDs stable, type custom meta data, and handle invalid records with validation="drop".

Every component takes the same photo shape, PhotoItem. Convert your CMS or API records to it once, where the data enters your app, and pass the result to any component.

Show a folder of images

For images in public/, let Nuxt Photo read the folder. It reads each file's size at build time and makes a blurred preview, so you write no photo data:

app/pages/trips.vue
<script setup lang="ts">
const photos = await usePhotoFolder('trips', {
  alt: {
    'lake-at-dawn': 'Fog over a lake at sunrise',
    'ridge-trail': 'Two hikers on a ridge trail',
  },
})
</script>

<template>
  <PhotoAlbum :photos="photos" />
</template>
  • usePhotoFolder is async: await it in <script setup>. It returns a Ref<PhotoItem[]>.
  • The server reads the folder. Only this folder's photos travel to the browser, with the page data. Static generation (nuxi generate) works the same way.
  • It reads the files directly in public/trips, not subfolders. The id is the path without the extension, such as trips/lake-at-dawn.
  • alt and caption are keyed by the file name without its extension. During development, a folder with photos that have no alt logs one warning.
  • sort: 'name-desc' reverses the file name order. Prefix file names with a number or date when the order matters.
  • New files in the folder appear during nuxt dev without a restart.

Leave out width and height for local files

A hand-written array can leave out width and height for files in public/ when you turn on localImages:

nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxt/image', '@lupinum/nuxt-photo'],
  nuxtPhoto: { localImages: true },
})
ts
const photos: PhotoItem[] = [
  { id: 'lake', src: '/trips/lake-at-dawn.jpg', alt: 'Fog over a lake at sunrise' },
]

localImages sends the size and preview of every image in public/ to the browser, in the app bundle. With many images that adds up; prefer usePhotoFolder for galleries. Remote images always need width and height.

Map your records

app/pages/gallery.vue
<script setup lang="ts">
import type { PhotoItem } from '@lupinum/nuxt-photo/app'

type Asset = {
  key: string
  url: string
  width: number
  height: number
  alternativeText?: string
  title?: string
}

const { data: assets } = await useFetch<Asset[]>('/api/photos')

const photos = computed<PhotoItem[]>(() =>
  (assets.value ?? []).map((asset) => ({
    id: asset.key,
    src: asset.url,
    width: asset.width,
    height: asset.height,
    alt: asset.alternativeText,
    caption: asset.title,
  })),
)
</script>

<template>
  <PhotoAlbum :photos="photos" />
</template>

useFetch loads the data on the server, so the album is laid out in the first HTML.

When the records have no ID, use another value that is unique and does not change, such as the image URL without its size or quality parameters. Never use the array index: the lightbox and the thumbnails would lose their link when the order changes.

Required and optional fields

FieldRequiredMeaning
idYesStable, unique string. Use the asset ID, never the array index.
srcYesImage URL. The lightbox uses it.
widthYes*Real pixel width of the image file, a positive number.
heightYes*Real pixel height of the image file, a positive number.
altNoAlternative text. Add it unless the image is decorative.
captionNoShort text under the photo in the lightbox.
descriptionNoLonger text in the lightbox.
thumbSrcNoAnother crop for thumbnails. It is resized like src.
srcsetNoYour own fixed sizes. Used only without an image provider.
placeholderSrcNoLow-quality preview until the image loads.
metaNoYour own data, passed to slots and providers.

* Not needed for files in public/ that come from usePhotoFolder or, with localImages, from a hand-written array. Without them, the error says so: photo "lake" has no width; pass width and height, or in Nuxt use localImages / usePhotoFolder for files in public/.

width and height describe the original file, even when your image service returns a smaller version. Nuxt Photo uses them for the aspect ratio: the layout before the image loads and the frame of the lightbox animation.

Get the image size at upload

Nuxt Photo does not measure images in the browser, because that would delay the layout until every image has downloaded. Store the size with the asset. Most CMSs and image services already return it; use that value.

When you handle uploads yourself, read the size on the server, for example with sharp:

server/utils/read-image-size.ts
import sharp from 'sharp'

export async function readImageSize(file: Buffer) {
  const { width, height } = await sharp(file).metadata()
  if (!width || !height) throw new Error('The uploaded image has no readable size.')
  return { width, height }
}

Call it before you save the upload, and store both values. For existing assets, run a one-time backfill on the server. Do not fetch URLs that users control without your normal server-side request protections.

Keep your own data on the photo

meta carries typed data to slots and providers:

ts
type Credit = { photographer: string }

const photos = records.map((record) => ({
  id: String(record.id),
  src: record.image.url,
  width: record.image.width,
  height: record.image.height,
  meta: { photographer: record.image.credit },
})) satisfies PhotoItem<Credit>[]

Handle invalid records

By default a component throws when a photo has no id, no src, a size that is not a positive number, or an id that is used twice. The error names the photo and the problem. Keep this default while you connect a new data source.

When the page must render even with bad records, skip them and report them:

vue
<script setup lang="ts">
import type { InvalidPhotosEvent } from '@lupinum/nuxt-photo/app'

function reportInvalidPhotos(event: InvalidPhotosEvent) {
  for (const issue of event.issues) console.warn(issue.code, issue.message)
}
</script>

<template>
  <PhotoAlbum :photos="photos" validation="drop" @invalid-photos="reportInvalidPhotos" />
</template>

validation="drop" works on every component. A Photo with an invalid photo then renders nothing and emits invalid-photos. To check records yourself, for example before you store them, call validatePhotos(records): it returns the valid photos and the issues, and never throws.

Keep URLs stable during server rendering

Create signed or expiring URLs before rendering and store the final URL on the photo. A URL computed from the current time or a random value differs between server and browser and causes a hydration mismatch.

Check the result

Test one portrait photo, one landscape photo, a record with a duplicate ID, and a record without a size. The first two keep their shape in the album; the last two stop with a clear error, or are skipped with validation="drop".