Skip to content

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.

json
{
  "version": 1,
  "stack": "nuxt",
  "locales": {
    "default": "en",
    "supported": ["en", "tr"]
  },
  "domains": ["marketing", "blog", "system"],
  "workflow": "auto-merge"
}
FieldTypeDescription
versionnumberConfig format version (currently 1)
stackstringFramework: nuxt, next, astro, sveltekit, remix, vue, react, other
locales.defaultstringDefault locale code
locales.supportedstring[]All supported locale codes
domainsstring[]Content domains (directory grouping)
workflowstringauto-merge or review

Model Definitions ​

Each model is a JSON file in .contentrain/models/{modelId}.json.

Collection Model ​

json
{
  "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 ​

json
{
  "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.

json
{
  "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.

json
{
  "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 ​

KindStorageEntry IDKey FormatFields
collectionJSON object map12-char hex{ entryId: { field: value } }Defined in model
singletonJSON flat objectN/A{ field: value }Defined in model
dictionaryJSON flat objectN/A{ "key": "string value" }None (free-form)
documentMarkdown filesSlugYAML frontmatter + bodyDefined in model

Content Files ​

Collection Content ​

File: .contentrain/content/{domain}/{modelId}/{locale}.json

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

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

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
markdown
---
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.

KindPathStructure
collection.contentrain/meta/{modelId}/{locale}.jsonObject-map: { entryId: EntryMeta }
singleton.contentrain/meta/{modelId}/{locale}.jsonSingle EntryMeta object
dictionary.contentrain/meta/{modelId}/{locale}.jsonSingle EntryMeta object
document.contentrain/meta/{modelId}/{slug}/{locale}.jsonSingle EntryMeta object

EntryMeta fields ​

FieldTypeDescription
statusstringdraft, in_review, published, rejected, or archived
sourcestringWho authored the entry: agent, human, or import
updated_bystringIdentity of the last writer -- an email or an agent id such as contentrain-mcp
approved_bystring | nullIdentity of the approver, or null when unapproved (optional)
versionstringOptional content version tag
publish_atstringOptional ISO timestamp for scheduled publishing
expire_atstringOptional ISO timestamp for content expiry

Collection meta -- an object-map keyed by entry ID:

json
{
  "a1b2c3d4e5f6": {
    "status": "published",
    "source": "agent",
    "updated_by": "contentrain-mcp",
    "approved_by": "user@example.com"
  }
}

Singleton, dictionary, and document meta -- a single object:

json
{
  "status": "draft",
  "source": "human",
  "updated_by": "user@example.com"
}

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:

json
{
  "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.

json
{
  "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"
  }
}
FieldTypeDescription
versionstringContext format version
lastOperation.toolstringTool that performed the write (e.g. contentrain_content_save)
lastOperation.modelstringModel the operation touched
lastOperation.localestringLocale the operation touched
lastOperation.entriesstring[]Entry IDs affected (optional)
lastOperation.timestampstringISO timestamp of the operation
lastOperation.sourcestringClient that performed the write: mcp-local, mcp-studio, or studio-ui
stats.modelsnumberTotal model count
stats.entriesnumberTotal entry count
stats.localesstring[]Locales in use
stats.lastSyncstringISO 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:

RuleValue
Indent2 spaces
EncodingUTF-8
Trailing newlineYes
Null valuesOmitted
Default valuesOmitted
Key orderingAlphabetically 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:

json
{
  "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.

Released under the AGPL-3.0 License.