Studio Docs

When to Use

Given an EXISTING Contentstack Content Type (already provisioned, entries populated), propose Studio Section boundaries by reading the CT’s schema tree. Outputs a Section inventory: one Section per binding scope (root / Group / Reference / Modular Block) with the correct linkedSchemaKind per boundary.

Use when adopting Studio on top of a stack that already has real content (Profile D, the “existing CT” starting state). Phrases: “we have blog_post entries already, how do I map that to Sections”, “I have a stack + populated content, what Sections should I build”, “propose Sections from my existing CT schema”. Complements discover-sections, which scans route files. This skill scans a CT schema instead. Run once per CT you’re adding Studio on top of. Do NOT use for greenfield installs (no existing CT), or when the CT is being redesigned (design-first flow via decompose-design instead).

Auth preflight: settle the credential before the first API call. Resolve it OAuth-first per authenticate-cma: CS_OAUTH_ACCESS_TOKEN, else the Contentstack MCP’s stored session. Never ask the user for a session authtoken. If nothing resolves, or a refresh fails with 400 invalid_refresh_token, hand them ! CONTENTSTACK_REGION=<code> npx @contentstack/mcp --auth (it needs a TTY and a browser, so it cannot be run for them) and wait. 403 error_code 316 is a valid credential aimed at another org: fix the org or the api_key, do not re-authenticate.

Discover Sections From an Existing Content Type

Context

Studio’s model is one Section per scope. A scope is a place in the CT schema tree where a Section binds: the CT root, a Group field, a Reference field, or a Modular Block. Each scope has its own linkedSchemaKind and its own set of visible fields. You can’t cross scope boundaries in one Section.

If a customer already has a well-populated CT (e.g. blog_post with 400 published entries), they don’t need decompose-design. The schema is settled. They need help mapping the existing schema shape onto Section boundaries. This skill produces that mapping.

The output is a Section proposal: a prioritized list of Section candidates, one per scope, with the fields visible in each scope + a proposed component structure per Section. You hand the top entry to build-section, ship it, then hand the next entry to build-section. The proposal stays valid until the CT schema changes.

This skill is discovery, not authoring. It doesn’t write any composition. It produces a markdown report.

Task

  1. Fetch the target CT schema. Given ctUid + a stack management token, GET /v3/content_types/<ctUid>. Extract schema (the field tree) and options (is_page, url_pattern if set).

  2. Walk the schema tree and enumerate scopes. Every scope is a Section candidate. Categories:

    • Root scope: the top-level CT. Fields at the root that aren’t nested in Group / Reference / Modular Block. Every CT has this.
    • Group scopes: each data_type: "group" field is a Section-authorable scope. Nested groups (group-of-group) are their own scopes.
    • Reference scopes: each data_type: "reference" field is a Section scope. Both single-reference (ref_multiple: false) and multi-reference (ref_multiple: true).
    • Modular Block scopes: each block type within a data_type: "blocks" field is its own Section scope. If the field has 3 block types, that’s 3 scopes.
  3. For each scope, list visible fields. Visible = fields declared at that scope + fields inherited via _content_type_uid on references. Do NOT include fields from parent or sibling scopes. Sections can’t bind across scope boundaries.

  4. Propose a component structure per Section. Best-effort inference from field names + types:

    • title / headline / heading maps to a Heading component with text bound to the field.

    • image / cover_image / hero_image maps to an Image component.

    • Rich text: decide by data_type/field_metadata, never by field name. These carry formatting, so Text/Heading would render markup as literal characters:

      Schema shapeComponentProp type
      data_type: "json_rte"RichTextjson_rte
      data_type: "text" + field_metadata.allow_rich_text (HTML RTE)RichTextany
      data_type: "text" + field_metadata.markdownRichTextany
      data_type: "text" + multiline, no rich-text flagsTextplaintext

      Choose CollapsibleText instead wherever the design truncates the copy (“read more”, accordion). If any such field permits embedded entries or assets, record it. The project needs one registerRTERenderer call, and unregistered embeds render as bare delivery-API spans. See register-json-rte.

    • body / content / description that is plain long text maps to a Text component, and record it as an upgrade candidate (below).

    Rich-text upgrade candidates: always surface these, never apply them silently. An existing model often holds prose in a plain text field simply because that was the quickest thing to pick when it was created. Report them and let the user decide:

    What you foundSuggestCost to accept
    Field is already json_rte / rich text, but the current code renders it through a plain text componentSwitch the component to RichTextNone: no schema change, no migration. Free correctness win
    Plain multiline text field holding body copy, description or proseConsider json_rte so authors can bold, link, list and embedA schema change on a live CT. See migrate-ct-schema. Existing entry values carry over as plain paragraphs
    Long prose the site truncates behind “read more” / an accordionCollapsibleText over the same fieldNone if the field is already rich text

    Phrase it as a recommendation with the cost attached (“summary is plain text today. Making it json_rte would let authors add links, but it’s a schema change on N live entries. Want it?”) and proceed with the existing type if they decline. Never migrate a field as a side effect of a discovery run.

    • cta_label + cta_url (or link) maps to a Button component.
    • is_featured / is_premium (boolean) maps to a Condition Block around premium-only children.
    • Multi-value fields inside a Repeater (already the scope’s iteration boundary if Modular Block or multi-reference).
    • Flag unknown fields in “Unmapped fields” so the developer decides.
  5. Score + prioritize. Sort scopes by rough authoring value:

    • Root scope + top-level fields: HIGHEST (every entry uses this Section).
    • Groups referenced from multiple contexts: HIGH.
    • Modular Blocks (composability wins): MEDIUM-HIGH.
    • References not yet used elsewhere: MEDIUM.
    • Rarely-populated fields (empty on >50% of entries. Check via a CDA sample of 10 entries): LOW.
  6. Emit the Section proposal as a markdown document at docs/_discovery/<ct_uid>-sections.md:

    # Section proposal — `<ct_uid>` CT
    
    ## Summary
    - CT UID: <ct_uid>
    - Total scopes: <N>
    - Recommended Sections to build: <top 3-5>
    
    ## Scope-by-scope Section candidates
    
    ### 1. `<ct_uid>` — root scope (linkedSchemaKind: entry)
    
    **Fields in scope:** `title`, `slug`, `hero_image`, `excerpt`, `published_date`, `is_featured`
    
    **Proposed Section: `<ct_uid>_hero`**
    - Layer 2 composition containing Heading + Image + Text + Condition Block
    - Bindings:
      - Heading.text ← `title`
      - Image.src ← `hero_image`
      - Text.value ← `excerpt`
    - Condition Block around a "Featured" badge, condition `is_featured == true`
    - Exposed props: `heading_level` (choice: h1/h2/h3), default h1
    - Priority: HIGH — used on every entry
    
    ### 2. `<ct_uid>.body_blocks` — Modular Block scope
    
    **Block types in this field:** `text_block`, `image_block`, `quote_block`
    
    Three Section candidates, one per block type:
    
    #### 2a. `<ct_uid>_body_text_block` (linkedSchemaKind: block, block_type: text_block)
    [...]
    
    #### 2b. `<ct_uid>_body_image_block` (linkedSchemaKind: block, block_type: image_block)
    [...]
    
    ### 3. `<ct_uid>.author` — Reference scope (linkedSchemaKind: reference)
    
    **Referenced CT:** `author`
    **Fields visible in this scope:** `author.name`, `author.avatar`, `author.bio`
    
    **Proposed Section: `<ct_uid>_author_card`**
    [...]
    
    ## Unmapped fields
    
    These fields exist in the CT but the skill couldn't confidently propose a Section shape. Decide per-field:
    
    - `<field_uid>` (data_type: <type>) — [suggestion, or "review manually"]
    
    ## Build order
    
    1. Build `<ct_uid>_hero` first (touches every entry).
    2. Then Modular Block Sections (2a, 2b, 2c) — enables authoring flexibility on body content.
    3. Then reference Sections (author_card, related_posts).
    4. Finally the Template composing all Sections in order.
    
    ## What this proposal is NOT
    
    - Not a schema migration — no CT fields change.
    - Not a design proposal — component names / layout are best-effort inference; feel free to override.
    - Not an author-experience proposal — governance choices (which props to expose per Template) are yours.
  7. Sanity-check the proposal.

    • Every listed scope has at least one populated entry (spot-check via CDA /v3/content_types/<ctUid>/entries?limit=1&only[BASE][]=<field>).
    • No proposed Section spans two scopes.
    • Every scope has a valid linkedSchemaKind value.
    • Unmapped fields flagged: do NOT silently drop them.

