# cms-edit create from-json — Complete Guide

`create from-json` creates a complete new page or article — including all components, collections, and CTA links — from inline JSON (`--json` or `--json-base64`). It is the recommended way to create new pages on hosted MCP.

```bash
cms-edit create from-json --json '{...}'
cms-edit create from-json --json '{...}' --dry-run   # validate without writing
cms-edit create from-json --json '{...}' --strict    # fail on unknown JSON keys
cms-edit create from-json --json '{...}' --if-not-exists  # return existing ID when slug exists
```

Run `cms-edit index sync` before using any `*AssetFilename` shorthand (index-backed exact `file.fileName` lookup).

---

## Full JSON Schema

```jsonc
{
  // "page" (default) or "article". Auto-detected as "article" if articleTypeId is present.
  "type": "page",

  // Required. The URL slug. Leading slash is stripped automatically. Trailing slash triggers a warning.
  "slug": "/solutions/hepatology",

  // Required. The page title (also used as cmsLabel if cmsLabel is omitted).
  "title": "Hepatology Solutions",

  // Optional. SEO meta description.
  "description": "Evidence generation for hepatology research.",

  // Optional. Override the Contentful entry label shown in the entry list.
  "cmsLabel": "Hepatology Solutions Page",

  // Page-only: link to an existing template entry by ID.
  "templateId": "4xKpzQ2abc123",

  // Page/article: featured image — asset ID (either key; warn if both differ).
  "featuredImage": "7yBnmR9xyz456",
  // "featuredImageId": "7yBnmR9xyz456",  // alias

  // Exact Contentful asset fileName (requires cms-edit index sync).
  // "featuredImageAssetFilename": "hero.jpg",

  // Article-only: download asset (ID or fileName shorthands).
  // "download": "assetId",
  // "downloadAssetFilename": "report.pdf",

  // Page-only: whether the page is indexed by search engines (default: not set = inherit).
  "indexed": true,

  // Page-only: whether the page is hidden from the sitemap (default: false).
  "hidden": false,

  // Article-only (required for articles): one of articleTypeId, articleTypeSlug, or articleTypeName.
  // Prefer slug for marketers: cms-edit list --type articleType
  "articleTypeSlug": "resources/blog",

  // Article-only: publication date in YYYY-MM-DD format. Defaults to today.
  "date": "2025-01-15",

  // Array of components to create and link to the page's content field.
  "components": [
    // ... see Component Schema below
  ]
}
```

---

## Explicit link shorthands (no guessing)

Use **named keys** only — the CLI does not infer whether a string is an asset ID, Media entry ID, or file name.

### Page / article (top-level)

| JSON key | Sets field | Value |
|----------|------------|-------|
| `featuredImage` or `featuredImageId` | `featuredImage` | Asset ID |
| `featuredImageAssetFilename` | `featuredImage` | Exact asset `fileName` (index) |
| `download` or `downloadId` | `download` | Asset ID (article) |
| `downloadAssetFilename` | `download` | Exact asset `fileName` (article) |
| `icon` / `iconId` / `iconAssetFilename` | `icon` | Asset (article, when field exists) |
| `templateId` | `template` | Entry ID |
| `articleTypeId` | `articleType` | Entry ID (articles) |
| `topContentIds`, `visualsIds`, … | See `cms-edit help fields-taxonomy` | Entry ID arrays |

### Component `fields`

| JSON key | Sets field | Value |
|----------|------------|-------|
| `visualId` | `visual` | Media / video entry ID |
| `visual` (string) | `visual` | Entry ID only (same as `visualId`) |
| `visualAssetId` | `visual` | Asset ID when the field is an Asset link |
| `visualAssetFilename` | `visual` | Exact asset `fileName` → reuse or create Media |
| `visualMediaName` | (with `visualAssetFilename`) | Optional Media entry name when creating (default: **asset title**) |
| `visualMedia` | `visual` | `{ "assetId", "name", "position?", "width?" }` — always creates Media; `name` should match asset title unless you need a distinct wrapper label |
| `mobileVisualId`, `mobileVisualAssetFilename`, … | `mobileVisual` | Same pattern |

Unknown top-level or `fields` keys that are not on the content type: **warning** by default; use `--strict` or `--dry-run` to **fail** instead.

