setup-section-preview
When to use
Add the canvas route that mounts <StudioCanvas />, then print the Canvas URL to set in Studio (browser-only, step 4). Required on every Studio project, not an optional extra. STOP before writing any file: read the project's environment and its per-locale URL by API (§ 0), show them, confirm the path (default /canvas), and wait for a yes.
Use right after install-studio to wire the section-authoring route. Also invoked by setup-template-preview-routes step 2 when it finds no canvas route: a catch-all matches /canvas like any other URL, so with nothing owning that path Studio's section iframe renders a composition lookup (blank frame, or the site's 404) instead of <StudioCanvas />. Phrases: "set up canvas route", "MISSING_CANVAS_URL", "where does StudioCanvas mount", "canvas iframe is blank in Studio". Do NOT use to add template/preview routes (use setup-template-preview-routes) or to fix a misbehaving canvas after setup (use troubleshoot-canvas).
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: Read that skill for where it is, even with no MCP connected. Never ask the user for a sessionauthtoken. If nothing resolves, or a refresh fails with400 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 316is a valid credential aimed at another org: fix the org or theapi_key, do not re-authenticate.
Set up the section preview route
Context
The "canvas route" is the page in the user's app that Studio loads inside its iframe to preview sections. Two things have to line up:
- A route in the app at a known path (e.g.
/canvas) that mounts<StudioCanvas /> - The same path set as Canvas URL in the Studio project's Settings, under Configuration
Without both, opening a section in Studio shows the MISSING_CANVAS_URL error.
Two unrelated "Base URLs": don't confuse them. (1) The "Base URL" under Visual Experience, then Custom Preview URL (set by enable-visual-experience) drives template preview URLs, not relevant here. (2) The canvas Base URL is the per-locale URL on the environment your Studio project targets (in Contentstack, open the stack's Settings, go to Environments, open <env>, and read the URL for <locale>). Studio composes the canvas iframe address as canvas Base URL (origin) + Canvas URL (path). This skill sets the path. The origin must already exist on the targeted environment, or the canvas stays blank.
Mounting <StudioCanvas /> is simple: it takes no required props. Studio drives it via URL parameters. The harder part is making sure the framework's routing wires it correctly (App Router vs Pages Router vs React Router vs Remix).
Prerequisite: install-studio has run, so @contentstack/studio-react is in the project.
Task
0. CONFIRM-AND-GO GATE: the canvas origin chain (do this BEFORE creating the route)
The canvas iframe address is canvas Base URL (origin) + Canvas URL (path). This skill sets the path (step 4). The origin comes from the per-locale URL on the environment your Studio project targets. If that URL is empty, Studio can't build the iframe address and the canvas stays blank, with an error that points at the wrong layer. Most "canvas won't load" reports at this stage are this, not the route. Do NOT create the canvas route until all three are confirmed.:
- Identify the dev origin the canvas runs at, e.g.
http://localhost:51723. If a dev server is running, it's that server's origin (the same one step 3 hits for its 200 check). If not, ask the user which origin they'll serve the app at locally. - Confirm an environment carries that origin as its per-locale URL: in Contentstack, open the stack's Settings, go to Environments, open
<env>, and the URL for<locale>must equal the dev origin, with no path: Studio appends the Canvas URL as plain text, sohttp://localhost:3000/enloads/en/canvas. (This is the canvas Base URL, NOT the Visual Experience Custom Preview URL.) If it's empty or wrong, guide the user to set it now before continuing. - Confirm that same environment is the one the project targets: in the Studio project, open Settings, then Configuration, and read the Environment field. If the project points at a different environment (e.g. an empty
preview), that mismatch is the trap: either set that environment's per-locale URL to the dev origin, or repoint the project's Environment (viaconfigure-studio) at the environment that already has it.
Recommended topology (avoids clobbering deployed envs): an environment record holds ONE URL per locale, so pointing preview at localhost breaks your deployed preview later. Prefer a dedicated development environment whose per-locale URL is localhost, with preview/production reserved for real origins, and point the Studio project's Environment at development for local authoring. Switch the project to preview/production when you deploy (see deploy-studio-site). This is a recommendation, not a requirement: if you deliberately use one shared environment, just make sure its per-locale URL matches wherever you're currently serving the canvas.
Only once 1 to 3 hold, continue.
-
Detect the framework. Look at
package.jsonand the project's directory structure:nextin deps plus anapp/folder means Next.js App Routernextin deps plus apages/folder means Next.js Pages Routervitein deps means Vite (look forsrc/routes/or check for react-router)@remix-run/*in deps means Remixreact-scriptsmeans CRA (probably with react-router)- otherwise, ask the user which framework
-
Create the canvas route file at the framework-appropriate path.
The canvas route MUST be isolated from your app's global chrome. The route that mounts
<StudioCanvas />should render ONLY that component: no site header/footer, no analytics tags (Lytics / Segment / GA), no third-party embeds, no global<Script>s (FontAwesome kits, commerce widgets, chat popups), no auth-gated wrappers. Every one of those runs inside Studio's canvas iframe when the user opens Studio, and any script thatthrows on failure surfaces as a runtime overlay INSIDE Studio's canvas (looks like Studio is broken, isn't).Framework-specific isolation patterns:
- Next.js App Router: put the canvas at a route group that has its own minimal
layout.tsxreturning{children}only, e.g.app/(studio)/canvas/page.tsxwithapp/(studio)/layout.tsxdoingexport default function StudioLayout({children}) { return <>{children}</>; }. Route groups let you opt out of the app's parent layouts (analytics, header, etc.) without changing URLs.- Alternative, layout guard: if you can't use a route group, guard the existing
RootLayoutwith a pathname check that skips analytics / third-party scripts when the pathname matches*/canvas.
- Alternative, layout guard: if you can't use a route group, guard the existing
- Next.js Pages Router: use
_app.tsxwith a pathname guard (router.pathname === "/canvas" ? <Component/> : <ChromeWrapper><Component/></ChromeWrapper>). - Vite / React Router / Remix: the CanvasRoute component sits at the router level, above your app chrome: the chrome only wraps the visitor routes.
Verify isolation before proceeding. Open
http://localhost:<port><canvasPath>in a browser, open DevTools, then the Network tab. You should see only the framework's assets +@contentstack/*, NO calls tokit.fontawesome.com,c.lytics.io, GA, chat providers, commerce embeds. If you see any of those, follow the isolation pattern above and re-verify. Skipping this step is fine short-term but every wrong-network-request shows up as a scary overlay to whoever opens Studio next.Next.js App Router (
app/<canvasPath>/page.tsx),<StudioCanvas/>must load client-only:"use client";import dynamic from "next/dynamic";import { getSSRStyles } from "@contentstack/studio-react";// StudioCanvas is client-only: it reads browser-only context (window.location,// post-message channel to Studio's parent frame). Server-rendering it produces// null on the server and a populated tree on the client — a guaranteed// hydration mismatch. Load it client-only.const StudioCanvas = dynamic(() => import("@contentstack/studio-react").then((m) => m.StudioCanvas),{ ssr: false },);export default function CanvasRoute() {return (<><style>{getSSRStyles(null)}</style><StudioCanvas /></>);}Next.js Pages Router (
pages/<canvasPath>.tsx), same SSR-mismatch reason, wrap indynamic({ ssr: false }):import dynamic from "next/dynamic";const StudioCanvas = dynamic(() => import("@contentstack/studio-react").then((m) => m.StudioCanvas),{ ssr: false },);export default function CanvasRoute() {return <StudioCanvas />;}Vite + React Router (
src/routes/CanvasRoute.tsx+ router config update), Vite is SPA-only, so a plain import is safe:import { StudioCanvas } from "@contentstack/studio-react";export default function CanvasRoute() {return <StudioCanvas />;}Plus add
{ path: "<canvasPath>", element: <CanvasRoute /> }to the existing router config.Remix (
app/routes/<canvasPath>.tsx), Remix server-renders by default. UseClientOnlyor auseEffect-mounted boundary:import { ClientOnly } from "remix-utils/client-only";import { StudioCanvas } from "@contentstack/studio-react";export default function CanvasRoute() {return <ClientOnly fallback={null}>{() => <StudioCanvas />}</ClientOnly>;}Every framework: render
<style>{getSSRStyles(null)}</style>beside<StudioCanvas />. In studio-react 1.9.0StudioCanvaswrites no design-token:rootvariables, so token styles (padding, max-width, colors) resolve to nothing on the canvas.Convert
<canvasPath>to the framework's expected filename (e.g./canvasbecomescanvas/page.tsxfor App Router, orcanvas.tsxfor Pages Router). - Next.js App Router: put the canvas at a route group that has its own minimal
-
Verify the route locally if a dev server is running. Hit
http://localhost:<port><canvasPath>and confirm it returns 200. If no dev server, print the URL the user should test once they start it. -
Set Canvas URL in Studio. This is the second half: the route file is useless without the matching Studio setting. Print clear manual instructions for the user to apply in Studio:
Open: <studioHost>/projects/<studioProjectId>/settings/configurationSet "Canvas URL" to: <canvasPath>Click Save. -
iframe-allowed config (REQUIRED on every dev server). Studio iframes the canvas-app from
https://app.contentstack.com/#!/studio. The browser will silently block the iframe unless the canvas-app's dev server allows being iframed from a different origin.For local dev,
http://localhost:<port>works as the Canvas URL origin: browsers treat localhost as a trusted origin (W3C Secure Contexts spec), so no HTTPS / mkcert is needed for the basic case. Runsetup-local-https-canvasonly if your app needs HTTPS-on-localhost for unrelated reasons (service workers, secure cookies, strict browser policy).For Vite-based apps, add to
vite.config.*. Configure BOTHserverandpreview: the canvas should be served fromvite preview(a prod build), not the dev server. Vite's dev optimizer can pre-bundle the SDK with a separately-loaded React (different?v=hash) and break hook calls in the canvas, even with one React innode_modules. Use the dev server for non-Studio iteration. The moment Studio iframes the app, switch tovite preview. Seeinstall-studiostep 9 for the full reasoning.server: {// ...existing config...allowedHosts: true, // Vite refuses unknown hosts by default; Studio iframe needs thisheaders: {'Access-Control-Allow-Origin': '*','Content-Security-Policy': "frame-ancestors *;",},},preview: {// RECOMMENDED canvas target — `<pm> build && <pm> exec vite preview` serves a deduped prod bundle// (no runtime optimizer to split React across passes)allowedHosts: true,headers: {'Access-Control-Allow-Origin': '*','Content-Security-Policy': "frame-ancestors *;",},},For Next.js, set the headers in
next.config.js:module.exports = {async headers() {return [{source: '/:path*',headers: [{ key: 'Content-Security-Policy', value: 'frame-ancestors *;' },{ key: 'Access-Control-Allow-Origin', value: '*' },],}];},};Why both:
allowedHosts: true(Vite) prevents the "host not allowed" 403 from the dev server. Theframe-ancestors *CSP header tells the browser the page may be embedded in any frame. Without either, Studio's iframe stays blank with no console error, a particularly hard failure to diagnose. -
Sanity check. Tell the user to:
- Open Studio at the project's Compositions list
- Click any section (Sections tab)
- Confirm the canvas iframe loads with
<StudioCanvas />content, no MISSING_CANVAS_URL banner
Inputs needed from the user
canvasPath(defaults to/canvas): the path to use. Confirm with user before creating the file in case they have a routing convention.studioProjectId(required): to set Canvas URL via API or to print the right manual link. Resolve it by listing the org's projects (configure-studio§ Select the Studio project), not by guessing from a name. Setting Canvas URL on the wrong project leaves the real one unconfigured and the canvas blank.studioHost(defaults tohttps://app.contentstack.com/#!/studio): to construct the right URL. Adjust for non-US regions. On a non-prod DC it'shttps://<env>-app.<non-prod-domain>/#!/studio(seeanalyze-project-fit§ 2b).
Acceptance
- BEFORE creating the route: confirmed the targeted environment has a non-empty per-locale URL equal to the dev origin, AND the Studio project targets that environment
- Canvas route file exists in the right location for the detected framework
- The file mounts
<StudioCanvas />and the token<style>(nothing else, no required props) - If a dev server was running, the route returns 200
- Canvas URL is set in Studio Project Settings, under Configuration, to match the path
- For local dev: canvas-app serves over trusted HTTPS (via
setup-local-https-canvas), ANDallowedHosts: true+frame-ancestors *header are set in the framework config - Opening a section in Studio loads the canvas iframe without errors (no blank frame, no "host not allowed", no mixed-content block)
If the Studio API call wasn't possible (no auth), the manual step is printed and the skill explicitly tells the user to complete it before claiming success.
See also
understand-canvas-url: concept explainer: what Canvas URL is, why a relative path, why/canvasis the standard, when Studio uses it vs the env base URL. Run this first if the user is unfamiliar with the concept.setup-local-https-canvas: optional, mkcert for local HTTPSinstall-studio: runs first, the SDK has to be installedconfigure-studio: covers Environment + Language alongside Canvas URLverify-setup: full layered smoke testtroubleshoot-canvas: when the iframe doesn't render after setup