Studio Docs

When to Use

Orchestrator for “make my existing project Studio-compatible”. Asks one scoping question (minimal conversion, one page, or the whole site), then drives the minimal conversion by chaining the install skills with their answers pre-filled: Studio SDK installed, /canvas mounted, and Studio wired as a fallback renderer behind the routes the app already owns, with the app’s own 404 kept. Localized apps get every locale wired, not only the default.

Use for whole-project Studio enable/setup requests, including an existing app or a new app. On a new app, run the same setup sequence and let route detection choose the optional catch-all. Use install-studio alone only for an explicit SDK-only request. Single-page migrations use migrate-page-to-studio; design-only inputs use decompose-site.

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.

Convert an Existing Project to Studio: Minimal First

Verification Status

Orchestrator over verified skills. The route pattern it wires is the documented one in partial-adoption-coexisting-with-a-code-driven-app (catch-all + code-route precedence). The locale step (§ Step 3) is not yet runtime-verified against a next-intl app. Until it is, treat the single Base URL rule there as the working model, run the redirect probe on every locale, and report anything that does not match rather than improvising around it.

Why This Skill Exists

“Convert this project to Studio” has been answered with two scopes: decompose the whole site, or pilot one page. Both rebuild pages. Neither is what most teams need first, which is Studio available in the app without any existing page changing. Without a skill for that, the agent improvises a scope question and steers the user into a migration they did not ask for.

The minimal conversion is a fixed contract. Every request resolves in this order:

  1. A code route matches. The project’s own page renders, unchanged. Code routes win by precedence (Next.js more-specific-wins, React Router first-match).
  2. No code route matches. The catch-all mounts <StudioComponent /> and Studio resolves a composition for that URL.
  3. Studio has no composition either. The project’s existing 404 page renders. Never a blank div for visitors, never a 500. Studio’s editor iframe stays exempt so unsaved templates still preview.

Plus the /canvas route for Section authoring. No page migrated, no component decomposed, no Section built. Authors can create Studio pages at new URLs the same day. The larger scopes are supersets of this, so minimal is always phase one.

Task

Step 0: Confirm this is a whole-project ask

If the user named one route or one page, stop and run migrate-page-to-studio via the plan-first rule instead. If the input is a design with no code, run decompose-site. Otherwise continue.

Step 1: Resolve scope from the request and prior answers

For a broad enable/setup request, use minimal conversion and state that it includes the canvas and wildcard fallback. Respect explicit SDK-only, dedicated-route, and migration requests. Carry existing answers forward; do not ask a question already settled in the conversation. Ask the following only when the requested migration scope is ambiguous:

Three ways to bring Studio into this project. Pick one:

1. Minimal conversion (recommended). I install the Studio SDK and mount Studio as a fallback behind your existing routes. Every page you have today keeps rendering exactly as it does now. Any URL your code does not claim is served by Studio, and if Studio has nothing for it either, your existing 404 page shows. Authors can start building new Studio pages at new URLs right away. Nothing is migrated or decomposed. About an hour.

2. Minimal, then migrate one page. Everything in 1, then we pick one route, plan it, and swap it to Studio so you can validate authoring on real content.

3. Minimal, then a full-site plan. Everything in 1, then a consolidated plan across every page type, built in dependency order.

Reply 1, 2, or 3.

Record the resolved scope. A reply such as “yes” or “start” accepts an immediately preceding concrete proposal. Options 2 and 3 run minimal conversion first, then the appropriate migration planning skill. Approval for setup does not authorize unrelated migrations, publishing, or changes to other projects.

Keep this non-secret progress record in the working context and resume it after login, restart, or compaction:

Scope and accepted choices:
Framework, package manager, route root, React/Studio versions:
MCP: callable/read-verified | unavailable (reason) | declined
Project, stack, region, branch, environment, locale map:
SDK initialization modules:
Canvas route and observed iframe URL:
Fallback route and observed template iframe URL:
Verification: passed | failed | unverified, with evidence per check
Next pending step:

Run all remaining setup steps within the accepted scope. A specialist’s “next steps” section is a handoff back here, not the end of project setup.

