nextjs-cms

Simple Section

A section with exactly one row. Editors get a single form instead of a list, which is ideal for settings and one-off pages.

import { simpleSection } from 'nextjs-cms/core/sections'

When to use it

Use a simple section when there should only ever be one record:

  • Site settings (name, contact email, social links, maintenance mode)
  • The homepage hero, an "About us" page, or legal pages
  • Feature flags and configuration values editors should control

The sidebar entry opens the edit form directly. There is no list page, no "New" button, and no delete.

Minimal example

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

export default simpleSection({
    name: 'settings',
    title: 'Settings',
    order: 1,
    db: { table: 'settings' },
    fields: [textField({ name: 'site_name', required: true })],
})

Options

OptionTypeDefaultDescription
namestringrequiredUnique section key.
titlestring | Record<locale, string>requiredSidebar and page title. Pass an object like { en: 'Settings', ar: 'الإعدادات' } to translate it for the admin UI.
ordernumberrequiredSidebar position.
db.tablestringrequiredTable name. It's the only db option simple sections accept: the identifier is always a numeric id equal to 1.
fieldsFieldConfig[] | FieldGroupConfig[]requiredThe form.
iconLucide icon namenoneSidebar icon.
hooksHooksnonebeforeUpdate, afterUpdate and onError. They run on every save, including the first. See Hooks.
galleryobjectnoneAttach a photo gallery. See Photo Gallery.
readonlybooleanfalseReserved, not enforced yet.

How the single row works

  • The row always has id = 1.
  • Until someone saves the form for the first time, the table is empty. The first save inserts row 1, and every later save updates it.
  • Your website should therefore handle "no row yet": fall back to defaults or show nothing.
website/lib/settings.ts
import { eq } from 'drizzle-orm'
import { db } from './db'
import { SettingsTable } from './cms-schema'

export async function getSettings() {
    const [row] = await db.select().from(SettingsTable).where(eq(SettingsTable.id, 1)).limit(1)
    return row ?? { siteName: 'My site', maintenanceMode: false } // defaults before the first save
}

Examples

Site settings with grouped fields

Group related settings into cards with fieldGroup. When you use groups, every entry in fields must be a group.

sections/settings.section.ts
import { simpleSection } from 'nextjs-cms/core/sections'
import {
    checkboxField,
    colorField,
    fieldGroup,
    photoField,
    textAreaField,
    textField,
} from 'nextjs-cms/core/fields'

export default simpleSection({
    name: 'settings',
    title: { en: 'Site settings', ar: 'إعدادات الموقع' },
    order: 100,
    icon: 'settings',
    db: { table: 'site_settings' },
    fields: [
        fieldGroup({
            title: 'General',
            fields: [
                textField({ name: 'site_name', label: 'Site name', required: true }),
                textAreaField({ name: 'tagline', maxLength: 200 }),
                photoField({
                    name: 'logo',
                    fileType: ['png', 'webp'],
                    thumbnail: { width: 200, height: 200, fit: 'contain' },
                }),
                colorField({ name: 'brand_color', label: 'Brand color', defaultValue: '#0070f3' }),
            ],
        }),
        fieldGroup({
            title: 'Contact',
            fields: [
                textField({ name: 'email', label: 'Contact email', required: true }),
                textField({ name: 'phone' }),
                textField({ name: 'whatsapp', label: 'WhatsApp number' }),
            ],
        }),
        fieldGroup({
            title: 'Social',
            fields: [
                textField({ name: 'instagram_url', label: 'Instagram URL' }),
                textField({ name: 'x_url', label: 'X (Twitter) URL' }),
                textField({ name: 'linkedin_url', label: 'LinkedIn URL' }),
            ],
        }),
        fieldGroup({
            title: 'Maintenance',
            fields: [
                checkboxField({ name: 'maintenance_mode', label: 'Maintenance mode' }),
                textAreaField({ name: 'maintenance_message', label: 'Message shown to visitors' }),
            ],
        }),
    ],
})

Show a field only when another is set

Use conditionalRules to hide inputs that don't apply. Here the maintenance message appears only when maintenance mode is on:

sections/settings.section.ts
const maintenanceMode = checkboxField({ name: 'maintenance_mode', label: 'Maintenance mode' })

const maintenanceMessage = textAreaField({
    name: 'maintenance_message',
    label: 'Message shown to visitors',
    required: true,
    conditionalRules: [{ field: maintenanceMode, condition: 'equals', value: true }],
})

A conditional field is only validated and saved when its rules match, so required: true here means "required when maintenance mode is on".

Translatable page content

With content localization enabled in cms.config.ts, mark the fields that need translating with localized: true. The form gets a locale switcher. Non-localized fields (like the hero image) are shared by all languages.

sections/home.section.ts
import { simpleSection } from 'nextjs-cms/core/sections'
import { photoField, richTextField, textField } from 'nextjs-cms/core/fields'

export default simpleSection({
    name: 'home',
    title: 'Homepage',
    order: 1,
    icon: 'house',
    db: { table: 'home_page' },
    fields: [
        textField({ name: 'hero_title', required: true, localized: true }),
        textField({ name: 'hero_subtitle', localized: true }),
        photoField({ name: 'hero_image', required: true, size: { width: 1920, height: 800, fit: 'cover' } }),
        richTextField({ name: 'intro', localized: true }),
    ],
})

The default locale is stored in home_page. Translations go to an automatically created home_page_locales table.

Validate and react to changes with hooks

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

export default simpleSection({
    name: 'settings',
    title: 'Settings',
    order: 1,
    db: { table: 'settings' },
    fields: [textField({ name: 'support_email', required: true })],
    hooks: {
        // Throwing cancels the save and shows the message in the admin.
        beforeUpdate: ({ values }) => {
            const email = String(values.support_email ?? '')
            if (!email.endsWith('@acme.com')) {
                throw new Error('Support email must be an @acme.com address')
            }
        },
        // Runs after a successful save. Failures are logged, never shown to the editor.
        afterUpdate: async () => {
            await fetch('https://acme.com/api/revalidate?tag=settings', { method: 'POST' })
        },
    },
})
Good to know:

Simple sections always go through the update path, even for the very first save (which inserts row 1 under the hood). So beforeUpdate and afterUpdate run on every save, and the create and delete hooks never run.

Common mistakes

  • Adding db.identifier or db.unique. Simple sections only accept db.table and fail validation otherwise.
  • Naming a field id. It's reserved for the automatic row id.
  • Expecting a row to exist. Read it defensively, as shown above.