7.2 KiB
| title | aliases | tags | sources | created | updated | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Hierarchy — Tree Structure for Collections |
|
|
|
2026-05-15 | 2026-05-15 |
Overview
The Hierarchy feature adds automatic tree structure management to any Payload collection. Enable it with a single hierarchy property — Payload handles parent-child relationships, circular reference protection, and on-demand breadcrumb path generation.
Use for: Nested pages, category trees, org charts, folder systems, blog posts under pages.
Quick Start
export const Pages: CollectionConfig = {
slug: 'pages',
admin: { useAsTitle: 'title' },
fields: [{ name: 'title', type: 'text', required: true }],
hierarchy: {
parentFieldName: 'parent', // auto-created if field doesn't exist
},
}
Auto-generated by enabling hierarchy:
parentrelationship field (self-referential)- Virtual
_h_slugPath— slugified URL path, computed on-demand - Virtual
_h_titlePath— human-readable breadcrumb path, computed on-demand - Hooks: circular reference validation, orphan cleanup on deletion
Auto-Generated Virtual Fields
Both fields are not stored in DB — computed by walking the parent chain on read.
| Field | Type | Example value |
|---|---|---|
_h_slugPath |
string or localized object |
'store/products/widgets' |
_h_titlePath |
string or localized object |
'Store/Products/Widgets' |
If admin.useAsTitle field is localized: true, both path fields are automatically localized too.
Requesting Path Computation
Paths are opt-in (prevents unnecessary DB queries on relationship loads).
Method 1 — Context flag (Local API):
const page = await payload.findByID({
collection: 'pages',
id: 'page-id',
context: { computeHierarchyPaths: true },
})
// page._h_slugPath → 'store/products/widgets'
Method 2 — Query param (REST API):
GET /api/pages/abc123?computeHierarchyPaths=true
Method 3 — Explicit select:
const page = await payload.findByID({
collection: 'pages',
id: 'page-id',
select: { title: true, _h_slugPath: true, _h_titlePath: true },
})
Performance
- Each document needing paths = 1 extra query for ancestors
- Request-scoped caching: shared ancestors fetched only once
- 50 sibling docs → only 2 queries total (1 for docs, 1 for shared parent)
- Avoid
computeHierarchyPathswhen populating relationships where paths aren't needed
Config Options
| Option | Type | Required | Description |
|---|---|---|---|
parentFieldName |
string |
Yes | Field name for parent relationship (auto-created if missing) |
relationTo |
string | string[] |
No | Collection(s) allowed as parents. Default: self-referential |
slugify |
function |
No | Custom slug function for path generation |
slugPathFieldName |
string |
No | Override virtual field name (default: _h_slugPath) |
titlePathFieldName |
string |
No | Override virtual field name (default: _h_titlePath) |
Custom Parent Field
If you define the field manually, it must be:
type: 'relationship'relationTo: 'same-collection-slug'(self-referential)hasMany: false
The relationTo in the manual field and in hierarchy config must match exactly.
Polymorphic Hierarchies
Allow documents to have parents from different collections:
// Posts can nest under pages OR other posts
export const Posts: CollectionConfig = {
slug: 'posts',
hierarchy: {
parentFieldName: 'parent',
relationTo: ['pages', 'posts'],
},
}
Creating cross-collection parent:
const post = await payload.create({
collection: 'posts',
data: {
title: 'First Post',
parent: { relationTo: 'pages', value: blogPage.id },
},
})
Common polymorphic patterns:
- Blog posts under pages, threaded replies under posts
- Products under categories, variants under products
- Folders + documents nested under pages or other folders
Important Behaviors
No Cascade Updates
Moving or renaming a document does not update descendants — only the parent field is changed. Paths always reflect current hierarchy because they're computed fresh on read.
// Move folder with 50 descendants — still fast, no cascade
await payload.update({ collection: 'pages', id: 'folder-id', data: { parent: 'new-parent' } })
// Descendants: unchanged. Paths: accurate on next read.
Circular Reference Protection
Automatic detection across collections. Setting a descendant as a parent throws before any DB change.
Draft Support
Path computation respects the draft context:
- Reading draft → ancestor titles from draft versions
- Reading published → ancestor titles from published versions
Locale Limitations
locale: 'all'skips hierarchy processing — update each locale individuallyparentfield is not localized: tree structure is the same across all localespublishSpecificLocalechanges parent for all locales (since parent is not localized)
Use Cases
// Nested pages
hierarchy: { parentFieldName: 'parent' }
// Nested categories
hierarchy: { parentFieldName: 'parentCategory' }
// Org chart with custom path field names
hierarchy: {
parentFieldName: 'parentDept',
slugPathFieldName: '_orgPath',
titlePathFieldName: '_orgBreadcrumb',
}
TypeScript
Virtual fields are included in generated types but are undefined unless paths are requested:
const page = await payload.findByID({
collection: 'pages',
id: 'page-id',
context: { computeHierarchyPaths: true },
})
const url: string = `/${page._h_slugPath}` // 'store/products/widget'
const crumbs = page._h_titlePath.split('/') // ['Store', 'Products', 'Widget']
Regenerate types after adding hierarchy:
payload generate:types
Key Takeaways
- Enable hierarchy with one config property — no custom code needed
- Virtual path fields (
_h_slugPath,_h_titlePath) are computed on-demand, not stored - Always opt-in with
computeHierarchyPaths: true— avoids unnecessary queries on relationship loads - No cascade updates: moving/renaming is O(1) regardless of descendants
- Polymorphic:
relationTo: ['pages', 'posts']lets documents parent across collections - Circular references caught automatically before any DB change
parentfield is never localized — tree structure is global, only path labels vary per locale- Deep trees (10+ levels) slow down path computation — keep trees shallow when possible
Related
- wiki/payloadcms/folders — folder grouping (beta), different from hierarchy
- wiki/payloadcms/fields-relationship — underlying field type used for
parent - wiki/payloadcms/fields-join — virtual reverse lookups (complements hierarchy)
- wiki/payloadcms/collection-config — full collection-level config reference
- wiki/payloadcms/localization — how localized titles feed into path computation
Sources
raw/hierarchy__overview.md— https://payloadcms.com/docs/hierarchy/overview