---
name: contentful-cms-core
description: "Use cms-edit CLI to read/edit Contentful content (open → snapshot → read → set/rtf → diff → save)."
---

# Skill: contentful-cms CLI

Use this skill when you need to read or edit content in Contentful CMS using the `cms-edit` CLI tool.

**Before editing:** use skill **`contentful-cms-editor-tasks`** to read `tasks-index` and the matching capability playbook for this site (hosted MCP or local `cms-edit/<site>/editor-pack/`).

## Overview

`cms-edit` lets you read and edit Contentful draft content without publishing. It uses a **snapshot → ref → edit → save** workflow:

1. `open` a page to load the content tree into a session
2. `snapshot` to see the tree with `@ref` labels
3. `read` an entry to inspect its fields
4. `set` / `rtf` to modify fields
5. `diff` to review changes
6. `save` to write drafts to Contentful (NEVER publishes)

## Safety Rules

**This tool CANNOT and WILL NOT:**
- Publish any entry
- Unpublish any entry
- Archive or delete published entries

All `save` operations create **draft** versions. A human must review and publish in Contentful.

**Assets:** Upload **is** supported (`asset upload`). Default workflow: **search and reuse** existing assets first (`index sync` → `asset search`). When uploading:
- **Hosted MCP:** always `cms_edit_request_staged_upload` → curl → `asset upload --staged` (never `--base64`)
- **Local CLI:** `--base64` or `--url` when user approves local CLI
- Use a **sensible fileName** and **descriptive title** (and alt text where the site uses it)
- Avoid oversized images — width over **2000px** is usually wasteful for web (exact limits vary by project; check `asset audit` / media review guidance)
- Prefer `--if-exists-by-filename` to avoid duplicates

## Prerequisites

- `.contentful-cms.json` must exist in the project root (or ancestor directory)
- A Contentful Management Token with editor-only permissions (no publish rights) must be set as an env var and referenced in the config

To get the path to the full README (e.g. for an LLM to read): `cms-edit --docs`

## Core Workflow

### Step 1: Open a page

```bash
# By page slug (trailing slash OK)
cms-edit open --page-slug /pricing

# By page cmsLabel (when editors use cmsLabel heavily)
cms-edit open --page-cms-label "Pricing page"

# By slug with explicit space
cms-edit --space om1 open --page-slug /pricing

# By Contentful entry ID
cms-edit open --id 4xKj2abcDef

# Home page (slug is 'index')
cms-edit open --page-slug /
```

**Flat lookup flags:** provide **exactly one** primary flag per `open` / `resolve` / `nav open`. See `cms-edit help lookup`. Run `cms-edit index sync` so catalog types resolve from the local index.

### Step 2: Read the snapshot

```bash
cms-edit snapshot
```

Output:
```
Page: /pricing  [4xKj2abc | published]  ·  om1

  @c0   Component[HeroSimple]    "Transparent Pricing"        [published]
  @c1   Component[RichText]      "How it works"               [published]
  @c2   Collection[CardGrid]     "Plans"                      [published]
    @c3   Component[Card]          "Starter"                  [published]
    @c4   Component[Card]          "Pro"                      [draft]
  @c5   Component[CTA]           "Get started today"          [published]
```

Use `snapshot -c` for compact output (types only, no labels).

**Ref cheat sheet:**
| Ref | Resolves to |
|-----|-------------|
| `@root` | Root entry of the current session (works in any session type) |
| `@p0` | Root entry — alias for `@root` in **page** and **article** sessions only |
| `@c0` | Same as `@root` (first entry in depth-first traversal order) |
| `@c1`, `@c2`, … | Child entries in depth-first traversal order |

In **navigation sessions**, use `@root` or `@c0` for the nav root — `@p0` is not valid.

### Step 3: Read entry fields

```bash
# All fields summary
cms-edit read @c0

# Specific field (shows plain text for scalars, Markdown for rich text)
cms-edit read @c0 heading
cms-edit read @c1 body
```

