Why a graph, not a database

Most content platforms store data in relational tables — a row for each place, a column for each field. That works until the schema changes, until you need a field that doesn’t fit the table, or until two content types need to relate in a way the database wasn’t designed for. Then you migrate, you add a join table, you write the glue.

CTXR avoids that whole category of problem by choosing a different data structure. Every piece of content is a node: a JSON record with a Schema.org @type. A Place, a Review, an ImageObject, a TouristTrip. The type describes what the node is, not which table it belongs to.

Schema.org as the vocabulary

Instead of inventing a custom data model, CTXR uses Schema.org types and properties directly. A Place has geo, containedInPlace, image. A Review has reviewBody, reviewRating, itemReviewed. These aren’t arbitrary field names — they’re a shared vocabulary that search engines, AI systems, and other platforms already understand.

This is a deliberate trade. We give up the freedom to name things however we like, and in return our content is meaningful beyond our own application. A node is valid structured data the moment it’s written. Nobody needs our documentation to work out that geo.latitude is a latitude.

Where real-world content doesn’t fit the vocabulary, you can add your own properties alongside the standard ones, or fall back to the most general type, Thing. Schema is the ambition; the fallback is the escape hatch. You’re never blocked because the standard didn’t anticipate your case.

Nodes, not rows

A node is self-contained. It carries its own type, its own identity, and its own data — all in one record. There’s no join, no foreign key, no migration. Need a new property? Add it to the JSON. Need a new type? Introduce it; the platform doesn’t need a schema definition to accept it.

Each node has four fixed identity fields — we call them the Bare Four:

{
  "@type": "Place",
  "@id":   "ctxr:a1b2c3",
  "@path": "fikkie",
  "@build": "pages/place"
}

@type says what it is, @id is its stable address, @path is its URL (if it has one), and @build names the template that renders it. Everything else on the node is free. Four conventions are enough to identify anything; fewer rules means less to remember, test, and break.

References instead of joins

Nodes reference each other through @id stubs. A Review points to the Place it reviews. A page curates a list of Places. A reference is just {"@id": "ctxr:a1b2c3"} — a pointer, nothing more.

A Review                          A Place
{                                 {
  "@type": "Review",                "@type": "Place",
  "reviewBody": "...",      ──▶      "@id": "ctxr:a1b2c3",
  "itemReviewed":                    "name": "Fikkie"
    {"@id": "ctxr:a1b2c3"}         }
}

This is the graph: nodes connected by pointers. What makes it fast and simple to serve is when those pointers get followed — which is the subject of the next page.

Why this matters

The data structure dictates everything downstream. Because nodes are typed JSON files with Schema.org semantics, the platform can generate structured data automatically, expose a query API without a database, compile a graph that serves instantly, and let templates read content without knowing the schema in advance.

It isn’t about being clever with files. It’s about choosing a data structure that eliminates problems — schema migrations, runtime joins, cache invalidation — rather than spending the rest of the project solving them.