Sections
A section is one content type. It defines a form, a database table, admin pages, and permissions, all from one file.
Choosing a section type
| Type | Helper | Rows | Admin pages | Use it for |
|---|---|---|---|---|
| Simple | simpleSection() | Exactly one | A single edit form | Site settings, contact info, homepage content, legal pages |
| Has-items | hasItemsSection() | Many | List (search, pagination) + create + edit | Posts, products, jobs, FAQs, team members, anything repeatable |
| Category | categorySection() | Many, as a tree | Tree browser + create + edit | Taxonomies, nested menus, product categories |
import { categorySection, hasItemsSection, simpleSection } from 'nextjs-cms/core/sections'One row. Settings-style content.
Has-Items SectionCollections with a list page.
Category SectionHierarchies up to any depth.
HooksRun code before and after writes.
Anatomy of a section file
import { hasItemsSection } from 'nextjs-cms/core/sections'
import { richTextField, textField } from 'nextjs-cms/core/fields'
// 1. Define fields as variables so other options can reference them.
const question = textField({ name: 'question', required: true })
const answer = richTextField({ name: 'answer', required: true })
// 2. Default-export exactly one section.
export default hasItemsSection({
name: 'faq', // unique key
order: 5, // sidebar position
icon: 'circle-help', // Lucide icon
title: { section: 'FAQ', singular: 'Question', plural: 'Questions' },
headingField: question, // a field object, not a string
db: { table: 'faq' },
fields: [question, answer],
})Options like headingField, coverPhotoField, search.searchFields, and db.unique take the field object you created, not its name. Declaring fields as variables first is the idiomatic way to write sections: it keeps references type-safe and renames safe.
Options shared by every section type
| Option | Type | Default | Description |
|---|---|---|---|
name | string | required | Unique section key (case-insensitive). Used in URLs, permissions, logs, and upload folders. |
order | number | required | Sort position in the admin sidebar. |
icon | Lucide icon name | none | Sidebar icon, e.g. 'newspaper', 'settings', 'folder-tree'. |
readonly | boolean | false | Reserved. Accepted and stored on the section, but not enforced by the admin yet. To stop edits today, control it with permissions or a beforeUpdate hook. |
db | object | required | Table name plus identifier, keys and indexes. See Database Options. Simple sections only accept db.table. |
fields | FieldConfig[] or FieldGroupConfig[] | required | The form. Either all fields, or all field groups. At least one. |
hooks | Hooks | none | Functions run around create, update and delete. See Hooks. |
gallery | object | none | Adds a multi-photo gallery stored in its own table. See Photo Gallery. |
Each type adds its own options (title, headingField, depth, search, ...), documented on its page.
Naming rules
nextjs-cms validates names when sections load, and update-sections refuses to run on these mistakes:
- Section names must be unique across the project, ignoring case.
- Field names must be unique within a section, ignoring case. They become column names, so use
snake_caseorcamelCaseconsistently. idis reserved for the automatic identifier. Use a different name, or make your field the identifier.localeis reserved as a column name (it's used by translation tables).- Table names must not end in
_locales. That suffix is reserved for translation tables.
Columns you get for free
Besides your fields, every section table gets bookkeeping columns:
| Column | Simple | Has-items / Category |
|---|---|---|
created_at | — | ✓ |
updated_at | ✓ | ✓ |
created_by | — | ✓ (username of the admin) |
updated_by | ✓ | ✓ |
Category sections with depth > 1 also get parent_id and level. See Category Section.
How a section becomes a table
- You save
sections/faq.section.ts. - You run
pnpm nextjs-cms --dev update-sections. - The CLI loads every section, builds the table definition each implies, and compares it with MySQL.
- For each difference it acts or asks: create a new table, add a column, detect a rename (it asks "new field or rename of X?"), change a column type, or remove a column that is no longer in your config (it asks first).
- It regenerates
dynamic-schemas/schema.tsif Drizzle schema generation is on, and grants theMasteradmin access to new sections.
See the CLI reference for flags and prompts.