Custom slugs
Replace the folderPath/pageSegment rule with your own buildSlug function — flat slugs, random IDs or a shallow hierarchy.
By default a slug is folderPath/pageSegment, for example appeals/2024/spring/my-page. The buildSlug option replaces that rule with any function you write.
Use it for:
- Flat slugs — use the tree to organise content without affecting URLs.
- Random IDs — generated slugs, with the update limitations below.
- Custom patterns — slug schemes that depend on the collection, locale or other document data.
Signature
type BuildSlugFn = (args: {
folderPath: string // e.g. "appeals/2024"; '' when the page has no folder
pageSegment: string // The page's own URL segment
doc: Record<string, unknown> // Incoming data, which may be a partial update
operation: 'create' | 'update'
}) => string | Promise<string>BuildSlugFn and BuildSlugArgs are exported from @delmaredigital/payload-page-tree. The function runs whenever the plugin would otherwise build a slug, and the tree's segment availability checks use it too.
Examples
Ignore the folder hierarchy. The URL is just the page segment, so reorganising the tree never changes a URL.
pageTreePlugin({
buildSlug: ({ pageSegment }) => pageSegment,
})A page with segment my-page in the folder appeals/2024 gets the slug my-page instead of appeals/2024/my-page.
Generate a random slug when the incoming data has none. This example does not guarantee a permanent URL.
npm i nanoidimport { nanoid } from 'nanoid'
import { pageTreePlugin } from '@delmaredigital/payload-page-tree'
pageTreePlugin({
buildSlug: ({ doc, operation }) => {
// Preserve a slug only when the update explicitly includes one
if (operation === 'update' && typeof doc.slug === 'string' && doc.slug) return doc.slug
return nanoid(21)
},
})doc is the incoming update data, not the stored document. Moves with Update URLs, Regenerate URLs and folder cascades can pass no doc.slug, causing this callback to generate a new ID. For a stable random URL, persist the ID separately and ensure your callback receives it on every slug rebuild; this example alone does not do that.
Use only the immediate parent folder, not the full path, so URLs never get deeper than one folder.
pageTreePlugin({
buildSlug: ({ folderPath, pageSegment }) => {
if (!folderPath) return pageSegment
// Take only the last folder segment
const lastFolder = folderPath.split('/').pop()
return `${lastFolder}/${pageSegment}`
},
})Errors fall back to the default
If buildSlug throws or returns an empty value, the plugin uses the default folderPath/pageSegment slug instead. A throw is logged as [payload-page-tree] buildSlug callback error, falling back to default.
Optional and additive
Configurations without buildSlug behave exactly as before. URL history, cascading updates and redirects all keep working with custom slugs.
URL history and redirects
Every page keeps its last 20 URLs. Restore any of them from the tree, or turn the history into 301 redirects for your frontend.
Plugin order
Register pageTreePlugin after any plugin that creates the collections it manages, such as payload-puck with a generated Pages collection.