### Step 4: Edit fields

**Scalar fields** (heading, title, slug, showHeading, etc.):
```bash
cms-edit set @c0 heading "New heading text"
cms-edit set @c0 showHeading true
cms-edit set @c0 preHeading "Featured"
cms-edit set @c0 anchor "hero-section"
```

**Bulk scalar updates** — set a field on multiple entries in one command:
```bash
cms-edit set @c1:cmsLabel="Hero — School avoidance" @c2:cmsLabel="What is school avoidance?" @c3:cmsLabel="Signs of school avoidance"
cms-edit set @c1:heading="Title A" @c2:heading="Title B"
```
Each token is `@ref:fieldName=value`. All values are scalar (string, boolean, number). Flags like `--link`, `--links`, `--json` do not apply in multi mode.

**Entry link fields** (template, articleType, etc. — value is the linked entry ID):
```bash
cms-edit set @c0 template 3I0HxGKbUd173wIpFCsbVr --link
```

**Entry link arrays** (`--links` works on any field that is an array of entry links — not just the standard page content fields):
```bash
# Standard page content arrays
cms-edit set @p0 content @c1,@c2,@c3 --links
cms-edit set @p0 bottomContent @c4 --links --append

# Navigation items array (use @root or @c0 in a nav session)
cms-edit set @root items <id1>,<id2>,<id3> --links
```

**Rich text fields** (body, additionalCopy):
```bash
printf '## Why it matters\n\nOur platform helps teams **move faster** with [confidence](https://example.com).\n\n- Instant setup\n- No code required\n- 99.9%% uptime\n' | cms-edit rtf @c1 body --markdown -
```

Markdown support:
- `# Heading 1` through `###### Heading 6`
- `**bold**`, `_italic_`, `***bold italic***`
- `[link text](https://url)`
- `- item` for unordered lists, `1. item` for ordered
- `> blockquote`
- `` `inline code` ``
- `---` for horizontal rule

For any content with multiple paragraphs or newlines, use `printf` piped to stdin — `\n` in a double-quoted shell string is **not** interpreted as a newline by bash:

```bash
# Correct — printf interprets \n properly
printf '## Why it matters\n\nOur platform helps teams **move faster**.\n\n- Instant setup\n- No code required\n' | cms-edit rtf @c1 body --markdown -

# Also correct — file input
cms-edit rtf @c1 body --markdown --content "# Heading\n\nBody."
cms-edit rtf @c1 body --markdown - < path/to/file.md
```

Single-line content (no newlines) can be passed as a quoted argument directly:

```bash
cms-edit rtf @c1 body --markdown "Simple single-paragraph body text with **bold**."
```

**Surgical rich text replace** (compliance / quidget tokens — does not re-import the whole field):

- Matching is **per Contentful text node** only. If the author split a phrase with bold (e.g. `**Annual fee: **$95`), the string `Annual fee: $95` will **not** match as one piece; use a shorter `--find` or full `rtf … --content`.
- Quidget strings contain `*`, which Markdown would treat as italic — use **`--replace-plain`**, not `--replace`.

```bash
# Replace every "$95" in this field with the quidget (e.g. multiple table cells)
cms-edit rtf replace @c2 body --find '$95' --replace-plain '{*credit_card_id*:*5048345*,*field*:*annual_fees*,*api*:*cc*}' --mode all

# Exactly one occurrence required (fails if 0 or 2+)
cms-edit rtf replace @c2 body --find '75,000 miles after spending $4,000 in the first 3 months' \
  --replace-plain '{*credit_card_id*:*5048345*,*field*:*bonus_miles*,*api*:*cc*}' --mode exactlyOne
```

### Step 5: Review changes

```bash
cms-edit diff
```

Shows a diff of all modified fields across all changed entries.

### Step 6: Save

```bash
cms-edit save
```

Writes all changes to Contentful as **drafts**. The changes are visible in Contentful's web app but NOT live until a human publishes them.

After saving, the session is cleared automatically if all changes were saved.

