When to Use
Replace a route-level user-state branch with a Contentstack Personalize-driven render: read variant alias from cookies/headers, pass to <StudioComponent /> via variantAlias query option.
Use during a hand-coded migration when migrate-page-to-studio surfaces a user-driven JSX branch (logged-in tier, persona, A/B, geolocation). Personalize handles audiences + variants. Studio renders whichever alias resolves. Do NOT use for content-driven branching (use Condition Block). Do NOT use for layout-shift needing different sections. That’s a separate-template decision.
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.
Wire a Route-Level Branch to Variant Aliases
Context
A common hand-coded pattern is to branch on user state inside the JSX:
{user.tier === "premium" ? <PremiumHero {...entry.hero} /> : <Hero {...entry.hero} />}
Three things are happening at once: (1) a visitor segment is computed, (2) a component swap is wired into the render, (3) the entry data is reused across both branches. Migration to Studio splits these:
- (1) Visitor segmentation stays in route code. Your cookie/header/middleware logic doesn’t change.
- (2) Component swap becomes a Contentstack variant alias. Each variant has its own version of the entry. The Studio template renders the variant’s overrides on top of the base entry.
- (3) Entry data reuse is automatic. Studio merges variant overrides onto the base entry. Unchanged fields fall through.
The result: ONE template, ONE route, N variants per entry. No code branch in the JSX. No template per persona.
This skill is for user-driven branching only. The variant is chosen based on who the visitor is (tier, persona, cookie, A/B). For content-driven branching (the variation lives in the entry’s own fields), use use-condition-block instead. That stays in the Studio canvas, no variant aliases needed.
Reference: docs/advanced/variant-aliases-deep-dive.md (full deep-dive), docs/recipes/migrating-hand-coded-pages-to-studio.md Step 6 (handling edges).
Task
Read routeFilePath and locate the branchExpression. Confirm it’s a user-driven branch (the condition references runtime state: user, cookies, headers, experiment, persona). If the condition references entry.<field>, STOP. That’s content-driven branching. Recommend use-condition-block instead.
Point the user at Contentstack Personalize to set up audiences + variant aliases. This skill does NOT call CMA, does NOT write to the stack, and does NOT touch entry data. Personalize is Contentstack’s product feature for audience-driven variants. Configure it in the Contentstack web app:
Open https://app.contentstack.com → your stack → Personalize. 1. Create an Experience targeted at the routes you're migrating (e.g. "<contentTypeUid> personalization"). An Experience is the wrapper that holds audiences + variant aliases together. 2. Define one or more Audiences. An Audience is the visitor-segment definition (e.g. "logged-in premium tier", "experiment B cohort"). Personalize lets you define audiences from cookies, query params, headers, or external signals. 3. For each non-base name in your variants list, create a Variant under the Experience: - Variant alias UID: <variant> ← match what your route will pass - Display name: <variant> ← what authors see in Studio - Targeting: link to one of the Audiences from step 2 4. Publish the Experience. Personalize handles the cookie/header orchestration server-side; your route only needs to read the variant cookie/header Personalize sets and forward it to <StudioComponent />.Per-entry variant overrides are authored in Contentstack’s normal entry editor, in the “Variants” tab of each entry of contentTypeUid. Print:
Open https://app.contentstack.com → Content → <contentTypeUid> → pick an entry → "Variants" tab. For each variant alias you created in Personalize: - Click "+ Add Variant". - Pick the alias UID (e.g. "premium"). - Override the fields that differed in your original JSX branch: <list the fields the original branch differed on — derived from the branchExpression> - Leave other fields empty — they inherit from the base entry. - Save.
The skill never touches entries. The user configures Personalize + populates variant content themselves.
Generate the route swap. Rewrite routeFilePath:
- export default async function ProductPage({ params }) { - const user = await getCurrentUser(); - const entry = await fetchEntry(params.sku); - return ( - <> - {user.tier === "premium" - ? <PremiumHero {...entry.hero} /> - : <Hero {...entry.hero} />} - <Body {...entry.body} /> - </> - ); - } + import { cookies } from "next/headers"; + import { sdk } from "@/lib/contentstack"; + import { StudioComponent } from "@contentstack/studio-react"; + + export default async function ProductPage({ params }) { + const variant = (await cookies()).get("variant-alias")?.value; + // fetchCompositionData THROWS on a miss — wrap it, or an unknown sku 500s + // instead of 404ing. resolveComposition: configure-csr-vs-ssr. + const { specOptions, notFound: miss } = await resolveComposition( + sdk, + { templateContentTypeUid: "product" }, + { templateEntryUid: params.sku, variantAlias: variant || undefined }, + ); + if (miss) notFound(); + return <StudioComponent specOptions={specOptions} />; + }Match the resolver style to visitorSegmentSource:
- cookie:variant-alias: read from cookies() (App Router), req.cookies (Pages Router), or useCookies() (CSR)
- header:x-variant: read from headers()
- searchParam:variant: read from URL search params
- custom: leave a TODO comment where the user wires their own resolver
Print the before/after diff for review. Wait for explicit confirmation before writing the file.
Hand off to the user:
- “In Contentstack, open Content Models, select <contentTypeUid>, open an entry, and go to the Variants tab. Fill in per-variant overrides for the fields the original branch differed on (e.g. for the premium variant: set a different hero.headline, swap hero.cover, etc.).”
- “In Studio, open the template the route maps to, then use the navbar’s variant picker to preview each variant against the preview entry. Confirm the layout renders the variant’s overrides.”
- “Wire your middleware / authentication layer to set the variant-alias cookie (or whichever segmentation source you picked) per visitor segment.”
Run verify-setup at the end. Layer 4 should render the template. Switching the cookie value in DevTools should re-fetch the page with the new variant on next request.
Inputs Needed From the User
- routeFilePath: required, real path.
- branchExpression: required. If the user pastes a content-driven branch by mistake (entry.is_premium ? ...), redirect to use-condition-block.
- variants: required. The first name is the base (the entry’s default state). Subsequent names are the variants you’re authoring overrides for.
- contentTypeUid: required.
- visitorSegmentSource: required. Pre-default to cookie:variant-alias. Ask if their app uses a different convention.
Acceptance
- The skill pointed the user at Contentstack Personalize for setting up the Experience + Audiences + Variant Aliases, and made clear it does NOT touch the stack itself, does NOT call CMA, and does NOT need auth tokens or Management Tokens.
- The skill printed step-by-step UI instructions for scaffolding per-variant entry overrides on one representative entry (via the entry editor’s “Variants” tab, the same UI authors use day-to-day).
- Route file is rewritten with the variant resolver matching visitorSegmentSource, and the JSX branch is removed.
- Before/after diff was shown to the user and they confirmed before write.
- verify-setup ran and Layer 4 (Studio canvas) renders the template against the base entry.
- User was given the three hand-off tasks: (1) fill in per-variant content in Contentstack, (2) preview each variant in Studio, (3) wire their middleware to set the segment source.
- If the supplied branch was content-driven (entry-field-based), the skill STOPPED and redirected to use-condition-block.
Common Pitfalls
| Pitfall | Why it bites | Fix |
|---|---|---|
| Using variant aliases for content-driven variation | Variant aliases are a per-VISITOR mechanism. Using them for entry-shape variation conflates the two concerns and makes future authoring confusing | Content-driven variation belongs in use-condition-block. Visitor-driven variation belongs here. |
| Forking the template per variant | One template per variant = N times the structural change cost. The whole point of variants is the same template renders all | Always one template. Variants live on the entry, not the composition |
| Resolving the variant client-side AFTER the server rendered | Hydration mismatch + content-flash. Variant must be resolved BEFORE the SSR fetch | Read cookie/header/middleware before fetchCompositionData. Pass variantAlias to the fetcher |
| Forgetting to set the variant in the cache key | CDN serves all visitors the variant of the first request | Vary on the segmentation header, or include the variant in the URL/cache key, or revalidate per request for variant-aware routes |
| Creating variants on the stack but not on the entry | Studio’s variant picker shows the alias but no overrides exist. The canvas renders the base everywhere | Step 3 of this skill scaffolds the entry-override skeleton. Confirm it ran |
See Also
- docs/advanced/variant-aliases-deep-dive.md, full deep dive: A/B, personalization, seasonal variants, cache strategy, edge cases
- use-condition-block: for content-driven branching (the variant decision lives in the entry, not in visitor state)
- migrate-page-to-studio: the final route-swap skill. This skill handles ONE specific edge that surfaces during migration (user-driven branch)
- docs/recipes/migrating-hand-coded-pages-to-studio.md Step 6: the recipe section that names this skill as the translation for user-driven branches
- wire-external-data: sibling skill for cases where the branch needs external data (not just visitor state) to decide