Skip to main content
OlonJS models relations between collections using $ref pointers stored on one side of the relationship. The inverse direction is computed at render time by filtering — nothing is stored twice. This keeps your source data free from duplication and eliminates the synchronisation bugs that arise when both sides of a relation maintain their own lists.

How relations work

The posts-to-tags relation is the canonical example. Each post in the posts collection stores its associated tags as an array of $ref pointers. Each pointer resolves to a single Tag object from the tags collection.
src/data/collections/posts/posts.json (excerpt)
The PostSchema tags field is typed to accept either a fully resolved Tag object or a raw CollectionPointer — a union that lets the schema validate data at both the authored stage (pointers) and the resolved stage (full objects):
src/collections/posts/schema.ts (tags field)
The post page resolves those $ref values to full Tag objects when rendering the related-tags section. The tag page, in turn, computes its own post list by filtering the entire posts collection for entries that carry the tag’s key — no tag ever maintains its own list of post IDs.

$ref pointer format

There are two forms of $ref pointer used in OlonJS.
A collection-level ref points to a whole JSON file and resolves to the full Record<string, Item> object. A section that receives the full collection can then filter, sort, or paginate the items as needed. An item-level ref appends a #/<key> fragment to address a single item within the collection record. The engine splits the path at #/ and uses the fragment as the lookup key. These are the pointers stored on the post side of the relation. Paths in both forms are relative to the file that contains the pointer.

Build-time resolution

OlonJS resolves all $ref pointers as part of the build process. Every pointer is replaced with the full value from the referenced collection file before the visitor site is deployed. The result is that visitors always receive fully resolved page bundles — no pointer resolution happens at request time for the production site. This makes page loads fast and eliminates any risk of a missing or broken pointer reaching a visitor. Studio works differently: it shows you the raw pointer values in the Inspector and resolves them live in the preview panel. This lets you see the current data while editing without waiting for a build cycle.

Inverse relations

The tags-to-posts direction illustrates the inverse pattern. Rather than storing a list of post IDs on each tag, the tag detail page uses the tag-posts section to compute the list at render time:
src/data/pages/tags/[slug].json (tag-posts section)
The tag-posts section receives the full posts collection and the current tag item. It filters the posts at render time, keeping only those whose tags array contains a pointer or resolved object that matches the current tag’s key. This means adding a tag to a post immediately makes that post appear on the tag’s page — no secondary update required anywhere in the tags data.
This is the one source of truth per relation principle: store $ref pointers on one side of the relation only (the post side, in this case). The other side (the tag) never stores a copy of the relationship data. When you need to read the relation in reverse, filter at render time. Storing the relation in both places creates two sources of truth that can drift out of sync.