### Discard

```bash
cms-edit discard
```

Throws away all unsaved local changes without writing to Contentful.

## Content Field Names

Based on the SE Studio content model:

### Component fields
| Field | Type | Notes |
|-------|------|-------|
| `cmsLabel` | string | Internal name (required) |
| `componentType` | string | Type discriminator (e.g. `HeroSimple`) |
| `heading` | string | Main heading (only shown when `showHeading` is true) |
| `showHeading` | boolean | Whether to display the heading |
| `preHeading` | string | Text above the heading |
| `postHeading` | string | Text below the heading |
| `body` | rich text | Main body copy |
| `additionalCopy` | rich text | Secondary body copy |
| `anchor` | string | Section anchor ID |
| `backgroundColour` | string | Background colour name |
| `textColour` | string | Text colour name |

### Collection fields
Same as Component plus:
| Field | Type | Notes |
|-------|------|-------|
| `collectionType` | string | Type discriminator (e.g. `CardGrid`) |

### Page fields
| Field | Type | Notes |
|-------|------|-------|
| `slug` | string | URL slug (no leading slash in CMS) |
| `title` | string | SEO/display title |
| `description` | string | Meta description |
| `indexed` | boolean | Whether to index in search engines |

## Order of operations: article + CTA

When adding body content and a CTA (e.g. PDF download) to an article:

1. `cms-edit open --article-slug <slug>` (or `open --id <id>`)
2. Set body: `cms-edit rtf @<ref> body --markdown "..."` or `--content` / `--base64`. If you need to add a body component first, use `add` then `rtf`.
3. `cms-edit add CTA --target bottomContent`
4. Set CTA links: use `--type external --label "Download PDF" --href <url>` for an external PDF URL, or `--type download --label "Download PDF" --asset-id <asset-id>` for a Contentful asset (get the ID from `cms-edit asset search "..."` or `asset info <id>`).
5. `cms-edit save`

## Structure Commands

### Add a new component

```bash
# Add at the end of page content
cms-edit add HeroSimple

# Add to article/page bottomContent (when session is an article or page)
cms-edit add CTA --target bottomContent

# Add after a specific component
cms-edit add HeroSimple --after @c1

# Add inside a collection (--parent derives target field from the parent's content type)
# collection parent → uses 'contents'; page/article parent → uses 'content' by default
cms-edit add Card --parent @c2
cms-edit add Hero --parent @c0 --target content   # explicit target when parent is a page

# Add an explicit collection type
cms-edit add CardGrid --content-type collection

# Link an EXISTING entry instead of creating a new one (e.g. a shared CTA used on every page)
cms-edit add CTA --content-type component --existing-id 4xKj2abcDef
```

Discover available types first:
```bash
cms-edit schema types component
cms-edit schema types collection
```

### Remove a component

```bash
cms-edit remove @c3
```

If the entry is a draft and unreferenced elsewhere, it will be deleted on save. If it's published, it's unlinked only.

### Move/reorder

```bash
# Move @c3 to after @c1
cms-edit move @c3 --after @c1

# Move to before @c1
cms-edit move @c3 --before @c1

# Move to first position
cms-edit move @c3
```

## Links (CTAs)

Put the ref **after the subcommand** (e.g. `links add @c5`). The `links add` command creates the link entry and attaches it to the CTA in one step.

- **External:** `--type external --label "..." --href <url>` (required: `--href`)
- **Internal:** use `--slug /page` to look up by slug, or `--id <entry-id>` to reference by Contentful entry ID (required: `--slug` or `--id`)
- **Download:** `--type download --label "..." --asset-id <asset-id>` (required: `--asset-id`; get the ID from `asset search` or `asset info`)

