Content Tree
One TreeNode table holds nodes for every site plus the global scope. Every entry takes a place in the tree — there are no orphan rows.
What a node carries
siteId— null = global, otherwise the siteparentId— null = root of the scopeentryId— theContentEntryit points at (always present going forward; legacynodeType=folder, entryId=nullrows are migrated on first scaffold run)path— materialized path (/parent/child)depth— 0..9 (maximum tree depth is 10)sortOrder— siblings ordered by this columnallowedChildTypes[]— per-node override (TreeNode wins over the parent CT'sallowedChildTypeswhen set)
Folders are content types
There is no special "folder" concept. The starter kit ships a Folder content type with isRoutable=false, isPublishable=false. Folders are entries of that type. The "New" context menu shows every type the parent permits — including Folder.
Default scaffold
Every new site auto-creates:
🌐 Global
└── _Shared ← Folder entry, allowedChildTypes left open
🌍 {Site name}
├── Home ← LandingPage / ComposablePage / BasePage child
├── _Data ← Folder, scoped to non-routable publishable types
└── _Settings
└── Site Settings ← if a "siteSettings" CT exists
POST /api/v1/projects/{slug}/sites invokes both scaffoldGlobalContent and scaffoldSiteContent. Re-running is idempotent.
Routing
RouteIndex is the source of truth for delivery URL → entry resolution. Updated on publish / unpublish. Per-site, per-locale, per-path uniqueness.
Resolution order at delivery time:
- Hostname → site
- Path + locale → RouteIndex entry
- Entry's content type → renderer dispatch
See Routing for the full flow.
Multi-site sharing
Two patterns:
- Global folder — put a hero block, footer, etc. under
/_shared(siteId=null) and reference it from per-site entries. availableSiteIdson a content type — restricts where entries of that type can be created. Empty = everywhere.
Move + reorder
POST /api/v1/projects/{slug}/tree/nodes/{id}/move
{ "parentId": "node_id", "position": 2 }
The server updates path + depth for the moved node and every descendant, reorders siblings to honor position, and refreshes RouteIndex rows for any entry-bound descendant published in any locale. Cross-site / cross-environment moves aren't supported in V1 — duplicate + delete in the target scope instead.
Common pitfalls
- Cross-site move via the move endpoint. Moves are scoped to a single site.
- Creating a routable entry under a folder that doesn't allow it. The new-entry dialog filters the dropdown, but direct API calls must respect
allowedChildTypes. - Putting per-site config in
Global. Global content is shared across every site. Site Settings entries belong under the site's_Settingsfolder.