# Contentrain + Nuxt 3

> Framework guide for consuming Contentrain-managed content in Nuxt 3 applications.

---

## 1. Setup

### 1.1 SDK Installation

```bash
pnpm add @contentrain/query   # or npm/yarn
npx contentrain generate
```

The generator reads `.contentrain/models/` and produces a typed client in `.contentrain/client/`. Your `package.json` must include the subpath import:

```json
{
  "imports": {
    "#contentrain": {
      "types": "./.contentrain/client/index.d.ts",
      "import": "./.contentrain/client/index.mjs",
      "require": "./.contentrain/client/index.cjs",
      "default": "./.contentrain/client/index.mjs"
    }
  }
}
```

The generator adds this automatically. After generation, restart the Nuxt dev server to pick up the new import map.

### 1.2 Bundler Alias

The `#contentrain` subpath import works natively in Node.js 22+ but does NOT resolve in Nuxt's bundler. Add a top-level alias:

```ts
// nuxt.config.ts
export default defineNuxtConfig({
  alias: {
    '#contentrain': './.contentrain/client/index.mjs',
  },
})
```

Also add a `paths` entry to `tsconfig.json` so the TypeScript language server resolves it:

```json
{
  "compilerOptions": {
    "paths": {
      "#contentrain": ["./.contentrain/client/index.d.ts"]
    }
  }
}
```

### 1.3 Watch Mode

Run the generator in watch mode alongside Nuxt dev:

```bash
npx contentrain generate --watch &
npx nuxt dev
```

Any model or content change under `.contentrain/` triggers client regeneration automatically.

---

## 2. Imports

All SDK functions are imported from the `#contentrain` subpath:

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

Treat this import as **server-only** in Nuxt. Use it in Nitro server routes, server utilities, route middleware, and other server execution contexts. Do **not** import `#contentrain` directly in client-rendered Vue components or generic `<script setup>` blocks.

---

## 3. Querying Content

### 3.1 Collections

Collections return arrays. The SDK converts the internal object-map storage to arrays automatically.

```ts
const posts = query('blog-post').locale('en').all()
const featured = query('blog-post').locale('en').where('featured', true).all()
const sorted = query('blog-post').locale('en').sort('publishedAt', 'desc').all()
```

Single entry by ID:

```ts
const post = query('blog-post').locale('en').where('id', 'abc-123').first()
```

### 3.2 Singletons

Singletons return a single object. Use `.get()` instead of `.all()`.

```ts
const hero = singleton('hero').locale('en').get()
```

### 3.3 Dictionaries

Dictionaries provide key-value access for UI strings.

```ts
const labels = dictionary('ui-labels').locale('en').get('submit_button')
const allLabels = dictionary('ui-labels').locale('en').get()
```

### 3.4 Documents

Documents are long-form content with metadata and a body field (markdown or MDX).

```ts
const article = document('blog-article').locale('en').bySlug('getting-started')
const docs = document('doc-page').locale('en').all()
```

### 3.5 Relations

Resolve relation fields to full objects with `.include()`:

```ts
const posts = query('blog-post').locale('en').include('author', 'tags').all()
// post.author → full author object instead of just the ID
// post.tags → array of full tag objects
```

---

## 4. Nuxt Integration Patterns

### 4.1 Page Data Loading

Load content through a server route, then consume it with `useAsyncData` in the page:

```ts
// server/api/blog-posts.get.ts
import { query } from '#contentrain'
import { getQuery } from 'h3'

export default defineEventHandler((event) => {
  const locale = getQuery(event).locale?.toString() ?? 'en'
  return query('blog-post').locale(locale).sort('publishedAt', 'desc').all()
})
```

```vue
<script setup lang="ts">
const { data: posts } = await useAsyncData('blog-posts', () => $fetch('/api/blog-posts'))
</script>
```

### 4.2 Dynamic Routes

For `pages/blog/[slug].vue`:

```vue
<script setup lang="ts">
const route = useRoute()
const { data: post } = await useAsyncData(
  `post-${route.params.slug}`,
  () => $fetch(`/api/blog-post/${route.params.slug}`),
)
</script>
```

With the matching server route:

```ts
// server/api/blog-post/[slug].get.ts
import { document } from '#contentrain'
import { getRouterParam } from 'h3'

export default defineEventHandler((event) => {
  const slug = getRouterParam(event, 'slug')
  return document('blog-article').locale('en').bySlug(slug ?? '')
})
```

### 4.3 Server Routes

Content is also accessible in Nuxt server routes (`server/api/`):

```ts
// server/api/posts.get.ts
import { query } from '#contentrain'

export default defineEventHandler(() => {
  return query('blog-post').locale('en').all()
})
```

---

## 5. i18n Integration

### 5.1 With @nuxtjs/i18n

Use the `useI18n()` composable to get the current locale, then pass it to SDK calls:

```vue
<script setup lang="ts">
const { locale } = useI18n()
const { data: posts } = await useAsyncData(
  `posts-${locale.value}`,
  () => $fetch('/api/blog-posts', { query: { locale: locale.value } }),
  { watch: [locale] }
)
</script>
```

### 5.2 Dictionary Strings as i18n Source

Contentrain dictionaries can serve as the i18n message source. Export dictionary content and feed it to `@nuxtjs/i18n` via a custom loader, or use SDK calls directly in components for content-driven strings.

---

## 6. Markdown and Document Content

### 6.1 Nuxt Content Module

If using `@nuxt/content`, point it to `.contentrain/content/` for markdown files:

```ts
// nuxt.config.ts
export default defineNuxtConfig({
  content: {
    sources: {
      contentrain: {
        driver: 'fs',
        base: '.contentrain/content'
      }
    }
  }
})
```

### 6.2 Rendering Document Bodies

For document-kind entries, the `body` field contains markdown. Render it with `@nuxt/content` or any markdown renderer:

```vue
<template>
  <ContentRenderer :value="parsedBody" />
</template>
```

---

## 7. Static Generation and Deployment

### 7.1 Static Generation

Contentrain content is file-based. `nuxt generate` reads all content at build time:

```bash
npx nuxt generate
```

All SDK calls resolve to static JSON reads — no runtime API calls during generation.

### 7.2 Deployment

Content lives in Git. The deployment flow:

1. Author or agent creates/updates content via MCP tools.
2. Changes are committed to a branch and reviewed.
3. After merge, push triggers platform rebuild (Vercel, Netlify, Cloudflare Pages).
4. `nuxt generate` runs, SDK reads `.contentrain/` content, static site is produced.

### 7.3 Hybrid Rendering

For SSR pages, SDK calls execute on the server at request time. Content is still read from the file system — no external API dependency.

---

## 8. Type Safety

The generated client includes full TypeScript types for every model:

```ts
import { query } from '#contentrain'

// Fully typed — IDE autocomplete for field names, return types
const posts = query('blog-post').locale('en').all()
// posts[0].title — string
// posts[0].author — Author (when using .include('author'))
```

Type definitions are regenerated whenever models change. Keep the generator running in watch mode during development.
