How to create a generic automatically generating Slug field for PayloadCMS
When writing content in PayloadCMS, a "slug" field is often necessary for creating user-friendly, SEO-optimized URLs. In this guide, I'll walk through creating a generic slug field implementation, using reusable components to keep your codebase clean and scalable. While this implementation is not perfect, it is a perfect start to learn how to create advanced fields in Payload.
Note: This is developed in Payload 3.0, as payload is still being actively developed changes to Payload may cause this to no longer work.
Overview of the Implementation
- A generic
slugField
function: This function defines the schema for the slug field and integrates validation and the admin interface configuration, you can reuse this field anywhere you want. - A
SlugInput
React component: This component provides an admin UI for the slug field, auto-generating the slug based on a tracking field (e.g., a title).
Here's how it looks
Step 1: Define the Slug Field Schema (slug.ts
)
Here's how to define a reusable slug field using PayloadCMS:
import { deepMerge, type Field } from 'payload';
type Slug = (options?: { trackingField?: string }, overrides?: Partial<Field>) => Field;
const slugField: Slug = ({ trackingField = 'title' } = {}, overrides = {}) =>
deepMerge(
{
label: 'Slug',
name: 'slug',
type: 'text',
unique: true,
index: true,
required: true,
admin: {
position: 'sidebar',
components: {
Field: {
path: 'slug.tsx',
exportName: 'SlugInput',
clientProps: {
trackingField,
},
},
},
},
validate: (value: string) => {
const slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
if (slugRegex.test(value)) {
return true;
}
return 'Invalid slug. Must be kebab-case (lowercase, words separated by hyphens)';
},
},
overrides,
);
export { slugField };
Key Features of slugField
- Customization: The
trackingField
option allows you to specify which field (e.g.,title
) the slug is derived from. - Validation: Ensures the slug is in kebab-case format.
Step 2: Create the Slug Input Component (slug.tsx
)
The SlugInput
component provides the user interface for managing the slug field. It automatically generates a slug based on the tracking field but allows manual overrides.
'use client';
import React, { type ChangeEvent, type ReactElement, useEffect, useRef } from 'react';
import { TextInput, useField } from '@payloadcms/ui';
import { toKebabCase } from 'payload/shared';
export interface SlugInputProps {
trackingField: string;
}
function SlugInput(props: SlugInputProps): ReactElement {
const { trackingField } = props;
const { value: slugValue = '', setValue: setSlugValue } = useField<string>({
path: 'slug',
});
const { value: trackingFieldValue } = useField<string>({
path: trackingField,
});
const prevTrackingFieldValueRef = useRef(trackingFieldValue);
const stopTrackingRef = useRef(false);
useEffect(() => {
if (!trackingField || stopTrackingRef.current) {
return;
}
if (trackingFieldValue === prevTrackingFieldValueRef.current) {
return;
}
const prevSlugValue = toKebabCase(prevTrackingFieldValueRef.current || '') as string;
prevTrackingFieldValueRef.current = trackingFieldValue;
if (prevSlugValue !== slugValue) {
return;
}
setSlugValue(toKebabCase(trackingFieldValue));
}, [setSlugValue, slugValue, trackingField, trackingFieldValue]);
return (
<div>
<TextInput
path="slug"
label="Slug"
readOnly
description={
slugValue
? `Auto generated based on ${trackingField}`
: `Will be auto-generated from ${trackingField} when saved`
}
value={slugValue}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
setSlugValue(e.target.value);
stopTrackingRef.current = true;
}}
/>
</div>
);
}
export { SlugInput };
Key Features of SlugInput
- Auto-Generation: Listens to the tracking field and auto-generates the slug in real time.
- Read-only: Personally I don't need to change the slug, so I've marked my input as Read-only, if you do wish to change slugs after generating, simply remove this attribute.
- Real-Time Updates: Updates the slug only when the tracking field changes.
Step 3: Use the Slug Field in Your Collections
To add the slug field to a collection, use the `slugField` function in the schema definition:
import { CollectionConfig } from 'payload/types';
import { slugField } from './fields/slug';
const Posts: CollectionConfig = {
slug: 'posts',
fields: [
{
name: 'title',
type: 'text',
required: true,
},
slugField({ trackingField: 'title' }),
],
};
export default Posts;
Additional improvements you can make.
Since this is a simple React component, you can imagine that this pattern can be applied in various ways, not just to create a slug field.
For instance, we can enhance this component by allowing content editors to optionally edit the field if they wish. Let's add this feature now. Please note that I have removed all other code that is not necessary for this functionality to work.
function SlugInput(props: SlugInputProps): ReactElement {
const [isEditable, setIsEditable] = useState(false)
return (
<TextInput
readOnly={!isEditable}
/>
<Button
buttonStyle="pill"
onClick={() => {
setIsEditable(!isEditable)
}}
>
{isEditable ? 'Cancel' : 'Edit'}
</Button>
)
}
Conclusion
By combining a reusable slugField
function with a custom SlugInput
component, you can streamline slug management in PayloadCMS. This implementation ensures a clean, consistent user experience while keeping your codebase modular and maintainable.