Skip to main content

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 site
  • parentId — null = root of the scope
  • entryId — the ContentEntry it points at (always present going forward; legacy nodeType=folder, entryId=null rows are migrated on first scaffold run)
  • path — materialized path (/parent/child)
  • depth — 0..9 (maximum tree depth is 10)
  • sortOrder — siblings ordered by this column
  • allowedChildTypes[] — per-node override (TreeNode wins over the parent CT's allowedChildTypes when 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:

  1. Hostname → site
  2. Path + locale → RouteIndex entry
  3. 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.
  • availableSiteIds on 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 _Settings folder.