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:
<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>usePhotoFolderis async:awaitit in<script setup>. It returns aRef<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. Theidis the path without the extension, such astrips/lake-at-dawn. altandcaptionare keyed by the file name without its extension. During development, a folder with photos that have noaltlogs 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 devwithout 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:
export default defineNuxtConfig({
modules: ['@nuxt/image', '@lupinum/nuxt-photo'],
nuxtPhoto: { localImages: true },
})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
<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
| Field | Required | Meaning |
|---|---|---|
id | Yes | Stable, unique string. Use the asset ID, never the array index. |
src | Yes | Image URL. The lightbox uses it. |
width | Yes* | Real pixel width of the image file, a positive number. |
height | Yes* | Real pixel height of the image file, a positive number. |
alt | No | Alternative text. Add it unless the image is decorative. |
caption | No | Short text under the photo in the lightbox. |
description | No | Longer text in the lightbox. |
thumbSrc | No | Another crop for thumbnails. It is resized like src. |
srcset | No | Your own fixed sizes. Used only without an image provider. |
placeholderSrc | No | Low-quality preview until the image loads. |
meta | No | Your 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:
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:
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:
<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".