Step 2: Preflight

Run analyze-project-fit. Carry forward: framework, package manager, React major, data centre, existing Contentstack SDKs, and the i18n row. Blockers (Path B stale SDK pin, C, G) stop this skill until resolved.

Run install-contentstack-mcp discovery before requesting stack credentials or doing API work, even if OAuth credentials already exist. This is a gate, not a silent fallback: first determine whether callable Contentstack MCP tools are available in the active host, then offer MCP configuration/authentication if they are not. Do not ask the user to fill the app’s .env merely because MCP is not yet connected.

Use MCP for supported CMA reads and writes after a successful read probe. Use OAuth HTTP or the Studio UI for operations MCP does not expose, especially Studio project listing/configuration, Live Preview enablement, and delivery/preview-token creation. Record the reason for each fallback. MCP does not eliminate the app runtime configuration: after discovery/provisioning, the app still needs the selected stack API key, delivery token, preview token, environment, locale map, and the Studio project’s exact contentTypeUid in its environment. Ask only for values MCP cannot discover or create, and never guess a project, stack, locale, or composition content type UID.

Then enable-visual-experience: stack-level Live Preview must be on before any app-side work.

Step 3: Locale step. Detect, map, wire, verify.

Studio treats locale as a context variable, not as part of a composition URL. Composition URLs are stored without a prefix (/rewards). Studio builds the preview iframe address as the environment’s per-locale Base URL plus that composition URL, and an environment holds exactly one Base URL per locale. A localized app that skips this step gets a blank canvas when an author switches language, a doubled prefix such as /en/en, or a middleware redirect that kills the iframe.

