Document Nesting With Payload's Nested Docs Plugin

The Payload CMS Nested Docs plugin allows you to organize and manage nested documents within your content structure. By using this plugin, you can create hierarchical relationships between different collections, making it easier to handle nested data models and maintain a clean content structure.
With the Nested Docs plugin you can easily link and manage content from different collections in a parent-child relationship, ensuring that your data is well-organized and accessible throughout the CMS.
In this article I'll covering the following:
- A Documentation Collection model using Nested Docs plugin
- Nesting categories using the Nested Docs plugin
- How to build a breadcrumbs path using the Nested Docs plugin
- How to query nested documents
- How to use Next.js with the Nested Docs plugin
Getting Started
If you would like to follow along, you can generate a Payload project with the following command.
npx create-payload-app
Installing the Payload Nested plugin
In order to add nested Collection support, the plugin needs to be installed.
pnpm add @payloadcms/plugin-nested-docs
The plugin needs to be added to the main payload.config.ts file. Later in the article, we'll add and configure the Collections.
/src/payload.config.ts
import { nestedDocsPlugin } from '@payloadcms/plugin-nested-docs'
import { buildConfig } from 'payload'
export default buildConfig({
plugins: [
nestedDocsPlugin({
}),
],
})
Building a Document Collection model
For this example I'll create a basic Documentation system which will have the following features:
- Document status: A select menu to set a single relationship to a document progress status indicator.
- Document tags: Create a Tags Collection to hold many tags that might be attached by documentation authors and editors.
- Nested categories: Uses the Payload
@payloadcms/plugin-nested-docs
plugin to automatically add nested docs support with breadcrumbs.
The Document Collection
Here's the documents Collection model for this example.
/src/payload/collections/Documents.ts
import type { CollectionConfig } from 'payload'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
export const Documents: CollectionConfig = {
slug: 'documents',
admin: {
useAsTitle: 'title',
},
fields: [
{
name: 'slug',
type: 'text',
admin: {
position: 'sidebar'
},
hooks: {
beforeChange: [
({value, operation, data}) => {
if (operation === 'create' || operation === 'update') {
if (data && !data?.slug) {
return data.title.trim().toLowerCase().replace(/[^a-zA-Z0-9]/g, ' ').replaceAll(' ', '-')
}
}
return value
},
],
},
},
{
name: 'title',
type: 'text',
required: true,
},
{
name: 'content',
type: 'richText',
required: true,
editor: lexicalEditor(),
},
{
name: 'documentStatus',
type: 'select',
interfaceName: 'DocumentStatus',
admin: {
position: 'sidebar',
},
hasMany: false,
options: [
{
label: 'On hold',
value: 'on-hold',
},
{
label: 'Rough draft',
value: 'rough-draft',
},
{
label: 'In progress',
value: 'in-progress',
},
{
label: 'Read for review',
value: 'ready-for-review',
},
{
label: 'Approved',
value: 'approved',
},
],
},
{
name: 'tags',
type: 'relationship',
hasMany: true,
relationTo: 'tags',
},
{
name: 'categories',
type: 'relationship',
admin: {
position: 'sidebar',
},
hasMany: false,
relationTo: 'categories',
},
]
}
Analyzing the Documents Collection
The Documents Collection defines the following fields:
- slug: A slug path that will be auto-generated using a simple slug optimization function if a value is not provided by the user.
- title: Document page title.
- content: Rich text editor with configurable inline block editing support.
- documentStatus: Adds a select field type menu to set a single status option by setting hasMany to false.
- catgories: Adds a category select field but provides nested category support with the use of the Payload plugin.
- tags: Adds a relationship field type to add many tags to a document.
The configuration for the tags only has one field:
- title: Holds the title for the tag.
/src/payload/collections/Tag.ts
import type { CollectionConfig } from 'payload'
export const Tags: CollectionConfig = {
slug: 'tags',
admin: {
useAsTitle: 'title'
},
fields: [
{
name: 'title',
type: 'text',
required: true,
}
],
}
The Categories Collection
W can easily add nested categories support using the Payload plugin @payloadcms/plugin-nested-docs
.
Why use nested Categories
In a documentation scenario, you may need to have certain features like:
- Breadcrumbs trail for navigation
- Categories have subcategories if needed.
- Helps organize topics for searching by category.
Create the Categories Collection
Here's all that's needed for the categories model since the plugin will automatically add some fields to the Collection.
/src/payload/collections/Categories.ts
import type { CollectionConfig } from 'payload'
export const Categories: CollectionConfig = {
slug: 'categories',
admin: {
useAsTitle: 'title',
},
fields: [
{
name: 'title',
type: 'text',
required: true,
},
],
}
Categories with Nested Docs Support
The categories configuration has just one field title but the plugin will automatically add the following fields to the categories Collection:
- title: Holds the title for the category.
- parent: (Added by plugin) Self-reference to categories Collection to reference a parent hierarchy. If the parent is null or not set, then it is considered a root category.
- breadcrumbs: (Added by plugin) A chain of categories all the way up to the first parent that is null or not set.
Configuring the Nested Docs Plugin
Now that we have all of the Collections ready, let's add them to the collections array in the main payload.config.ts file.
Let's also add the Payload Nested Docs configuration details so the categories Collection can be automatically managed.
/src/payload.config.ts
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import { nestedDocsPlugin } from '@payloadcms/plugin-nested-docs'
import { buildConfig } from 'payload'
import { Documents } from './payload/collections/Documents'
import { Categories } from './payload/collections/Categories'
import { Tags } from './payload/collections/Tags'
export default buildConfig({
collections: [Documents, Categories, Tags],
editor: lexicalEditor(),
plugins: [
nestedDocsPlugin({
collections: ['categories'],
generateLabel: (_, doc) => doc.title as string,
generateURL: (docs, currentDoc) => {
return `/${currentDoc.id}`
},
}),
],
})
Here's what the plugin is configured to do:
- collections: It will add nested support to any Collection slug listed in this array.
- generateLabel: A function to customize the category label.
- generateURL: A function that will customize the breadcrumb item's url.
If each breadcrumb needs the full category url path, then the following generateURL could be use instead.
Creates a full category path to parent
plugins: [
nestedDocsPlugin({
generateURL: (docs) =>
docs.reduce((url, doc) => `${url}/${doc.slug}`, '')
}),
],
Running the Documentation System
Now that all the changes are in place, start up the dev server so the changes can take affect.
The Collections that were created can now be viewed in the Admin UI.
http://localhost:3000/admin/documents
http://localhost:3000/admin/catgories
http://localhost:3000/admin/tags
Admin UI for Documents
The documents Collection will generate the following Admin UI for managing a document's content:
The documentStatus field will populate the options provided in the configuration to set a documents status.
The documentStatus select field will actually create a one-to-one mapping with another table using a foreign key database reference.
So, even though it looks like a regular HTML dropdown select menu, there is a good amount of database mapping that Payload simplifies behind the scenes.
The select field type is used for select one (one-to-one) or select many (one-to-many) menu.
However, for a many-to-many type relationship, you'll want to use a relationship field, which is why tags has its own Collection.
The Tags configuration will generate the following Admin UI to list and edit tags.
Using the useAsTitle field lets the relationship select field know what value to show as the label.
Admin useAsTitle for select and list
admin: {
useAsTitle: 'title'
}
The select and relationship field types will use the id value as a label by default. This might be confusing to users, so it's better to indicate which field to use for a label with useAsTitle admin setting.
One of the great features of the select field type, is that it provides a way to create new items on the fly, using the "+" icon.
Since tags is a separate Collection, each document can have many tags.
Admin UI for Categories
The categories configuration generates the following Admin UI pages for editing categories.
Adding Nested Categories
All categories can now have a parent category.
- A category is a root category, if its parent is null.
- A category is subcategory, if its parent is another category.
- The breadcrumbs array will contain the current category all of its parents up to the first null value.
The breadcrumbs array field is readonly because the plugin will keep the paths in sync as they change over time.
If you've ever tried to maintain a breadcrumbs path, this plugin will make the job so much easier.
This plugin is not limited to simple category nesting but any Collection can use the parent/child relationship if needed.
Document query response with REST
To fetch from the documents Collection, simply use the REST API as follows.
http://localhost:3000/api/documents
http://localhost:3000/api/documents/2
Here's the documents query response using Payload's REST API endpoints.
As you can see the response returns the relationship data as well, including:
- documentStatus
- tags
- categories - which includes the breadcrumbs
- content: Serialized JSON rich text content.
The rich text and dates are omitted since it's quite lengthy.
Response with rich text content and dates omitted
{
"id": 2,
"slug": "all-about-payload-collections",
"title": "All About Payload Collections",
"content": {},
"documentStatus": "ready-for-review",
"tags": [
{
"id": 4,
"title": "Media"
},
{
"id": 2,
"title": "Collections"
},
{
"id": 6,
"title": "GraphQL"
}
],
"categories": {
"id": 3,
"title": "Collections",
"parent": 2,
"breadcrumbs": [
{
"id": "67d905d018fc884ea8d9beea",
"doc": 1,
"url": "/1",
"label": "Development"
},
{
"id": "67d905d018fc884ea8d9beeb",
"doc": 2,
"url": "/2",
"label": "Payload"
},
{
"id": "67d905d018fc884ea8d9beec",
"doc": 3,
"url": "/3",
"label": "Collections"
}
]
}
}
Document breadcrums example
Here's an example of how to use the category breadcrumbs to build a breadcrumbs path in a Next.js page.
The Next.js page looks for a category id path param named cid, which then does a fetch to get all the documents related to a specific category.
/src/app/(frontend)/documents/[cid]/page.tsx
import React from 'react'
import { type Document as DocumentType } from '@payload-types'
import { PaginatedDocs } from 'payload'
type Args = {
params: Promise<{
cid?: string
}>
}
export default async function Page({ params: paramsPromise }: Args) {
const { cid } = await paramsPromise
const res = await fetch(`http://localhost:3000/api/documents?[where][categories.id]=${cid}`, {
method: 'GET',
})
const data: PaginatedDocs<DocumentType> = await res.json()
const document = data?.docs?.[0]
const { categories } = document
const breadcrumbs = typeof categories === 'object' ? categories?.breadcrumbs : []
return (
<nav className="flex" aria-label="Breadcrumb">
<ol className="inline-flex items-center">
{breadcrumbs?.map((crumb) => {
return (
<li className="inline-flex items-center">
<svg
className="w-6 h-6 text-gray-400"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"
></path>
</svg>
<a href={`/documents${crumb ? (crumb?.url as string) : ''}`}>{crumb.label}</a>
</li>
)
})}
</ol>
</nav>
)
}
In Conclusion
So by now we've seen how to use Payload's Nested Docs Plugin in a practical way.
Nested Collections can be extremely powerful and can take the complexity out of building it by hand.
The Nested Docs Plugin is not limited to simple categories and can be used for very complex Collection models.
Hope this article helps.