---
name: content-formats
description: "Directory structure, config.json, content file formats, canonical serialization, system fields, and localization rules."
---

# Content Formats Reference

## Directory Structure

All Contentrain data lives under `.contentrain/` at the project root.

```
.contentrain/
  config.json               # Project configuration (stack, workflow, locales, domains)
  vocabulary.json            # Canonical terms for consistent terminology
  context.json               # Project intelligence -- MCP writes, agents READ ONLY
  assets.json                # Media asset registry
  models/
    {model-id}.json          # Model definitions (one file per model)
  content/
    {domain}/
      {model-id}/
        en.json              # Content per locale (i18n: true)
        tr.json
        data.json            # Content without locale (i18n: false)
        {slug}/              # Document kind only
          en.md
          tr.md
  meta/
    {model-id}/
      {locale}.json          # System-managed metadata
      {slug}/                # Document kind only
        {locale}.json
  client/                    # SDK generated client -- auto-generated, NEVER edit
  assets/                    # Media files (images, videos, documents)
```

### Critical Boundaries

- **NEVER write to `.contentrain/meta/`** -- metadata is system-managed
- **NEVER edit `.contentrain/client/`** -- auto-generated by `contentrain generate`
- **NEVER edit `.contentrain/context.json`** -- MCP writes it; agents read it
- **ALWAYS use MCP tools** to create/update content and models. Do not write JSON files directly

## config.json Structure

Created by `contentrain_init`. Controls global behavior.

```json
{
  "version": 1,
  "stack": "nuxt",
  "workflow": "auto-merge",
  "repository": {
    "provider": "github",
    "owner": "contentrain",
    "name": "demo",
    "default_branch": "main"
  },
  "locales": {
    "default": "en",
    "supported": ["en", "tr"]
  },
  "domains": ["blog", "marketing", "system"],
  "assets_path": ".contentrain/assets",
  "branchRetention": 30
}
```

### Field Reference

| Field | Type | Description |
|-------|------|-------------|
| `version` | number | Config schema version. Always `1` |
| `stack` | string | Framework: `"nuxt"`, `"next"`, `"astro"`, `"sveltekit"`, `"react"`, `"node"` |
| `workflow` | string | `"auto-merge"` (solo) or `"review"` (team governance) |
| `repository` | object | Git remote info. Provider is `"github"` in v1 |
| `locales.default` | string | Primary locale (ISO 639-1) |
| `locales.supported` | string[] | All locales including default |
| `domains` | string[] | Organizational groups (e.g., `"blog"`, `"marketing"`, `"system"`) |
| `assets_path` | string | Path for media files |
| `branchRetention` | number | Days to keep merged branches |

## Content File Formats by Kind

### Singleton

One JSON object per locale file. No `id` field.

**Path:** `.contentrain/content/{domain}/{model-id}/{locale}.json`

```json
{
  "cta": "Get Started",
  "subtitle": "The modern content platform",
  "title": "Build faster"
}
```

### Collection

Object-map on disk: keys are entry IDs, sorted lexicographically. The `id` field is the key itself and is NOT stored inside the entry object.

**Path:** `.contentrain/content/{domain}/{model-id}/{locale}.json`

```json
{
  "a1b2c3d4e5f6": {
    "avatar": "assets/ahmet.jpg",
    "name": "Ahmet",
    "role": "CEO"
  },
  "f6e5d4c3b2a1": {
    "avatar": "assets/jane.jpg",
    "name": "Jane",
    "role": "CTO"
  }
}
```

**MCP output format differs from storage:** MCP tools return collections as arrays with `id` injected.

### Document

Markdown with YAML-like frontmatter. One file per slug per locale.

**Path:** `.contentrain/content/{domain}/{model-id}/{slug}/{locale}.md`

```markdown
---
title: Getting Started
slug: getting-started
author: a1b2c3d4e5f6
tags: [tutorial, intro]
---
# Getting Started with Contentrain

Your markdown body content here...
```

### Dictionary

Flat key-value JSON per locale. No `fields` in model definition -- all values are strings.

**Path:** `.contentrain/content/{domain}/{model-id}/{locale}.json`

```json
{
  "auth.expired": "Session expired",
  "auth.failed": "Authentication failed",
  "validation.required": "{field} is required"
}
```

## Canonical Serialization Rules

ALL JSON files in `.contentrain/` MUST follow these 7 rules:

