slugField
A URL-friendly identifier generated from another text field as the editor types.
import { slugField } from 'nextjs-cms/core/fields'Basic usage
A slug always follows a source textField, passed as for:
const title = textField({ name: 'title', required: true })
const slug = slugField({ name: 'slug', for: title })Typing "Hello, World! 2026" in the title fills the slug with hello-world-2026. Editors can still edit the slug by hand.
With every option
const title = textField({ name: 'title', required: true, localized: true })
const slug = slugField({
name: 'slug',
label: 'URL slug',
for: title,
required: true,
localized: true,
order: 2,
placeholder: 'generated-from-the-title',
minLength: 3,
maxLength: 80,
defaultValue: undefined,
editProtection: {
enabled: true,
alert: {
title: { en: 'Changing the URL', ar: 'تغيير الرابط' },
description: {
en: 'Existing links to this page will break. Add a redirect on the website first.',
ar: 'ستتعطل الروابط الحالية لهذه الصفحة.',
},
},
},
})
// Make it unique (recommended)
db: { table: 'posts', unique: [{ columns: [slug] }] }Options
| Option | Type | Default | Description |
|---|---|---|---|
for | textField config | required | The source field the slug is generated from. |
editProtection | boolean | { enabled?, alert? } | false | On edit forms, stop the slug from following the source field automatically. |
placeholder | string | none | Hint in the empty input. |
minLength | number | none | Minimum length, checked after normalization. |
maxLength | number | none | Maximum length, checked after normalization. |
defaultValue | string | none | Initial value. |
Plus the common options.
How slugs are normalized
The value is normalized on the server on every save, whatever the editor typed:
- Lowercased and trimmed.
- Whitespace runs become
-. - Everything except letters, numbers and
-is removed. Letters from any script are kept, so Arabic, Cyrillic or accented slugs work (مرحبا-بالعالم,café-crème). - Repeated hyphens are collapsed, and leading/trailing hyphens removed.
| Input | Stored slug |
|---|---|
Hello, World! | hello-world |
Next.js 16 Release | nextjs-16-release |
Crème brûlée | crème-brûlée |
مرحبا بالعالم | مرحبا-بالعالم |
Slugs keep non-ASCII letters. If your website needs pure ASCII URLs, transliterate in a beforeCreate/beforeUpdate hook.
Options in detail
Uniqueness
slugField doesn't enforce uniqueness by itself. Add it to db.unique:
db: { table: 'posts', unique: [{ columns: [slug] }] }The CMS checks it before saving and shows a readable error naming the field, instead of a raw MySQL duplicate-key error. For a localized slug, uniqueness is enforced per locale.
Unique per category (two categories can each have an "intro" post):
db: { table: 'posts', unique: [{ columns: [categoryId, slug] }] }editProtection
Once a page is published, its URL is shared, bookmarked and indexed. Auto-updating the slug when someone fixes a typo in the title silently breaks those links. editProtection keeps the slug frozen on edit forms (new items still auto-generate):
// Simple on/off
slugField({ name: 'slug', for: title, editProtection: true })With alert, editors who try to change a protected slug see a warning first. Both texts can be translated:
slugField({
name: 'slug',
for: title,
editProtection: {
enabled: true,
alert: {
title: 'This changes the public URL',
description: 'Links shared before this change will stop working.',
},
},
})Localized slugs
Make both the source and the slug localized to get a slug per language:
const title = textField({ name: 'title', localized: true, required: true })
const slug = slugField({ name: 'slug', for: title, localized: true, required: true })Your website then resolves /en/hello-world and /ar/مرحبا-بالعالم to the same item. See Localization.
Storage
| Column | VARCHAR(255) CHARACTER SET utf8mb4 |
| Value | The normalized slug |