Project Structure
A tour of the files create-nextjs-cms generates, and which ones you are expected to edit.
A new project is a regular Next.js 16 App Router application. The admin UI lives in app/ and components/, and the CMS engine comes from the nextjs-cms package in node_modules.
Top-level files and folders
my-cms/
├── sections/ ← your content types (*.section.ts)
├── cms.config.ts ← CMS configuration
├── .env ← auth secrets, API keys
├── .env.development ← database credentials (dev)
├── dynamic-schemas/ ← generated Drizzle schema for your sections
├── .uploads/ ← uploaded files (media.upload.path)
├── public/ ← static assets, logo
├── app/ ← the admin application (App Router)
│ ├── (auth)/ ← login page
│ ├── (rootLayout)/ ← authenticated admin pages
│ │ ├── (plugins)/ ← catch-all route that renders plugin pages
│ │ ├── browse/ new/ edit/ section/ categorized/
│ │ ├── admins/ settings/ log/ dashboard/
│ ├── _trpc/ ← tRPC client and server callers
│ └── api/ ← auth, submit, file, and tRPC route handlers
├── components/ ← admin UI components (forms, inputs, layout)
├── lib/ ← helpers
├── env/env.ts ← typed environment variables (@t3-oss/env-nextjs)
├── proxy.ts ← CSRF protection for mutating requests
├── drizzle.config.ts ← used by the CLI to create system tables
└── next.config.tsFiles you edit
| Path | Purpose |
|---|---|
sections/*.section.ts | One file per section. This is where you spend most of your time. See Sections. |
cms.config.ts | Branding, media and image defaults, localization, plugins. See Configuration. |
.env* | Secrets and connection strings. See Environment variables. |
public/ | Your logo (referenced by ui.logo) and any static files. |
Files you usually leave alone
| Path | Purpose |
|---|---|
app/, components/ | The admin application. You own this code and can customize it, but most projects never need to. Upgrading nextjs-cms mostly means bumping the nextjs-cms package. |
dynamic-schemas/schema.ts | Regenerated by update-sections when schemaGeneration.drizzle.enabled is on. Don't edit it by hand. See Querying Content. |
.uploads/ | Where uploaded photos, videos and documents are written. Serve it from your website or a CDN. See Media & Uploads. |
proxy.ts | Rejects POST, PUT, PATCH, and DELETE requests without a valid CSRF token. |
The sections folder
The CMS loads every file matching *.section.ts inside the folder set by sections.path in cms.config.ts. Subfolders are fine:
sections/
├── blog.section.ts
├── settings.section.ts
├── shop/
│ ├── products.section.ts
│ └── categories.section.ts
└── shared/
└── seo-fields.ts ← not a section: no .section.ts suffixRules for section files:
- The file name must end in
.section.ts. Other files in the folder are ignored, so you can keep shared helpers next to your sections. - Each file has exactly one
export default: the object returned bysimpleSection(),hasItemsSection()orcategorySection(). - Section
names must be unique across the project (case-insensitive).
import { textAreaField, textField } from 'nextjs-cms/core/fields'
// Plain helpers: call them from any section file to reuse field definitions.
export const seoFields = () => [
textField({ name: 'meta_title', label: 'Meta title', maxLength: 70 }),
textAreaField({ name: 'meta_description', label: 'Meta description', maxLength: 160 }),
]Section files can import other section files. That's how you point a selectField at another section (see Relationships). Always import them without a file extension:
import categoriesSection from './categories.section' // ✅
// import categoriesSection from './categories.section.ts' ❌ fails to load
// import categoriesSection from './categories.section.js' ❌ fails to loadSection files are loaded at runtime with jiti, not by Next.js' bundler. In development, the CMS watches **/*.section.ts and reloads sections when you save, so you don't have to restart next dev.