```bash
# List current links
cms-edit links list @c5

# Add an external link
cms-edit links add @c5 --type external --label "Learn more" --href https://example.com

# Add an external PDF CTA (URL to the file)
cms-edit links add @c5 --type external --label "Download PDF" --href "https://www.example.com/whitepaper.pdf"

# Add an internal link by slug
cms-edit links add @c5 --type internal --label "Pricing" --slug /pricing

# Add an internal link by entry ID
cms-edit links add @c5 --type internal --label "About" --id 4xKj2abcDef

# Add a download link (Contentful asset)
cms-edit links add @c5 --type download --label "Download PDF" --asset-id 5xKj2abcDef

# Remove link at index 1
cms-edit links remove @c5 1

# Move link from index 2 to index 0
cms-edit links move @c5 2 0
```

You can also bulk-replace the `links` array on a component using `set --links`:

```bash
# Replace the entire links array with specific IDs
cms-edit set @c5 links linkId1,linkId2,linkId3 --links

# Append a link entry ID to the existing array
cms-edit set @c5 links linkId4 --links --append
```

## Assets

**Content index:** Run `cms-edit index sync` once per space before asset search, media-by-filename list, asset audit, or `--if-exists-by-filename`. The same sync indexes the full **content catalog** (pages, articles, taxonomy, navigation, templates) for fast `list`, `open`, and `resolve`. Default sync uses the **Preview API** (draft content). See `cms-edit help index-sync` and `cms-edit help lookup`.

```bash
cms-edit index sync
cms-edit index status
cms-edit list --type page
cms-edit list --type template
cms-edit resolve --tag-type-slug topic

# Published-only index (optional)
cms-edit index sync --published

# Search for an asset by title (requires index)
cms-edit asset search "hero background"

# Search by fileName
cms-edit asset search --filename istockphoto-123.jpg
cms-edit asset search --filename-match 'istockphoto-.*'

# Local CLI — URL or base64 (hosted MCP rejects --base64)
cms-edit asset upload --url https://example.com/image.jpg --if-exists-by-filename
cms-edit asset upload --base64 "$B64" --mime image/png --file-name poster.png

# Hosted MCP — always staged upload for binary files
# cms_edit_request_staged_upload → curl uploadUrl → consumeArgs
# cms_edit ["asset", "upload", "--staged", "<uploadId>", "--mime", "image/png", "--file-name", "photo.png"]

# Upload and create a Media wrapper in one step (--media-name defaults to asset title)
cms-edit asset upload ./figure.png --with-media --media-position Middle

# Create a Media wrapper for an existing asset (name defaults to asset title)
cms-edit create media --asset-id <asset-id>

# Fix a badly labelled Media wrapper (migration "Wrapper for …" labels)
cms-edit batch set <media-entry-id>:name=<asset-title>
cms-edit batch save

# Get asset details
cms-edit asset info 5xKj2abcDef

# Set a visual field
cms-edit asset set @c0 visual 5xKj2abcDef
```

JSON output for upload (`CMS_EDIT_JSON=1`): `{ ok, id, fileName, url, mediaId?, mediaIds? }`. Max upload size is **10MB** per file.

**Idempotent upload:** `--if-exists-by-filename` is check-then-create. Do not upload the same `fileName` in parallel — serialize per fileName or re-run to converge.

## Navigation

```bash
# Open a navigation entry (navigation uses `name`, not slug)
cms-edit nav open --nav-name "Main navigation"

# Add a new item
cms-edit nav add --label "Pricing" --slug /pricing
cms-edit nav add --label "Docs" --href https://docs.example.com --after @c1

# Link an existing NavigationItem (share items across navigations)
cms-edit nav add --existing-id <navigationItem-entry-id>

# Clone a navigation (duplicates all items)
cms-edit nav clone <source-nav-id>
cms-edit nav clone <source-nav-id> --label "LP Nav" --slug lp-nav
```

## Create New Entries

