@contentrain/skills 0.8.1 → 0.9.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 CHANGED
@@ -26,7 +26,7 @@ pnpm add @contentrain/skills
26
26
  ### Via skills CLI (install skills into your AI agent)
27
27
 
28
28
  ```bash
29
- # Install all 15 skills
29
+ # Install all 16 skills
30
30
  npx skills add Contentrain/ai/packages/skills
31
31
 
32
32
  # Install a specific skill
@@ -64,6 +64,7 @@ Published under `skills/` — 15 production skills:
64
64
  | `contentrain-serve` | Start the local review and normalize UI |
65
65
  | `contentrain-diff` | View content diffs between branches |
66
66
  | `contentrain-doctor` | Diagnose project health issues |
67
+ | `contentrain-migrate-wordpress` | Import a WordPress site and wire it to a framework |
67
68
 
68
69
  Each skill directory contains:
69
70
 
package/dist/index.cjs CHANGED
@@ -66,6 +66,10 @@ const AGENT_SKILLS = [
66
66
  {
67
67
  name: "contentrain-doctor",
68
68
  description: "Diagnose project health issues"
69
+ },
70
+ {
71
+ name: "contentrain-migrate-wordpress",
72
+ description: "Import a WordPress site into a .contentrain store and wire it to a framework"
69
73
  }
70
74
  ];
71
75
  const WORKFLOW_SKILLS = [
package/dist/index.d.cts CHANGED
@@ -50,6 +50,9 @@ declare const AGENT_SKILLS: readonly [{
50
50
  }, {
51
51
  readonly name: "contentrain-doctor";
52
52
  readonly description: "Diagnose project health issues";
53
+ }, {
54
+ readonly name: "contentrain-migrate-wordpress";
55
+ readonly description: "Import a WordPress site into a .contentrain store and wire it to a framework";
53
56
  }];
54
57
  type AgentSkillName = (typeof AGENT_SKILLS)[number]['name'];
55
58
  declare const WORKFLOW_SKILLS: readonly ["contentrain-init", "contentrain-model", "contentrain-content", "contentrain-bulk", "contentrain-normalize", "contentrain-validate-fix", "contentrain-review", "contentrain-diff", "contentrain-doctor", "contentrain-serve", "contentrain-translate", "contentrain-generate"];
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../src/index.ts"],"mappings":";;AASA;;;;;cAAa,YAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAkBD,cAAA,WAAyB,YAAA;AAAA,cAIxB,eAAA;AAAA,KAeD,aAAA,WAAwB,eAAA;AAAA,cAIvB,gBAAA;AAAA,KACD,cAAA,WAAyB,gBAAA"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/index.ts"],"mappings":";;AASA;;;;;cAAa,YAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAmBD,cAAA,WAAyB,YAAA;AAAA,cAIxB,eAAA;AAAA,KAeD,aAAA,WAAwB,eAAA;AAAA,cAIvB,gBAAA;AAAA,KACD,cAAA,WAAyB,gBAAA"}
package/dist/index.d.mts CHANGED
@@ -50,6 +50,9 @@ declare const AGENT_SKILLS: readonly [{
50
50
  }, {
51
51
  readonly name: "contentrain-doctor";
52
52
  readonly description: "Diagnose project health issues";
53
+ }, {
54
+ readonly name: "contentrain-migrate-wordpress";
55
+ readonly description: "Import a WordPress site into a .contentrain store and wire it to a framework";
53
56
  }];
54
57
  type AgentSkillName = (typeof AGENT_SKILLS)[number]['name'];
55
58
  declare const WORKFLOW_SKILLS: readonly ["contentrain-init", "contentrain-model", "contentrain-content", "contentrain-bulk", "contentrain-normalize", "contentrain-validate-fix", "contentrain-review", "contentrain-diff", "contentrain-doctor", "contentrain-serve", "contentrain-translate", "contentrain-generate"];
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;AASA;;;;;cAAa,YAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAkBD,cAAA,WAAyB,YAAA;AAAA,cAIxB,eAAA;AAAA,KAeD,aAAA,WAAwB,eAAA;AAAA,cAIvB,gBAAA;AAAA,KACD,cAAA,WAAyB,gBAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;AASA;;;;;cAAa,YAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAmBD,cAAA,WAAyB,YAAA;AAAA,cAIxB,eAAA;AAAA,KAeD,aAAA,WAAwB,eAAA;AAAA,cAIvB,gBAAA;AAAA,KACD,cAAA,WAAyB,gBAAA"}
package/dist/index.mjs CHANGED
@@ -65,6 +65,10 @@ const AGENT_SKILLS = [
65
65
  {
66
66
  name: "contentrain-doctor",
67
67
  description: "Diagnose project health issues"
68
+ },
69
+ {
70
+ name: "contentrain-migrate-wordpress",
71
+ description: "Import a WordPress site into a .contentrain store and wire it to a framework"
68
72
  }
69
73
  ];
70
74
  const WORKFLOW_SKILLS = [
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @contentrain/skills — AI agent skills for Contentrain\n *\n * Agent Skills standard format: SKILL.md + references/ for progressive disclosure.\n * Also includes workflow procedures and framework integration guides.\n */\n\n// ─── Agent Skills Catalog ───\n\nexport const AGENT_SKILLS = [\n { name: 'contentrain', description: 'Contentrain CMS architecture, content formats, and MCP tool usage' },\n { name: 'contentrain-normalize', description: 'Two-phase normalize: extract hardcoded strings and patch source files' },\n { name: 'contentrain-quality', description: 'Content quality, SEO, accessibility, and media rules' },\n { name: 'contentrain-sdk', description: 'Query SDK usage: #contentrain imports, QueryBuilder, type-safe content access' },\n { name: 'contentrain-content', description: 'Create and manage content entries for existing models' },\n { name: 'contentrain-model', description: 'Design and save model definitions' },\n { name: 'contentrain-init', description: 'Initialize a new Contentrain project' },\n { name: 'contentrain-bulk', description: 'Batch operations on content entries' },\n { name: 'contentrain-validate-fix', description: 'Validate content and auto-fix structural issues' },\n { name: 'contentrain-review', description: 'Review content changes before publishing' },\n { name: 'contentrain-translate', description: 'Translate content across supported locales' },\n { name: 'contentrain-generate', description: 'Generate the typed SDK client from models' },\n { name: 'contentrain-serve', description: 'Start the local review and normalize UI' },\n { name: 'contentrain-diff', description: 'View content diffs between branches' },\n { name: 'contentrain-doctor', description: 'Diagnose project health issues' },\n] as const\n\nexport type AgentSkillName = (typeof AGENT_SKILLS)[number]['name']\n\n// ─── Workflow Skills (backward compat) ───\n\nexport const WORKFLOW_SKILLS = [\n 'contentrain-init',\n 'contentrain-model',\n 'contentrain-content',\n 'contentrain-bulk',\n 'contentrain-normalize',\n 'contentrain-validate-fix',\n 'contentrain-review',\n 'contentrain-diff',\n 'contentrain-doctor',\n 'contentrain-serve',\n 'contentrain-translate',\n 'contentrain-generate',\n] as const\n\nexport type WorkflowSkill = (typeof WORKFLOW_SKILLS)[number]\n\n// ─── Framework Guides ───\n\nexport const FRAMEWORK_GUIDES = ['nuxt', 'next', 'astro', 'sveltekit', 'vue', 'react', 'expo', 'react-native', 'node'] as const\nexport type FrameworkGuide = (typeof FRAMEWORK_GUIDES)[number]\n"],"mappings":";;;;;;;AASA,MAAa,eAAe;CAC1B;EAAE,MAAM;EAAe,aAAa;EAAqE;CACzG;EAAE,MAAM;EAAyB,aAAa;EAAyE;CACvH;EAAE,MAAM;EAAuB,aAAa;EAAwD;CACpG;EAAE,MAAM;EAAmB,aAAa;EAAiF;CACzH;EAAE,MAAM;EAAuB,aAAa;EAAyD;CACrG;EAAE,MAAM;EAAqB,aAAa;EAAqC;CAC/E;EAAE,MAAM;EAAoB,aAAa;EAAwC;CACjF;EAAE,MAAM;EAAoB,aAAa;EAAuC;CAChF;EAAE,MAAM;EAA4B,aAAa;EAAmD;CACpG;EAAE,MAAM;EAAsB,aAAa;EAA4C;CACvF;EAAE,MAAM;EAAyB,aAAa;EAA8C;CAC5F;EAAE,MAAM;EAAwB,aAAa;EAA6C;CAC1F;EAAE,MAAM;EAAqB,aAAa;EAA2C;CACrF;EAAE,MAAM;EAAoB,aAAa;EAAuC;CAChF;EAAE,MAAM;EAAsB,aAAa;EAAkC;CAC9E;AAMD,MAAa,kBAAkB;CAC7B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AAMD,MAAa,mBAAmB;CAAC;CAAQ;CAAQ;CAAS;CAAa;CAAO;CAAS;CAAQ;CAAgB;CAAO"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @contentrain/skills — AI agent skills for Contentrain\n *\n * Agent Skills standard format: SKILL.md + references/ for progressive disclosure.\n * Also includes workflow procedures and framework integration guides.\n */\n\n// ─── Agent Skills Catalog ───\n\nexport const AGENT_SKILLS = [\n { name: 'contentrain', description: 'Contentrain CMS architecture, content formats, and MCP tool usage' },\n { name: 'contentrain-normalize', description: 'Two-phase normalize: extract hardcoded strings and patch source files' },\n { name: 'contentrain-quality', description: 'Content quality, SEO, accessibility, and media rules' },\n { name: 'contentrain-sdk', description: 'Query SDK usage: #contentrain imports, QueryBuilder, type-safe content access' },\n { name: 'contentrain-content', description: 'Create and manage content entries for existing models' },\n { name: 'contentrain-model', description: 'Design and save model definitions' },\n { name: 'contentrain-init', description: 'Initialize a new Contentrain project' },\n { name: 'contentrain-bulk', description: 'Batch operations on content entries' },\n { name: 'contentrain-validate-fix', description: 'Validate content and auto-fix structural issues' },\n { name: 'contentrain-review', description: 'Review content changes before publishing' },\n { name: 'contentrain-translate', description: 'Translate content across supported locales' },\n { name: 'contentrain-generate', description: 'Generate the typed SDK client from models' },\n { name: 'contentrain-serve', description: 'Start the local review and normalize UI' },\n { name: 'contentrain-diff', description: 'View content diffs between branches' },\n { name: 'contentrain-doctor', description: 'Diagnose project health issues' },\n { name: 'contentrain-migrate-wordpress', description: 'Import a WordPress site into a .contentrain store and wire it to a framework' },\n] as const\n\nexport type AgentSkillName = (typeof AGENT_SKILLS)[number]['name']\n\n// ─── Workflow Skills (backward compat) ───\n\nexport const WORKFLOW_SKILLS = [\n 'contentrain-init',\n 'contentrain-model',\n 'contentrain-content',\n 'contentrain-bulk',\n 'contentrain-normalize',\n 'contentrain-validate-fix',\n 'contentrain-review',\n 'contentrain-diff',\n 'contentrain-doctor',\n 'contentrain-serve',\n 'contentrain-translate',\n 'contentrain-generate',\n] as const\n\nexport type WorkflowSkill = (typeof WORKFLOW_SKILLS)[number]\n\n// ─── Framework Guides ───\n\nexport const FRAMEWORK_GUIDES = ['nuxt', 'next', 'astro', 'sveltekit', 'vue', 'react', 'expo', 'react-native', 'node'] as const\nexport type FrameworkGuide = (typeof FRAMEWORK_GUIDES)[number]\n"],"mappings":";;;;;;;AASA,MAAa,eAAe;CAC1B;EAAE,MAAM;EAAe,aAAa;EAAqE;CACzG;EAAE,MAAM;EAAyB,aAAa;EAAyE;CACvH;EAAE,MAAM;EAAuB,aAAa;EAAwD;CACpG;EAAE,MAAM;EAAmB,aAAa;EAAiF;CACzH;EAAE,MAAM;EAAuB,aAAa;EAAyD;CACrG;EAAE,MAAM;EAAqB,aAAa;EAAqC;CAC/E;EAAE,MAAM;EAAoB,aAAa;EAAwC;CACjF;EAAE,MAAM;EAAoB,aAAa;EAAuC;CAChF;EAAE,MAAM;EAA4B,aAAa;EAAmD;CACpG;EAAE,MAAM;EAAsB,aAAa;EAA4C;CACvF;EAAE,MAAM;EAAyB,aAAa;EAA8C;CAC5F;EAAE,MAAM;EAAwB,aAAa;EAA6C;CAC1F;EAAE,MAAM;EAAqB,aAAa;EAA2C;CACrF;EAAE,MAAM;EAAoB,aAAa;EAAuC;CAChF;EAAE,MAAM;EAAsB,aAAa;EAAkC;CAC7E;EAAE,MAAM;EAAiC,aAAa;EAAgF;CACvI;AAMD,MAAa,kBAAkB;CAC7B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AAMD,MAAa,mBAAmB;CAAC;CAAQ;CAAQ;CAAS;CAAa;CAAO;CAAS;CAAQ;CAAgB;CAAO"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentrain/skills",
3
- "version": "0.8.1",
3
+ "version": "0.9.1",
4
4
  "license": "MIT",
5
5
  "description": "AI agent skills for Contentrain — workflow procedures, framework integration guides",
6
6
  "type": "module",
@@ -61,8 +61,9 @@
61
61
  "tsdown": "^0.21.0",
62
62
  "typescript": "^5.7.0",
63
63
  "vitest": "^3.0.0",
64
- "@contentrain/mcp": "3.1.8",
65
- "@contentrain/rules": "0.7.1"
64
+ "@contentrain/mcp": "3.3.1",
65
+ "@contentrain/rules": "0.7.1",
66
+ "@contentrain/wp-import": "0.2.2"
66
67
  },
67
68
  "scripts": {
68
69
  "build": "tsdown src/index.ts --format esm,cjs --dts",
@@ -416,7 +416,8 @@ Run git-backed batch operations on existing content entries.
416
416
  | `slugs` | string[] | Conditional | For `update_status` on documents — documents are keyed by slug, not entry ID |
417
417
  | `locale` | string | No | Scope `update_status` to one locale (i18n models); default is every supported locale |
418
418
  | `status` | string | Conditional | For `update_status` |
419
- | `confirm` | boolean | Conditional | For `delete_entries` (must be `true`) |
419
+ | `confirm` | boolean | Conditional | For `delete_entries` (must be `true`; not needed while `dry_run: true`) |
420
+ | `dry_run` | boolean | No | Preview: runs the operation in a throwaway worktree and reports what would change, committing nothing. Default `false` |
420
421
 
421
422
  #### Operations
422
423
 
@@ -428,8 +429,9 @@ Run git-backed batch operations on existing content entries.
428
429
 
429
430
  #### Rules
430
431
 
432
+ - ALWAYS preview with `dry_run: true` first — the response carries `would_copy` / `would_replace`, `would_update` + `not_found`, or `would_delete` + `files_to_remove`, and nothing survives the run
431
433
  - ALWAYS verify the target model with `contentrain_describe` or `contentrain_status` before running bulk
432
- - `copy_locale` MUST NOT be used on non-i18n models
434
+ - `copy_locale` MUST NOT be used on non-i18n models, and it REPLACES the target locale — `would_replace` in the preview is how many records that costs
433
435
  - Bulk operations create branches and commits like other write tools
434
436
  - Validate afterward when content shape may have changed
435
437
 
@@ -49,6 +49,8 @@ Confirm:
49
49
  - `publish_at` on `contentrain_content_save` does **not** publish — it gates delivery of an already-published entry. `update_status` is the only way to change status.
50
50
  - `delete_entries` is collection-only
51
51
  - `update_status` rewrites every supported locale unless you pass `locale`. Pass it when restoring one locale's status, or you will change the other's too.
52
+ - **preview first**: `dry_run: true` runs the operation in a throwaway worktree and reports what would change (`would_copy`/`would_replace`, `would_update` + `not_found`, `would_delete` + `files_to_remove`) without committing anything. A preview needs no `confirm`
53
+ - `copy_locale` REPLACES the target locale — the preview's `would_replace` is how many records that costs
52
54
  - confirm entry IDs before delete operations
53
55
  - batch only related entries together
54
56
 
@@ -0,0 +1,147 @@
1
+ ---
2
+ name: contentrain-migrate-wordpress
3
+ description: "Import a WordPress site into a .contentrain content store and wire it to a framework. Use when migrating from WordPress, running contentrain import, handling a WXR export or REST URL, or connecting imported content to Astro, Nuxt or Next."
4
+ metadata:
5
+ author: Contentrain
6
+ version: "1.0.0"
7
+ ---
8
+
9
+ # Skill: Migrate a WordPress Site
10
+
11
+ > WordPress content → canonical `.contentrain` store → validated → typed client or Astro collections.
12
+
13
+ ---
14
+
15
+ ## When to Use
16
+
17
+ The user wants to move a WordPress site's **content** into Contentrain: "import my WordPress site", "migrate from WP", "I have a WXR export", "pull posts from wordpress.com", "turn this WP blog into Astro".
18
+
19
+ **This skill moves content, not design.** Themes, page-builder layouts and rendered HTML are a different problem (a full site clone is the Contentrain migration service). What this skill produces is a clean content store the user's own frontend reads.
20
+
21
+ ---
22
+
23
+ ## Steps
24
+
25
+ ### 1. Pick the source
26
+
27
+ Two sources, and the choice is usually made for you:
28
+
29
+ | Source | Use when | Command |
30
+ |---|---|---|
31
+ | **REST URL** | The site is online and `/wp-json/wp/v2/` answers | `npx contentrain import https://example.com` |
32
+ | **WXR export** | The site is offline, behind auth, or the user has a `.xml` from Tools → Export | `npx contentrain import ./export.xml` |
33
+
34
+ REST returns more than WXR (rendered HTML, media metadata, per-type discovery); WXR is complete but rawer. If both exist, prefer REST.
35
+
36
+ **Private or partial REST.** A site can answer `/wp-json` but hide drafts, private types and some fields. An Application Password lifts the run to `rest_auth`:
37
+
38
+ ```bash
39
+ npx contentrain import https://example.com --auth "user:xxxx xxxx xxxx xxxx"
40
+ ```
41
+
42
+ Check first, so you tell the user what they will get rather than discovering it after:
43
+
44
+ ```bash
45
+ curl -s "https://example.com/wp-json/wp/v2/types" | head -c 400
46
+ ```
47
+
48
+ A `401`/`403` means auth is needed. A `404` or an HTML page means REST is disabled or blocked — use WXR.
49
+
50
+ ### 2. Import
51
+
52
+ ```bash
53
+ npx contentrain import <source> --out ./my-site
54
+ ```
55
+
56
+ `--force` is required to overwrite an existing `.contentrain/`. **Never pass it without asking** — it replaces models and content the user may have edited.
57
+
58
+ What lands on disk:
59
+
60
+ - `.contentrain/models/*.json` + `.contentrain/content/**` — the canonical store
61
+ - `import-report.json` — what was inferred, rewritten or dropped
62
+ - `entry-source-map.json` — `wp_post_id → { model_id, entry_id }`, the only record connecting the new store to the old site
63
+ - `comments-export.json` — written when the source has comments (`contentrain-comments@1`)
64
+
65
+ Models the importer creates: `posts`, `pages` (and one collection per custom post type), `authors`, `media`, `menus`, `menu-items`, `comments`, a `site` singleton, and one collection per taxonomy.
66
+
67
+ ### 3. Read the report before declaring success
68
+
69
+ Open `import-report.json` and tell the user what it says. These fields decide whether the import is usable:
70
+
71
+ | Field | Means | What to do |
72
+ |---|---|---|
73
+ | `skipped_types` | Post types not imported | Ask if any are needed; a second run with `--auth` may reach them |
74
+ | `dropped_relations` | Relations pointing outside the import | Usually a partial import — check the skipped types first |
75
+ | `slug_rewritten` / `slug_fallback` | Slugs changed to be valid | Affects URLs → the user needs redirects |
76
+ | `title_fallback` | Entries that had no title | They exist but are unlabelled; worth a pass |
77
+ | `acf_fields` / `meta_fields` | Custom fields found and their inferred types | Verify the types match intent before content is edited |
78
+
79
+ **Known gap:** post bodies carry Gutenberg block comments and shortcodes verbatim. They render as-is in HTML but they are WordPress syntax, not portable content. Say so — do not present the import as clean markup when it is not.
80
+
81
+ ### 4. Validate
82
+
83
+ ```bash
84
+ npx contentrain validate
85
+ ```
86
+
87
+ Fix what it reports before anything reads the store. Structural problems are cheapest here, and the SDK generator will refuse a broken store anyway. See `contentrain-validate-fix` for the fix loop.
88
+
89
+ ### 5. Wire the content to the framework
90
+
91
+ **Astro** — the content layer, no client generation:
92
+
93
+ ```ts
94
+ // src/content.config.ts
95
+ import { defineCollection, z } from 'astro:content'
96
+ import { contentrainLoader } from '@contentrain/query/astro'
97
+
98
+ export const collections = {
99
+ posts: defineCollection({
100
+ loader: contentrainLoader({ model: 'posts' }),
101
+ schema: z.object({ title: z.string(), slug: z.string() }).passthrough(),
102
+ }),
103
+ }
104
+ ```
105
+
106
+ Then `getCollection('posts')` in pages. Documents also get `<Content />`.
107
+
108
+ **Everything else** (Nuxt, Next, SvelteKit, plain Node) — the generated client:
109
+
110
+ ```bash
111
+ npx contentrain generate
112
+ ```
113
+
114
+ ```ts
115
+ import { query } from '#contentrain'
116
+ const posts = query('posts').where('status', '=', 'published').all()
117
+ ```
118
+
119
+ See `contentrain-generate` for the generator and `contentrain-sdk` for the query API.
120
+
121
+ ### 6. Hand back what is still open
122
+
123
+ Close the loop honestly. Three things survive the import and need a decision:
124
+
125
+ 1. **Comments.** `comments-export.json` is a payload, not a running service. Either a comments service imports it, or comments stay on WordPress, or they are dropped. The user chooses.
126
+ 2. **URLs.** Any rewritten slug is a broken inbound link. `entry-source-map.json` plus the report's slug counts are what a redirect list is built from.
127
+ 3. **Media.** The importer records media metadata; the files themselves still live on the old host until they are moved.
128
+
129
+ ---
130
+
131
+ ## Guardrails
132
+
133
+ - **Read the report, always.** An import that "worked" while dropping half the post types is worse than one that failed — it looks finished.
134
+ - **`--force` is destructive.** Ask first, every time.
135
+ - **Never invent content.** If a field is empty after import, it was empty (or unreachable) in the source. Say that instead of filling it.
136
+ - **Do not edit `entry-source-map.json`.** It is the only link back to the WordPress ids; comments intake and redirects both read it.
137
+ - **Re-importing is not syncing.** A second import overwrites; it does not merge changes made in Contentrain since the first one.
138
+
139
+ ---
140
+
141
+ ## Related
142
+
143
+ - `contentrain-validate-fix` — the validation loop
144
+ - `contentrain-generate` — typed client generation
145
+ - `contentrain-sdk` — querying the generated client
146
+ - `contentrain-model` — reshaping the imported models
147
+ - `references/wordpress-mapping.md` — how WordPress concepts land in Contentrain
@@ -0,0 +1,87 @@
1
+ # WordPress → Contentrain mapping
2
+
3
+ What `contentrain import` actually produces, so you can answer "where did my X go?" without opening the importer.
4
+
5
+ ## Models
6
+
7
+ | WordPress | Contentrain model | Kind | Domain |
8
+ |---|---|---|---|
9
+ | Posts | `posts` | collection | `blog` |
10
+ | Pages | `pages` | collection | `blog` |
11
+ | Custom post type `event` | `event` | collection | `blog` |
12
+ | Categories | `categories` | collection | `blog` |
13
+ | Tags | `tags` | collection | `blog` |
14
+ | Custom taxonomy `product_line` | `product-line` | collection | `blog` |
15
+ | Users | `authors` | collection | `blog` |
16
+ | Media library | `media` | collection | `assets` |
17
+ | Menus | `menus` + `menu-items` | collection | `site` |
18
+ | Comments | `comments` | collection | `blog` |
19
+ | Site title / tagline / URL / language | `site` | **singleton** | `site` |
20
+
21
+ Every model is `i18n: false`. WordPress multilingual plugins (WPML, Polylang) keep translations as separate posts, so they arrive as separate entries — not as locales. Reshaping them into one i18n model is a modelling decision the agent makes with the user, not something the importer guesses.
22
+
23
+ ## Post fields
24
+
25
+ Fields are added **only when at least one entry in that post type has the data**, so an import of a site without excerpts has no `excerpt` field at all.
26
+
27
+ | Field | Type | Present when |
28
+ |---|---|---|
29
+ | `title` | string (required) | always |
30
+ | `slug` | slug (required, unique) | always |
31
+ | `excerpt` | text | any entry has an excerpt |
32
+ | `body` | richtext | any entry has content |
33
+ | `published_at` | datetime | any entry has a date |
34
+ | `author` | relation → `authors` | any entry has an author |
35
+ | `<taxonomy>` | relations → that taxonomy | the type uses that taxonomy |
36
+ | `cover` | relation → `media` | any entry has a featured image |
37
+ | `wp_id` | integer (required, unique) | always |
38
+ | `modified_at` | datetime | always |
39
+ | `link` | url | always — the original permalink |
40
+ | `parent` | relation → same model | any entry has a parent |
41
+ | `menu_order`, `sticky`, `template` | integer / boolean / string | the source uses them |
42
+ | `format` | select | the type uses post formats |
43
+ | `visibility` | select `public\|private\|password` | any entry is private or password-protected |
44
+ | `comments_open` | boolean | the source carries comment status |
45
+
46
+ ## Status
47
+
48
+ WordPress status becomes Contentrain meta, not a field:
49
+
50
+ | WP | Contentrain |
51
+ |---|---|
52
+ | `publish`, `inherit` | `published` |
53
+ | `future` | `draft` + `publish_at` (the scheduled date survives) |
54
+ | `pending` | `in_review` |
55
+ | `trash` | `archived` |
56
+ | everything else (`draft`, `private`, …) | `draft` |
57
+
58
+ Comments map by approval: `1` → published · `spam`/`trash` → archived · anything else → `in_review`.
59
+
60
+ ## Custom fields
61
+
62
+ - **Open meta keys** become fields with a type inferred from the value, and are listed in `import-report.json → meta_fields`.
63
+ - **ACF fields** become fields tagged `description: "ACF"`, listed under `acf_fields`. Object-shaped values land as `object`.
64
+ - **Underscore-prefixed meta** (`_edit_lock`, `_thumbnail_id`, `_elementor_*`, …) is WordPress plumbing and is dropped.
65
+ - **Plugin meta** matching `jetpack|wpdc|discourse|footnotes|inline_featured|spay_|advanced_seo|rank_math|yoast` is dropped — it is plugin state, not editorial content.
66
+
67
+ Inferred types are a guess from one value. Check `meta_fields` and `acf_fields` in the report before anyone edits content: fixing a wrong type later means touching every entry.
68
+
69
+ ## What is not imported
70
+
71
+ Post types matching `attachment|nav_menu_item|wp_*|jp_*|pattern|revision|oembed_cache|customize_changeset|user_request|custom_css` are skipped and listed in `report.skipped_types`. Attachments are not skipped content — they arrive as the `media` collection.
72
+
73
+ Also outside the import: themes, widgets, plugin settings, users' passwords and roles, and the media **files** themselves (only their metadata and URLs come across).
74
+
75
+ ## Ids and the link back
76
+
77
+ Entry ids are deterministic hex derived from the WordPress id (`media:12`, `comments:34`, `menus:<slug>`), so re-running an import produces the same ids. `entry-source-map.json` records `wp_post_id → { model_id, entry_id }` — it is what a comments intake and a redirect list are built from, and nothing else records it.
78
+
79
+ ## Relations that point nowhere
80
+
81
+ A relation whose target was not part of the import (a comment on a skipped post type, a term never listed) is either dropped and counted in `report.dropped_relations`, or — for terms referenced but not listed — added to the pool from the reference itself. A high `dropped_relations` usually means the import was partial: check `skipped_types` and whether `--auth` would reach more.
82
+
83
+ ## Known gaps
84
+
85
+ - **Gutenberg and shortcodes** stay verbatim in `body`. `<!-- wp:paragraph -->` and `[gallery]` render as-is in HTML but they are WordPress syntax; converting them is a separate pass.
86
+ - **Media files** stay on the old host until moved.
87
+ - **Redirects** are not generated. `slug_rewritten` + `slug_fallback` in the report say how many URLs changed.
@@ -143,7 +143,7 @@ works bundled behaves the same on CDN.
143
143
  | `in` | `.where('category', 'in', ['a','b'])` | In array |