### Comparison with `taxonomy-from-json`

| | `taxonomy-from-json` | `create from-json` |
|--|----------------------|-------------------|
| Unknown fields on dry-run | Error (strict) | Error (strict) |
| Unknown fields on create | Warn | Warn |
| `--strict` on create | — | Supported |

---

## Component Schema

Each item in `components` (and `items` for collections) follows this shape:

```jsonc
{
  // --- Option A: Create a new entry ---

  // Contentful content type ID. One of: component, collection, externalComponent, person
  // Default: "component"
  "contentType": "component",

  // The type discriminator value (e.g. HeroSimple, TextBlock, CardGrid).
  // Run `cms-edit schema types <content-type>` to see valid values.
  "type": "HeroSimple",

  // Optional. Human-readable label in Contentful's entry list. Defaults to `type`.
  "cmsLabel": "Homepage Hero",

  // Which field on the parent to attach to. Default: "content" (page) or "contents" (collection).
  // Use "topContent" or "bottomContent" for template slots.
  "target": "content",

  // Scalar and rich text fields to set. See "Field Values" section below.
  "fields": {
    "heading": "Welcome to Hepatology",
    "preHeading": "Our Speciality",
    "body": "We have been building cohorts...\n\nWith over a decade of experience.",
    "backgroundColour": "Navy",
    "textColour": "White"
  },

  // CTA link entries to create and attach to the component's `links` field.
  "links": [
    // ... see Link Schema below
  ],

  // Child items for collection components. Same schema as a component.
  "items": [
    // ... nested components
  ],

  // --- Option B: Link an existing entry (no fields set) ---
  // "existingId": "4xKpzQ2abc123",
  // "comment": "Shared footer CTA — managed separately"
}
```

---

## Field Values

Fields in the `fields` object accept several value formats.

### Strings — Automatic RichText Conversion

**The tool uses the Contentful content model to determine if a field is RichText.** For confirmed RichText fields, any string is always converted to a RichText document — no special markers needed.

For fields where the schema is unavailable, the tool falls back to a heuristic. To trigger Markdown conversion in the heuristic path, use one of these markers:

| Marker | Example | Produces |
|---|---|---|
| `\n\n` (double newline) | `"First.\n\nSecond."` | Two paragraphs |
| `- ` (list item) | `"- Item one\n- Item two"` | Unordered list |
| `# ` (heading) | `"# Heading\n\nBody."` | Heading + paragraph |
| `**text**` | `"**bold** text"` | Bold inline |
| `> ` (blockquote) | `"> A quote"` | Blockquote |

**For single-sentence body fields:** Always include `\n\n` at the end to ensure conversion:
```json
"body": "Pedestal Health has been building cohorts in this space for over a decade.\n\n"
```

### Explicit Format Object

Use an explicit format wrapper to be unambiguous:
```jsonc
"body": { "value": "Plain text paragraph.", "format": "text" }
"body": { "value": "## Heading\n\nContent.", "format": "markdown" }
"body": { "value": "<p>HTML content</p>", "format": "html" }
```

Accepted formats: `"text"`, `"markdown"`, `"html"`, `"json"` (raw Contentful RichText document).

### Other Value Types

```jsonc
"indexed": true,        // boolean
"heading": null,        // null clears the field
"someNumber": 42        // number
```

---

## Link Schema

```jsonc
{
  // Link type. One of: "external" (default), "internal", "download", "blank"
  "type": "internal",

  // Display label for the CTA button.
  "label": "Learn More",

  // For "internal": resolve by slug (preferred) or by entry ID.
  "slug": "/solutions/hepatology",
  // "id": "4xKpzQ2abc123",   // alternative to slug

  // For "external": the URL.
  // "href": "https://example.com",

  // For "download": the asset ID.
  // "assetId": "7yBnmR9xyz456",

  // Link to an existing link entry (skips creation).
  // "existingId": "2aZxQw1def789"
}
```

---

## Critical Rules

### Rule 1: Create Leaf Pages First

Internal links resolve at creation time. If page A links to page B and B does not yet exist in Contentful, the creation fails and **page A is left partially created** (the page entry exists but with no content).

**Always create leaf pages (the targets of links) before the pages that link to them.**

