> ## Documentation Index
> Fetch the complete documentation index at: https://docs.olonjs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Collections in OlonJS: typed and schema-validated registries

> Learn how OlonJS collections work: typed, schema-validated sets of entities like posts and tags that power dynamic pages and cross-collection relations.

Collections in OlonJS are typed registries of content entities. Each collection is defined by a Zod schema that enforces the shape of every item, and stored as a JSON record on disk. The framework ships with two built-in collections — `posts` and `tags` — but you can add as many as your project needs. Collections power dynamic pages, feed section data through `$ref` pointers, and are the foundation of cross-collection relations.

## What is a collection?

A collection is a `Record<string, Item>` where each value is a typed object that conforms to a Zod schema. Items are addressed by their key — the string that appears as the property name in the JSON file. That key also becomes the URL slug for the item's dynamic page.

```text title="Collection file structure" theme={null}
src/
  data/
    collections/
      posts/
        posts.json      ← Record<string, Post>
      tags/
        tags.json       ← Record<string, Tag>
  collections/
    posts/
      schema.ts         ← PostSchema (Zod)
      types.ts          ← TypeScript types derived from schema
      index.ts          ← re-exports
    tags/
      schema.ts         ← TagSchema (Zod)
      types.ts
      index.ts
```

Each key in the JSON record doubles as the item's `id`. The `designing-with-constraints` key in `posts.json` means the post is accessible at `/posts/designing-with-constraints`.

## Collection schema

The schema is the authoritative contract for every item in a collection. Here is the `PostSchema` from `src/collections/posts/schema.ts`:

```typescript title="src/collections/posts/schema.ts" theme={null}
import { z } from 'zod';
import { BaseCollectionItem, ImageSelectionSchema } from '@olonjs/core';
import { TagSchema } from '@/collections/tags/schema';

/** A raw $ref pointer as authored; resolved to the full target Tag at build and render time. */
const CollectionPointerSchema = z.object({
  $ref: z.string(),
});

export const PostSchema = BaseCollectionItem.extend({
  title: z.string().describe('ui:text'),
  excerpt: z.string().describe('ui:textarea'),
  body: z.string().describe('ui:textarea'),
  image: ImageSelectionSchema.optional(),
  date: z.string().describe('ui:text'),
  author: z.string().describe('ui:text'),
  readingTime: z.string().describe('ui:text'),
  // Relation posts -> tags via $ref pointers.
  // Authored as: { $ref: "../tags/tags.json#/<id>" }; resolved to full Tag objects at render time.
  // The inverse (tags -> posts) is computed at render time by filtering the posts collection.
  tags: z
    .array(z.union([TagSchema, CollectionPointerSchema]))
    .describe('ui:collection-ref:tags'),
});

export const PostsCollectionSchema = z.record(z.string(), PostSchema);
```

The fields on `PostSchema` are:

| Field | Type | Description |
| - | - | - |
| `id` | `string` | The item key from the JSON record — inherited from `BaseCollectionItem`. |
| `title` | `string` | Display title of the post. |
| `excerpt` | `string` | Short teaser shown in list views. |
| `body` | `string` | Full essay body in plain text or Markdown. |
| `image` | `ImageSelection` (optional) | Hero image with `url` and `alt` fields. |
| `date` | `string` | ISO 8601 publication date, for example `2026-06-28`. |
| `author` | `string` | Author display name. |
| `readingTime` | `string` | Human-readable estimate, for example `6 min`. |
| `tags` | `(Tag \| CollectionPointer)[]` | Relation to the tags collection via `$ref` pointers. Resolved to full `Tag` objects at render time. |

The `describe('ui:text')` calls are hints consumed by Studio's Inspector panel to render the correct input widget for each field.

## The CollectionRegistry

The `CollectionRegistry` is a single object that tells the OlonJS engine which collections exist and what Zod schema validates each one. Register every collection here so the engine can validate data, resolve `$ref` pointers, and power the Studio editor.

```typescript title="src/lib/CollectionRegistry.ts" theme={null}
import { PostsCollectionSchema } from '@/collections/posts';
import { TagsCollectionSchema } from '@/collections/tags';

export const CollectionRegistry = {
  posts: PostsCollectionSchema,
  tags: TagsCollectionSchema,
} as const;

export type CollectionType = keyof typeof CollectionRegistry;
```

The key (`posts`, `tags`) must match the `source` field in any `collection` block on a dynamic page (see [Pages](/concepts/pages#static-vs-dynamic-pages)) and the filename convention under `src/data/collections/`.

## Using collections in sections

Section `data` fields can reference collection data through `$ref` pointers instead of inlining content. The engine resolves the pointers before the section component receives its props.

There are two pointer forms:

**Collection-level ref** — resolves to the entire `Record<string, Item>` object. Use this when a section needs the full collection, such as a list component that will filter or paginate on the client.

**Item-level ref** — resolves to a single item by key. The fragment after `#/` is the item key.

```json title="Collection-level $ref in a section" theme={null}
{
  "id": "home-featured-posts",
  "type": "posts-list",
  "data": {
    "title": "Latest from the desk",
    "items": { "$ref": "../collections/posts/posts.json" }
  },
  "settings": { "paddingTop": "xl" }
}
```

```json title="Item-level $ref in a post's tags array" theme={null}
{
  "id": "designing-with-constraints",
  "title": "Designing with constraints, on purpose",
  "tags": [
    { "$ref": "../tags/tags.json#/design" },
    { "$ref": "../tags/tags.json#/process" }
  ]
}
```

The path in a `$ref` is relative to the file that contains it. From `src/data/collections/posts/posts.json`, `../tags/tags.json#/design` resolves to the `design` key in `src/data/collections/tags/tags.json`.

<Tip>
  OlonJS resolves all `$ref` pointers as part of the build process, so your production visitor site always receives fully resolved data — no pointer look-ups happen at request time. Studio shows the raw pointer values in the Inspector and resolves them live in the preview panel, so you always see current data while authoring without waiting for a deploy.
</Tip>

## Adding a new collection

Follow these steps to add a collection to your OlonJS project:

1. **Create the schema** — add `src/collections/my-collection/schema.ts` and export a Zod schema that extends `BaseCollectionItem`. Export a `z.record(z.string(), MyItemSchema)` as the collection schema.

2. **Add the data file** — create `src/data/collections/my-collection/my-collection.json` as a `Record<string, MyItem>` JSON object. Use the item key as the `id` field value.

3. **Register the collection** — open `src/lib/CollectionRegistry.ts` and add your new collection schema:

```typescript title="src/lib/CollectionRegistry.ts" theme={null}
import { MyCollectionSchema } from '@/collections/my-collection';

export const CollectionRegistry = {
  posts: PostsCollectionSchema,
  tags: TagsCollectionSchema,
  'my-collection': MyCollectionSchema, // add this line
} as const;
```

Your collection is now available for `$ref` pointers in page bundles and section data, and Studio's Inspector can display its items in relation pickers.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.