The single rule this skill enforces:

  • Composition URLs never carry a locale prefix.
  • The environment’s per-locale Base URL carries the app’s prefix when the app is prefixed (http://localhost:3000/en, http://localhost:3000/fr), and is the bare origin when it is not.
  • The catch-all strips the app’s locale prefix from the request path before calling Studio (/en/rewards becomes url: "/rewards" with locale: "en-us"), so a doubled prefix cannot come from the route side. If compositions were already authored with prefixed URLs, list them and stop. Do not guess which side to change.

3a. Detect app-side i18n.

# Packages
grep -E '"(next-intl|next-i18next|i18next|react-i18next|react-intl|@lingui/[a-z]+|next-international)"' package.json
# Route segment
find app src/app -maxdepth 2 -type d \( -name '[[]locale[]]' -o -name '[[]lang[]]' \) 2>/dev/null
# Next config i18n key, locale middleware, React Router locale param
grep -n "i18n" next.config.* 2>/dev/null
grep -ln "locale\|createMiddleware" middleware.* src/middleware.* 2>/dev/null
grep -rn 'path="/:\(lang\|locale\)' src 2>/dev/null | head -3

Record: prefix strategy (always, as-needed, or none), the default app locale code, every app locale code. Nothing found means un-localized: note that in one line and run only 3d for the default locale.

3b. Read stack-side locales.

# $CS_AUTH is the resolved credential header from `authenticate-cma` — on the
# OAuth path it is `authorization: Bearer <access_token>`. A bare
# `authorization: <token>` is the MANAGEMENT-token form and 401s under OAuth.
curl -sS "https://<cma-host>/v3/locales" -H "api_key: <stackApiKey>" -H "$CS_AUTH"

Record every enabled code and which one is fallback_locale-less (the master).

3c. Build the locale map and show it. App code to stack code (en to en-us, fr to fr-fr). Print the table and ask the user to confirm. Halt on any app locale with no stack locale — that one is fatal, since the app would serve a URL the stack cannot resolve. Do not invent a mapping and do not proceed with the intersection.

The reverse is not fatal: a shared stack routinely carries locales this app doesn’t serve (other sites, other brands, locales not launched yet). List those as out of scope in one line, wire every app-served locale, and continue. Halting there would block the common shared-stack setup for a condition the app can’t act on.

App prefixStack localeBase URL to set on <env>
en (default)en-ushttp://localhost:3000/en
frfr-frhttp://localhost:3000/fr

3d. Pre-flight every Base URL, not only the default. Read GET /v3/environments as in install-studio step 3 and check urls[] for every mapped locale. For a missing or wrong entry, print the exact PUT /v3/environments/<env> body with all locale URLs, ask for a yes, then write it. Do not write without the yes.

3e. Redirect probe, per locale. Run the curl from setup-template-preview-routes § middleware 307 against <base URL> and <base URL>/canvas for every mapped locale. Any 3xx stops this skill with the three documented fixes. Prefer “locale in the Base URL” since 3d already did it. A 308 on a trailing slash means skipTrailingSlashRedirect: true in next.config.

3f. Answers carried into the route step. The catch-all lives inside the locale segment (app/[locale]/[...slug]/page.tsx, or <Route path="/:lang/*"> registered last). It strips the prefix before building url. It passes the mapped stack code as { locale } on the SSR fetch and the same value to useCompositionData on CSR. It forwards searchQuery untouched so Studio’s own locale override still wins inside the iframe. Its not-found branch renders the app’s localized 404 (app/[locale]/not-found.tsx where one exists).

Step 4: Install

Run install-studio. Take the minimal-add branch whenever @contentstack/delivery-sdk or @contentstack/live-preview-utils is already present: do not re-init the stack, create a separate lib/studio.ts. The per-locale Base URL check inside it is already satisfied by 3d. Do not register components in this step. Built-in components are enough for a first Studio page, and register-component is offered after the conversion, not inside it.

Step 5: Canvas route

Run setup-section-preview. A route mounting StudioCanvas must exist before the catch-all. Record the actual Section iframe URL generated by the selected project/environment and ensure that exact path mounts the canvas without site chrome. Preserve a working root or locale-prefixed canvas route; do not infer its address from the template URL or add a duplicate based on a guessed concatenation rule. Verify every served locale in the browser. Canvas URL remains the configured path of that route.

Step 6: Fallback route

Run setup-template-preview-routes in wildcard mode with these answers fixed. Do not re-ask them:

Question it asksAnswer for the minimal conversion
Route mode (step 3)A. Wildcard. Code routes win by precedence, so nothing the app owns changes
Opt-outsNone needed. Code routes already win. React Router only: register /api/* and any non-page handler above the catch-all
[...slug] vs [[...slug]] (step 5)[...slug] when a root page exists (it nearly always does on an existing app), else [[...slug]]. On a locale-prefixed app, inside the segment
chromeOwner (step 5a)code. Header and footer stay in the layout
404 branch (step 5a iii)Reuse the project’s existing not-found (app/not-found.tsx, pages/404.tsx, or the NotFound component). Localized apps: the per-locale one. Create a file only when none exists. Editor-mode exemption on
LocaleFrom Step 3f

Framework scope: Next.js App Router, Next.js Pages Router (pages/[...slug].tsx, existing pages/404.tsx), Vite + React Router (path="*" registered last, the old <Route path="*" element={<NotFound />} /> removed and NotFound rendered from the no-spec visitor branch). Remix, Astro, or a custom router: stop and say the pack is under-specified for that router rather than guessing, per the pack’s rule that silence is reported, not filled.

Step 7: Configure Studio

Run configure-studio. Environment = the one whose Base URLs were set in 3d. Language = the mapped default stack locale. Canvas URL = /canvas.

Step 8: Verify

Run verify-setup, then this smoke, once per mapped locale:

URLExpected
An existing code page (<base URL>/products/some-slug)Renders exactly as before. Same header, same footer, same markup
A bogus URL (<base URL>/definitely-not-a-page)The project’s own 404 page. Not a 500, not a blank page, not an empty div
<base URL>/canvas<StudioCanvas /> renders. The 404 page here means the catch-all claimed it
The redirect probe from 3e200, empty redirect_url

Then in Studio: create a template, do not save it, open it. The canvas renders it (editor exemption works). On a localized project switch the canvas language once. The iframe reloads at the other locale’s Base URL rather than going blank.

Step 9: Report and hand off

Print, filled in:

Minimal conversion done.
  Existing routes:   untouched (<n> code routes win by precedence)
  Fallback route:    <path> mounts <StudioComponent /> for unclaimed URLs
  404:               <file> for visitors, editor iframe exempt
  Canvas route:      /canvas (<file>)
  Locales:           <en-us, fr-fr | single locale en-us>, Base URLs set on <env>
  Studio project:    env <env>, language <locale>, Canvas URL /canvas

Definition of done for this scope (from complete-the-build):
  [x] cslp.appendTags in the SDK init
  [x] stack Live Preview + preview token
  [x] /canvas mounted
  [x] per-locale Base URL for every locale served
  [ ] component studioAttributes / thumbnailUrl / Section ui_preview / publishing: not applicable until something is authored

Next: author a first Studio page at a URL no code route claims, then register your components (register-component) when you want them in the palette.

Option 2 or 3 chosen in Step 1: now run plan-studio-architecture for the scoped page(s). Option 1: stop here.

Inputs Needed From the User

  1. scope: minimal for a broad setup request, or the user’s explicit migration/SDK-only scope from Step 1.
  2. Confirmation of the locale map (3c) and of every Base URL write (3d).

Acceptance

This skill succeeds only when ALL of the following are true.

  • Scope and prior accepted choices are recorded. Broad setup includes canvas and fallback routing; explicit user constraints take precedence.
  • MCP availability was checked in the active host and a read operation verified its target, or a concrete fallback reason is recorded.
  • The verification report distinguishes HTTP reachability from rendering and live editing. Unverified browser checks are not marked passed.
  • analyze-project-fit ran and no blocker path (B / C / G) was open when install started.
  • Locale detection ran. On a localized app the map was confirmed by the user, every mapped locale has a Base URL on the targeted environment, and the redirect probe returned 200 for every locale. On an un-localized app this was stated in one line.
  • No existing route file was modified or deleted. The only route files added are the canvas route and the catch-all.
  • The per-locale smoke in Step 8 passed for every mapped locale: existing page unchanged, bogus URL renders the project’s own 404 with no 500, /canvas renders <StudioCanvas />.
  • No component was registered, decomposed, or authored as part of the minimal conversion.
  • The report block in Step 9 was printed with the definition-of-done items marked as above.

Common Pitfalls

PitfallWhy it bitesFix
Skipping the Step 1 gate and improvising “full site or one page”Neither scope is what “make it compatible” asks for, and both start rebuilding pagesAsk the three-option gate verbatim. Minimal is the default
Wiring the catch-all before /canvas existsThe catch-all claims /canvas and every Section renders as a composition miss or the 404 pageStep 5 before Step 6, always
Optional [[...slug]] on an app with a root pageBoth match /. Next.js reports a collision or the root page loses[...slug] whenever any root page.* exists, including app/[locale]/page.*
Treating “no pages found” as greenfieldA fixed path list misses src/app and locale segments, so a catch-all lands in an app full of pagesUse the find in setup-template-preview-routes step 3 and confirm with ls before believing an empty result
404 branch returns an empty <div /> to visitorsThe site never 404s. Correct for the editor iframe, wrong for everyone elseGate on isStudioEditorMode: empty div in the editor, the project’s 404 page for visitors
Unwrapped fetchCompositionDataIt throws on a miss, so every bogus URL is a 500 instead of the 404 pageUse resolveComposition from configure-csr-vs-ssr, never a hand-rolled catch
Only the default locale has a Base URLSwitching language in the canvas produces a blank iframe with an error pointing at the Studio projectStep 3d checks and sets every mapped locale
Catch-all passes the prefixed path to Studio/en/rewards never matches a composition stored as /rewards, or resolves to a doubled /en/enStrip the prefix, pass the stack locale code separately, forward searchQuery untouched
App locale codes assumed equal to stack codesen is not en-us. The fetch resolves nothing and the page 404s in one language onlyBuild and confirm the map in 3c. Halt on a mismatch
Decomposing or registering “while we are here”Turns a one-hour conversion into a migration the user did not chooseNothing beyond the fallback route until the user picks option 2 or 3

See Also