---
title: "References plugin"
description: "Let one document point at another, like a post at its author, and inline the target when you query."
canonical_url: "https://content.comark.dev/plugins/built-in/references"
---
# References plugin

> Let one document point at another, like a post at its author, and inline the target when you query.

The `references` plugin lets a document point at another one: a post at its author, or at related posts in the same instance.

Mark the field in a [source schema](https://content.comark.dev/reference/types/content#option-and-method-types) with [`reference()`](#defining-references), store the target's path in your content, and ask for it to be inlined when you query:

```ts
const list = await posts.list({ populate: ['author'] })
list[0].data.author?.data.name // 'Ada Lovelace'
```

You get autocomplete on the fields you can populate, and the return type is rewritten to the target's shape. Nothing is loaded until you ask for it, so listings stay cheap.

::callout{to="https://content.comark.dev/examples/vite/vue-references" icon="i-lucide-play"}
Check out the **Vite + Vue references example** for a runnable demo with `createContentClient`.
::

## Defining references

Use `reference()` in a source schema to mark a field as a link to another document. Pass the target [instance name](https://content.comark.dev/reference/content/comark-content#name), or omit it for a self-reference:

```ts [content.ts]
import { comarkContent, contentHub, type JsonSchema } from 'comark-content'
import fs from 'comark-content/sources/fs'
import json from 'comark-content/plugins/json'
import yaml from 'comark-content/plugins/yaml'
import references, { reference } from 'comark-content/plugins/references'

// Authors are plain documents, the targets we point at.
const authorSchema: JsonSchema = {
  type: 'object',
  properties: {
    name: { type: 'string' },
    role: { type: 'string' },
  },
  required: ['name'],
}

const postSchema: JsonSchema = {
  type: 'object',
  properties: {
    title: { type: 'string' },
    // A single reference to a document in the `authors` instance
    author: reference('authors'),
    // An array of references. No argument means a self-reference,
    // so these resolve within this instance.
    relatedPosts: {
      type: 'array',
      items: reference(),
    },
  },
  required: ['title'],
}

// One instance per body of content, composed into a hub. The hub wires each
// instance's references to its siblings, so `reference('authors')` resolves.
export const authors = comarkContent('authors', {
  source: fs('./content/authors', { schema: authorSchema }),
  plugins: [json(), yaml(), references()],
})

export const posts = comarkContent('posts', {
  source: fs('./content/posts', { schema: postSchema }),
  plugins: [json(), yaml(), references()],
})

export const content = contentHub([authors, posts])
```

Referencing across instances needs the [hub](https://content.comark.dev/advanced/hub): it is what lets `posts` see `authors`. A self-reference works on a lone instance.

In your content, a reference is a plain string: the target's public **path**, or its [**`meta.key`**](https://content.comark.dev/guide/files-and-paths#internal-identifiers) (`<instance name>/<file key>`):

```yaml [posts/design-systems.yaml]
title: Design Systems That Survive Contact With Reality
author: /grace-hopper # reads content/authors/grace-hopper.yaml
relatedPosts:
  - posts/scaling-comark.json # the meta.key form: <instance name>/<file key>
  - /nonexistent-post # no such document, dropped when populated
```

Nothing is inlined into `data` at load time: read the document without `populate` and you get the strings back exactly as stored.

| Export               | Description                                                                                                                                                                         |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `reference(source?)` | Returns `{ type: 'string', 'x-content': { type: 'reference', source? } }`. `source` is the target's instance name. Omit it for a self-reference (the instance that owns the field). |

Lookup goes through [`content.stat()`](https://content.comark.dev/reference/content/stat), and the target must belong to the declared instance. A `/grace-hopper` value on a field declared `reference('posts')` will not resolve.

## Reading references

Three ways to resolve, depending on what you already have:

| I want to…                                       | Use                               |
| ------------------------------------------------ | --------------------------------- |
| Inline references while listing                  | `content.list({ populate })`      |
| Inline references on a single document           | `content.get(path, { populate })` |
| Resolve one field on a document I already loaded | `content.populate(file, field)`   |

The first two rewrite `data` in place, so `data.author` becomes the target document. `content.populate()` leaves the document alone and returns the targets.

```ts [query.ts]
// Stored strings only
const raw = await posts.list()
raw[0].data.author // '/grace-hopper'

// Inlined
const list = await posts.list({ populate: ['author', 'relatedPosts'] })
list[0].data.author?.data.name // 'Grace Hopper'
list[0].data.relatedPosts // broken entry dropped

// Or one field at a time
const post = await posts.get('/design-systems')
const related = await posts.populate(post, 'relatedPosts')
```

### Partial vs full targets

By default targets are **partial**: manifest listing entries ([`ContentListFile`](https://content.comark.dev/reference/types/content#contentlistfiletdata)), which carry `data` but no `nodes`. That keeps listings cheap. Pass `resolve: 'full'` when the UI needs the target's rendered body:

```ts
const list = await posts.list({ populate: ['author'], resolve: 'full' })
list[0].data.author?.nodes // now available
```

## API

The plugin adds populate support to `list` and `get` and one new method:

### `references()`{lang="ts"}

Returns a [`ContentPlugin`](https://content.comark.dev/reference/types/plugins#contentplugin) that:

1. Overrides `list` and `get` with optional `{ populate }`.
2. Adds `content.populate()` for single-field resolution.
3. Hooks [`typegen:field`](https://content.comark.dev/reference/content/hooks#typegenfield) so generated **data** types emit branded [`Ref<'source'>`](#type-generation) for reference fields (row types stay `string`).

**Returns:** [`ContentPlugin`](https://content.comark.dev/reference/types/plugins#contentplugin)

No options. Behaviour is controlled per call.

### `content.list(opts?)`

With the plugin installed, `list()` accepts populate options. Field names in `populate` autocomplete to keys typed as `Ref` / `Ref[]` on the instance's data type. The return type rewrites those fields to the resolved target shape.

| Parameter       | Type                  | Description                                                               |
| --------------- | --------------------- | ------------------------------------------------------------------------- |
| `opts.populate` | `string[]`            | Top-level reference field names to inline.                                |
| `opts.resolve`  | `'partial' \| 'full'` | `'partial'` (default): listing entries. `'full'`: fully parsed documents. |

```ts
const list = await posts.list({ populate: ['author', 'relatedPosts'] })
// list[n].data.author?: ContentListFile<AuthorsData> | null
// list[n].data.relatedPosts: ContentListFile<PostsData>[]

const full = await posts.list({ populate: ['author'], resolve: 'full' })
// full[n].data.author?: ContentFile<AuthorsData> | null  (has nodes)
```

### `content.get(path, opts?)`

Same populate options as `list`. Rewrites the returned document's `data` for the listed fields.

```ts
const post = await posts.get('/design-systems', { populate: ['author'] })
post?.data.author // ContentListFile | null
```

### `content.populate(file, field, opts?)`

Resolve one reference field on an already-loaded file. Always returns an **array** (length 0–1 for a single ref, N for an array ref), and unresolved values are dropped.

| Parameter      | Type                                                  | Description                                                            |
| -------------- | ----------------------------------------------------- | ---------------------------------------------------------------------- |
| `file`         | `ContentFile \| ContentListFile \| null \| undefined` | Document that owns the field.                                          |
| `field`        | `string`                                              | Top-level data field name.                                             |
| `opts.resolve` | `'partial' \| 'full'`                                 | Default `'partial'` → listing entries; `'full'` → full `ContentFile`s. |

```ts
const post = await posts.get('/design-systems')
const author = (await posts.populate(post, 'author'))[0] // ContentListFile | undefined
const related = await posts.populate(post, 'relatedPosts') // ContentListFile[]
const fullAuthor = await posts.populate(post, 'author', { resolve: 'full' })
```

## How population works

A few rules govern what gets resolved:

- Only **top-level** reference fields on `data` are supported (not nested paths like `seo.author`).
- Single refs become the target entry, or `null` when unresolved.
- Array refs become the list of resolved targets; unresolved entries are omitted.
- Non-reference fields named in `populate` are left untouched.

## Type generation

When `references` is registered, [`writeSourceTypes()`](https://content.comark.dev/guide/typescript#writesourcetypes) emits branded path strings on **data** interfaces:

```ts
// Generated stored shape, nothing populated yet
export interface PostsData {
  title: string
  author?: Ref<'authors'>
  relatedPosts?: Array<Ref<'posts'>>
}
```

[`Ref<K>`](https://content.comark.dev/reference/types/content) is a branded `string` carrying the target instance name, so `populate` can suggest only the fields that are actually references. Query **row** types keep plain `string` for those columns.

```ts
const list = await posts.list({ populate: ['author'] })
list[0].data.author // ContentListFile<AuthorsData> | null | undefined
list[0].data.relatedPosts // still Ref<'posts'>[], not populated
```

Without the plugin, fields type as `string` even if the schema carries `x-content` metadata.

## HTTP client

Pair the server plugin with the client half so browser/HTTP apps get the same API over [`content.handler()`](https://content.comark.dev/reference/content/handler):

```ts [client]
import { createContentClient } from 'comark-content/client'
import referencesClient from 'comark-content/plugins/references/client'

const posts = createContentClient('posts', {
  basePath: '/api/content',
  plugins: [referencesClient()],
})

const list = await posts.list({ populate: ['author', 'relatedPosts'] })
```

The client plugin overrides `list` / `get` to send query params:

| Query      | Meaning                                                        |
| ---------- | -------------------------------------------------------------- |
| `populate` | JSON array of field names to inline                            |
| `resolve`  | `partial` (default, listing entries) or `full` (parsed bodies) |

`content.populate(file, field)` on the client re-fetches the document with `{ populate: [field] }` and extracts the inlined value.

::note
Make sure the Content instance on the server has the `references()` plugin installed too.
::

## Notes

Resolution never fails a request: a missing target becomes `null` for a single ref, and is dropped from the array for an array ref. Content and links move independently, so a typo or a deleted document shouldn't take a page down. A strict mode that throws on unresolved references may be added later.

[`schema-validation`](https://content.comark.dev/plugins/built-in/schema-validation) checks the *shape* of the stored strings, not link integrity. It won't tell you a reference points at a document that no longer exists.


## Sitemap

See the full [sitemap](https://content.comark.dev/sitemap.md) for all pages.
