@contentrain/skills 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -60
- package/dist/index.cjs +65 -1
- package/dist/index.d.cts +50 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +50 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +65 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -1
- package/skills/contentrain/SKILL.md +149 -0
- package/skills/contentrain/references/architecture.md +212 -0
- package/skills/contentrain/references/content-formats.md +279 -0
- package/skills/contentrain/references/i18n.md +298 -0
- package/skills/contentrain/references/mcp-pipelines.md +195 -0
- package/skills/contentrain/references/mcp-tools.md +308 -0
- package/skills/contentrain/references/model-kinds.md +330 -0
- package/skills/contentrain/references/schema-types.md +177 -0
- package/skills/contentrain/references/security.md +146 -0
- package/skills/contentrain/references/workflow.md +285 -0
- package/skills/contentrain-bulk/SKILL.md +99 -0
- package/skills/contentrain-content/SKILL.md +162 -0
- package/skills/contentrain-diff/SKILL.md +62 -0
- package/skills/contentrain-doctor/SKILL.md +62 -0
- package/skills/contentrain-generate/SKILL.md +183 -0
- package/skills/contentrain-generate/references/generated-client.md +198 -0
- package/skills/contentrain-init/SKILL.md +99 -0
- package/skills/contentrain-model/SKILL.md +141 -0
- package/skills/contentrain-normalize/SKILL.md +185 -0
- package/skills/contentrain-normalize/references/extraction.md +164 -0
- package/skills/contentrain-normalize/references/reuse.md +146 -0
- package/skills/contentrain-normalize/references/what-is-content.md +115 -0
- package/skills/contentrain-quality/SKILL.md +180 -0
- package/skills/contentrain-quality/references/accessibility.md +160 -0
- package/skills/contentrain-quality/references/content-quality.md +299 -0
- package/skills/contentrain-quality/references/media.md +170 -0
- package/skills/contentrain-quality/references/seo.md +229 -0
- package/skills/contentrain-review/SKILL.md +170 -0
- package/skills/contentrain-sdk/SKILL.md +145 -0
- package/skills/contentrain-sdk/references/bundler-config.md +135 -0
- package/skills/contentrain-serve/SKILL.md +96 -0
- package/skills/contentrain-translate/SKILL.md +180 -0
- package/skills/contentrain-validate-fix/SKILL.md +92 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: generated-client
|
|
3
|
+
description: "Generated client structure, framework bundler configs, and API patterns"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Generated Client Reference
|
|
7
|
+
|
|
8
|
+
## Bundler Alias Configuration
|
|
9
|
+
|
|
10
|
+
The `#contentrain` subpath import works natively in Node.js 22+ but **does NOT resolve in browser bundlers**. Configure an alias for your framework.
|
|
11
|
+
|
|
12
|
+
### Vite (Vue, React, Svelte, Astro)
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
// vite.config.ts
|
|
16
|
+
import { resolve } from 'node:path'
|
|
17
|
+
export default defineConfig({
|
|
18
|
+
resolve: {
|
|
19
|
+
alias: {
|
|
20
|
+
'#contentrain': resolve(__dirname, '.contentrain/client/index.mjs'),
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
})
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Also add a `paths` entry to `tsconfig.json` so the TypeScript language server resolves the alias:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"compilerOptions": {
|
|
31
|
+
"paths": {
|
|
32
|
+
"#contentrain": ["./.contentrain/client/index.d.ts"]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Next.js (webpack)
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
// next.config.js
|
|
42
|
+
const path = require('path')
|
|
43
|
+
module.exports = {
|
|
44
|
+
webpack: (config) => {
|
|
45
|
+
config.resolve.alias['#contentrain'] = path.resolve(__dirname, '.contentrain/client/index.mjs')
|
|
46
|
+
return config
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Add the same `tsconfig.json` paths entry:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"compilerOptions": {
|
|
56
|
+
"paths": {
|
|
57
|
+
"#contentrain": ["./.contentrain/client/index.d.ts"]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Nuxt 3
|
|
64
|
+
|
|
65
|
+
Nuxt provides a top-level `alias` option — no Vite config needed:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
// nuxt.config.ts
|
|
69
|
+
export default defineNuxtConfig({
|
|
70
|
+
alias: {
|
|
71
|
+
'#contentrain': './.contentrain/client/index.mjs',
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Add the `tsconfig.json` paths entry as above. Nuxt auto-extends `tsconfig.json` via `.nuxt/tsconfig.json`, so ensure the paths entry is in the project root `tsconfig.json`.
|
|
77
|
+
|
|
78
|
+
### SvelteKit
|
|
79
|
+
|
|
80
|
+
SvelteKit uses Vite internally. Add the alias in `vite.config.ts` as shown in the Vite section above.
|
|
81
|
+
|
|
82
|
+
### Expo / React Native (Metro)
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
// metro.config.js
|
|
86
|
+
const path = require('path')
|
|
87
|
+
module.exports = {
|
|
88
|
+
resolver: {
|
|
89
|
+
extraNodeModules: {
|
|
90
|
+
'#contentrain': path.resolve(__dirname, '.contentrain/client/index.mjs'),
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Pure Node.js / SSR-only
|
|
97
|
+
|
|
98
|
+
No alias needed. Node.js 22+ resolves `#contentrain` from `package.json` imports natively.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Complete SDK API Reference
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { query, singleton, dictionary, document } from '#contentrain'
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### QueryBuilder (for collection models) -- SYNC, no await needed
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
const posts = query('blog-post')
|
|
112
|
+
.locale('en') // set locale
|
|
113
|
+
.where('status', 'published') // exact match filter
|
|
114
|
+
.sort('date', 'desc') // sort by field
|
|
115
|
+
.limit(10) // limit results
|
|
116
|
+
.offset(5) // skip results
|
|
117
|
+
.include('author', 'tags') // resolve relation fields (1 level deep)
|
|
118
|
+
.all() // --> T[] (returns array)
|
|
119
|
+
.first() // --> T | undefined (first match)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### SingletonAccessor (for singleton models)
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
const hero = singleton('hero')
|
|
126
|
+
.locale('en')
|
|
127
|
+
.include('featured_post') // resolve relations on singletons too
|
|
128
|
+
.get() // --> T (single object)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### DictionaryAccessor (for dictionary models)
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
const allLabels = dictionary('ui-labels').locale('en').get() // --> Record<string, string>
|
|
135
|
+
const oneLabel = dictionary('ui-labels').locale('en').get('key') // --> string | undefined
|
|
136
|
+
const withParams = dictionary('ui-labels').locale('en').get('add-entry', { model: 'blog' }) // --> "Add a new entry to blog"
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Parameterized templates use `{placeholder}` syntax in dictionary values. The `get(key, params)` overload replaces `{name}` with the provided value. Unmatched placeholders are left as-is.
|
|
140
|
+
|
|
141
|
+
### DocumentQuery (for document models -- markdown + frontmatter)
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
const article = document('blog-article')
|
|
145
|
+
.locale('en')
|
|
146
|
+
.where('category', 'tech')
|
|
147
|
+
.include('author') // resolve relations in frontmatter
|
|
148
|
+
.bySlug('getting-started') // --> T | undefined (find by slug)
|
|
149
|
+
|
|
150
|
+
const docs = document('doc-page').locale('en').all() // --> T[]
|
|
151
|
+
const first = document('doc-page').locale('en').first() // --> T | undefined
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### DOES NOT EXIST -- never use these
|
|
155
|
+
|
|
156
|
+
- `.filter()` -- use `.where(field, value)` instead
|
|
157
|
+
- `.byId()` -- use `.where('id', value).first()` instead
|
|
158
|
+
- `.count()` -- use `.all().length` instead
|
|
159
|
+
- `dictionary().all()` -- use `.get()` instead
|
|
160
|
+
- Queries are SYNC -- do not use `await` with `query()`, `singleton()`, `dictionary()`, or `document()`
|
|
161
|
+
- `.where('field', 'eq', value)` -- just `.where('field', value)`
|
|
162
|
+
- `.get()` on QueryBuilder -- use `.all()` or `.first()`
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## With Relations
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
// Relations are resolved 1 level deep via .include()
|
|
170
|
+
const posts = query('blog-post')
|
|
171
|
+
.locale('en')
|
|
172
|
+
.include('author', 'tags')
|
|
173
|
+
.all()
|
|
174
|
+
// posts[0].author --> { id: '...', name: 'John', ... } (resolved object)
|
|
175
|
+
|
|
176
|
+
// Without include:
|
|
177
|
+
const raw = query('blog-post').locale('en').all()
|
|
178
|
+
// raw[0].author --> 'author-id-123' (raw string ID, NOT resolved)
|
|
179
|
+
|
|
180
|
+
// Singletons support include too:
|
|
181
|
+
const hero = singleton('hero').locale('en').include('featured_post').get()
|
|
182
|
+
|
|
183
|
+
// Documents support include too:
|
|
184
|
+
const article = document('blog-article').locale('en').include('author').bySlug('my-post')
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Framework-Specific Patterns
|
|
190
|
+
|
|
191
|
+
| Stack | Usage Pattern | Alias Setup |
|
|
192
|
+
|---|---|---|
|
|
193
|
+
| Nuxt 3 | `useAsyncData(() => singleton('hero').locale(locale).get())` | `nuxt.config.ts` alias |
|
|
194
|
+
| Next.js | In RSC: `const data = singleton('hero').locale('en').get()` | `next.config.js` webpack alias |
|
|
195
|
+
| Astro | In frontmatter: `const posts = query('blog-post').locale('en').all()` | `vite.config.ts` alias |
|
|
196
|
+
| SvelteKit | In `+page.server.ts`: `export const load = () => ({ hero: singleton('hero').locale('en').get() })` | `vite.config.ts` alias |
|
|
197
|
+
| Expo / RN | `const hero = singleton('hero').locale('en').get()` | `metro.config.js` resolver |
|
|
198
|
+
| Node.js / SSR | Direct import — no alias needed | Native subpath imports |
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: contentrain-init
|
|
3
|
+
description: "Initialize a new Contentrain project. Use when setting up .contentrain/ directory, configuring stack and locales, or scaffolding templates."
|
|
4
|
+
metadata:
|
|
5
|
+
author: Contentrain
|
|
6
|
+
version: "1.0.0"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Skill: Initialize Contentrain in a Project
|
|
10
|
+
|
|
11
|
+
> Set up Contentrain content infrastructure in an existing project.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## When to Use
|
|
16
|
+
|
|
17
|
+
The user wants to add Contentrain to their project, or says something like "set up contentrain", "initialize contentrain", "add content management".
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Steps
|
|
22
|
+
|
|
23
|
+
### 1. Detect the Tech Stack
|
|
24
|
+
|
|
25
|
+
Read `package.json` to identify the framework:
|
|
26
|
+
|
|
27
|
+
| Dependency | Stack |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `nuxt` | Nuxt 3 |
|
|
30
|
+
| `next` | Next.js |
|
|
31
|
+
| `@astrojs/astro` or `astro` | Astro |
|
|
32
|
+
| `@sveltejs/kit` | SvelteKit |
|
|
33
|
+
|
|
34
|
+
If none match, treat as a generic Node.js project. Report the detected stack to the user before proceeding.
|
|
35
|
+
|
|
36
|
+
### 2. Ask for Configuration
|
|
37
|
+
|
|
38
|
+
Ask the user for:
|
|
39
|
+
|
|
40
|
+
- **Supported locales:** Which languages will the project support? Default: `["en"]`. Use ISO 639-1 codes.
|
|
41
|
+
- **Source locale:** Which locale is the primary/source locale? Default: `"en"`.
|
|
42
|
+
- **Domain names:** Suggest domains based on the project structure (e.g., `marketing`, `blog`, `app`, `docs`). Ask the user to confirm or modify.
|
|
43
|
+
|
|
44
|
+
### 3. Initialize the Project
|
|
45
|
+
|
|
46
|
+
Call the MCP tool:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
contentrain_init(stack: "<detected>", locales: ["en", ...], domains: ["marketing", ...])
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
This creates the `.contentrain/` directory structure, `config.json`, and initial `context.json`.
|
|
53
|
+
|
|
54
|
+
### 4. Analyze Project Conventions
|
|
55
|
+
|
|
56
|
+
After initialization, review the project for conventions to inform content creation:
|
|
57
|
+
|
|
58
|
+
- **Tone:** Analyze existing copy in the project (README, landing page, UI text) to determine the appropriate tone (e.g., `professional`, `casual`, `technical`).
|
|
59
|
+
- **Naming conventions:** Note any patterns in file naming, component naming, or content structure.
|
|
60
|
+
|
|
61
|
+
> **Note:** Do NOT manually edit `context.json`. It is managed automatically by MCP tools and updated after every write operation.
|
|
62
|
+
|
|
63
|
+
### 5. Suggest Initial Models
|
|
64
|
+
|
|
65
|
+
Based on the project analysis, suggest models that would be useful:
|
|
66
|
+
|
|
67
|
+
- If the project has a landing page: suggest `hero` (singleton), `features` (singleton or collection).
|
|
68
|
+
- If the project has a blog section: suggest `blog-post` (document), `categories` (collection), `authors` (collection).
|
|
69
|
+
- If the project has UI text: suggest `ui-labels` (dictionary), `error-messages` (dictionary).
|
|
70
|
+
|
|
71
|
+
Present suggestions to the user and ask which to create. For selected models, call `contentrain_model_save` for each.
|
|
72
|
+
|
|
73
|
+
### 6. Offer Scaffold Templates
|
|
74
|
+
|
|
75
|
+
If the project matches a common pattern, offer to use a scaffold template:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
contentrain_scaffold(template: "blog", locales: ["en"], with_sample_content: true)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Available templates: `blog`, `landing`, `docs`, `ecommerce`, `saas`, `i18n`, `mobile`.
|
|
82
|
+
|
|
83
|
+
### 7. Set Up Agent Rules
|
|
84
|
+
|
|
85
|
+
Copy the appropriate rules file to the project:
|
|
86
|
+
|
|
87
|
+
- For Claude Code projects: append Contentrain rules to the project's `CLAUDE.md` or create a new section.
|
|
88
|
+
- For Cursor projects: create or update `.cursorrules` with Contentrain conventions.
|
|
89
|
+
|
|
90
|
+
Include a reference to the framework-specific guide (nuxt.md, next.md, astro.md, sveltekit.md).
|
|
91
|
+
|
|
92
|
+
### 8. Final Summary
|
|
93
|
+
|
|
94
|
+
Report to the user:
|
|
95
|
+
|
|
96
|
+
- What was created in `.contentrain/`.
|
|
97
|
+
- Which models were set up.
|
|
98
|
+
- How to import content in their code (show the `#contentrain` import).
|
|
99
|
+
- Next steps: create content, run `npx contentrain generate`, start the dev server.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: contentrain-model
|
|
3
|
+
description: "Design and save Contentrain model definitions. Use when creating models, updating schemas, adding fields, or changing model structure."
|
|
4
|
+
metadata:
|
|
5
|
+
author: Contentrain
|
|
6
|
+
version: "1.0.0"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Skill: Design and Save Models
|
|
10
|
+
|
|
11
|
+
> Create or evolve Contentrain model definitions safely.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## When to Use
|
|
16
|
+
|
|
17
|
+
Use this when the user wants to:
|
|
18
|
+
|
|
19
|
+
- add a new model
|
|
20
|
+
- change fields on an existing model
|
|
21
|
+
- choose between `singleton`, `collection`, `document`, and `dictionary`
|
|
22
|
+
- define relations, locales, or custom content paths
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Steps
|
|
27
|
+
|
|
28
|
+
### 1. Inspect Project State
|
|
29
|
+
|
|
30
|
+
Call `contentrain_status` first:
|
|
31
|
+
|
|
32
|
+
- confirm the project is initialized
|
|
33
|
+
- see existing model IDs and domains
|
|
34
|
+
- avoid creating duplicates
|
|
35
|
+
|
|
36
|
+
If the user is extending an existing model, call `contentrain_describe(model: "<model-id>")`.
|
|
37
|
+
|
|
38
|
+
### 2. Confirm Storage Contract
|
|
39
|
+
|
|
40
|
+
Call `contentrain_describe_format` before proposing structure changes.
|
|
41
|
+
|
|
42
|
+
Use it to confirm:
|
|
43
|
+
|
|
44
|
+
- model kinds and storage expectations
|
|
45
|
+
- locale behavior
|
|
46
|
+
- document vs JSON model tradeoffs
|
|
47
|
+
- how custom `content_path` and `locale_strategy` affect files
|
|
48
|
+
|
|
49
|
+
### 3. Choose the Right Kind
|
|
50
|
+
|
|
51
|
+
- `singleton`: one object per locale, e.g. hero, navigation, footer
|
|
52
|
+
- `collection`: repeated entries with IDs, e.g. testimonials, faq items, authors
|
|
53
|
+
- `document`: long-form markdown content with frontmatter, e.g. blog posts, docs pages
|
|
54
|
+
- `dictionary`: flat key-value strings, e.g. UI labels, error messages
|
|
55
|
+
|
|
56
|
+
Default rules:
|
|
57
|
+
|
|
58
|
+
- use `dictionary` for UI/system strings
|
|
59
|
+
- use `document` for markdown-heavy long-form content
|
|
60
|
+
- use `collection` for repeated structured items
|
|
61
|
+
- use `singleton` for one page/section config per locale
|
|
62
|
+
|
|
63
|
+
### 4. Design Fields
|
|
64
|
+
|
|
65
|
+
Follow these rules:
|
|
66
|
+
|
|
67
|
+
- model IDs must be kebab-case
|
|
68
|
+
- field names must be snake_case
|
|
69
|
+
- relation fields must define a target `model`
|
|
70
|
+
- prefer small, explicit schemas over large generic `object` blobs
|
|
71
|
+
- only mark fields as `required` when the content truly cannot function without them
|
|
72
|
+
|
|
73
|
+
Relation guidance:
|
|
74
|
+
|
|
75
|
+
- `relation`: single reference
|
|
76
|
+
- `relations`: multiple references
|
|
77
|
+
- use `slug`-driven relations for document-like linking
|
|
78
|
+
|
|
79
|
+
### 5. Decide i18n and Path Strategy
|
|
80
|
+
|
|
81
|
+
Ask or infer:
|
|
82
|
+
|
|
83
|
+
- should this model be localized?
|
|
84
|
+
- should it use the default `.contentrain/content/...` path or a custom `content_path`?
|
|
85
|
+
- if custom path is needed, which `locale_strategy` matches the project layout?
|
|
86
|
+
|
|
87
|
+
Use custom paths only when the framework or repo structure clearly benefits from it.
|
|
88
|
+
|
|
89
|
+
### 6. Present the Proposed Model
|
|
90
|
+
|
|
91
|
+
Before writing, show:
|
|
92
|
+
|
|
93
|
+
- model ID
|
|
94
|
+
- kind
|
|
95
|
+
- domain
|
|
96
|
+
- `i18n` behavior
|
|
97
|
+
- field list with types
|
|
98
|
+
- relation targets
|
|
99
|
+
- any custom `content_path` / `locale_strategy`
|
|
100
|
+
|
|
101
|
+
Get user confirmation before saving.
|
|
102
|
+
|
|
103
|
+
### 7. Save the Model
|
|
104
|
+
|
|
105
|
+
Call `contentrain_model_save` with the approved definition.
|
|
106
|
+
|
|
107
|
+
Example:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"id": "hero",
|
|
112
|
+
"name": "Hero",
|
|
113
|
+
"kind": "singleton",
|
|
114
|
+
"domain": "marketing",
|
|
115
|
+
"i18n": true,
|
|
116
|
+
"fields": {
|
|
117
|
+
"title": { "type": "string", "required": true },
|
|
118
|
+
"subtitle": { "type": "text" },
|
|
119
|
+
"cta_label": { "type": "string" },
|
|
120
|
+
"featured_post": { "type": "relation", "model": "blog-post" }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### 8. Validate Downstream Impact
|
|
126
|
+
|
|
127
|
+
After changing a model:
|
|
128
|
+
|
|
129
|
+
- review whether existing content now needs updates
|
|
130
|
+
- call `contentrain_validate`
|
|
131
|
+
- if the model is consumed by the SDK, recommend `contentrain generate`
|
|
132
|
+
|
|
133
|
+
### 9. Final Summary
|
|
134
|
+
|
|
135
|
+
Report:
|
|
136
|
+
|
|
137
|
+
- model created or updated
|
|
138
|
+
- kind and domain
|
|
139
|
+
- field count
|
|
140
|
+
- any relation targets
|
|
141
|
+
- whether content migration is needed next
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: contentrain-normalize
|
|
3
|
+
description: "Two-phase normalize flow: extract hardcoded strings from source code into .contentrain/ (Phase 1) and patch source files with content references (Phase 2). Use when normalizing, extracting content, or replacing hardcoded strings."
|
|
4
|
+
metadata:
|
|
5
|
+
author: Contentrain
|
|
6
|
+
version: "1.0.0"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Contentrain Normalize
|
|
10
|
+
|
|
11
|
+
Normalize converts a codebase with hardcoded strings into a Contentrain-managed content architecture. It runs in two independent phases, each producing a separate branch for review.
|
|
12
|
+
|
|
13
|
+
- **Phase 1 (Extraction):** Pull content from source code into `.contentrain/` structure. Source files are NOT modified.
|
|
14
|
+
- **Phase 2 (Reuse):** Patch source files to replace hardcoded strings with content references. Requires completed extraction.
|
|
15
|
+
|
|
16
|
+
Phase 1 alone is valuable: content becomes manageable in Studio, translatable, and publishable without touching source code.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Two-Phase Architecture
|
|
21
|
+
|
|
22
|
+
| Aspect | Phase 1: Extraction | Phase 2: Reuse |
|
|
23
|
+
|--------|-------------------|----------------|
|
|
24
|
+
| Purpose | Pull content from source to `.contentrain/` | Patch source files with content references |
|
|
25
|
+
| Scope | Full project scan | Per model or per domain |
|
|
26
|
+
| Source files modified | No | Yes |
|
|
27
|
+
| Branch pattern | `contentrain/normalize/extract/{domain}/{timestamp}` | `contentrain/normalize/reuse/{model}/{locale}/{timestamp}` |
|
|
28
|
+
| Prerequisite | Initialized `.contentrain/` | Completed extraction (content exists in `.contentrain/`) |
|
|
29
|
+
| Workflow mode | Always `review` | Always `review` |
|
|
30
|
+
| Standalone value | Yes -- content is manageable in Studio immediately | Depends on Phase 1 |
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Agent vs MCP Responsibilities
|
|
35
|
+
|
|
36
|
+
### Agent (Intelligence Layer)
|
|
37
|
+
|
|
38
|
+
- **Decide what is content vs code.** Semantic judgment requiring context understanding.
|
|
39
|
+
- **Assign domain grouping.** Determine which domain each piece of content belongs to.
|
|
40
|
+
- **Determine model structure.** Group related content into models, choose the right kind.
|
|
41
|
+
- **Create replacement expressions.** Stack-specific, requires framework knowledge (Vue/Nuxt, React/Next, Svelte, Astro, SDK).
|
|
42
|
+
- **Filter false positives** from scan candidates.
|
|
43
|
+
- **Evaluate and group** candidates into logical models.
|
|
44
|
+
|
|
45
|
+
### MCP (Deterministic Infrastructure)
|
|
46
|
+
|
|
47
|
+
- **Scan the file system** (build graph, find candidates).
|
|
48
|
+
- **Read and write files** in `.contentrain/`.
|
|
49
|
+
- **Patch source files** with exact string replacement (agent provides the replacement expression).
|
|
50
|
+
- **Create branches, commit, validate** all changes.
|
|
51
|
+
- **Enforce guardrails** (file limits, type restrictions, dry-run requirement).
|
|
52
|
+
|
|
53
|
+
MCP is framework-agnostic. It does NOT know how `{t('key')}` differs from `{{ $t('key') }}`. The agent provides all stack-specific logic.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Guardrails
|
|
58
|
+
|
|
59
|
+
| Guardrail | Limit | Rationale |
|
|
60
|
+
|-----------|-------|-----------|
|
|
61
|
+
| Allowed source file types | `.vue`, `.tsx`, `.jsx`, `.ts`, `.js`, `.mjs`, `.astro`, `.svelte` | Only scan files that can contain UI content |
|
|
62
|
+
| Max files per scan | 500 | Prevent scanning entire monorepos |
|
|
63
|
+
| Max files per apply | 100 | Keep diffs reviewable |
|
|
64
|
+
| Dry-run before apply | **MANDATORY** | Preview all changes before executing |
|
|
65
|
+
| Workflow mode | Always `review` | Normalize changes are never auto-merged |
|
|
66
|
+
| Reuse scope | Per model or per domain | No whole-project reuse in one operation |
|
|
67
|
+
| Reuse prerequisite | Extraction must be complete | Content must exist in `.contentrain/` before patching source |
|
|
68
|
+
|
|
69
|
+
Attempting to apply without a prior dry-run will be rejected. Exceeding file limits will truncate results with a warning.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Phase 1: Extraction (Step-by-Step)
|
|
74
|
+
|
|
75
|
+
### 1. Check Project State
|
|
76
|
+
|
|
77
|
+
Call `contentrain_status` to confirm the project is initialized and note supported locales. Call `contentrain_describe_format` to understand the four storage formats (dictionary, collection, singleton, document) before deciding model structure.
|
|
78
|
+
|
|
79
|
+
### 2. Build the Project Graph
|
|
80
|
+
|
|
81
|
+
Call `contentrain_scan(mode: "graph")` to build the import/component dependency graph. Use this to understand which components belong to which pages, identify shared vs page-specific components, and prioritize files.
|
|
82
|
+
|
|
83
|
+
### 3. Find Candidates
|
|
84
|
+
|
|
85
|
+
Call `contentrain_scan(mode: "candidates")` to find hardcoded strings. The scan returns candidates with file paths, line numbers, string values, and surrounding context.
|
|
86
|
+
|
|
87
|
+
### 4. Evaluate Candidates (Agent Intelligence)
|
|
88
|
+
|
|
89
|
+
- **Filter false positives:** Remove CSS values, technical identifiers, import paths, variable names, config values, log messages, test strings. See [What Is Content](references/what-is-content.md) for full heuristics.
|
|
90
|
+
- **Assign domains:** Group candidates by domain (e.g., `marketing`, `blog`, `ui`, `system`).
|
|
91
|
+
- **Determine model types:** Choose dictionary, singleton, collection, or document kind for each group.
|
|
92
|
+
- **Structure fields:** Define field names and assign candidate strings to fields.
|
|
93
|
+
|
|
94
|
+
### 5. Preview Extraction
|
|
95
|
+
|
|
96
|
+
Call `contentrain_apply(mode: "extract", dry_run: true)` to generate a preview. Review the dry-run output: verify model definitions, content assignments, and check for missed or misclassified candidates. Show the preview to the user.
|
|
97
|
+
|
|
98
|
+
### 6. Execute Extraction
|
|
99
|
+
|
|
100
|
+
After user approval, call `contentrain_apply(mode: "extract", dry_run: false)`. This creates model definitions and content files in `.contentrain/` on a `contentrain/normalize/extract/{timestamp}` branch. Source files are NOT modified.
|
|
101
|
+
|
|
102
|
+
### 7. Validate and Submit
|
|
103
|
+
|
|
104
|
+
Call `contentrain_validate` to check schema compliance, i18n completeness, and duplicate entries. Call `contentrain_submit` to push the branch for review. Normalize operations always use `review` workflow mode.
|
|
105
|
+
|
|
106
|
+
Tell the user: "Phase 1 complete. Content is now in Contentrain and can be managed, translated, and published from Studio. When ready, proceed with Phase 2 to update source files."
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Phase 2: Reuse (Step-by-Step)
|
|
111
|
+
|
|
112
|
+
Only start after Phase 1 is reviewed and merged.
|
|
113
|
+
|
|
114
|
+
### 1. Select Scope
|
|
115
|
+
|
|
116
|
+
List the extracted models and ask the user which model or domain to process first. Process one model or domain at a time to keep diffs small and reviewable.
|
|
117
|
+
|
|
118
|
+
### 2. Determine Replacement Expressions (Agent Intelligence)
|
|
119
|
+
|
|
120
|
+
Based on the project's tech stack, determine the correct replacement pattern. See [Reuse Details](references/reuse.md) for the full replacement expressions table by stack. Also determine any necessary import statements for patched files.
|
|
121
|
+
|
|
122
|
+
### 3. Preview Reuse
|
|
123
|
+
|
|
124
|
+
Call `contentrain_apply(mode: "reuse", scope: { model: "<model-id>" }, patches: [...], dry_run: true)`. Review: verify each replacement is correct, import statements will be added, no non-content strings are being replaced, component structure is preserved.
|
|
125
|
+
|
|
126
|
+
### 4. Execute Reuse
|
|
127
|
+
|
|
128
|
+
After user confirmation, call `contentrain_apply(mode: "reuse", scope: { model: "<model-id>" }, patches: [...], dry_run: false)`. This patches source files and creates a `contentrain/normalize/reuse/{model}/{timestamp}` branch.
|
|
129
|
+
|
|
130
|
+
### 5. Validate and Submit
|
|
131
|
+
|
|
132
|
+
Call `contentrain_validate` to verify source files parse correctly, content references resolve, and no strings were missed or double-replaced. Call `contentrain_submit` to push the branch for review.
|
|
133
|
+
|
|
134
|
+
### 6. Repeat
|
|
135
|
+
|
|
136
|
+
Ask the user which model or domain to process next. Repeat steps 1-5 for each remaining model until all extracted content is referenced in source code.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Domain and Model Grouping Guidelines
|
|
141
|
+
|
|
142
|
+
### Domain Assignment
|
|
143
|
+
|
|
144
|
+
| Content Location | Suggested Domain |
|
|
145
|
+
|------------------|-----------------|
|
|
146
|
+
| Landing page, marketing sections | `marketing` |
|
|
147
|
+
| Blog, articles, posts | `blog` |
|
|
148
|
+
| Navigation, footer, header | `ui` |
|
|
149
|
+
| Error messages, validation | `system` |
|
|
150
|
+
| Product pages, e-commerce | `product` |
|
|
151
|
+
| Documentation, help | `docs` |
|
|
152
|
+
| User-facing app strings | `app` |
|
|
153
|
+
|
|
154
|
+
### Model Kind Selection
|
|
155
|
+
|
|
156
|
+
| Content Pattern | Kind | Rationale |
|
|
157
|
+
|----------------|------|-----------|
|
|
158
|
+
| One set of fields per page section | `singleton` | Hero, features, pricing header |
|
|
159
|
+
| Multiple items of same type | `collection` | Team members, FAQs, testimonials |
|
|
160
|
+
| Long-form with metadata | `document` | Blog posts, documentation pages |
|
|
161
|
+
| Key-value UI strings | `dictionary` | Error messages, button labels, form labels |
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Common Mistakes
|
|
166
|
+
|
|
167
|
+
| Mistake | Correct Approach |
|
|
168
|
+
|---------|-----------------|
|
|
169
|
+
| Extracting CSS values as content | Only extract user-visible text |
|
|
170
|
+
| Creating one model per component | Group related content into shared models |
|
|
171
|
+
| Skipping dry-run | ALWAYS preview before apply |
|
|
172
|
+
| Auto-merging normalize changes | Normalize ALWAYS uses review mode |
|
|
173
|
+
| Reusing before extraction is merged | Wait for extraction review and merge first |
|
|
174
|
+
| Processing all models in one reuse | Scope reuse to one model/domain at a time |
|
|
175
|
+
| Ignoring project graph | Use graph output to understand component relationships |
|
|
176
|
+
| Hardcoding replacement patterns | Detect the project's i18n stack and use its conventions |
|
|
177
|
+
| Extracting strings with runtime variables | Split parameterized strings: extract the static part, leave interpolation in code |
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## References
|
|
182
|
+
|
|
183
|
+
- [Extraction Details](references/extraction.md) -- Phase 1 extraction rules, parameters, and examples
|
|
184
|
+
- [Reuse Details](references/reuse.md) -- Phase 2 replacement expressions by stack and patching rules
|
|
185
|
+
- [What Is Content](references/what-is-content.md) -- Heuristics for identifying content vs code strings
|