Skip to content
Back to skills

Contentrain Sdk

ASecurity

Use the @contentrain/query SDK to query Contentrain content with type safety. Use when importing from #contentrain, using QueryBuilder, SingletonAccessor, DictionaryAccessor, or DocumentQuery.

  • 5 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 23, 2026
ai-agentstypescriptgobashreactvuenextjsnodeapi

Works with

  • cli
  • api
  • mcp

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned September 23, 2026

npx -y skills add Contentrain/ai --skill contentrain-sdk --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Contentrain Sdk?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Contentrain Sdk
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/contentrain-contentrain-sdk/badge)](https://www.skillsdirectory.com/skills/contentrain-contentrain-sdk)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: contentrain-sdk
description: "Use the @contentrain/query SDK to query Contentrain content with type safety. Use when importing from #contentrain, using QueryBuilder, SingletonAccessor, DictionaryAccessor, or DocumentQuery."
metadata:
  author: Contentrain
  version: "1.0.0"
---

# Contentrain Query SDK

## Overview

`@contentrain/query` is a Prisma-pattern generated client that provides type-safe access to Contentrain content. Generated output lives in `.contentrain/client/` — never edit it manually.

## Quick Start

```bash
npx contentrain generate     # Generate client from models
```

```typescript
import { query, singleton, dictionary, document } from '#contentrain'

// Collection
const posts = query('blog-post').where('category', 'eq', 'engineering').sort('published_at', 'desc').limit(10).all()

// Singleton
const hero = singleton('hero').get()

// Dictionary
const t = dictionary('ui-texts').get()
const loginLabel = t['auth.login.button']

// Document
const doc = document('blog-post').bySlug('getting-started')
```

## Runtime API

### QueryBuilder (collections)

| Method | Description |
|--------|-------------|
| `.all()` | Get all entries |
| `.first()` | Get first entry |
| `.count()` | Return count of matching entries |
| `.where(field, value)` | Equality filter (shorthand for `eq`) |
| `.where(field, op, value)` | Operator filter: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `contains` |
| `.sort(field, direction?)` | Sort (`asc`/`desc`, default `asc`) |
| `.limit(n)` | Limit results |
| `.offset(n)` | Skip results |
| `.include(relation)` | Resolve relation (1 level deep) |

### SingletonAccessor

| Method | Description |
|--------|-------------|
| `.get()` | Get the singleton data |
| `.include(relation)` | Resolve relation fields |

### DictionaryAccessor

| Method | Description |
|--------|-------------|
| `.get()` | Get all key-value pairs |
| `.get(key)` | Get single value by key |
| `.get(key, params)` | Get with interpolation: `{placeholder}` → value |

### DocumentQuery

| Method | Description |
|--------|-------------|
| `.all()` | Get all documents |
| `.bySlug(slug)` | Get document by slug |
| `.first()` | Get first document |
| `.count()` | Return count of matching documents |
| `.where(field, value)` | Equality filter (shorthand) |
| `.where(field, op, value)` | Operator filter (same ops as QueryBuilder) |
| `.include(relation)` | Resolve relation fields |

### Media

Media / image / file field values are plain strings. How you turn them into a URL depends on the storage model:

- **Studio-CDN content** already carries absolute delivery URLs — the write path normalizes `media/...` references on save — so use the field value directly, no resolution needed.
- **Relative-path content** (OSS / local-file model, values like `media/...`) resolves through optional `media()`/`mediaBody()` helpers baked into the generated client. Set `config.json > cdn.url` (or run `contentrain generate --mediaBaseUrl <base>`) and the client exports them:

```typescript
import { media, mediaBody } from '#contentrain'

media('media/original/hero.webp')          // → '{mediaBaseUrl}/media/original/hero.webp'
media('https://images.unsplash.com/x.jpg') // → unchanged (external pass-through)
mediaBody('See ![cover](media/hero.webp)') // → resolves `](media/...)` refs inside markdown
```

Both take an optional second argument overriding the baked base for that call — a Nuxt app passes its own `useRuntimeConfig()` value here instead of rebaking per environment. The Astro loader (`contentrainLoader({ mediaBaseUrl })`) applies the same resolver automatically.

`media()`/`mediaBody()` do not exist on the client at all when no base is configured at generate time — not a pass-through, an absent export. A host that never bakes a base in (reading it purely from its own runtime config or env, e.g. Nuxt) must import `resolveMediaUrl(value, base)` / `resolveMediaRefsInBody(markdown, base)` from `@contentrain/query` directly instead of going through the generated client. Both take a nullable base and no-op when it is absent, so calling them unconditionally is safe. `media()`, when present, is idempotent — external URLs (`http(s)://`, `//`, `data:`) and already-absolute delivery URLs pass through untouched. It is the **local-mode** counterpart of CDN mode's `client.media()` (which returns a `MediaAccessor` over the media manifest, below).

## CDN Mode (Remote Data)

For server-side or client-side apps that fetch content from Contentrain Studio CDN:

