nextjs-cms

cms.config.ts

Every option of the CMS configuration file, with types and defaults.

cms.config.ts
import { defineConfig } from 'nextjs-cms/core/config'

export default defineConfig({
    ui: {},
    dates: { timezone: 'UTC' },
    debug: false,
    sections: {},
    media: {},
    schemaGeneration: {},
    plugins: [],
    i18n: {},
    localization: undefined,
    dashboard: {},
})

Every option is optional. The file is loaded from the project root (cms.config.ts, .mts, .cts, .js, .mjs, .cjs, or .json), validated, merged with the defaults below, and reloaded when it changes in development.

Complete example

cms.config.ts
import { resolve } from 'node:path'
import { cacheRevalidationPlugin } from '@nextjscms/plugin-cache-revalidation'
import { pluginDictionaries } from '@nextjscms/plugin-cache-revalidation/translations'
import ar from '@nextjscms/translations/ar'
import { defineConfig } from 'nextjs-cms/core/config'
import en from 'nextjs-cms/translations/base/en'

export default defineConfig({
    ui: {
        title: 'Acme CMS',
        logo: '/img/logo.svg',
        logoText: { text: 'Acme', position: 'right' },
        defaultTheme: 'dark',
    },
    dates: { timezone: 'UTC' },
    debug: false,
    sections: {
        path: resolve(process.cwd(), 'sections'),
        strict: false,
        category: { allowRecursiveDelete: false },
    },
    media: {
        upload: { path: '.uploads', preserveExtension: true },
        images: {
            background: { r: 0, g: 0, b: 0, alpha: 0 },
            image: { quality: 85 },
            thumbnail: { size: { width: 400, height: 300, fit: 'cover' }, quality: 80 },
            watermark: false,
        },
    },
    schemaGeneration: {
        drizzle: {
            enabled: true,
            outDir: './dynamic-schemas',
            fileName: 'schema.ts',
            naming: { tables: 'pascalCase', columns: 'camelCase' },
        },
    },
    plugins: [{ plugin: cacheRevalidationPlugin, options: { contentTypes: ['posts'] } }],
    i18n: {
        supportedLanguages: {
            en: { ...en, ...pluginDictionaries.en },
            ar: { ...ar, ...pluginDictionaries.ar },
        },
        fallbackLanguage: 'en',
    },
    localization: {
        locales: [
            { code: 'en', label: 'English' },
            { code: 'ar', label: 'العربية', rtl: true },
        ],
        defaultLocale: 'en',
    },
    dashboard: { override: '/cache-revalidation' },
})

dates

OptionTypeDefaultDescription
dates.timezonestring'UTC'Valid IANA timezone used by dateField and dateRangeField for input, display, defaults and bounds.

For example, dates: { timezone: 'Europe/Istanbul' }. A field's timezone overrides this default. Date-only values remain calendar dates; datetime/timestamp values are stored as UTC. Changing the setting never rewrites stored rows. See date field timezone behavior and upgrade notes.

ui

OptionTypeDefaultDescription
ui.titlestring'nextjs-cms'Browser tab title. Shown in the sidebar when neither logo nor logoText is set. Required when ui is given.
ui.logostring'/nextjscms.webp' (only when ui is omitted)Image path in public/.
ui.logoText.textstringnoneText shown beside the logo, or alone.
ui.logoText.position'left' | 'right' | 'top' | 'bottom''right'Side of the logo the text sits on. Ignored without a logo.
ui.defaultTheme'light' | 'dark''dark'Initial admin theme.

When you provide ui, omitted logo/logoText mean "don't show one" rather than falling back to the default branding.

debug

OptionTypeDefaultDescription
debugbooleanfalseVerbose server logging.

sections

OptionTypeDefaultDescription
sections.pathstring'sections'Folder scanned for **/*.section.ts.
sections.strictbooleanfalseWhen true, one invalid section prevents all sections from loading.
sections.category.allowRecursiveDeletebooleanfalseGlobal switch for recursive category deletes. See Category Section.

media

OptionTypeDefaultDescription
media.upload.pathstring'./public/content'Root folder for uploads, relative to the project root. Required when upload is given.
media.upload.preserveExtensionbooleantrueReserved. Not used by the current version; see removeExtension on photoField.
media.images.background{ r, g, b, alpha }{ r: 0, g: 0, b: 0, alpha: 0 }Padding color for fit: 'contain' when a field doesn't set one.
media.images.image.size{ width, height, fit, strict? }{ width: 1200, height: 630, fit: 'cover' }Reserved. Not applied to photo fields in the current version.
media.images.image.qualitynumber (50–100)80Output quality for main images when a field's size.quality isn't set.
media.images.thumbnail.size{ width, height, fit: 'cover' | 'contain' }{ width: 200, height: 200, fit: 'contain' }Thumbnail size when a field or gallery doesn't set thumbnail.
media.images.thumbnail.qualitynumber (50–100)80Thumbnail quality default.
media.images.watermarkfalse | WatermarkObjectfalseGlobal watermark. See watermarks.

schemaGeneration

OptionTypeDefaultDescription
schemaGeneration.drizzle.enabledbooleantrueWrite a Drizzle schema on update-sections.
schemaGeneration.drizzle.outDirstring'./dynamic-schemas'Output folder.
schemaGeneration.drizzle.fileNamestring'schema.ts'Output file.
schemaGeneration.drizzle.naming.tables'camelCase' | 'snakeCase' | 'pascalCase''pascalCase'Style of table identifiers (PostsTable).
schemaGeneration.drizzle.naming.columns'camelCase' | 'snakeCase' | 'pascalCase''camelCase'Style of column identifiers (publishedAt).

See Querying Content.

plugins

An array. Each entry is either a plugin descriptor or an object:

OptionTypeDescription
plugindescriptorThe plugin.
optionsRecord<string, unknown>Passed to the plugin's createPlugin().
override.titlestring | Record<locale, string>Rename the plugin (or its only page).
override.iconLucide icon nameChange its icon.
override.routes{ [path]: { title?, icon? } }Per-page overrides.
groupRoutesbooleanGroup multi-page plugins in the sidebar. Default true.

See Using Plugins.

i18n

Admin interface languages.

OptionTypeDefaultDescription
i18n.supportedLanguagesRecord<code, Dictionary>{ en }Languages offered to admins, with their dictionaries. At least one.
i18n.fallbackLanguagestringfirst keyUsed on the login page and when an admin's language isn't offered. Must be a key of supportedLanguages.

See Admin UI languages.

localization

Content translation. Off unless set.

OptionTypeDescription
localization.locales{ code, label, rtl?, flag? }[]Content locales, at least one. Codes must be unique.
localization.defaultLocalestringOne of the codes. Stored in the section tables; the others in {table}_locales.

See Localization.

dashboard

OptionTypeDefaultDescription
dashboard.overridestring | nullnullPath of a plugin page to show as the dashboard. Visible to all signed-in admins.

Validation errors

The config is validated when it loads. These stop the app with a clear message:

  • localization.defaultLocale isn't in localization.locales.
  • Two locales share a code.
  • i18n.fallbackLanguage isn't a key of i18n.supportedLanguages.
  • A value has the wrong type (for example media.images.image.quality: 30, below the minimum of 50).