> ## 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.

# Define, register, and use content collections in OlonJS

> Step-by-step guide to defining a collection in OlonJS: create a Zod schema, add collection data, register it, and use it in sections.

Collections let you manage any set of typed content entities — posts, products, team members, or any other structured domain object — as JSON records. Each record is validated against a Zod schema, visible in Studio, and queryable across related collections. This guide walks you through creating a collection from scratch.

<Steps>
  ### Define the schema

  Create `src/collections/my-collection/schema.ts` and define your schema by extending `BaseCollectionItem` from `@olonjs/core`. Every field you want to appear in Studio needs a `.describe()` annotation — see the [Field descriptor annotations](#field-descriptor-annotations) section below for the full list.

  ```typescript src/collections/my-collection/schema.ts theme={null}
  import { z } from 'zod';
  import { BaseCollectionItem } from '@olonjs/core';

  export const MyItemSchema = BaseCollectionItem.extend({
    title: z.string().describe('ui:text'),
    summary: z.string().describe('ui:textarea'),
    category: z.enum(['news', 'update', 'release']).describe('ui:select'),
    order: z.number().optional().describe('ui:number'),
    highlights: z.array(z.string()).optional().describe('ui:list'),
  });

  export const MyCollectionSchema = z.record(z.string(), MyItemSchema);
  ```

  `BaseCollectionItem` contributes the `id` field that OlonJS uses as the record key. You must **not** redefine `id` in your own `.extend()` call.

  Here is the real `PostSchema` from the Inkwell Journal demo as a richer reference:

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

  /** Authored collection pointer; bake/runtime resolves to the target Tag. */
  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.
    // Studio renders this as a multi-select dropdown for the tags collection.
    tags: z.array(z.union([TagSchema, CollectionPointerSchema])).describe('ui:collection-ref:tags'),
  });

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

  ### Export types

  Create a `types.ts` file that derives TypeScript types from your schema, and an `index.ts` barrel that re-exports everything.

  ```typescript src/collections/my-collection/types.ts theme={null}
  import { z } from 'zod';
  import { MyItemSchema, MyCollectionSchema } from './schema';

  export type MyItem = z.infer<typeof MyItemSchema>;
  export type MyCollection = z.infer<typeof MyCollectionSchema>;
  ```

  ```typescript src/collections/my-collection/index.ts theme={null}
  export { MyItemSchema, MyCollectionSchema } from './schema';
  export type { MyItem, MyCollection } from './types';
  ```

  ### Add collection data

  Create the JSON data file at `src/data/collections/my-collection/my-collection.json`. The file must be a `Record<string, Item>` where each key matches the `id` field of its entry.

  ```json src/data/collections/my-collection/my-collection.json theme={null}
  {
    "first-entry": {
      "id": "first-entry",
      "title": "My first entry",
      "summary": "A short description of this entry.",
      "category": "news",
      "order": 1,
      "highlights": ["Fast", "Composable", "Schema-driven"]
    },
    "second-entry": {
      "id": "second-entry",
      "title": "My second entry",
      "summary": "Another entry in the collection.",
      "category": "update",
      "order": 2
    }
  }
  ```

  <Tip>
    Keep the file key and the `id` field in sync. OlonJS uses the key for `$ref` resolution and URL slug generation on dynamic pages.
  </Tip>

  ### Register the collection

  Open `src/lib/CollectionRegistry.ts` and add your new collection schema alongside the existing ones.

  ```typescript src/lib/CollectionRegistry.ts theme={null}
  import { PostsCollectionSchema } from '@/collections/posts';
  import { TagsCollectionSchema } from '@/collections/tags';
  import { MyCollectionSchema } from '@/collections/my-collection'; // add this

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

  export type CollectionType = keyof typeof CollectionRegistry;
  ```

  The key you choose here (`'my-collection'`) becomes the canonical name used in `$ref` paths and in Studio's collection picker.

  ### Use in a section

  Reference your collection from a page's section data using a `$ref` pointer. OlonJS resolves the pointer at build and render time and passes the full collection to the section component.

  ```json src/data/pages/my-page.json (excerpt) theme={null}
  {
    "id": "my-list-section",
    "type": "posts-list",
    "data": {
      "title": "My entries",
      "items": {
        "$ref": "../collections/my-collection/my-collection.json"
      }
    },
    "settings": {}
  }
  ```

  To reference a **single item** rather than the whole collection, append a `#/<key>` fragment:

  ```json theme={null}
  {
    "$ref": "../collections/my-collection/my-collection.json#/first-entry"
  }
  ```

  ### Build

  Run the standard build command. OlonJS bakes collection data and copies everything to `public/collections/` as part of the build.

  ```bash theme={null}
  npm run build
  ```
</Steps>

***

## Field descriptor annotations

The `.describe()` string on a Zod field controls the input widget Studio renders for that field. Without an annotation, Studio falls back to a plain text input.

| Descriptor | Studio widget |
| - | - |
| `ui:text` | Single-line text input |
| `ui:textarea` | Multi-line text area |
| `ui:select` | Dropdown menu (uses the `z.enum` values as options) |
| `ui:number` | Number input |
| `ui:list` | Repeatable list of text items |
| `ui:collection-ref` | Generic collection selector (any registered collection) |
| `ui:collection-ref:<collectionName>` | Collection selector scoped to a specific collection — e.g. `ui:collection-ref:tags` renders a dropdown of tag entries |

These annotations are read-only hints to the Studio UI. They have no effect on schema validation or the runtime data shape.


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