Nodes & types

Everything in a space is a node: a place, a review, an image, a menu item, a route stop. A node is a JSON record with a Schema.org @type, a stable identity, and whatever properties the content needs. There are no tables, no per-node config files, no companion sidecars — one node, one record.

The Bare Four

Four fields identify any node. We call them the Bare Four, and they’re the only fixed structure:

{
  "@type":  "Place",
  "@id":    "ctxr:a1b2c3",
  "@path":  "fikkie",
  "@build": "pages/place"
}
Field Means Mutable?
@id Stable identity, assigned at creation No — fixed forever
@type What it is, in Schema.org terms Rarely
@path Its URL, if it has one Yes
@build The template that renders it Yes

@id is the anchor: references point to it, the registry tracks it, the API addresses it. @path and name are mutable labels on top of that identity. A node is routable only when it has a @path — no path, no URL. (Note it’s @build, never @template.)

Everything else on a node is free. Need a new property? Add it to the JSON. Need a new type? Use it; the platform doesn’t need a schema definition to accept it.

Routable pages and embedded blocks

@build decides how a node renders, and that splits nodes into two families. A node with a @path is a page: it gets a URL and a pages/* template. A node without a path is embedded: it has no URL of its own and renders through a blocks/* or menus/* template wherever a page pulls it in. The same Place can be a full page at /fikkie or a card embedded in a list — the path is the only thing that decides.

Some types are inherently one or the other. A SiteNavigationElement is always a menu, a WebPageElement is always a block — they never get a URL, no matter what. The path determines routability; the type sometimes fixes the answer in advance.

What the platform adds

A node carries more than what you write into it. Two kinds of field are managed for you:

Automatic dates. dateCreated and dateModified are derived by the platform, not set by hand — created once, updated on every save. They’re always present and always accurate, so you can sort and display them without maintaining them.

Runtime keys. When a template receives a node, the platform may inject _-prefixed keys — _isEditor, _hasDraft, _diff and the like. These are ephemeral: they describe the current request, never live on disk, and are stripped from public output. You read them; you never store them.

Bridging to external systems

When content also lives in another system — a CRM, a booking tool, a legacy database — the Schema.org identifier property links the two without polluting the node’s identity:

{
  "@type": "Place",
  "name": "Fikkie",
  "identifier": [
    { "@type": "PropertyValue", "propertyID": "crm", "value": "1234" }
  ]
}

The platform keeps a reverse index of these, so an integration can look a node up by its external id rather than its @id. Your @id stays stable and internal; the identifier is the bridge outward.

Types are a vocabulary, not a schema

CTXR uses Schema.org types and properties directly rather than inventing a data model. A Place has geo, image, containedInPlace. A Review has reviewBody, reviewRating, itemReviewed. These aren’t names we chose — they’re a shared vocabulary that search engines and AI systems already understand, which is why a node is valid structured data the moment it’s written.

It helps to sort types into four families by the role they play:

Structural types describe the shape of a page — the composition skeleton. WebPage is a page, SiteNavigationElement is a menu, WebPageElement is a block, ItemList/ListItem are lists. They carry the scaffolding.

Content types describe the subject. Place, Event, Review, Person — curated Schema.org collections that carry the meaning.

Media types are the MediaObject family: ImageObject, VideoObject, AudioObject, 3DModel. Their distinguishing trait is that they’re bound to a file.

Custom is Thing, the root of the vocabulary — the exit for anything no standard type fits.

A typical page is a structural type curating content and media: a WebPage whose mainEntity is an ItemList of Place nodes, each with an ImageObject.

Media items are nodes

A media item isn’t an embedded blob hidden inside a page — it’s a node, like any other. Upload an image and you get an ImageObject; upload a video and you get a VideoObject. The shared metadata lives on that node: the file (url / contentUrl), plus name, description, caption, credit.

Because media are nodes, they get everything nodes get. They have identity (an @id), so they can be referenced and reused across many pages without copying the file. They run the same lifecycle. And they carry their own SEO- and AI-readable metadata, which travels with the file rather than being trapped in one page’s markup. How a page places a media node — with its own focus point and alt text — is covered in Template helpers.

Custom properties and the Thing fallback

Real content rarely fits a standard perfectly. Two escape hatches keep you unblocked:

{
  "@type": "Place",
  "name": "Fikkie",
  "geo": { "@type": "GeoCoordinates", "latitude": "51.92" },
  "x-trailDifficulty": "moderate"        // custom property, alongside the standard
}

Add your own properties next to the Schema.org ones — they round-trip through save, compile, and the API like any other field. And when no standard type fits at all, fall back to Thing, the root of the whole vocabulary. Schema is the ambition; Thing is the uncluttered exit. You’re never stuck because the standard didn’t anticipate your case.

Why the type matters for editing

The type isn’t decoration — it’s what lets a form edit a node safely. Because the type names a known vocabulary, the platform knows that geo wants two coordinates, that image is an ImageObject, that reviewRating is a number in a range. A form built from the type can offer the right field for each property and refuse the ones that would break the node — without you writing per-field validation. The type tells the editor what’s editable and how to show it.

The four identity fields are immutable; the type is a contract; everything else is open. That’s the whole data model — and it’s what makes the graph possible.