Skip to content

Relationships

Studio writes this format for you. The content-type builder creates reference fields visually, and this page documents the schema they produce and how references resolve at build time.

A relationship links one content entry to another: a blog post to its author, a product to its tags. You declare it once, in the field's JSON Schema inside the content section of project.json . That single declaration drives validation, build-time resolution, and Studio's entry pickers.

The reference form

A reference field's schema points at the target content type with a #/content/<type> pointer:

{
  "content": {
    "authors": { "source": "./content/authors/", "format": "Markdown" },
    "blog": {
      "source": "./content/blog/",
      "format": "Markdown",
      "schema": {
        "type": "object",
        "properties": {
          "title": { "type": "string" },
          "author": { "$ref": "#/content/authors" }
        }
      }
    }
  }
}

Cardinality is expressed with plain JSON Schema; there is no separate relationship syntax:

Cardinality Field schema
To-one { "$ref": "#/content/authors" }
To-many { "type": "array", "items": { "$ref": "#/content/tags" } }

What entries store

Entries store the target's entry id : a string for to-one, an array of strings for to-many. For a Markdown collection the id is the entry's filename, so a post in content/blog/ referencing content/authors/jane-doe.md looks like:

---
title: My Post
author: jane-doe
tags: [intro, tooling]
---

Anything other than a string (or array of strings, for to-many) fails the field's validation.

Resolution

References resolve when content loads, at build and in the dev server and in Studio preview alike. The stored id is replaced in place with the full referenced entry, so by the time a page sees the data, author is no longer "jane-doe" but the whole author entry ( id , data , body). Templates walk straight through:

{ "tagName": "span", "textContent":  }

For a to-many field, each id in the array is replaced by its entry the same way.

An id that matches no entry in the target type is left untouched, and the raw string stays in the data. Guard templates accordingly, or keep ids and filenames in sync.

Beyond content

The same reference form generalizes: a pointer targets any project section whose extension declares it referenceable, as #/<sectionKey>/<name> . File-based content resolves at load time as described above; connection-backed table sections (from the connector extension) store the id in a column and resolve at request time instead. One direction is disallowed outright: a content schema cannot reference a table section, because content is loaded once at build time while table rows are live, so model that association from the table side.