# Content Client SDK

> How to use the Uniform Content Client to fetch, search, and filter headless content entries.

Source: https://docs.uniform.app/docs/sdk/content-client

> **Deprecated:**
>
> `ContentClient` is deprecated. It is replaced by `EntryDeliveryClient`,  `EntryManagementClient`, and `ContentTypeClient`. See [content API clients](https://docs.uniform.app/docs/sdk/content-api-clients) for the current clients and a migration guide.

The Content Client (`ContentClient` from `@uniformdev/canvas`) provides programmatic access to headless content entries stored in Uniform. Use it for server-side data fetching, search indexing, custom API routes, or any scenario where you need to query content outside of the standard composition rendering pipeline.

> **Note:**
>
> Within the App Router SDK's composition rendering flow, content is resolved automatically. The Content Client is for use cases where you need to access content directly -- such as building sitemaps, search indexes, custom API endpoints, or fetching content that isn't part of a composition.

---

## Installation

The Content Client is part of the `@uniformdev/canvas` package:

```bash
npm install @uniformdev/canvas
```

## Initializing the client

```ts
import { ContentClient } from "@uniformdev/canvas";

const contentClient = new ContentClient({
  apiKey: process.env.UNIFORM_API_KEY,
  apiHost: process.env.UNIFORM_CLI_BASE_URL || "https://uniform.global",
  projectId: process.env.UNIFORM_PROJECT_ID,
});
```

The App Router SDK does not provide a pre-configured Content Client factory; initialize the client directly as shown above. For composition access, the App Router SDK provides a pre-configured [Canvas Client](https://docs.uniform.app/docs/sdk/canvas-client#within-the-app-router-sdk).

---

## Fetching entries

### Get entries by content type

```ts
const response = await contentClient.getEntries({
  type: "blogPost",
});

// response.entries is an array of content entries
for (const entry of response.entries) {
  console.log(entry.entry.name, entry.entry.fields);
}
```

### Pagination

```ts
const response = await contentClient.getEntries({
  type: "blogPost",
  limit: 20,
  offset: 0,
});

console.log(`Total entries: ${response.totalCount}`);
```

---

## Search and filtering

The Content Client supports advanced filtering using a structured query syntax. Filters are passed as key-value pairs where the key includes the field path and operator.

### Filter syntax

```
filters.<field>[<operator>]=<value>
```

- **`field`**: The field to filter by. Use `type` for content type, `fields.<fieldName>` for custom fields, or system properties like `name`, `slug`, `created`, `modified`.
- **`operator`**: The comparison operator.
- **`value`**: The literal value to compare against.

### Example: filter by content type and field value

```ts
const response = await contentClient.getEntries({
  filters: {
    "type[eq]": "brand",
    "fields.brandName[match]": "adidas",
  },
});
```

### Available operators

| Operator | Description |
| --- | --- |
| `eq` | Equals |
| `neq` | Not equal |
| `match` | Contains (text search) match |
| `starts` | Starts with. Value limited to letters, numbers, `_`, `.`, `-`, and spaces |
| `lt` / `lte` | Less than / less than or equal to |
| `gt` / `gte` | Greater than / greater than or equal to |
| `in` | Matches any value in a comma-separated list (OR) |
| `nin` | Does not match any value in a list |
| `all` | List-valued fields must contain every value in a comma-separated list (AND) |
| `def` | `true` or `false`; whether the field has a value at all |

### Supported operators by field

Not every operator is valid for every field; the allowed set depends on the field's type. An unsupported combination returns a 400 error listing the supported operators.

#### Entry metadata

| Field | Supported operators |
| --- | --- |
| `entityId`, `type`, `uiStatus`, `locale`, `creatorSubject`, `authorSubject` | `eq`, `neq`, `in`, `nin` |
| `editionId`, `releaseId`, `patternId`, `workflowId`, `workflowStageId`, `categoryId` | `eq`, `neq`, `in`, `nin`, `def` |
| `created`, `modified` | `eq`, `neq`, `lt`, `lte`, `gt`, `gte`, `in`, `nin` |
| `name`, `slug` | `match`, `starts`, `eq`, `neq`, `in`, `nin`, `def` |
| `creator`, `author` | `match`, `starts`, `eq`, `neq`, `in`, `nin` |

#### Content fields (`fields.*`)

| Field type | Supported operators |
| --- | --- |
| Text, select | `match`, `starts`, `eq`, `neq`, `in`, `nin`, `def` |
| Number, date, datetime | `eq`, `neq`, `lt`, `lte`, `gt`, `gte`, `in`, `nin`, `def` |
| Checkbox | `eq`, `neq`, `def` |
| Multi-select | `eq`, `neq`, `in`, `nin`, `all`, `def` |
| Rich text | `match`, `starts`, `def` |
| Reference, asset (by ID) | `eq`, `neq`, `in`, `nin`, `def` |

#### Sub-properties of reference, asset, and link fields

Reference, asset, and link fields can also be filtered by sub-properties of the item they point to (for example `fields.speaker.slug`). Text-valued sub-properties take the text operators; ID-valued sub-properties take the same operators as reference fields:

| Sub-property | Supported operators |
| --- | --- |
| Reference `.name`, `.slug`; asset `.url`, `.title`, `.description`; link path | `match`, `starts`, `eq`, `neq`, `in`, `nin`, `def` |
| Reference `.type`; link `.type`, `.projectMapNodeId`; asset `.mediaType` | `eq`, `neq`, `in`, `nin`, `def` |

### Filtering reference fields

For reference fields, you can filter by the referenced entry's properties:

```ts
const response = await contentClient.getEntries({
  filters: {
    "type[eq]": "eventSession",
    "fields.speaker.slug[eq]": "jane-doe",
  },
});
```

Filterable reference properties: `name`, `slug`, `uiStatus`, `type`.

### Combining multiple filters

Combine multiple filters to narrow results:

```ts
const response = await contentClient.getEntries({
  filters: {
    "type[eq]": "product",
    "fields.category[eq]": "electronics",
    "fields.price[gte]": 50,
    "fields.price[lte]": 500,
  },
});
```

---

## Selecting fields with data projection

The `select` option fetches a subset of each entry instead of the whole thing. The API prunes fields, field types, and slots before values are resolved, so pruned content skips asset resolution, rich text reference expansion, and data resource fetches -- reducing both payload size and response time.

`select` accepts a `ProjectionSpec` object, which the client serializes into `select.*` query parameters (mirroring the `filters.*` syntax). The full wire-level projection syntax is documented in the _For the US region:_

_For the EU region:_

.

```ts
const response = await contentClient.getEntries({
  filters: { "type[eq]": "article" },
  select: {
    fields: { only: ["title", "coverImage"] },
  },
});
```

Every article in the response contains only its `title` and `coverImage` fields; everything else -- body, metadata, tags, author references -- is absent.

### Projection buckets

A projection spec has three buckets:

| Bucket | Selects by | Example |
| --- | --- | --- |
| `fields` | Field name | `fields: { only: ["title", "slug"] }` |
| `fieldTypes` | Field type ID (e.g. `text`, `richText`, `asset`) | `fieldTypes: { except: ["richText"] }` |
| `slots` | Slot name (compositions only) | `slots: { only: ["hero"] }` |

#### `fields` operators

| Operator | Type | Description |
| --- | --- | --- |
| `only` | `string[]` | Keep only fields whose name matches one of these patterns |
| `except` | `string[]` | Drop fields whose name matches one of these patterns |
| `locales` | `string[]` | For matching fields that survive filtering, return the full per-locale value map instead of only the requested locale's value |
| `blockDepth` | `number \| "preserveAll"` | Limit how many levels of block field children are kept. `0` removes all block fields; `"preserveAll"` prevents projection from trimming fields inside block children |

#### `fieldTypes` operators

| Operator | Type | Description |
| --- | --- | --- |
| `only` | `string[]` | Keep only fields of the named types |
| `except` | `string[]` | Drop fields of the named types |

#### `slots` operators

| Operator | Type | Description |
| --- | --- | --- |
| `only` | `string[]` | Keep only the named slots |
| `except` | `string[]` | Drop the named slots |
| `depth` | `number` | Limit how many levels of nested components are kept |
| `named` | `{ [slotName]: { depth } }` | Per-slot depth caps; override the container-wide `depth` for that slot |

### Projection behavior

- **Wildcards**: values accept a single `*` wildcard matching zero or more characters -- `seo_*`, `*Title`, and `meta*Published` are all legal.
- **Recursive by default**: projection applies at every component and block in the returned tree, not just the root, and is forwarded into entries resolved through reference fields.
- **Exclusion wins**: when operators combine, all `only` sets are intersected first, then `except` sets are subtracted. If rules contradict, the exclusion applies.
- **Unknown names are silent no-ops**: asking for a field a node doesn't have produces an empty field bag, not an error. The tree shape is preserved; non-matching content is simply absent.
- **Empty `only` strips everything**: `fields: { only: [] }` removes every field; `slots: { only: [] }` flattens the component tree. `except: ["*"]` is equivalent.
- **Depth resets across references**: `depth` and `blockDepth` count nesting within a single fetched tree and reset inside referenced entries.

### Example: strip rich text everywhere

Drop rich text fields you can't render, without needing to know their names -- including inside entries resolved through reference fields:

```ts
const response = await contentClient.getEntries({
  filters: { "type[eq]": "article" },
  select: {
    fieldTypes: { except: ["richText"] },
  },
});
```

### Example: per-locale slugs for hreflang tags

Fetch a lean payload but keep every locale's value on the `slug` field:

```ts
const response = await contentClient.getEntries({
  filters: { "type[eq]": "article" },
  select: {
    fields: { only: ["title", "slug"], locales: ["slug"] },
  },
});
```

The `select` option is also available on [`CanvasClient.getCompositionList`](https://docs.uniform.app/docs/sdk/canvas-client#selecting-fields-with-data-projection) (where the `slots` bucket controls the component tree) and on [`RouteClient.getRoute`](https://docs.uniform.app/docs/sdk/route-client#fetching-a-subset-with-select).

---

## Filtering compositions

The [Canvas Client](https://docs.uniform.app/docs/sdk/canvas-client#filtering-compositions) supports the same filter and projection syntax for compositions, with `parameters` in place of `fields`.

---

## Building a search index

A common use case for the Content Client is building a search index. Here is a pattern that retrieves all compositions via the project map and the [Route Client](https://docs.uniform.app/docs/sdk/route-client), then extracts text content for indexing:

```ts
import { RouteClient } from "@uniformdev/canvas";
import { ProjectMapClient } from "@uniformdev/project-map";

const projectMapClient = new ProjectMapClient({
  apiKey: process.env.UNIFORM_API_KEY,
  projectId: process.env.UNIFORM_PROJECT_ID,
});

const routeClient = new RouteClient({
  apiKey: process.env.UNIFORM_API_KEY,
  projectId: process.env.UNIFORM_PROJECT_ID,
  edgeApiHost: "https://uniform.global",
});

async function buildSearchIndex() {
  // 1. Get all project map nodes with compositions
  const { nodes } = await projectMapClient.getNodes({
    projectMapId: "your-project-map-id",
  });

  const compositionNodes = (nodes ?? []).filter((node) => node.compositionId);

  // 2. Fetch each composition and extract text
  const indexData = await Promise.all(
    compositionNodes.map(async (node) => {
      const route = await routeClient.getRoute({
        path: node.path,
        projectMapId: "your-project-map-id",
        state: 0, // published state
      });

      if (route.type !== "composition") return null;

      const { composition } = route.compositionApiResponse;
      const title = composition.parameters?.pageTitle?.value as string;

      return {
        path: node.path,
        title,
        compositionId: node.compositionId,
      };
    })
  );

  return indexData.filter(Boolean);
}
```

> **Note:**
>
> For large sites, consider running index rebuilds outside the Next.js build process to avoid build timeout limits. You can trigger rebuilds via a webhook when content is published.

---

## Limitations

- **Nested object search is not supported**: You cannot filter by a parameter of a component within a composition, or by a field of a block within an entry.
- **Faceting** is available for numeric and short text fields, but requires specifying a single content type filter.

## Import reference

| Export | Package | Description |
| --- | --- | --- |
| `ContentClient` | `@uniformdev/canvas` | Client for fetching content entries |
| `CanvasClient` | `@uniformdev/canvas` | Client for [fetching compositions](https://docs.uniform.app/docs/sdk/canvas-client) |
| `RouteClient` | `@uniformdev/canvas` | Client for [route resolution](https://docs.uniform.app/docs/sdk/route-client) |
| `ProjectionSpec` | `@uniformdev/canvas` | Type for the `select` data projection option |
