nextjs-cms

Field Groups

Split long forms into titled cards. Groups are purely visual; they don't change the table.

import { fieldGroup } from 'nextjs-cms/core/fields'

Basic usage

sections/events.section.ts
import { hasItemsSection } from 'nextjs-cms/core/sections'
import { dateRangeField, fieldGroup, mapField, richTextField, textField } from 'nextjs-cms/core/fields'

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

export default hasItemsSection({
    name: 'events',
    order: 3,
    title: { section: 'Events', singular: 'Event', plural: 'Events' },
    headingField: title,
    db: { table: 'events' },
    fields: [
        fieldGroup({
            title: 'Details',
            fields: [title, richTextField({ name: 'description' })],
        }),
        fieldGroup({
            title: 'When & where',
            fields: [
                dateRangeField({ startName: 'starts_at', endName: 'ends_at', format: 'datetime', required: true }),
                textField({ name: 'venue' }),
                mapField({ name: 'location' }),
            ],
        }),
    ],
})

Each group renders as a separate card with its title. The table still gets one column per field.

Options

OptionTypeDefaultDescription
titlestring | Record<locale, string>noneCard heading. Translatable: { en: 'Details', ar: 'التفاصيل' }.
fieldsFieldConfig[]requiredFields in this group. Groups can't be nested.
ordernumbernoneSort position among groups.

Rules

All or nothing. A section's fields array holds either only fields or only groups, never a mix. If you want some fields "ungrouped", put them in a group without a title:

fields: [
    fieldGroup({ fields: [title, slug] }), // no title: renders without a heading
    fieldGroup({ title: 'SEO', fields: [metaTitle, metaDescription] }),
]

A field lives in one group. Field names must be unique across the whole section, groups included.

Referencing grouped fields still works. headingField, coverPhotoField, search.searchFields, db.unique, and conditionalRules can point at fields inside any group, because you pass the field object:

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

export default hasItemsSection({
    // ...
    headingField: title,
    coverPhotoField: cover,
    fields: [
        fieldGroup({ title: 'Text', fields: [title] }),
        fieldGroup({ title: 'Media', fields: [cover] }),
    ],
})

Reusable groups

Because groups are plain values, you can build them in a helper and share them between sections:

sections/shared/seo.ts
import { fieldGroup, textAreaField, textField } from 'nextjs-cms/core/fields'

export function seoGroup() {
    return fieldGroup({
        title: { en: 'SEO', ar: 'تحسين محركات البحث' },
        fields: [
            textField({ name: 'meta_title', label: 'Meta title', maxLength: 70, localized: true }),
            textAreaField({ name: 'meta_description', label: 'Meta description', maxLength: 160, localized: true }),
        ],
    })
}
sections/pages.section.ts
import { seoGroup } from './shared/seo'

// ...
fields: [fieldGroup({ title: 'Content', fields: [title, body] }), seoGroup()],
Good to know:

Call a factory function (seoGroup()) rather than exporting one shared object. Each section then gets its own field instances, so nothing leaks between sections.