```bash
# Create a new page (session opens on the new entry)
# All flags after --title are optional
cms-edit create page --slug /about-us --title "About Us" --description "Learn about our team"
cms-edit create page --slug /about-us --title "About Us" \
  --template-id <template-entry-id> \
  --featured-image <asset-id> \
  --indexed \
  --cms-label "About Us (internal)"

# Create a new article (requires articleType entry ID)
cms-edit create article --slug /blog/my-post --title "My Post" --article-type-id 3abcDef456

# Create taxonomy entries (draft only)
cms-edit create tag-type --slug conference-venue --name "Conference Venue"
cms-edit create article-type --slug resources/publications --name "Publications"
cms-edit create tag --slug poster-presentation --name "Poster Presentation" --tag-type-slug presentation-type
# cmsLabel defaults to "<tagType name> — <tag name>" when --cms-label omitted
cms-edit create tag --slug easl-congress-2026 --name "EASL Congress 2026" --tag-type publication-source \
  --description "Research presented at EASL Congress 2026." --featured-image <logoAssetId>

# Full fields via JSON
cms-edit create tag --json '{"slug":"presentation"}' --tag-type presentation-type --if-not-exists

# Batch bootstrap
cms-edit create taxonomy-from-json --json '{"tagTypes":[...]}' --if-not-exists

# Idempotent single-entry creates
cms-edit create tag-type --slug conference-venue --name "Conference Venue" --if-not-exists
cms-edit create tag --slug asco-2025 --name "ASCO 2025" --tag-type conference-venue \
  --description "Annual ASCO conference." --featured-image <logoAssetId> --if-not-exists
```

See `cms-edit help fields-taxonomy` and `cms-edit help taxonomy-from-json` for field reference and batch schema.

## Person entries

Key fields: `name`, `slug`, `jobTitle`, `description`, `media` (featured image), **`bio`** (rich text).

```bash
# Add a person to a collection or page content array
cms-edit add "Dr. Jane Smith" --content-type person

# Scalars
cms-edit set @c0 name "Dr. Jane Smith"
cms-edit set @c0 slug jane-smith
cms-edit set @c0 jobTitle "Chief Medical Officer"

# Bio (same RTF workflow as component body)
printf '## Background\n\nBoard-certified with 20 years of experience.\n' | cms-edit rtf @c0 bio --markdown -
cms-edit read @c0 bio
```

**Site/runtime:** Resolved **person links** (`IPersonLink` from the REST API — article authors, collection contents, `contentfulAllPersonLinks`, related-people helpers) include **`bio`** when set in CMS. Use for team cards, related-people collections, and markdown/search export — not only full person detail pages.

**Idempotent taxonomy:** `--if-not-exists` is check-then-create — safe for sequential imports, not for parallel creates on the same slug.

```bash
# Resolve taxonomy IDs without a session
cms-edit resolve --tag-type-slug presentation-type
cms-edit resolve --tag-name "ASCO 2025" --tag-type-name "Conference Venue"
```

Use flat flags: `--page-slug`, `--article-cms-label`, `--tag-slug`, `--template-label`, etc. (`cms-edit help lookup`).

## Search and Discovery

```bash
# Search entries full-text (searches all content types by default)
cms-edit search "pricing page"
cms-edit search "hero" --type component

# List valid type values
cms-edit schema types component
cms-edit schema types collection

# List entries with filters
cms-edit list --type article --sort date -n 10     # recent articles by publication date
cms-edit list --type article --has-download       # articles with a download asset
cms-edit list --type article --slug my-article    # article with a specific slug
cms-edit list --type article --has-field tags      # articles where tags field is non-empty

# Find draft articles, then peek/open (both support drafts via CMA)
cms-edit list --type article --sort date
cms-edit peek --article-slug resources/blog/my-post
# Only list --published / resolve --published are published-only

# Resolve any entry or asset by ID (no open session required)
cms-edit resolve --id 4xKj2abcDefGhijK             # entry: shows type, title, slug, status
cms-edit resolve --id 7pQrStuvWxyzAbc              # asset: shows title, fileName, URL
cms-edit resolve --tag-type-slug presentation-type
cms-edit resolve --tag-slug asco-2025 --tag-type-slug conference-venue
cms-edit resolve --tag-type-slug presentation-type --json

# Find Media entries by asset
cms-edit list --type media --asset-id 7pQrStuvWxyzAbc
cms-edit list --type media --asset-filename istockphoto-123.jpg
cms-edit list --type media --asset-filename-match 'istockphoto-.*'
```

