Tree and paths
The doc_pages table is an adjacency list. Each row points at its parent through parent_id and stores its own materialised path, so the sidebar and routing read one column without walking the tree.
Columns
| Column | Type | Notes |
|---|---|---|
parent_id |
foreign doc_pages, nullable |
cascadeOnDelete — removing a doc drops its subtree |
user_id |
foreign users |
author; restrictOnDelete |
slug |
string | URL fragment, unique within the parent |
path |
string, unique | full route segment, rebuilt on save |
position |
unsigned int | sibling order, defaults to 0 |
body |
longText | Markdown content |
published_at |
timestamp, nullable | null or future = draft |
Path building
Doc::buildPath returns parent->path . '/' . slug when a parent exists, otherwise just the slug. The saving hook writes that result into path on every write, so the column is never stale.
When path, slug, or parent_id changed during a save, the saved hook re-saves each child. Each child save re-fires saving → buildPath → saved, so the rebuild cascades down the subtree in one pass. The same hook calls DocOutline::flushCache() so the public sidebar picks up the new paths immediately.
Position and nesting
Siblings are ordered by position then id (see Doc::children and DocOutline::fetchDocs). DocOutline::nest($doc) reparents a doc under its previous sibling and parks it at the end of that sibling's children; unnest($doc) lifts it back to its parent's level, slotted one position after the parent. moveUp / moveDown swap positions with the previous or next sibling without touching parent_id.
Outline query
DocOutline::tree() loads the whole outline in a single query that selects only id, parent_id, title, slug, path, position, published_at — body is excluded because it is the large column. The published set is cached under filament-docs:outline:v2 and busted by every create, save, delete, and restore event on Doc. The cache is opt-in (DOCS_OUTLINE_CACHE env) and off by default on Magic Containers.
Example
A root guide with a child install and a grandchild requirements:
guide→path = guideinstall→path = guide/installrequirements→path = guide/install/requirements
graph LR
A["guide<br/>path: guide"] --> B["install<br/>path: guide/install"]
B --> C["requirements<br/>path: guide/install/requirements"]
S["Doc saved<br/>(path/slug/parent changed)"] --> R["buildPath rewrites path"]
R --> K["saved hook re-saves children"]
K --> F["DocOutline::flushCache"]