1. **Keys sorted lexicographically** within every object (including nested objects)
2. **2-space indent** for all nesting levels
3. **UTF-8 encoding** without BOM
4. **Trailing newline** at end of file (single `\n` after closing brace/bracket)
5. **Omit null values** -- do not write `"field": null`
6. **Omit default values** -- if `required` is `false`, do not include it; if `default` is `null`, do not include it
7. **Storage uses lexicographic sort** for determinism (field key order in content follows sort order)

### Why This Matters

- Deterministic output means identical data always produces identical files
- Sorted keys prevent artificial Git diffs from key reordering
- Clean diffs enable meaningful code review of content changes

## System Fields

Several fields are managed by the platform. Agents MUST NOT set or modify them.

| Field | Where | Managed By |
|-------|-------|------------|
| `id` | Collection entry key | Auto-generated 12-char hex UUID |
| `slug` | Document directory name | Derived from content or set by agent via `slug` field |
| `createdAt` | Not stored | Derived from Git commit history |
| `updatedAt` | Not stored | Derived from Git commit history |
| `status` | `.contentrain/meta/` | Platform workflow engine |
| `source` | `.contentrain/meta/` | Set by MCP: `"agent"`, `"human"`, or `"import"` |
| `updated_by` | `.contentrain/meta/` | Set by MCP: agent name or user email |
| `updated_at` | `.contentrain/meta/` | Set by MCP on every write, ISO 8601 UTC. Absent on entries written before the field existed — absent means unknown, never backfilled |
| `approved_by` | `.contentrain/meta/` | Set by Studio on review approval |

**Rule:** When creating content via `contentrain_content_save`, provide only the content fields defined in the model. Do not include `id`, `createdAt`, `updatedAt`, `status`, or any metadata fields.

## Localization Rules

| Rule | Detail |
|------|--------|
| `i18n: true` on model | Each locale gets its own file: `en.json`, `tr.json`, etc. |
| `i18n: false` on model | Single file: `data.json` (or `{slug}.md` for documents) |
| Locale code | ISO 639-1: `en`, `tr`, `de`, `fr`, `ja`, `ar` |
| ID/slug are locale-agnostic | Same entry ID or document slug across all locales |
| Entry parity (collections) | All locales MUST have the same set of entry IDs |
| Key parity (dictionaries) | All locales MUST have the same set of keys |

### Validation Checks

| Check | Severity | Example |
|-------|----------|---------|
| Missing locale file | Error | `hero: tr.json missing` |
| Collection entry ID mismatch | Error | `team-members: entry "member-2" missing in tr` |
| Dictionary key missing | Warning | `error-messages: tr missing 5 keys` |
| Document missing translation | Warning | `blog-post: "getting-started" has no tr translation` |

## Vocabulary

The `vocabulary.json` file provides canonical terms for consistent content.

```json
{
  "version": 1,
  "terms": {
    "email": { "en": "Email", "tr": "E-posta" },
    "sign-up": { "en": "Sign Up", "tr": "Kayit Ol" }
  }
}
```

### Rules

- Check vocabulary FIRST before writing any content. Use existing terms when they match
- Add new terms to the vocabulary when creating content with reusable terminology
- Term keys are kebab-case identifiers
- Each term maps to locale-specific approved strings
- Vocabulary ensures consistency across models, locales, and agent sessions

## context.json

MCP writes `context.json` after every write operation. Provides project intelligence for agents and Studio synchronization.

```json
{
  "version": "1.0",
  "lastOperation": {
    "tool": "content_save",
    "model": "blog-post",
    "locale": "en",
    "entries": ["a1b2c3d4e5f6"],
    "timestamp": "2026-03-11T14:30:00Z",
    "source": "mcp-local"
  },
  "stats": {
    "models": 5,
    "entries": 142,
    "locales": ["en", "tr"],
    "lastSync": "2026-03-11T14:30:00Z"
  }
}
```

### Rules

- **Read only** -- agents MUST NOT write to this file
- **Use it for awareness** -- check last operation, entry counts, locale coverage
- `source` values: `"mcp-local"` (IDE), `"mcp-studio"` (Studio server-side), `"studio-ui"` (Studio direct)
- Updated only on write operations (not reads)
- Git-tracked within `.contentrain/`

## Assets

Media files are stored in the configured `assets_path`. The `assets.json` file tracks registered assets.

```json
[
  { "path": "assets/hero.webp", "type": "image/webp", "size": 245680, "alt": "Hero background" }
]
```

### Rules for Media References

- Use relative paths from the `.contentrain/` root in content fields
- Media field types (`image`, `video`, `file`) store string path references
- In v1, media fields are URL/path strings only. Upload and processing are out of scope
- Always provide `alt` text for images when the information is available