To resolve a **tag entry ID** to its slug or label (e.g. for constructing article URLs such as `/resources/publications/<tag>/<slug>`):

```bash
cms-edit resolve <tag-entry-id>
# Output includes: content type, title/slug, status
# Use the slug field value to build the URL path segment
```

## Multi-space

```bash
# Specify space explicitly
cms-edit --space brightline open --page-slug /home
cms-edit --space om1 open --page-slug /pricing
```

Or set `CMS_EDIT_SPACE=om1` environment variable.

## Complete Example: Update a Hero Section

```bash
# 1. Open the page
cms-edit open --page-slug /about-us

# 2. Find the hero component
cms-edit snapshot

# 3. Read it to understand current state
cms-edit read @c0

# 4. Update the heading
cms-edit set @c0 heading "We build for the future"

# 5. Update the body copy (use printf for multiline content)
printf '## Our mission\n\nWe help companies **ship faster** and _smarter_.\n\n- Founded in 2018\n- 500+ clients\n- [Join us](/careers)\n' | cms-edit rtf @c0 body --markdown -

# 6. Review
cms-edit diff

# 7. Save as draft
cms-edit save
# → "Saved 1 entry as draft. A human must publish for changes to go live."
```

## Common Patterns

### Update an existing article
```bash
cms-edit open --article-slug /blog/old-title  # or cms-edit open --id <id>
cms-edit set @p0 title "New Article Title"   # @p0 works in article sessions too
cms-edit set @p0 slug new-article-slug
cms-edit set @p0 description "Updated description"
cms-edit save
```

### Add a new section to a page
```bash
cms-edit open --page-slug /products
cms-edit schema types component  # discover what's available
cms-edit add CTA --after @c3
cms-edit set @c4 heading "Ready to get started?"
cms-edit rtf @c4 body --markdown "Join thousands of teams who trust us."
cms-edit links add @c4 --type external --label "Start free trial" --href https://app.example.com
cms-edit save
```

### Reorder page sections
```bash
cms-edit open --page-slug /home
cms-edit snapshot  # see current order
cms-edit move @c3 --after @c1  # move section 3 to position 2
cms-edit save
```

## Visual checks

cms-edit does not capture screenshots. Use preview URL commands, then open the URL with a browser MCP (e.g. Chrome DevTools) to verify layout after edits.

**Config:** Add `devBaseUrl` to the space in `.contentful-cms.json`:

```json
{
  "spaces": {
    "om1": {
      "devBaseUrl": "http://localhost:3013"
    }
  }
}
```

```bash
# Page (explicit slug or open session root)
cms-edit preview url /pricing
cms-edit open --page-slug /pricing
cms-edit preview url

# Component or collection showcase (mock data)
cms-edit preview showcase --component HeroSimple
cms-edit preview showcase --collection CardGrid --param backgroundColour=Navy
```

For CMS guidelines batch PNG capture, use `cms-capture-screenshots` from `@se-studio/project-build` (separate from cms-edit; requires agent-browser).

## Undo changes

There is no field-level revert command. To abandon unsaved edits: `cms-edit diff` to review, then `cms-edit discard` (or `discard --all`). To roll back saved drafts, use the Contentful web app version history.

Note: Entries created via `add` that you have not saved can be removed with `cms-edit remove <ref>`.

## Health Check

Validate config and connectivity before running a workflow.

```bash
cms-edit health   # Check config file, space config, Contentful connectivity
```

## Schema Inspection

Inspect field definitions for a content type. Use `cms-edit schema types <ct>` for type-discriminator values only.

```bash
cms-edit schema component         # Show all fields, types, enum values, link targets
cms-edit schema component --json  # Full JSON output
```