Inputs Needed From the User

  1. Content Type UID: the CT to discover Sections against.
  2. Stack API key + management token: for the CMA read.
  3. Optional: sample size for populated-check: default 10 entries. Increase for larger stacks.
  4. Optional: output path: default docs/_discovery/<ct_uid>-sections.md.

Acceptance

  • Every scope in the CT schema is enumerated.
  • Each Section candidate has a valid linkedSchemaKind matching its scope.
  • Fields are correctly attributed to scopes, no cross-scope leakage.
  • Component-structure inference is documented as a proposal (not a decision).
  • Unmapped fields are flagged with the field UID + data type.
  • Build order is topologically sound: foundational Sections before dependents.
  • Output file emitted at the requested path.

Common Pitfalls

PitfallWhy it bitesFix
Proposing a Section that binds fields from two scopesStudio rejects cross-scope bindings at author timeEvery scope gets its own Section. If you want to combine, use a Template that composes both Sections.
Assuming Modular Block = one SectionEach block type is its own scope. A 3-block-type field is 3 SectionsEnumerate block types explicitly.
Skipping empty scopesA rarely-populated field might still need a Section for the entries that DO have itInclude, mark LOW priority, let developer decide.
Over-inferring component structureComponent name doesn’t have to match field name. Developer’s design system may not have “Heading”Propose, flag as suggestion, developer overrides.
Missing _content_type_uid resolution on referencesReference scopes need the referenced CT’s schema fetched too, otherwise fields aren’t visibleFor each reference field, fetch the referenced CT’s schema too.
Forgetting to check populated-nessSkill proposes 15 Sections. 8 of them cover fields that no entry actually populatesSample 10 entries. Downgrade scopes where >50% of samples don’t populate.

Handoff to Downstream Skills

After the proposal is emitted, the customer walks the build order:

  1. Read the proposal: the developer + design lead review + edit.
  2. Register any missing components via register-component.
  3. Build the top-priority Section via build-section. Hand it the scope + field list from the proposal.
  4. Verify it renders against a real entry via verify-setup Layer 6-7.
  5. Repeat for each Section in the build order.
  6. Assemble the Template via build-connected-template.

See Also

  • discover-sections (Section discovery from EXISTING ROUTE FILES (Profile C) hand-coded migration). Same output shape, different input.
  • decompose-design (Section discovery from a DESIGN (Profile B) greenfield with a design).
  • build-section: authors each Section this proposal identifies.
  • from-designs-to-sections: the three-layer mental model + scope rules (one Section per scope).
  • migrate-ct-schema: evolves the CT AFTER this proposal, if scope boundaries need adjusting.