---
name: troubleshoot-canvas
---

## When to use

Symptom-mapped diagnostic for a non-working Studio canvas. Given a user-visible failure, narrow it to the underlying cause and propose the fix. Run a Section-test pre-flight first.

Use when Studio's canvas misbehaves with a specific symptom: "canvas is blank", "Component Loading Error", "canvas shows wrong composition", "preview entry doesn't update", "iframe never finishes loading". If the symptom is ambiguous, route via `troubleshoot` first. Do NOT use to validate a fresh install (use `verify-setup`). Do NOT use for authoring-quality issues: those are skill-correctness.

> **Auth preflight: settle the credential before the first API call.** Resolve it OAuth-first per [`authenticate-cma`](authenticate-cma.md): `CS_OAUTH_ACCESS_TOKEN`, else the Contentstack MCP's stored session: Read that skill for where it is, even with no MCP connected. **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.

# Troubleshoot Studio canvas issues

## Context

Studio's canvas iframe is downstream of four moving parts: Delivery SDK auth, Live Preview's post-message channel, the canvas-app's React rendering, and the saved composition spec. A symptom can come from any of them. This skill maps each symptom to its root cause.

Pair with `verify-setup` if the user is freshly set up: verify-setup walks all four layers, troubleshoot-canvas assumes you already know the symptom.

Reference: `docs/30-look-it-up/05-reference/50-error-message-index.md` (the canonical table of symptoms and their fixes).

## Step 0: is the app actually being served?

**Do this before the section-test, before anything.** Studio iframes your app. If nothing answers on that origin, every downstream symptom appears: blank canvas, "SDK Not Initialized", "Template Did Not Load", "localhost didn't send any data". None of them says "your dev server isn't running", and it is a routine first cause: the server was never started, it crashed, it took a different port on restart, or you're serving `vite preview` on `:4173` while Studio points at `:5173`.

```bash
# 1. Is anything listening on the port Studio targets?
curl -sS -o /dev/null -w "%{http_code}\n" http://localhost:<port>/canvas

# 2. What origin does Studio actually iframe? (must match, scheme included)
#    Sections  -> env base URL + Canvas URL
#    Templates -> env base URL + the template path
#    GET /v3/environments/<env> -> urls[].url
```

A connection error or a non-2xx/3xx on step 1 means **stop**. Start the server (or fix the port/scheme mismatch) and re-test. Only when the URL Studio targets returns content does the rest of this skill apply.

Cheap, and it costs nothing when it passes. Also re-run it after any crash, port change, or switch between `npm run dev` and `vite preview`.

## Run this SECOND: the section-test pre-flight

Before reading the symptom table (especially for **"SDK Not Initialized"**, blank canvas, or any popup that names the SDK as broken), run this one diagnostic. It cuts the longest debugging path from hours to seconds.

**The section-test:** open any Section (not a Template) in Studio. Does it render with real data?