144
144
  | `contains` | `.where('tags', 'contains', 'vue')` | String/array contains |
145
145
 
146
- ### CDN Entry Metadata, Media & Forms
146
+ ### CDN Entry Metadata, Media, Forms & Comments
147
147
 
148
148
  ```typescript
149
149
  // Entry metadata (status, publish_at, expire_at)
@@ -154,10 +154,16 @@ const media = client.media()
154
154
  const asset = await media.asset('hero.jpg')
155
155
  const url = media.url(asset, 'thumb')
156
156
 
157
- // Forms (config fetch + submit)
157
+ // Forms — public /api/forms/v1, no API key (values go under `data`; captcha + honeypot beside it)
158
158
  const form = client.form()
159
- const config = await form.config('contact')
160
- const result = await form.submit('contact', { name: 'Alice' })
159
+ const config = await form.config('contact') // fields keyed by id, captcha, captchaSiteKey, honeypotField
160
+ const result = await form.submit('contact', { name: 'Alice' }, { captchaToken, honeypot: '' })
161
+ // { success, message } | { success: false, errors: [{ field, message }] }; 403/404/429 throw ContentrainError
162
+
163
+ // Comments — public /api/comments/v1, approved comments only, body is plain text (render escaped)
164
+ const comments = client.comments()
165
+ const thread = await comments.thread('posts', entryId, { locale: 'en' })
166
+ await comments.submit('posts', entryId, { author: { name: 'Ada', email }, body: 'Great post', parentId }, { locale: 'en' })
161
167
 
162
168
  // Conversation API (external AI content operations)
163
169
  const conv = client.conversation()
@@ -38,6 +38,8 @@ Confirm:
38
38
 
39
39
  - never use `copy_locale` on non-i18n models
40
40
  - `update_status` and `delete_entries` are collection-only
41
+ - **preview first**: `dry_run: true` runs the operation in a throwaway worktree and reports what would change (`would_copy`/`would_replace`, `would_update` + `not_found`, `would_delete` + `files_to_remove`) without committing anything. A preview needs no `confirm`
42
+ - `copy_locale` REPLACES the target locale — the preview's `would_replace` is how many records that costs
41
43
  - confirm entry IDs before delete operations
42
44
  - batch only related entries together
43
45