```bash
# Wrong order: /solutions links to /solutions/hepatology which doesn't exist yet
cms-edit create from-json --json '{...}'      # FAILS mid-run

# Correct order: leaf page first, parent page second
cms-edit create from-json --json '{...}'     # succeeds
cms-edit create from-json --json '{...}'      # succeeds — slug resolves
```

Use `--dry-run` to catch missing slugs before writing anything (see below).

### Rule 2: Always Run --dry-run First

`--dry-run` validates the plan without creating anything in Contentful. It prints the component tree and checks that all internal link slugs exist:

```bash
cms-edit create from-json --json '{...}' --dry-run
```

Example output when a slug is missing:
```
[dry-run] Would create page:
  slug: /solutions
  title: Solutions
  ...

✗ Internal link slug not found in Contentful: "/solutions/hepatology"
✗ Internal link slug not found in Contentful: "/solutions/dermatology"
  Create the target pages first, then re-run. See: cms-edit help create-from-json
```

### Rule 3: Use body Strings Consistently

Even if a body field is short, include `\n\n` at the end to ensure the string is treated as a paragraph of rich text:

```jsonc
// Good — explicit double newline ensures conversion
"body": "A single-sentence description.\n\n"

// Also good — multi-paragraph body
"body": "First paragraph.\n\nSecond paragraph."

// Risky for heuristic-path fields — no markers, may pass as plain string
"body": "A single-sentence description."
```

---

## Recovery: Partial Page Created

If a run fails mid-way (e.g. a missing internal link slug), the page entry is already in Contentful but with no content. The error message shows the page ID:

```
✗ Component creation failed: No entry found with slug "/solutions/hepatology".
✗ The page entry was already created in Contentful (partial state) — ID: 4xKpzQ2abc123, slug: /solutions
  Inspect with: cms-edit open --page-slug /solutions
  Remove with: delete the entry in Contentful at https://app.contentful.com/spaces/...
  Tip: run with --dry-run first to validate all internal link slugs exist.
```

Options:
1. Delete the partial entry in Contentful, fix the dependency order, and re-run.
2. Repair the partial entry using `cms-edit batch run` with targeted `set`/`rtf` operations.
3. Use `cms-edit open --page-slug /solutions` + individual `add` operations to rebuild the content.

---

## Complete Example

A two-component page:

```json
{
  "type": "page",
  "slug": "/solutions/hepatology",
  "title": "Hepatology Solutions",
  "description": "Evidence generation for hepatology research.",
  "components": [
    {
      "contentType": "component",
      "type": "HeroSimple",
      "cmsLabel": "Hepatology Hero",
      "fields": {
        "heading": "Hepatology Evidence Generation",
        "preHeading": "Our Speciality",
        "body": "We have been building cohorts in hepatology for over a decade.\n\nOur work spans NASH, HCC, and liver transplantation.",
        "backgroundColour": "Navy",
        "textColour": "White"
      },
      "links": [
        { "type": "internal", "label": "Contact Us", "slug": "/contact" }
      ]
    },
    {
      "contentType": "collection",
      "type": "CardGrid",
      "cmsLabel": "Hepatology Capabilities",
      "fields": {
        "heading": "What We Offer"
      },
      "items": [
        {
          "contentType": "component",
          "type": "Generic",
          "cmsLabel": "Capability — Cohort Building",
          "fields": {
            "heading": "Cohort Building",
            "body": "- Disease characterization\n- Protocol design\n- Long-term follow-up"
          }
        },
        {
          "contentType": "component",
          "type": "Generic",
          "cmsLabel": "Capability — Evidence Review",
          "fields": {
            "heading": "Evidence Review",
            "body": "Systematic literature reviews and meta-analyses.\n\nSuitable for regulatory submission."
          }
        }
      ]
    }
  ]
}
```

Run:
```bash
cms-edit create from-json --json '{...}' --dry-run   # validate first
cms-edit create from-json --json '{...}'             # create for real
cms-edit open --page-slug /solutions/hepatology              # inspect result
```

---

## After Creation

Once created:
1. Run `cms-edit open --page-slug /<slug>` to inspect the result.
2. Fix any field values with `cms-edit set` or `cms-edit rtf`.
3. Publish in the Contentful web app when ready.

The tool prints the Contentful web app URL for the new entry after creation.