## Content catalog (`list`)

Browse entries by content type (index-backed for catalog types). Run `cms-edit index sync` once per space, or let commands auto-sync.

```bash
cms-edit list --type page                         # All pages (label sort)
cms-edit list --type page --sort updated          # Most recently updated first
cms-edit list --type article --sort date          # Articles by publication date
cms-edit list --type pageVariant                  # A/B or variant pages
cms-edit list --type tag --tag-type-name "Topic"  # Tags under a tag type
cms-edit list --type tagType                      # Tag types
cms-edit list --type articleType                  # Article types
cms-edit list --type navigation                   # Navigations
cms-edit list --type person --force-cma           # People (not in catalog index)
```

## Peek

Inspect a page without affecting your active session.

```bash
cms-edit peek --page-slug /pricing   # Show snapshot of /pricing; your active session is unchanged
cms-edit peek --id <entryId>         # Look up by entry ID
```

## Batch Operations (batch run)

Run a sequence of operations from a JSON file or stdin.

```bash
cms-edit batch run --json '[...]'            # Run batch ops from inline JSON
echo '[...]' | cms-edit batch run             # Pipe from stdin
cms-edit batch run --json '[...]' --dry-run  # Validate without saving
```

Supported ops: `open`, `set`, `rtf`, `rtf-replace`, `save`, `add`, `links-add`.

| Op | args | opts |
|----|------|------|
| `open` | `["/slug"]` | — |
| `set` | `[ref, field, value]` | — |
| `rtf` | `[ref, field, markdown]` | — |
| `rtf-replace` | `[ref, field]` | `{ find, replacePlain?, mode? }` |
| `save` | — | — |
| `add` | `[type, contentType]` | `{ after?, parent?, target?, existingId? }` |
| `links-add` | `[ref]` | `{ type, label, href?, slug?, id?, assetId? }` |

The `add` op returns the new `@ref` in the step message; subsequent ops can reference it.

Example `ops.json` — update an existing page with a new CTA that has two buttons:
```json
[
  { "cmd": "open", "args": ["/pricing"] },
  { "cmd": "add", "args": ["CTA", "component"], "opts": { "target": "bottomContent" } },
  { "cmd": "set", "args": ["@c5", "heading", "Ready to get started?"] },
  { "cmd": "links-add", "args": ["@c5"], "opts": { "type": "external", "label": "Start free trial", "href": "https://app.example.com" } },
  { "cmd": "links-add", "args": ["@c5"], "opts": { "type": "internal", "label": "Learn more", "slug": "/about" } },
  { "cmd": "save" }
]
```

## Create Page or Article from JSON

Create a complete page or article — including all components, fields, and CTA links — from a single declarative JSON file. This is the recommended approach for creating multiple pages or articles in bulk.

```bash
cms-edit create from-json --json '{...}'            # Create from inline JSON
cms-edit create from-json --json '{...}' --dry-run  # Preview without writing
cat page.json | cms-edit create from-json             # Pipe from stdin
```

**Page JSON schema:**

```json
{
  "type": "page",
  "slug": "/parent-coaching/digital-life-safety/cyberbullying",
  "title": "Cyberbullying",
  "description": "Optional meta description (SEO)",
  "templateId": "contentful-template-entry-id",
  "indexed": true,
  "components": [
    {
      "contentType": "component",
      "type": "HeroSimple",
      "cmsLabel": "Hero — Protecting Your Child from Cyberbullying",
      "target": "content",
      "fields": {
        "heading": "Protecting Your Child from Cyberbullying",
        "body": "## Why it matters\n\nContent here with **bold** and [links](https://example.com).",
        "backgroundColour": "Yellow"
      },
      "links": [
        { "type": "external", "label": "Download Guide", "href": "https://example.com/guide.pdf" },
        { "type": "internal", "label": "Learn More", "slug": "/resources" }
      ]
    },
    {
      "contentType": "collection",
      "type": "CardGrid",
      "cmsLabel": "Related topics grid",
      "fields": { "heading": "Related topics" },
      "items": [
        {
          "contentType": "component",
          "type": "Card",
          "cmsLabel": "Card — Screen Time",
          "fields": { "heading": "Screen Time" }
        },
        {
          "existingId": "abc123def456",
          "comment": "Reuse existing 'Emotional Regulation' card"
        }
      ]
    },
    {
      "existingId": "xyz789ghi012",
      "comment": "Shared 'Join BLK Now' CTA reused on every page"
    }
  ]
}
```

