Contentrain Directory Format
The .contentrain/ directory is the contract between Studio and the Git repository. It contains configuration, model definitions, content data, metadata, and project context.
Directory Structure
.contentrain/
├── config.json # Project configuration
├── vocabulary.json # Shared terminology across locales
├── context.json # Project stats and last operation
├── models/ # Model definitions (one file per model)
│ ├── blog-post.json
│ ├── hero.json
│ └── ui-strings.json
├── content/ # Content data organized by domain
│ └── {domain}/
│ └── {modelId}/
│ ├── en.json # Localized content (i18n: true)
│ ├── tr.json
│ └── data.json # Non-localized content (i18n: false)
├── meta/ # Entry metadata (timestamps, authors)
│ └── {modelId}/
│ ├── en.json
│ └── data.json
└── client/ # Generated SDK output (gitignored)config.json
The root configuration file defines the project stack, locales, domains, and workflow.
{
"version": 1,
"stack": "nuxt",
"locales": {
"default": "en",
"supported": ["en", "tr"]
},
"domains": ["marketing", "blog", "system"],
"workflow": "auto-merge"
}| Field | Type | Description |
|---|---|---|
version | number | Config format version (currently 1) |
stack | string | Framework: nuxt, next, astro, sveltekit, remix, vue, react, other |
locales.default | string | Default locale code |
locales.supported | string[] | All supported locale codes |
domains | string[] | Content domains (directory grouping) |
workflow | string | auto-merge or review |
Model Definitions
Each model is a JSON file in .contentrain/models/{modelId}.json.
Collection Model
{
"id": "blog-post",
"name": "Blog Post",
"kind": "collection",
"domain": "blog",
"i18n": true,
"description": "Blog articles with authors and categories",
"fields": {
"title": {
"type": "string",
"required": true,
"max": 200,
"description": "Post title"
},
"excerpt": {
"type": "text",
"max": 500
},
"author": {
"type": "relation",
"model": "team-members",
"required": true
},
"tags": {
"type": "array",
"items": "string"
},
"cover": {
"type": "image"
},
"publishDate": {
"type": "date"
}
}
}Singleton Model
{
"id": "hero",
"name": "Hero Section",
"kind": "singleton",
"domain": "marketing",
"i18n": true,
"fields": {
"headline": { "type": "string", "required": true },
"tagline": { "type": "text" },
"illustration": { "type": "image" }
}
}Dictionary Model
Dictionaries have no fields property. All content is free-form key-value string pairs.
{
"id": "ui-strings",
"name": "UI Strings",
"kind": "dictionary",
"domain": "system",
"i18n": true,
"description": "User-facing UI labels, buttons, headings, and messages"
}Document Model
Documents store content as markdown files with YAML frontmatter.
{
"id": "docs",
"name": "Documentation",
"kind": "document",
"domain": "blog",
"i18n": true,
"fields": {
"title": { "type": "string", "required": true },
"category": { "type": "select", "options": ["guide", "reference", "tutorial"] }
}
}Content Kinds Summary
| Kind | Storage | Entry ID | Key Format | Fields |
|---|---|---|---|---|
collection | JSON object map | 12-char hex | { entryId: { field: value } } | Defined in model |
singleton | JSON flat object | N/A | { field: value } | Defined in model |
dictionary | JSON flat object | N/A | { "key": "string value" } | None (free-form) |
document | Markdown files | Slug | YAML frontmatter + body | Defined in model |
Content Files
Collection Content
File: .contentrain/content/{domain}/{modelId}/{locale}.json
{
"a1b2c3d4e5f6": {
"title": "Getting Started with Studio",
"excerpt": "Learn how to manage content with AI",
"author": "d4e5f6a1b2c3",
"tags": ["tutorial", "getting-started"],
"publishDate": "2026-01-15"
},
"b2c3d4e5f6a1": {
"title": "Advanced Content Models",
"excerpt": "Deep dive into field types and relations"
}
}Singleton Content
File: .contentrain/content/{domain}/{modelId}/{locale}.json
{
"headline": "Welcome to Contentrain Studio",
"tagline": "The conversation-first CMS for teams",
"illustration": "media/original/hero.webp"
}Dictionary Content
File: .contentrain/content/{domain}/{modelId}/{locale}.json
{
"auth.sign_in_title": "Sign in to your account",
"auth.sign_in_button": "Sign In",
"auth.magic_link_label": "Sign in with magic link",
"common.save": "Save",
"common.cancel": "Cancel"
}WARNING
All dictionary values must be strings. Non-string values cause validation errors.
Non-Localized Content
When i18n: false, content is stored in data.json instead of {locale}.json:
File: .contentrain/content/{domain}/{modelId}/data.json
Document Content
Documents are stored as individual markdown files:
- i18n + slug:
.contentrain/content/{domain}/{modelId}/{slug}/{locale}.md - No i18n + slug:
.contentrain/content/{domain}/{modelId}/{slug}.md
---
category: guide
title: Getting Started
---
Welcome to Contentrain Studio. This guide walks you through...Meta Files
Meta files track the editorial state and authorship of each entry. They live in a separate .contentrain/meta/ tree -- never inlined into the content -- so content files stay clean and diff-friendly.
The shape of a meta file mirrors the model kind. Collections use an object-map keyed by entry ID; every other kind stores a single EntryMeta object.
| Kind | Path | Structure |
|---|---|---|
collection | .contentrain/meta/{modelId}/{locale}.json | Object-map: { entryId: EntryMeta } |
singleton | .contentrain/meta/{modelId}/{locale}.json | Single EntryMeta object |
dictionary | .contentrain/meta/{modelId}/{locale}.json | Single EntryMeta object |
document | .contentrain/meta/{modelId}/{slug}/{locale}.json | Single EntryMeta object |
EntryMeta fields
| Field | Type | Description |
|---|---|---|
status | string | draft, in_review, published, rejected, or archived |
source | string | Who authored the entry: agent, human, or import |
updated_by | string | Identity of the last writer -- an email or an agent id such as contentrain-mcp |
approved_by | string | null | Identity of the approver, or null when unapproved (optional) |
version | string | Optional content version tag |
publish_at | string | Optional ISO timestamp for scheduled publishing |
expire_at | string | Optional ISO timestamp for content expiry |
Collection meta -- an object-map keyed by entry ID:
{
"a1b2c3d4e5f6": {
"status": "published",
"source": "agent",
"updated_by": "contentrain-mcp",
"approved_by": "[email protected]"
}
}Singleton, dictionary, and document meta -- a single object:
{
"status": "draft",
"source": "human",
"updated_by": "[email protected]"
}INFO
Creation and update timestamps are derived from Git history, not stored in meta. The EntryMeta.source field (agent | human | import) records who authored the entry. This is distinct from the context.json operation source described below, which records which client performed the last write.
vocabulary.json
Shared terminology that should be consistent across locales:
{
"version": 1,
"terms": {
"brand_name": {
"en": "Contentrain",
"tr": "Contentrain"
},
"product_name": {
"en": "Studio",
"tr": "Studio"
}
}
}The vocabulary is included in the agent system prompt so the AI uses consistent terminology.
context.json
Project-level context tracking updated after every write operation. The MCP server writes it; Studio reads it to keep the IDE and the dashboard in sync.
{
"version": "1",
"lastOperation": {
"tool": "contentrain_content_save",
"model": "blog-post",
"locale": "en",
"entries": ["a1b2c3d4e5f6"],
"timestamp": "2026-01-15T14:30:00.000Z",
"source": "studio-ui"
},
"stats": {
"models": 8,
"entries": 59,
"locales": ["en"],
"lastSync": "2026-01-15T14:30:00.000Z"
}
}| Field | Type | Description |
|---|---|---|
version | string | Context format version |
lastOperation.tool | string | Tool that performed the write (e.g. contentrain_content_save) |
lastOperation.model | string | Model the operation touched |
lastOperation.locale | string | Locale the operation touched |
lastOperation.entries | string[] | Entry IDs affected (optional) |
lastOperation.timestamp | string | ISO timestamp of the operation |
lastOperation.source | string | Client that performed the write: mcp-local, mcp-studio, or studio-ui |
stats.models | number | Total model count |
stats.entries | number | Total entry count |
stats.locales | string[] | Locales in use |
stats.lastSync | string | ISO timestamp of the last sync |
WARNING
context.json's lastOperation.source (mcp-local | mcp-studio | studio-ui) is the write client, not the content author. Do not confuse it with EntryMeta.source (agent | human | import), which records who authored the entry.
Canonical JSON Format
All JSON files follow the canonical format:
| Rule | Value |
|---|---|
| Indent | 2 spaces |
| Encoding | UTF-8 |
| Trailing newline | Yes |
| Null values | Omitted |
| Default values | Omitted |
| Key ordering | Alphabetically sorted (recursive) |
This ensures byte-identical output across Studio, MCP, and manual edits.
Content Path Override
Models can override their content storage path with the content_path property:
{
"id": "docs",
"kind": "document",
"content_path": "content/docs"
}This stores files at {contentRoot}/content/docs/ instead of the default .contentrain/content/{domain}/{modelId}/ path. Path traversal (..) and sensitive directories (.github, node_modules, .env) are blocked.
Related Pages
- Field Types -- field definition schema and validation
- Content Engine -- how files are read and written
- Agent System -- how content is used in the system prompt