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
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
| Option | Type | Default | Description |
|---|---|---|---|
name | string | required | Unique section key. |
title | string | Record<locale, string> | required | Sidebar and page title. Pass an object like { en: 'Settings', ar: 'الإعدادات' } to translate it for the admin UI. |
order | number | required | Sidebar position. |
db.table | string | required | Table name. It's the only db option simple sections accept: the identifier is always a numeric id equal to 1. |
fields | FieldConfig[] | FieldGroupConfig[] | required | The form. |
icon | Lucide icon name | none | Sidebar icon. |
hooks | Hooks | none | beforeUpdate, afterUpdate and onError. They run on every save, including the first. See Hooks. |
gallery | object | none | Attach a photo gallery. See Photo Gallery. |
readonly | boolean | false | Reserved, 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.
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.
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:
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.
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
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' })
},
},
})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.identifierordb.unique. Simple sections only acceptdb.tableand 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.