- **If YES**, the SDK, the credentials, the editing handshake, the Live Preview channel, and the Delivery SDK auth are ALL working. Whatever symptom you're seeing on a Template is downstream of the SDK. Skip the SDK / init / token rows in the table. Jump straight to `troubleshoot-composition-resolution`. The most common cause is the connected template's URL match (literal URL, missing leading slash, or API-set `user_specified_pattern` that reverted to default, see #9 in the missing-skills audit).
- **If NO** (sections also fail), THEN the SDK / init / token rows below are honest. Continue to the symptom table.

**Why this matters:** Studio's "SDK Not Initialized" popup is a **red herring** in the most common case. When a Template's `useCompositionData` never resolves (URL match fails), `<StudioComponent />` never mounts, Studio's editing handshake times out, and Studio surfaces that as "SDK Not Initialized", blaming a layer that isn't broken. Re-running `install-studio` won't help. The install is fine. The section-test rules SDK-broken in or out before you spend hours in the wrong layer.

## Diagnostic tooling: inspecting a stored composition's `ui`

When a row below points at the stored composition (node tree, bindings, data sources, URL metadata), fetch via the Studio API. The **`ui`** field is **`zlib:<base64>`**: a literal `zlib:` prefix + base64-encoded zlib-deflated JSON. Decode for read-only inspection:

```js
// Node 18+. Save as inspect-ui.js
import zlib from "node:zlib";

function decodeUi(ui) {
  if (!ui.startsWith("zlib:")) return JSON.parse(ui);  // legacy plain-JSON fallback
  const bytes = Buffer.from(ui.slice("zlib:".length), "base64");
  return JSON.parse(zlib.inflateSync(bytes).toString("utf8"));
}

// Usage: pipe the composition JSON in on stdin, get the decoded ui on stdout
const composition = JSON.parse(await new Response(process.stdin).text());
console.log(JSON.stringify(decodeUi(composition.ui), null, 2));
```

```bash
# Quick CLI: fetch a composition and pipe through the decoder
curl -s "<studio-api-host>/v1/projects/<projectId>/compositions/<compositionId>" \
  -H "$CS_AUTH" -H "organization_uid: <org>" | node inspect-ui.js
# $CS_AUTH is either credential: 'authorization: Bearer <oauth>' (preferred) or
# 'authtoken: <session>'. Both work since 2026-08-25 — see authenticate-cma.
```

**Read-only (for debugging).** Do not author or modify compositions through the API. You'd lose UI-generated metadata (`url_queries`, etc.) that the Edit-URL modal sets behind the scenes. If you want to write a composition by hand, step back into Studio's UI.

## Task

1. **Match the symptom to the canonical category** using the table below. Pick the closest match: exact wording matters less than category.

2. **Run the "Check first" diagnostic** for that category. If it confirms the diagnosis, propose the fix. If not, run the secondary check.

3. **Report** the symptom you matched, then the likely cause, then the exact thing to check, then the fix. Do NOT propose more than two checks. If both come back negative, escalate to reading `docs/30-look-it-up/05-reference/50-error-message-index.md` or running `verify-setup`.

### Symptom and cause matrix

| Symptom (user-visible) | Most likely cause | Check first | Fix |
| --- | --- | --- | --- |
| **Visual Editor / Visual Builder (not Studio) stuck on "Initializing the editor…"**, nothing clickable | Live Preview runs in its default `mode: "preview"`, which never starts the editing layer | `grep -rn "mode:" <the ContentstackLivePreview.init file>` | `mode: "builder"`: [`make-visual-editor-editable`](make-visual-editor-editable.md) § 1 |
| **Canvas iframe is completely blank** (no Studio chrome inside) | The composed iframe address is wrong. Studio builds it as **Base URL (origin) + Canvas URL (path)**: the origin is the per-locale URL on the environment the project targets, the path is the Canvas URL. Either half can be broken. An **empty env Base URL** is the most common cause (and surfaces as a "no base URL"-style error blaming the Studio project, not the environment). | (a) In Contentstack, open the stack's Settings, go to Environments, open `<env>`, and check the URL for `<locale>`: is it set to where the canvas-app serves (`http://localhost:5173` for local dev, or the deploy origin)? (b) In Studio, open the project's Settings, then Configuration: is the project's **Environment** that same env, and is **Canvas URL** the route path (`/canvas`, not a full origin)? | Set the env's per-locale Base URL to the canvas-app origin. Set Canvas URL to the route path. Ensure the project targets that env. Re-open the composition. See `setup-section-preview`. |
| **Canvas loads but shows the host app's home page** instead of the composition | The host app doesn't have a `/canvas` route mounting `<StudioCanvas />` | `grep -r "StudioCanvas" src/` in the canvas-app | Add a `/canvas` route that mounts `<StudioCanvas />`. See `docs/10-set-up/05-install-studio/40-mount-the-section-preview-route.md`. |
| **"Component Loading Error: Component with type 'X' is not registered"** where `X` is `repeater` / `condition-block` / `section-slot` / `box` / `text` / another **built-in** | The SDK auto-registers built-ins on the **authoring canvas** path. Seeing this on the canvas means the registry instance was bypassed (e.g. importing `getComponentRegistryInstance` from a different worker/realm than the one rendering the canvas), or a bundler quirk shipped an older SDK build. | Confirm all `@contentstack/studio-*` packages are on the same major and run in the same JS realm. | Track the SDK issue for system-component auto-registration on the render paths. |
| **Visitor page falls back to "No page here yet" / a `<NotFound/>` branch** even though the composition is published, and the fetch fails with `[Contentstack Studio] COMPONENT_NOT_REGISTERED: Studio's own components are not registered: page` (also seen for `box`, `text`) | The `@contentstack/studio-react` package root was never imported in the code that fetches, so the built-in components (`page`, `box`, `text`, …) never registered themselves. A server-only bundle can also tree-shake that import out. | Search the browser console or server log for `COMPONENT_NOT_REGISTERED` with `Studio's own components are not registered`. Then check that the fetching module, or one it imports, has `import "@contentstack/studio-react";`. | Add `import "@contentstack/studio-react";` once, above the fetch, in the bundle that runs it. Full entry: `docs/30-look-it-up/05-reference/50-error-message-index.md#COMPONENT_NOT_REGISTERED`. |
| **"Component Loading Error: Component with type 'X' is not registered"** where `X` is a section composition UID OR one of the user's component types | Section ref resolves to a section that isn't preloaded OR a registered component for `type: 'X'` doesn't exist | Open the composition in Studio, then check Layers for a node with `type: "X"`. Is it a section ref (X is another composition's UID) or a registered component (X is a registered type)? | If section ref: confirm the SDK auto-registers sections, see F-018 fix in `docs-verification-findings.md`. If registered component: run `register-component` for that type. |
| **"Component Loading Error / Composition Loading Error" for MANY or ALL of your components at once** (e.g. every `home-hero` / `cs-*` section errors, not just one), often the whole composition fails to load | **NOT a per-component registration gap**: the canvas is iframing an app that has NONE of your `registerComponent` calls, because the project's **Configuration is unset or points at the wrong app**. A blank/disabled **Base URL**, a missing **Canvas URL**, or an **Environment** that resolves to a deployed build without your registrations all produce this. The registrations exist in your running app. The canvas just isn't loading it. | In Studio, open the project's **Settings**, then **Configuration**: is **Environment** set (to the env whose Base URL points at your running app), is **Base URL** set to that app's origin (e.g. `http://localhost:3000`, `http://localhost` needs no HTTPS), and is **Canvas URL** set (`/canvas`, required)? Any one blank or disabled produces this error. | In Settings, under Configuration, set **Environment**, ensure the env's **Base URL** points at your running app, and set **Canvas URL** to `/canvas`. **Save**, then re-open the composition. This is the single most common "everything's not registered" cause. See [`configure-studio`](configure-studio.md). |
| **"Template Did Not Load"** + the iframe shows your app's **404** (the requested URL has a **doubled locale**, e.g. `/en/en`) | The locale prefix is carried twice. Studio previews at **per-locale Base URL + the composition's own URL**. On a locale-prefixed app the Base URL for `en-us` is `http://localhost:3000/en` by design, so the doubling comes from the other side: the composition URL was authored as `/en` (or `/en/...`) instead of `/`, or the catch-all passes the prefixed request path to Studio instead of stripping it. | Read the exact requested URL in the dev-server log / Network tab. A doubled `/en/en` confirms it. Then check the composition's `url` in Studio and the `url` your catch-all passes to `fetchCompositionData`. | Composition URLs never carry a locale prefix: fix the authored URL to `/`. The catch-all strips the app prefix before calling Studio and passes the stack locale as `{ locale }`. Keep the prefix in the per-locale Base URL. On an app with **no** locale prefix the Base URL is the bare origin. The single rule is in [`convert-project-to-studio`](convert-project-to-studio.md) § Step 3. |
| Images render on the **published/visitor** page but are **broken in the canvas / Live Preview** (broken URLs look like `assets.contentstack.io/spaces/…` and return 404) | The image field is backed by a **DAM / Assets-Manager** asset (`am…` uid, `parent_uid`). Live-Preview (draft) resolves it to the DAM **preview** delivery URL (`/spaces/…`), which 404s in a plain browser. The published CDA resolves the same asset to `images.contentstack.io/v3/assets/…` (HTTP 200). | Compare the draft (preview token) vs published (delivery token) URL for the same field. | Link the asset so it delivers the `images.contentstack.io` form: re-pick it via the DAM/asset picker, or set the asset uid with a **session authtoken** (OAuth Bearer CMA writes reject DAM assets, see `author-composition-via-api` pitfalls). Then re-publish + re-open. |
| **401 / 403 / 412 or `STACK_NOT_FOUND_ON_HOST`** in the canvas | Delivery Token or Preview Token in `.env` is wrong or has been revoked | In Contentstack, open the stack's Settings, go to Tokens, open the relevant Delivery Token, and copy the current Delivery Token plus its paired Preview Token into `.env` | Restart the dev server. |
| **"Component Loading Error: HTTP status 0" / CORS error on the CDA preview host** in the canvas iframe ("Access to XMLHttpRequest at `https://<cdn>-preview.contentstack.com/…` blocked by CORS policy: No 'Access-Control-Allow-Origin' header"). Looks like a CORS issue but **almost always isn't**. | The **Preview Token** the canvas-app passes to the Delivery SDK / Live Preview SDK is invalid, missing, "expired" (its paired Delivery Token was deleted/regenerated), literally `"undefined"`, empty, or scoped to a different stack. Contentstack's preview API returns **401** with a clear body (`{"error_message":"You're not allowed in here unless you're logged in.","error_code":105,"errors":{"preview_token":["is not valid."]}}`) but **without CORS headers** (error responses don't carry them). The browser hides the body and surfaces a CORS error instead. Common forms: token left as the literal string `"undefined"` by a provisioning script that couldn't mint it via CMA. The paired Delivery Token was deleted/regenerated (Preview Tokens can't be revoked individually, they die with their Delivery Token). Copy-paste truncation / trailing whitespace. Token belongs to a different stack than the configured API key. | **Bypass the CORS masking with `curl`**. CORS doesn't apply to `curl`, so you see the real status code + body. Plug in the values your canvas-app uses (wherever it loads them from: `.env.local`, `.env`, runtime config, secrets manager, hardcoded):<br><br>`curl -i "https://<preview-host>/v3/content_types/<CT>/entries?environment=<env>&locale=<locale>" -H "api_key: <stack-api-key>" -H "preview_token: <preview-token>"`<br><br>For the NA prod region the preview host is `rest-preview.contentstack.com`. Non-prod / non-NA regions use a different host (see `install-studio` § Regional host map). Outcomes:<br>• **HTTP 200** + JSON entries means the token is fine, cause is elsewhere<br>• **HTTP 401** + `"errors":{"preview_token":["is not valid."]}` means the token is bad (revoked, `"undefined"`, empty, wrong stack), most likely<br>• **HTTP 403** means the token is valid but lacks scope for this CT / environment<br>• **HTTP 422** means an environment / locale name typo<br>• **HTTP 400** + `"Please provide live preview hash in request"` means the token IS valid. The 400 is just because `curl` didn't send the live-preview hash header your canvas-app's SDK adds automatically. Treat this as a pass. | (a) Open the file/config where your canvas-app loads its Preview Token (project-specific: Vite `.env.local`, Next `.env.local`, CRA `.env`, Remix env, runtime fetch, secrets manager, whatever your project uses) and verify the value is NOT the literal string `undefined` or empty. (b) In Contentstack, open Settings, go to Tokens, then Delivery Tokens, open the Delivery Token your app uses, and confirm a **Preview Token (read only)** field is visible. If only a **Create Preview Token** button shows, click it to mint one (see `enable-visual-experience` Step 4). If the Delivery Token doesn't exist or looks unfamiliar, the original was deleted. Create a fresh Delivery Token, mint a new Preview Token on it. (c) Copy the current Preview Token (starts with `cs`) into wherever your canvas-app loads it from. (d) **Restart the canvas-app server**: env vars and bundle-time config bake in at build/start time. Hot-reload typically does NOT pick up env-file changes for `import.meta.env.*` / `process.env.*` reads. (e) Hard-reload the Studio canvas. The CORS-shaped error disappears once the API can authenticate. |
| **Preview token looks right in `.env` but the canvas still 401s**: fresh provision, you re-checked the token character-by-character against the Delivery Token page, it matches, the canvas still fails | **`.env.local` is shadowing `.env`.** Vite / CRA / Next load env files in this order: `.env` first, then `.env.local`, then `.env.[mode]`, then `.env.[mode].local`. Later files **override earlier ones for the same key** and `.env.local` is loaded in every mode. A stale `.env.local` left from a previous provisioning run (often carrying `<PREFIX>_CONTENTSTACK_PREVIEW_TOKEN=undefined` as a literal string) silently wins. Symptom is indistinguishable from a bad token in `.env`. | `ls -la .env.local .env`: does `.env.local` exist? Print its preview-token value: `grep PREVIEW_TOKEN .env.local`. Is it `undefined`, empty, or different from `.env`? | Either (a) delete the stale `.env.local`, (b) rewrite it from `.env.example` with the current values, or (c) `unset` the offending key from `.env.local`. **Restart the dev server**: env files bake in at start time. Then re-run the preview-token `curl` from the row above to confirm 200/400 (pass), not 401. See also `install-studio` step 7 § `.env.local` shadowing trap. |
| **HTTP 412 "We can't find that Stack. Please try again."** on CMA/CDA calls + **"No composition for /…"** at runtime, most commonly after scaffolding a new project by copying a working app's directory | **Stale `.env.local` carrying the previous project's `api_key`.** Same `.env.local` shadowing trap as the 401 row, but the failing field is the stack api_key (not the preview token). The app initializes the OLD stack while Studio's project points at the NEW one, and that mismatch produces a 412 from the CDA. | `grep -E "API_KEY\|api_key" .env.local .env`: compare the values. If `.env.local` carries a stack key from a different project, that's the cause. Also verify with a CDA probe: `curl -i "https://<cdn>/v3/content_types" -H "api_key: <what-your-app-actually-loads>" -H "access_token: <delivery-token>"`. A 412 confirms wrong stack. | Delete or rewrite the stale `.env.local` from `.env.example`. **Restart the dev server**. Re-probe with the CDA `curl`: 200 means the stack init now matches Studio's project. | 
| **Canvas loads but preview entry is the wrong one** | The preview-entry override isn't reaching the SDK fetch | Studio navbar shows the entry picker. Did the user pick a different entry but the iframe doesn't update? | Hard-reload the canvas iframe. If still wrong: verify the canvas route mounts `<StudioCanvas />` (which reads Studio's iframe params itself). Do NOT have your own code parse Studio's internal `cs-*` URL params: that contract is internal and may change. If you fetch via `sdk.fetchCompositionData` on the server, ensure the full request `searchQuery` is forwarded. |
| **Canvas iframe never finishes loading (spinner forever)** | The canvas-app is starting and failing silently | Open the canvas URL directly (not through Studio). What does the host app show? | If host app errors: fix the host app first. If host app loads fine: clear the iframe's `beforeunload` handler (run `window.onbeforeunload = null` in DevTools console). |
| **"Live Preview not configured"** banner in the canvas | App is missing `@contentstack/live-preview-utils` install OR `ContentstackLivePreview.init()` was never called | `grep -r "live-preview-utils" package.json src/` | Install the package and call `init()` at app shell. See `install-live-preview` skill. |
| **Bindings render the literal path** (e.g. "{{entry.title}}") | The Delivery SDK call inside the canvas-app didn't fetch the entry. Bindings can't resolve | Open Network tab in the canvas-app's DevTools. Was a CDA call made? | If no CDA call: the host app's `<StudioCanvas />` mount is missing the SDK setup. If CDA call returned 200 but body is empty: preview entry UID is wrong. |
| **Compositions list is empty** | Stack or project mismatch | In the Studio top bar, which project is selected? Is the stack key in `.env` the one that owns this project? Compare the `connectedStackApiKey` from `GET /v1/projects` against the api_key the app initializes. | Select the right project. [`configure-studio` § Select the Studio project](configure-studio.md#select-the-studio-project) lists them and checks the stack match. Otherwise re-run provision against the right stack. |
| **"Beforeunload" dialog blocks every navigation** | Studio's leave-without-save guard is firing | Save any unsaved edits, OR `window.onbeforeunload = null` in DevTools to force-clear | Save state first. The guard is intentional. |
| **Section drops on the canvas show "Add a condition for X schema"** | Repeater is bound to a Reference or Modular Block but no Condition Block wraps the iteration | In Layers, does the Repeater have a Condition Block as a direct child? | Accept the inline prompt to wrap, OR invoke `use-condition-block` skill. |
| **Whole composition renders blank or partially blank** (only some sections paint, the rest of the page is empty) AND console shows `TypeError: Cannot read properties of undefined (reading 'slots')` together with `Cannot find a descendant at path [...] in node` | **An empty Section Slot is fatal page-wide.** The SDK renderer walks the composition tree. When it hits a Section Slot whose `children` array is empty (component removed but slot kept, or slot never filled), it throws, aborting the ENTIRE composition's render, not just that slot. Every section that would have painted after the throw goes blank. (This is a Studio renderer bug: an empty slot should render nothing, not throw. Report upstream.) | (a) Decode the path in the error (`[<page-slot-uid>, <index>, <repeater-slot-uid>, <index>, …]`) into the composition tree. (b) Inflate the published `ui` via the `zlib:<base64>` decoder snippet in [`author-composition-via-api`](author-composition-via-api.md) § Diagnostic tooling. Walk the same path. (c) The terminal node is the empty Section Slot. Properties panel for that node also confirms with "The slot is empty, with no component added yet." | Either drop a component back into the slot, OR delete the slot from the section. If authoring via API, ensure every `slot: { <uid>: [...] }` value is a non-empty array AND no node references a removed descendant. Validate before publishing. The `adv-post-message` ACK / `highlight-node` console lines that often accompany this are benign editor-hover noise, not this bug. |
| **Live site (or CDA fetch) shows a different tree than the canvas Layers panel**: fewer sections, missing recent edits, "this works in Studio but not in production" | The canvas **Layers panel is the DRAFT**. The published composition (what CDA serves and the live site renders) is whatever was last **Saved + Deployed**. Unsaved or undeployed canvas edits exist only in the draft session. A broken draft can also mask what's actually published. | (a) Fetch the published composition via CDA: `curl -i "https://<cdn-host>/v3/content_types/<compositions-CT>/entries/<entry-uid>?environment=<env>" -H "api_key: <key>" -H "access_token: <delivery-token>"`. (b) Inflate the `ui` field (it's `zlib:<base64>`, use the decoder in [`author-composition-via-api`](author-composition-via-api.md) § Diagnostic tooling). (c) Diff the inflated tree against what the canvas Layers panel shows. Match = same composition, mismatch = there are unsaved or undeployed edits. | **Save** (Cmd/Ctrl+S in canvas) then **Deploy** (top-right button). Re-fetch via CDA. The inflated tree should now match Layers. Pair with [`deploy-studio-site`](deploy-studio-site.md) for the full Save and Deploy pipeline rationale. |
| **Canvas iframe is blank in Chrome v136+** AND DevTools console shows `net::ERR_BLOCKED_BY_PRIVATE_NETWORK_ACCESS_CHECKS` (or you see no console errors at all but the iframe never paints) | Chrome's Local Network Access protection blocks `localhost:<port>` iframes loaded from a non-local origin (e.g. `app.contentstack.com` loading `localhost:6846`). Affects all Chrome v136+ installs. | In DevTools, open the Network tab and find the canvas iframe request: does it show as blocked, or with a `(failed)` status and `LocalNetworkAccess` in the reason? | Relaunch Chrome with `--disable-features=LocalNetworkAccessChecks`. macOS: `open -a "Google Chrome" --args --disable-features=LocalNetworkAccessChecks`. Add the same flag to your Playwright MCP launch one-liner if applicable. |
| **Canvas iframe is blank AND you're using Chrome's default profile in Studio MCP/automation setup** | Chrome v136+ on org-managed devices (MDM/policy-enrolled) refuses to start the `--remote-debugging-port` listener on the default user profile. Silent failure: `lsof -i :9222` is empty even though `open -a "Google Chrome"` returned 0. | `lsof -i :9222`: empty? `chrome://policy` lists `DeveloperToolsAvailability` or similar enterprise policy? | Use a dedicated `--user-data-dir="$HOME/.chrome-mcp"` profile (the default in `install-playwright-mcp`). The policy applies to the default profile only. An empty user-data-dir is treated as a personal profile. |
| **"CT not found" / 404 from Delivery SDK** when querying compositions, e.g. `stack.ctQuery('compositions')` returns 404 | The compositions CT UID the app reads doesn't match what Studio's provisioning created. Agent-provisioned projects often use `agent_compositions`, not `compositions`. Value is per-project. | In Studio, open the project's **Settings**, then **General**, then **Integration code** (URL: `/studio/projects/<projectId>/settings/general`). Copy the CT UID literal from the snippet. Then grep the canvas-app for where the UID actually lives: could be `.env*`, `src/lib/contentstack.ts`, a config file, or a hard-coded `ctQuery("…")` call: `grep -rn "compositions\|contentTypeUid\|COMPOSITIONS_CT" src/ lib/ app/ config/ .env*`. | Update **every** location the UID appears (don't assume it's in `.env` only) to Studio's integration-code value. Restart the dev server so env-derived reads pick up the change. |
| **Studio canvas iframe loads but Studio's hover/edit overlays don't appear. `studio-renderer.tsx` console.log of URL params shows the wrong key** | Studio sends the composition UID as `cs-composable-uid` in its iframe URL, but older `studio-renderer.tsx` templates parse `cs-composition-uid`. A mismatched key makes the renderer read `undefined`, so there are no overlays and no composition mount. **Likely a real bug worth upstreaming.** | `grep -n "cs-composition-uid\|cs-composable-uid" src/` in the canvas-app | Patch `studio-renderer.tsx` to accept BOTH keys: `const uid = params.get("cs-composable-uid") ?? params.get("cs-composition-uid");`. File an issue against the renderer template repo so the official template gets fixed too. |
| **"Invalid hook call" / minified React #321** when the canvas tries to render a registered component | The SDK peer-deps `react ^18.0.0 \|\| ^19.0.0`, so React 19 itself is fine. The error means either (a) two React instances in the bundle (a peer override or a nested dep pulls a second copy, or a stale `@contentstack/studio-react` 1.5.0 pin was forced through on React 19), (b) Vite's dev-optimizer pre-bundled the SDK with a separate React even though `node_modules` is deduped, (c) the registered component has arity 0 (`function Header() { … }`): the SDK treats parameterless functions in `registerComponent({ component })` as lazy-loader thunks (`() => import("...")`), wraps them with `React.lazy`, and ends up calling the function outside React's render path. | (a) `npm ls react` must show exactly one entry, 18.x or 19.x. Two entries means a dep or an override pulls a second copy. `npm ls @contentstack/studio-react` showing 1.5.0 on React 19 means a stale pin. (b) Stack trace shows two optimized React bundles with different `?v=` hashes. (c) Find the `component:` value in the registration that has zero parameters. | (a) One React major in `ls react`, `@types/*` matching it, and `@contentstack/studio-react` 1.5.1 or later. Drop any `overrides`/`resolutions` that force a second copy. (b) Serve a **prod build** to the canvas (`vite build && vite preview`). Rollup dedupes, the dev optimizer can split. (c) Wrap the component with `(props) => createElement(Comp, props)` or add a `props` arg to the source. See `register-component` arity rule. |
| **"SDK Not Initialized" popup** | **The popup is misleading: this is a red herring in the common case.** Studio surfaces "SDK Not Initialized" whenever the canvas-app's editing handshake times out, regardless of why it timed out. The most common cause is a connected template whose `useCompositionData` never resolves (URL match failed: literal `url`, missing leading slash, API-set `user_specified_pattern` that reverted). `<StudioComponent />` never mounts, handshake times out, popup appears blaming the SDK. The SDK itself is usually fine. | **Confirm the dev server answers (Step 0), then run the section-test pre-flight.** Open any Section (not a Template). If it renders, the SDK is fine. Jump to `troubleshoot-composition-resolution`. If sections ALSO fail, suspect SDK / init / token (see install rows below). Do NOT re-run `install-studio` before doing this test. | If sections render: resolve the underlying URL match per `troubleshoot-composition-resolution`. Typically: ensure the entry's `url` field starts with `/`, the composition `url_metadata.url_source` is `content_type_url_pattern` for a page-type CT, and the composition `url` contains a `{{entry.x}}` pattern (a literal URL never resolves). If sections also fail: re-check SDK init order, credentials, and that `studioSdk.init(...)` was actually called before the canvas mounts. |
| **Connected-template canvas shows "localhost didn't send any data" / "SDK Not Initialized"** while sections render fine | Connected templates iframe at the **environment's base URL** + the template path, NOT at `canvasUrl` (only sections use that). If the env base URL is HTTP and the dev server is HTTPS-only, the iframe gets nothing back. | `GET /v3/environments/<env>`: check `urls[].url`. Compare to the served app's scheme (HTTPS for local mkcert dev). | `PUT /v3/environments/<env>` to set `urls[].url` to the HTTPS URL of the served app (must match the scheme `canvasUrl` uses). |
| **"Template Did Not Load"** in the editor, but the deployed/SSR site renders the template correctly | The template has `section-composition` nodes in its `ui` but the template entry's **`linked_sections` reference field is empty/null** (P27). The Studio editor builds `spec.sectionCompositions` from `?include[]=linked_sections` on load: an empty field yields no section specs, so it cannot resolve the nodes and shows "Template Did Not Load". SSR walks the `ui` nodes directly via `compositionUID` and works regardless. **API-authored templates** are the common culprit: the Studio UI auto-populates `linked_sections` when an author drags a section onto the canvas. API authoring must do it explicitly. | (a) Confirm SSR works: `await sdk.fetchCompositionData({ url })` from a one-off server route returns a usable `spec.data.section_scoped_data` (or just renders the template). (b) Inspect the template entry's `linked_sections` directly: `curl -s "<cdn>/v3/content_types/<comp-CT>/entries/<template-entry-uid>?include[]=linked_sections" -H "api_key:…" -H "access_token:…" \| jq '.entry.linked_sections \| length'`. If 0, that's the cause. | Populate the template entry's `linked_sections` with `[{ uid: "<section-comp-entry-uid>", _content_type_uid: "<compositions-CT-uid>" }, …]`. One entry per distinct placed section, deduped. Publish. See `author-composition-via-api` § Authoring a Template: populate linked_sections for the recipe. |
| **An empty render that was working a minute ago, with no code changes** (empty grid in a section that rendered products, blank Compositions list, blank Templates tab, and the thing rendered correctly seconds earlier) | A **transient 5xx from the Studio API**: `/composable-studio-api/v1/projects/...` returns 503 / 502 / 504 intermittently. Studio gets no data, renders empty. Not your binding, not your CT, not your config. | In DevTools, open the Network tab, filter to `composable-studio-api`, and look for a 503 / 502 / 504 around the time the render went empty. If you find one, that's the cause. If everything in Network shows 200s, the empty render is something else, keep debugging. | **Reload.** That's the fix when it's transient. Do NOT chase a transient 5xx as a binding bug: empty renders look identical to real binding failures and you'll burn an hour walking through the wrong cause space. If the 5xx reproduces consistently (not just one-off), it's no longer transient. Escalate with the API path, project ID, and timestamp. |
| **Repeater (or any data-driven container) still empty AFTER toggling Preview Mode on** | Preview Mode controls whether bindings resolve, not whether they resolve correctly. Once on, flipping it again does nothing: the binding shape is wrong. | Confirm Preview Mode is on (in the right panel, under Configuration). If on AND empty, the binding is wrong. | Diagnose the binding: re-check Bind items source, confirm items exist in payload (DevTools Network), verify scope (see `use-repeater` § The scope rule, in user terms), confirm a Condition Block wraps Reference / Modular Block iteration. See `troubleshoot-data-binding`, `build-repeating-section`. |
| **A registered component (or child Section) dropped into a Section Slot renders full-bleed / full-screen**, looks correct on the live storefront, broken in Studio | The parent Section is missing a layout container around the Slot. See `use-section-slot.md` § Layout container for the canonical rule. | Open the parent Section, go to Layers, and confirm the Slot sits inside a sized Box. If it's a direct child of the Section root, that's the bug. | Wrap the Slot in a layout container (grid / sized Box). Do NOT add `max-width` on the component. See `use-section-slot.md` § Layout container. |
| **"No Studio Code on This Route"** ("The front-end code for this URL doesn't use Studio yet") for a route that DOES mount `<StudioComponent>` / `<StudioCanvas>` | This banner reflects what the **iframe's response** looked like, not your source code. A local build/syntax error (a broken import, an unterminated statement, a stray character from mid-edit) makes the dev server return a compile-error overlay instead of your app's real output. Studio can't recognize that as Studio-integrated markup and falls back to the generic "custom code" message. | Open the exact URL directly in a plain browser tab (not through Studio's iframe). Do you see your app, or a Next.js / Vite / Turbopack error overlay? Also check the dev-server terminal for a parse/compile error timestamped around the request. | Fix the actual build error, reload. The banner clears once the route serves real content again — don't chase the Studio integration, chase the build error. |
| **Live Preview intermittently doesn't apply**: the same composition/entry resolves against `rest-preview.contentstack.com` (drafts visible) on one internal fetch and falls back to `cdn.contentstack.io` (published-only) on the next, within the same resolution, with no code change in between | Shared, mutable Live Preview state on a stack instance reused elsewhere in the app. See § Live Preview state resets mid-request below. | Log the `live_preview` request header across consecutive outgoing calls to the same stack (see § below for the exact snippet). Does it go from populated to empty partway through one resolution? | Give Studio's `stackSdk` its own independent stack instance instead of reusing one shared with the rest of the app. Full mechanism and fix in § Live Preview state resets mid-request. |

## Inputs needed from the user

1. `symptom`: one sentence. If the symptom doesn't clearly match a row, ask the user to describe what they see (canvas-iframe content vs surrounding UI) before guessing.

## Acceptance

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

- [ ] The reported symptom was matched to one row in the table above.
- [ ] The "Check first" diagnostic was run (or the user was directed to run it).
- [ ] If the diagnostic confirmed the cause, the fix was applied or the user was given the exact action to take.
- [ ] If the diagnostic failed to confirm, the second-most-likely cause was offered (or escalation to `verify-setup` / `docs/30-look-it-up/05-reference/50-error-message-index.md` was suggested).
- [ ] The user was NOT given more than two diagnostic guesses in a row. If both miss, escalate to systematic verification.

## Third-party global-script overlays inside the canvas: how to spot + fix

> Widget/banner overlap (OneTrust, chat bubbles) or analytics pollution is NOT this section: that's `gate-third-party-scripts`, which applies the SDK's `isStudioCanvas()` editor-mode contract. This section is for third-party loaders that crash with error overlays.

**Symptom:** the Studio canvas renders fine, but a runtime error overlay pops in the iframe: **`pageerror: Load error!`** / `Failed to load lytics` / **`net::ERR_ABORTED`** from `kit.fontawesome.com` / a `next/script` "Load error!" from a commerce embed. The composition renders correctly BELOW the overlay: the error isn't from Studio.

**Root cause:** the app's canvas route inherits parent layouts, and those layouts inject analytics tags / third-party embeds / global `<Script>`s. Many of those loaders `throw new Error(...)` in their `onload`/`onerror` handler when their remote asset 403s or their init misconfigures. The throw surfaces as a runtime-error overlay inside Studio's canvas iframe. Nothing to do with Studio, everything to do with the loader running on the canvas route.

**Diagnose in one minute:**

1. Open the canvas route directly in a normal browser tab: `http://localhost:<port><canvasPath>`.
2. In DevTools, open Network and filter by "Failed" or by hostname (`lytics`, `fontawesome`, third-party CDN of your embed).
3. In DevTools, open Console: any `pageerror` line points at a script URL. That script is your culprit.

**Fix in order of preference:**

1. **Isolate the canvas route from app chrome** (best). Route-group / layout guard so analytics + embeds + global scripts do NOT run when the path matches `*/canvas`. Full recipe in [`setup-section-preview`](setup-section-preview.md) § Canvas route isolation. This is the correct long-term shape: the canvas route should be minimal by design.
2. **Patch loaders that `throw`.** Some SDKs let you swap the `throw` for `console.warn`, e.g. Lytics `jstag`'s tag-load handler can be modified to `warn` + `return` instead of `throw`. Keeps the tag stub on other pages that use it, kills the overlay.
3. **Remove vestigial loaders.** FontAwesome kits often persist in `<Script>` tags after the app moved to a different icon system. Grep the app for `fa-` class usages. If there are none, delete the kit script.
4. **Prefer plain `<script async>` over `next/script`.** `next/script` (Next.js) throws its own "Load error!" when a network error occurs. A plain async `<script>` fails silently to the console. Only relevant when the script can legitimately fail (marketing tags, optional embeds).

**Rule of thumb:** every request the canvas iframe makes that is not to the Studio SDK or your app's own bundles is a candidate for elimination. If it doesn't need to run under Studio's iframe, don't run it.

## Live Preview state resets mid-request: shared stack instance hazard

**Symptom:** resolving one composition/URL makes several internal CDA/CMA calls (a literal-URL search, a candidate-ranking search, an entry lookup), and only the FIRST of them actually uses Live Preview (`rest-preview.contentstack.com`, populated `live_preview` header, drafts visible). The rest silently fall back to the plain Delivery API (`cdn.contentstack.io`, empty `live_preview` header, published-only), with no error and no code difference between the calls. Net effect: an unpublished/draft-only composition resolves on the lucky first attempt and 404s (`NO_COMPOSITION_FOR_URL`) the moment resolution needs a second internal query — which is the common case for anything but an exact literal-URL match.

**Root cause:** `stack.livePreviewQuery(...)` does not scope to one call. It mutates a single shared object living on the stack instance itself — `_client.stackConfig.live_preview` in `@contentstack/delivery-sdk` (`node_modules/@contentstack/delivery-sdk/dist/modern/lib/stack.js`), the equivalent field on the legacy `contentstack` v3 SDK. The actual host-switch happens centrally, in `@contentstack/core`'s request layer:

```js
// @contentstack/core/dist/esm/src/lib/request.js
if (instance.stackConfig && instance.stackConfig.live_preview) {
  const livePreviewParams = instance.stackConfig.live_preview;
  if (livePreviewParams.preview_token) {
    instance.defaults.headers.live_preview = livePreviewParams.live_preview;
  }
  if (livePreviewParams.enable && livePreviewParams.live_preview !== 'init') {
    url = 'https://' + livePreviewParams.host + url;   // only switches host here
  }
}
```

This reads whatever the shared `stackConfig.live_preview` object currently holds, at the moment each individual HTTP call fires — not a value scoped to the higher-level operation that started the fetch.

**The collision:** this is only a problem when TWO code paths share the SAME stack instance and have DIFFERENT live-preview intentions on the SAME request. This is exactly the shape of `install-studio`'s minimal-add pattern (Studio's `stackSdk` reusing an existing app's stack). Concretely, in a Next.js App Router app:

1. Studio's SDK (`@contentstack/studio-client`) reads the `hash` param from the incoming request's `searchQuery` and calls `stackSdk.livePreviewQuery(hash)` before its first internal query — correctly switches that ONE call to the preview host.
2. A parent layout on the SAME request does its own unrelated data fetch (e.g. homepage data for SEO metadata) via a helper that calls `stack.livePreviewQuery(live_preview ?? {})` with no override — a perfectly reasonable thing for THAT call to want (metadata generation arguably should reflect published state, not an in-progress draft). But since `query.live_preview` is falsy, the delivery-sdk's own `livePreviewQuery` takes its reset branch and explicitly sets `live_preview: ""` on the SHARED object.
3. Next.js runs a page's layout(s) and the page itself concurrently on one request. That reset can land in the gap between Studio's first and second internal queries, wiping the hash Studio just set before its next call goes out.

Neither call site is "wrong" in isolation — each wants the correct behavior for its own purpose. The bug is architectural: the API conflates "no live preview for my call" with "no live preview for anyone sharing this object right now," because state lives on a mutable singleton instead of being scoped per-call.

**Check first:** confirm it empirically rather than reasoning from source alone. Patch Node's `http`/`https` layer to log the `live_preview` header on every outgoing Contentstack request (must be added before any Contentstack package's module graph loads, so restart the dev server after adding it, not hot-reload):

```js
// lib/debug-fetch-interceptor.js — diagnostic only, delete when done
import http from "node:http";
import https from "node:https";
const CS_HOST_PATTERN = /contentstack\.(io|com)/;
function patch(mod) {
  if (mod.__csPatched) return;
  mod.__csPatched = true;
  const originalRequest = mod.request;
  mod.request = function (...args) {
    const req = originalRequest.apply(this, args);
    const host = req.getHeader?.("host") || req.host;
    if (host && CS_HOST_PATTERN.test(host)) {
      console.log(`[cs-http] -> ${req.method} https://${host}${req.path}`);
      console.log("[cs-http] live_preview header:", req.getHeader?.("live_preview"));
    }
    return req;
  };
}
if (!globalThis.__csHttpPatched) {
  globalThis.__csHttpPatched = true;
  patch(http);
  patch(https);
}
```

Import it for side effects at the top of the route under test, restart the dev server (not hot-reload — module-load order matters), and trigger the failing resolution. If the logged `live_preview` header is populated on the first call and empty on later ones for the SAME request, this is the cause. Then grep for every other call site sharing the same stack instance: `grep -rn "livePreviewQuery(" src/ app/ lib/`.

**Fix:** give Studio's `stackSdk` its own independent stack instance, separate from whatever stack the rest of the app reuses for its own content fetching. Reuse the same credentials/env vars to construct it — just not the same object. This removes the collision entirely: the rest of the app can keep calling `.livePreviewQuery({})` for its own reasons on its own stack, and Studio's hash stays untouched because it's a different instance. See [`install-studio`](install-studio.md) § Existing Contentstack app? Take the minimal-add branch.

## Common pitfalls

| Pitfall | Why it bites | Fix |
| --- | --- | --- |
| Guessing the cause from prior probability instead of running the diagnostic | Wastes user time. The same symptom has different causes in different setups | Always run the "Check first" diagnostic before proposing a fix |
| Treating "canvas blank" and "canvas shows host home page" as the same symptom | Blank = wrong URL. Home page = missing route. Opposite fixes. | Look at the actual iframe contents before classifying |
| Suggesting `npm install` as a generic fix | Hides the real issue when the user has a stale `.env` or wrong Canvas URL | Only suggest npm-level fixes when the diagnostic actually finds a missing package |
| Recommending the user re-paste credentials when the Canvas URL is the real problem | Touching `.env` is no-op if credentials weren't the issue. Wastes the user's time | Check the Canvas URL config FIRST before recommending any credential change |
| Diagnosing "No Studio Code on This Route" as a routing/registration gap | The banner reflects a broken iframe response, which a local build/syntax error also produces. Chasing Studio wiring wastes time when the real cause is a compile error | Load the URL directly in a plain tab first — see it your app, or a build-error overlay? |
| Treating an intermittent Live-Preview failure as a bad token or Studio misconfiguration | Same token, same hash, same request — it's a shared-stack race, not a credentials problem. Re-pasting tokens or reconfiguring Studio does nothing | Check whether Studio's `stackSdk` shares a stack instance with the rest of the app; see § Live Preview state resets mid-request |

## See also

- `docs/30-look-it-up/05-reference/50-error-message-index.md`: the canonical reference of symptoms and their fixes (read this if the matrix above doesn't fit)
- `verify-setup`: layered smoke test, run this if you're not sure WHICH layer is failing
- `install-studio`, `install-live-preview`: for actual reinstall steps
- `register-component`: for "Component Loading Error" cases involving registered components
