nextjs-cms

tagsField

Free-form tags typed by editors. Stored as a comma-separated list, or as rows in a join table with auto-completion.

import { tagsField } from 'nextjs-cms/core/fields'

Use tagsField when editors should be able to invent values ("nextjs", "tutorial", "2026"). If they must choose from a fixed or managed list, use selectMultipleField instead.

Basic usage

tagsField({ name: 'keywords' })

Editors type a tag and press Enter or comma. Stored as nextjs,cms,tutorial in a LONGTEXT column.

With every option

tagsField({
    name: 'tags',
    label: { en: 'Tags', ar: 'الوسوم' },
    required: false,
    order: 7,
    localized: true,
    conditionalRules: [{ field: isPublic, condition: 'equals', value: true }],

    placeholder: 'Add a tag and press Enter',
    sanitize: true,
    validation: {
        regex: /^[a-z0-9-]{2,30}$/,
        message: 'Tags use 2–30 lowercase letters, digits or hyphens.',
    },
    destinationDb: {
        table: 'post_tags',
        itemIdentifier: 'post_id',
        selectIdentifier: 'tag',
    },
    autoCompletion: true, // requires destinationDb
})

Options

OptionTypeDefaultDescription
placeholderstringnoneHint in the empty input.
sanitizebooleanfalseHTML-escape each tag before saving.
validation{ regex: RegExp; message: string }noneEvery tag must match regex; otherwise the save fails with message.
destinationDb{ table, itemIdentifier, selectIdentifier }noneStore one row per tag in a join table.
autoCompletionbooleanfalseSuggest existing tags while typing. Requires destinationDb.

Plus the common options.

Options in detail

destinationDb and autoCompletion

Without a destination table, tags live in one text column and there's nothing to search for suggestions. With one, each tag is a row, and autoCompletion suggests tags already used by any item:

tagsField({
    name: 'tags',
    destinationDb: { table: 'post_tags', itemIdentifier: 'post_id', selectIdentifier: 'tag' },
    autoCompletion: true,
})
post_tags
post_id │ tag
────────┼───────────
 12     │ nextjs
 12     │ tutorial
 15     │ nextjs

autoCompletion without destinationDb is a TypeScript error and also throws at runtime.

validation

Checked per tag. Use it to keep tags consistent:

// Lowercase slugs only
validation: { regex: /^[a-z0-9]+(?:-[a-z0-9]+)*$/, message: 'Use lowercase words separated by hyphens.' }

// Hashtags
validation: { regex: /^#\w{2,}$/, message: 'Tags must start with # and have at least 2 characters.' }
Good to know:

Tags can't contain commas: the comma separates tags, both in the input and in the stored value.

Examples

SEO keywords (simple)

tagsField({ name: 'keywords', placeholder: 'Keywords for search engines' })
website/app/posts/[slug]/page.tsx
export async function generateMetadata({ params }) {
    const post = await getPost((await params).slug)
    return { keywords: post.keywords?.split(',') ?? [] }
}

Tag pages on your website

import { eq } from 'drizzle-orm'

// All posts tagged "nextjs"
const posts = await db
    .select({ post: PostsTable })
    .from(PostTagsTable)
    .innerJoin(PostsTable, eq(PostsTable.id, PostTagsTable.postId))
    .where(eq(PostTagsTable.tag, 'nextjs'))

// Tag cloud with counts
import { count, desc } from 'drizzle-orm'
const cloud = await db
    .select({ tag: PostTagsTable.tag, uses: count() })
    .from(PostTagsTable)
    .groupBy(PostTagsTable.tag)
    .orderBy(desc(count()))

Storage

ModeColumnValue
DefaultLONGTEXTComma-separated tags
destinationDbnoneOne row per tag