**Article JSON schema:**

```json
{
  "type": "article",
  "slug": "/resources/publications/cyberbullying/new-research-2025",
  "title": "New Research on Cyberbullying 2025",
  "description": "Key findings from the 2025 cyberbullying study.",
  "articleTypeId": "3abcDef456ghi",
  "date": "2025-04-14",
  "components": [
    {
      "contentType": "component",
      "type": "RichText",
      "fields": {
        "body": "## Introduction\n\nNew research shows..."
      }
    },
    {
      "contentType": "component",
      "type": "CTA",
      "target": "bottomContent",
      "links": [
        { "type": "external", "label": "Download Full Report", "href": "https://example.com/report.pdf" }
      ]
    }
  ]
}
```

- `type` defaults to `"page"`. Set `"type": "article"` (or include `articleTypeId`) for articles.
- `articleTypeId` is required for articles. Find valid IDs with `cms-edit list --type articleType`.
- `date` defaults to today (YYYY-MM-DD) if omitted.

**Key points:**
- Any component entry in `components` or `items` may use `{ existingId }` to link an existing entry instead of creating a new one.
- The same applies to `links` entries — use `{ existingId }` to attach an existing link entry.
- `cmsLabel` sets the human-readable label shown in Contentful's entry list. Defaults to `type` or `contentType` if omitted. **Always set `cmsLabel` on components when a page has multiple instances of the same type** (e.g. three CTAs) — editors need to distinguish them.
- String `fields` values become Rich Text when they auto-detect as Markdown (same heuristic), HTML (tag-like), or Rich Text JSON (`nodeType: document`). Otherwise strings stay as-is — use `{ "value": "…", "format": "text"|"markdown"|"html"|"json" }` or `cms-edit rtf` with `--text|--markdown|--html|--rtf-json`.
- `target` sets which content array to use: `topContent`, `content` (default), or `bottomContent`.
- Slugs must not have a trailing slash — `from-json` strips it with a warning, but avoid it in source JSON.
- All entries are created as **drafts**. A human must publish in Contentful.
- `--dry-run` prints the full plan without writing anything.

## Find Existing Entries (for existingId)

When building page JSON that references existing entries, find their Contentful IDs first:

```bash
# Search for a shared component by name
cms-edit search "join blk now" --type component --json
# → output includes "id" field — use this as existingId in your page JSON

# Browse all components of a specific type
cms-edit list --type component --filter 'fields.componentType=CTA'

# Confirm an entry before referencing it
cms-edit resolve 4xKj2abcDef
```

## Inventory & Status

Use `list --type <type>` for inventory. Catalog types (page, article, pageVariant, template, taxonomy, navigation) use the local index. Use `--force-cma` for other types (person, customType, pageTest) or published-only with `--published`.

```bash
cms-edit list --type page
cms-edit list --type article --sort date -n 20
cms-edit index dump --reference-only          # Full catalog snapshot in terminal
```

**Note on bulk publish:** The tool cannot publish entries — all saves create drafts only. This is intentional. Review and publish drafts from the Contentful web app.

## Concurrent Sessions

Use `--session <name>` or `CONTENTFUL_CMS_SESSION=<name>` to isolate parallel workflows.

```bash
cms-edit --session agent-1 open --page-slug /pricing
cms-edit --session agent-2 open --article-slug /blog
```

## Related skills

For templates see the **templates** skill; for navigation see the **navigation** skill; for rich text and embeds see the **rich-text** skill.
