nextjs-cms

Photo Gallery

Give every item an unlimited, sortable set of photos, stored in a table of its own.

A photoField holds one image in a column. A gallery holds any number of photos per item: editors drag-and-drop uploads, reorder them, and delete them from the edit page.

Basic usage

sections/properties.section.ts
import { hasItemsSection } from 'nextjs-cms/core/sections'
import { textField } from 'nextjs-cms/core/fields'

const title = textField({ name: 'title', required: true })

export default hasItemsSection({
    name: 'properties',
    order: 2,
    title: { section: 'Properties', singular: 'Property', plural: 'Properties' },
    gallery: {
        db: { tableName: 'property_photos' },
    },
    headingField: title,
    db: { table: 'properties' },
    fields: [title],
})

Run update-sections and it creates the gallery table next to the section's table.

property_photos
┌──────────────┬───────────────────────────────────────────────────────────┐
│ reference_id │ the item's identifier (same type as the section's)        │
│ photo        │ generated file name, primary key                          │
│ meta         │ LONGTEXT, free-form per-photo data (nullable)             │
│ sort_order   │ INT, position set by drag-and-drop                        │
│ locale       │ only for localized galleries (part of the primary key)    │
│ created_at   │                                                           │
│ created_by   │                                                           │
└──────────────┴───────────────────────────────────────────────────────────┘

Files are written like any photo field's: the full image to <uploads>/.photos/<section>/ and a thumbnail to <uploads>/.thumbs/<section>/.

Options

OptionTypeDefaultDescription
db.tableNamestringrequiredGallery table name.
db.identifierFieldstring'reference_id'Column pointing to the item.
db.photoNameFieldstring'photo'Column with the file name.
db.metaFieldstring'meta'Free-form metadata column.
thumbnail{ width, height, fit: 'cover' | 'contain', quality }media.images.thumbnail from cms.config.tsThumbnail generated for each photo.
watermarkfalse | WatermarkObjectmedia.images.watermarkOverride the global watermark. false turns it off for this gallery. See watermarks.
localizedbooleanfalseA separate set of photos per locale.

Gallery uploads accept jpg, jpeg, png and webp files up to 2 MB each. The full-size image is stored as uploaded (not resized); only the thumbnail is generated.

Examples

Custom column names

Match an existing table or your naming style:

gallery: {
    db: {
        tableName: 'product_images',
        identifierField: 'product_id',
        photoNameField: 'file_name',
        metaField: 'caption',
    },
}

Square thumbnails and a watermark

gallery: {
    db: { tableName: 'listing_photos' },
    thumbnail: { width: 320, height: 320, fit: 'cover', quality: 75 },
    watermark: {
        imagePath: './public/watermark.png',
        position: 'bottom-right',
        size: 0.18,
        opacity: 0.6,
        margin: 24,
    },
}
gallery: {
    db: { tableName: 'campaign_visuals' },
    localized: true, // each locale gets its own photos
}

Requires content localization. The table gains a locale column, and the primary key becomes (photo, locale).

website/lib/properties.ts
import { asc, eq } from 'drizzle-orm'
import { db } from './db'
import { PropertyPhotosTable } from './cms-schema'

export function getPropertyPhotos(propertyId: number) {
    return db
        .select()
        .from(PropertyPhotosTable)
        .where(eq(PropertyPhotosTable.referenceId, propertyId))
        .orderBy(asc(PropertyPhotosTable.sortOrder))
}

// photo.photo → `${MEDIA_URL}/.photos/properties/${photo.photo}`
// thumbnail  → `${MEDIA_URL}/.thumbs/properties/${photo.photo}`

See Media & Uploads for serving the uploads folder.

When an item is deleted, its gallery rows and files are deleted too.