```typescript
import { createContentrain } from '@contentrain/query/cdn'

const client = createContentrain({
  projectId: '350696e8-...',
  apiKey: 'crn_live_xxx',
  // baseUrl: 'https://studio.contentrain.io/api/cdn/v1'  (default)
})

// All CDN queries are async (return Promise)
const posts = await client.collection('faq').locale('en').all()
const hero = await client.singleton('hero').locale('en').get()
const t = await client.dictionary('ui').locale('en').get()
const doc = await client.document('docs').locale('en').bySlug('intro')
```

### CDN `document()` — `all()` returns no bodies

The CDN splits documents in two: `all()`/`first()` read the model's `_index`
(frontmatter only), `bySlug()` reads the per-slug document (`{ frontmatter, body,
html }`). Reading `.body` off an `all()` result is a compile error — fetch it
with `bySlug()`.

```ts
const q = client.document<GuideSection>('guide-sections').locale('tr')
const index = await q.sort('order', 'asc').all()   // titles, slugs, order — no body
const full = await Promise.all(index.map(s => q.bySlug(s.slug)))  // bodies
```

The bundled runtime's `all()` *does* include bodies. Do not assume code that
works bundled behaves the same on CDN.

### CDN Collection Query Operators

| Operator | Example | Description |
|----------|---------|-------------|
| `eq` | `.where('status', 'eq', 'published')` | Equals |
| `ne` | `.where('status', 'ne', 'draft')` | Not equals |
| `gt` | `.where('order', 'gt', 5)` | Greater than |
| `gte` | `.where('order', 'gte', 5)` | Greater than or equal |
| `lt` | `.where('price', 'lt', 100)` | Less than |
| `lte` | `.where('price', 'lte', 100)` | Less than or equal |
| `in` | `.where('category', 'in', ['a','b'])` | In array |
| `contains` | `.where('tags', 'contains', 'vue')` | String/array contains |

### CDN Entry Metadata, Media, Forms & Comments

```typescript
// Entry metadata (status, publish_at, expire_at)
const posts = await client.collection('blog').locale('en').withMeta().all()

// Media manifest & variant URLs
const media = client.media()
const asset = await media.asset('hero.jpg')
const url = media.url(asset, 'thumb')

// Forms — public /api/forms/v1, no API key (values go under `data`; captcha + honeypot beside it)
const form = client.form()
const config = await form.config('contact')   // fields keyed by id, captcha, captchaSiteKey, honeypotField
const result = await form.submit('contact', { name: 'Alice' }, { captchaToken, honeypot: '' })
// { success, message } | { success: false, errors: [{ field, message }] }; 403/404/429 throw ContentrainError

// Comments — public /api/comments/v1, approved comments only, body is plain text (render escaped)
const comments = client.comments()
const thread = await comments.thread('posts', entryId, { locale: 'en' })
await comments.submit('posts', entryId, { author: { name: 'Ada', email }, body: 'Great post', parentId }, { locale: 'en' })

// Conversation API (external AI content operations)
const conv = client.conversation()
const response = await conv.send('Create a blog post about Vue 4')
const history = await conv.history(response.conversationId)
```

### CDN vs Local

| Aspect | Local (`#contentrain`) | CDN (`createContentrain()`) |
|--------|----------------------|---------------------------|
| Data source | Bundled `.mjs` files | HTTP fetch from CDN |
| Return type | Sync (`T[]`) | Async (`Promise<T[]>`) |
| Auth | None | API key required |
| Caching | In-memory (embedded) | ETag-based HTTP cache |
| Use case | SSG, build-time | SSR, client-side, serverless |

## Key Rules

- **Local mode** queries are **synchronous** — no `await` needed
- **CDN mode** queries are **async** — always `await`
- Relation resolution is **1 level deep** — no recursive resolution
- Locale fallback chain: explicit → config default → first available
- Generated files in `.contentrain/client/` are **immutable** — always regenerate, never edit
- Run `contentrain generate` after any model change
- **Media**: Studio-CDN fields already carry absolute URLs; for relative `media/...` values set `config.cdn.url` (or `generate --mediaBaseUrl`) and resolve with the generated `media()`/`mediaBody()` helpers

## Framework Integration

| Framework | Import | Notes |
|-----------|--------|-------|
| Nuxt 3 | `import { query } from '#contentrain'` | Server-only (server routes, plugins) |
| Next.js | `import { query } from '#contentrain'` | Works in RSC and API routes |
| Astro | `import { query } from '#contentrain'` | Works in `.astro` frontmatter |
| SvelteKit | `import { query } from '#contentrain'` | Works in `+page.server.ts` |
| Vue + Vite | `import { query } from '#contentrain'` | Requires Vite alias config |
| React + Vite | `import { query } from '#contentrain'` | Requires Vite alias config |
| Node.js | `import { query } from '#contentrain'` | ESM with subpath imports |

## References

| Reference | Description |
|-----------|-------------|
| [Bundler Configuration](references/bundler-config.md) | Vite, Next.js, Nuxt, SvelteKit, Metro alias setup |

## Related Skills

- **contentrain-generate** — Generate the SDK client before using it
- **contentrain-quality** — Content quality rules for entries you query
- **contentrain** — Core architecture and MCP tool catalog

Files in this skill

  • SKILL.md9.6 KB
  • references/bundler-config.md2.9 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.