diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index a86b657..5093964 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,14 +1,14 @@ { "$schema": "https://anthropic.com/claude-code/marketplace.schema.json", "name": "contentstack-skills", - "description": "Contentstack agent skills for CMS modeling, Delivery SDK, Live Preview, Studio, Brand Kit, Developer Hub apps, Launch, and migrations.", + "description": "Contentstack agent skills plus the hosted Contentstack MCP server: CMS modeling and APIs, Delivery SDK and GraphQL, Live Preview, Studio, Brand Kit, Personalize, Lytics, Launch, Automate, Developer Hub and Marketplace apps, the CLI, and migrations.", "owner": { "name": "Contentstack", "url": "https://www.contentstack.com" }, "plugins": [ { "name": "contentstack-skills", "source": "./", - "description": "Contentstack agent skills for CMS modeling, Delivery SDK, Live Preview, Studio, Brand Kit, Developer Hub apps, Launch, and migrations.", - "version": "0.3.0", + "description": "Contentstack agent skills plus the hosted Contentstack MCP server: CMS modeling and APIs, Delivery SDK and GraphQL, Live Preview, Studio, Brand Kit, Personalize, Lytics, Launch, Automate, Developer Hub and Marketplace apps, the CLI, and migrations.", + "version": "0.4.1", "category": "development", "tags": ["contentstack", "cms", "headless-cms", "studio"] } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 24779cd..043e0d9 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,10 +1,15 @@ { "name": "contentstack-skills", - "version": "0.3.0", - "description": "Contentstack agent skills for CMS modeling, Delivery SDK, Live Preview, Studio, Brand Kit, Developer Hub apps, Launch, and migrations.", + "version": "0.4.1", + "description": "Contentstack agent skills plus the hosted Contentstack MCP server: CMS modeling and APIs, Delivery SDK and GraphQL, Live Preview, Studio, Brand Kit, Personalize, Lytics, Launch, Automate, Developer Hub and Marketplace apps, the CLI, and migrations.", "author": { "name": "Contentstack", "url": "https://www.contentstack.com" }, "homepage": "https://developers.contentstack.com", "repository": "https://github.com/contentstack/contentstack-agent-skills", "license": "MIT", + "icon": "assets/icon.png", + "documentationUrl": "https://www.contentstack.com/docs/developers/mcp-profile-hub", + "supportUrl": "https://www.contentstack.com/support", + "privacyPolicyUrl": "https://www.contentstack.com/legal/privacy", + "termsOfServiceUrl": "https://www.contentstack.com/legal/terms-of-service", "keywords": ["contentstack", "cms", "headless-cms", "delivery-sdk", "live-preview", "studio", "launch"] } diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index 32d7b60..c5c8e09 100644 --- a/.cursor-plugin/marketplace.json +++ b/.cursor-plugin/marketplace.json @@ -5,7 +5,7 @@ { "name": "contentstack-skills", "source": "./", - "description": "Contentstack agent skills for CMS modeling, Delivery SDK, Live Preview, Studio, Brand Kit, Developer Hub apps, Launch, and migrations." + "description": "Contentstack agent skills plus the hosted Contentstack MCP server: CMS modeling and APIs, Delivery SDK and GraphQL, Live Preview, Studio, Brand Kit, Personalize, Lytics, Launch, Automate, Developer Hub and Marketplace apps, the CLI, and migrations." } ] } diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index e51e220..14507a0 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,7 +1,15 @@ { "name": "contentstack-skills", - "version": "0.3.0", - "description": "Contentstack agent skills for CMS modeling, Delivery SDK, Live Preview, Studio, Brand Kit, Developer Hub apps, Launch, and migrations.", + "version": "0.4.1", + "description": "Contentstack agent skills plus the hosted Contentstack MCP server: CMS modeling and APIs, Delivery SDK and GraphQL, Live Preview, Studio, Brand Kit, Personalize, Lytics, Launch, Automate, Developer Hub and Marketplace apps, the CLI, and migrations.", "author": { "name": "Contentstack", "url": "https://www.contentstack.com" }, - "skills": "./skills/" + "homepage": "https://developers.contentstack.com", + "repository": "https://github.com/contentstack/contentstack-agent-skills", + "license": "MIT", + "keywords": ["contentstack", "cms", "headless-cms", "delivery-sdk", "live-preview", "studio", "launch", "mcp"], + "logo": "assets/icon.png", + "skills": "./skills/", + "mcpServers": { + "contentstack": { "url": "https://mcp-profile-hub.contentstack.com/api/mcp" } + } } diff --git a/.github/workflows/routing-evals.yml b/.github/workflows/routing-evals.yml new file mode 100644 index 0000000..8b481a2 --- /dev/null +++ b/.github/workflows/routing-evals.yml @@ -0,0 +1,46 @@ +name: Routing evals + +# Checks that each prompt in evals/ loads the right skill. Each run is a real Claude session +# (about $0.10-0.15), so this runs once per case on PRs that touch skills or evals. +on: + pull_request: + paths: ["skills/**", "evals/**"] + workflow_dispatch: + inputs: + runs: + description: Runs per case + default: "3" + +permissions: + contents: read + +jobs: + routing: + runs-on: ubuntu-latest + timeout-minutes: 60 + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + steps: + - name: Skip without an API key + if: env.ANTHROPIC_API_KEY == '' + run: echo "::notice::ANTHROPIC_API_KEY is not set (forks and Dependabot PRs), so routing evals were skipped." + - uses: actions/checkout@v4 + if: env.ANTHROPIC_API_KEY != '' + - uses: actions/setup-node@v4 + if: env.ANTHROPIC_API_KEY != '' + with: + node-version: 22 + - name: Install Claude Code + if: env.ANTHROPIC_API_KEY != '' + run: npm install -g @anthropic-ai/claude-code + - name: Run routing evals + if: env.ANTHROPIC_API_KEY != '' + run: >- + claude plugin eval . --tag routing --ablation none + --runs ${{ inputs.runs || '1' }} -j 6 --max-cost-usd 40 + --trust-plugin --no-publish --json evals/results/routing.json + - uses: actions/upload-artifact@v4 + if: always() && env.ANTHROPIC_API_KEY != '' + with: + name: routing-evals + path: evals/results/ diff --git a/.gitignore b/.gitignore index 20febc2..60e78cc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ .DS_Store .obsidian +evals/results/ +__pycache__/ +*.pyc diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..9faf817 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "contentstack": { + "type": "http", + "url": "https://mcp-profile-hub.contentstack.com/api/mcp" + } + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 79dee73..284ea03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,107 @@ All notable changes to the Contentstack agent skills bundle. Versions match the `version` field in every plugin manifest that declares one. +## 0.4.1 + +This release adds nine skills, connects every plugin to the hosted Contentstack MCP server, and corrects claims in the existing skills that didn't match the product. The `studio` skill now matches Studio SDK 2.0. + +### Highlights + +- **The Contentstack MCP server comes with the plugin.** Installing the skills also connects the hosted MCP Profile Hub, so an agent can work on your stacks instead of only advising. You sign in through the browser the first time a Contentstack tool runs. No API key is needed. +- **Nine new skills:** Personalize, Lytics, Launch hosting, Automate, the `csdx` CLI, the MCP server, GraphQL, JSON RTE and Marketplace apps. +- **Accuracy pass.** Every skill was checked against the product wiki, with the public docs and OpenAPI specs as tiebreaker. Webhook signature verification, taxonomy query operators and Launch environment updates were wrong before and are fixed now. +- **Studio SDK 2.0.** The `studio` skill was followed end to end on a Next.js 16 app with `@contentstack/studio-react` 2.0.1, and every changed snippet is typechecked against that release. New guides cover CSR vs SSR and 404 pages. +- **One template for every skill**, listing the MCP tools that carry out each step. + +### Upgrading + +- Update the plugin the usual way for your tool (see [Release & versioning](https://github.com/contentstack/contentstack-agent-skills/blob/main/docs/contributing/release-and-versioning.md#how-updates-reach-users)). The MCP server arrives with the update. +- No skills were renamed or removed. + +### New skills + +- `personalize`: experiences, audiences, attributes and events, the Personalize Edge SDK and Edge API, the Management API, and edge middleware that resolves variants before rendering. +- `lytics`: the Lytics JavaScript tag, identity resolution, schema, audiences and the Segment API, consent and deletion, and sending audiences to Personalize. +- `launch-platform`: hosting on Launch, covering builds, Cloud and Edge Functions, `launch.json`, caching and revalidation, deploy hooks, domains, access control, log targets, monorepos and limits. +- `automations`: Contentstack Automate rules, triggers, connectors, control flow, custom code, executions and debugging. +- `dx-cli`: the `csdx` CLI for auth, export, import, seed, clone, bulk operations, migration scripts, branch merges and plugins. +- `dx-mcp-server`: connecting AI agents through the local `@contentstack/mcp` server or the hosted MCP Profile Hub, and when to use each. +- `cms-graphql`: the GraphQL Content Delivery API, covering endpoints, schema, references, modular blocks, JSON RTE, limits and Live Preview. +- `cms-json-rte`: rendering and serializing JSON Rich Text Editor content with `@contentstack/utils`, render options, embedded entries and assets, GraphQL, React, HTML↔JSON conversion and Live Preview tags. +- `marketplace-apps`: choosing, installing and configuring first-party Marketplace apps, app configuration vs field configuration, and reading app data on delivery. + +### Contentstack MCP server + +- **Bundled with every plugin.** Each plugin connects the hosted MCP Profile Hub (`https://mcp-profile-hub.contentstack.com/api/mcp`) with OAuth sign-in: + - Claude Code and claude.ai: `.mcp.json` (`type: http`). + - Codex, ChatGPT and other Agent Plugins clients: `mcp.json` (`type: streamable-http`). + - Cursor: `mcpServers` in `.cursor-plugin/plugin.json`, which Cursor reads in place of `mcp.json`. + - Gemini CLI: `mcpServers` in `gemini-extension.json` (`httpUrl`). +- **MCP tools in every skill.** Each skill names the Contentstack MCP tools that carry out its steps, taken from the server's published definitions, or says that none exist and what to use instead. +- **Advisory by default.** Skills use write, publish, deploy or delete tools only when you ask, after naming the target and getting your confirmation. +- **Launch action skills** use the `launch` MCP tools (`update_an_environment`, `create_deployment`, `get_deployment_logs`) when they're connected. + +### Accuracy fixes + +- **`cms-webhooks`:** signature verification now uses `X-Contentstack-Request-Signature` (RSA-PSS SHA-256) or the opt-in `X-Contentstack-HMAC-Signature` over `{timestamp}.{body}`. Payload fields, workflow events, retry timing and branch scoping are corrected, and unverified limits are removed. +- **`cms-taxonomy`:** the CDA operators are now the documented `$in`, `$exists`, `$eq_below`, `$below`, `$eq_above` and `$above` (with `levels`), replacing operators that don't exist. Depth and plan limits are added. +- **`launch-sync-environment-variables-from-env-example`:** updates go through `PUT` with the full environment object (there is no PATCH), with regional hosts and auth. +- **`launch-trigger-and-monitor-launch-deployments`:** logs come from the deployment-logs endpoint, the status list is complete with `LIVE` as success, and rollback is added. +- **`brand-kit-assistant`:** Voice Profile dials are 1–4, request headers are corrected, and v2 kits (Documents) and the other Brand Kit surfaces are added. +- **`cms-branches-aliases`:** the branch limit is plan-dependent, module scoping follows the public docs, and compare and merge strategies are added. +- **`cms-entries` and `cms-variants-personalization`:** both now have real instructions. Before, they were empty. +- **Other CMS skills:** + - `cms-localization`: Unlocalize is documented. + - `cms-releases`: 25 items per request, plus Releases v2. + - `cms-roles-permissions`: permissions are a union of roles with no deny, and M2M apps are covered. + - `cms-workflows`: self-advancement prevention is a per-stage option. + - `cms-assets`: Assets DAM limits and routing are added. + - `cms-environments-publishing`: all CDA hosts and the rate-limit split are listed. + - `cms-tokens-authentication`: M2M apps and preview tokens are covered. + - `cms-data-modeling-best-practices`: adds the limits to design around. +- **`developer-hub-app-architect`:** adds the Content Type Sidebar and Global Full Page locations, covers M2M apps, lifecycle webhooks and per-installation secrets, and links to its one coding reference instead of reference files that didn't exist. +- **`dx-delivery-sdk` and `dx-migrate-js-to-ts-sdk`:** the Security sections copied from Launch are replaced, and the package name is now `@contentstack/persistence-plugin`. +- **`asset-management`:** user-defined field types match the current docs. +- **Formatting:** instructions that were raw JSON are now prose, and the broken Product Context bullets are fixed. + +### Studio + +- **New guide, `csr-and-ssr-requirements`:** what a Studio page needs as CSR or SSR on the visitor site, the Live Preview pane, Visual Editor and the canvas. Covers the five settings that must agree, preview mode vs builder mode, where the Edit button shows, which refetch to call on studio-react 2.0.1, and converting a page from one mode to the other. +- **New guide, `handle-404-pages`:** unknown URLs and bad slugs render the site's own 404 page on Next.js (App and Pages Router), Remix and React Router. SSR sends a real 404, and the 404 is decided by `isCompositionNotFound` only, so a bad token or an outage stays an error. +- **Upgrading from 1.x:** `upgrade-studio-sdk` has a 1.x → 2.0 section, checked by installing 1.9.0 and 2.0.1 side by side. It separates what 2.0 actually changed from older rules that behave the same in both versions. +- **Builds that looked done but were wrong:** + - Every plan includes a primitive library step, and one Section-per-region rule replaces the conflicting reuse advice. + - `resolveComposition` resets the preview hash, so drafts no longer leak to visitors. + - The live site fetches only bound fields, and image props bind `.url`. + - Section Slot nodes omit the slot key, which the API rejects with a 422. +- **SDK 2.0.1 APIs:** `registerLazyComponent` for lazy registrations, `properties` and `items` on `object` and `array` props, `styles` as style groups, built-in component type names (`header`, `text`, `rich-text`, `hstack`), and `getSSRStyles` for server styles. +- **Errors match the SDK:** missing built-ins surface as `COMPONENT_NOT_REGISTERED` (fix: import `@contentstack/studio-react` above the fetch), key and token failures as `STACK_NOT_FOUND_ON_HOST`, and entry lookups that miss as `URL_MATCHED_NO_ENTRY`. +- **New paths:** apps that already have a catch-all route, Live Preview without the Delivery SDK, Next.js 16 `proxy.ts` and Turbopack, Tailwind v4 `@theme` tokens, interactive embeds in rich text, static generation and caching for Studio routes, and the canvas route layout and headers. +- **Compositions and MCP:** one canonical list recipe (Repeater, item Box, Condition Block), one wire-shape table for the CMA and the Compositions API, the compositions content type as JSON, and the MCP content-type snapshot with its HTTP fallback. +- **Consistency:** the definition of done is ten checks everywhere, `analyze-project-fit` runs first, shell snippets work in zsh, and `verify-setup` is project-agnostic. + +### One skill template + +- **Every skill uses the same sections:** When to use, Before you answer, Instructions, MCP tools, Output, Security, References, Examples. `scripts/validate.mjs` rejects missing, extra or out-of-order sections. The duplicated Description, User Problem, Success Criteria, Expected Inputs/Outputs, Workflow Summary, Tooling Notes and Product Context blocks are folded in, so the existing skills are about 770 lines shorter. See [the skill template](https://github.com/contentstack/contentstack-agent-skills/blob/main/docs/contributing/skill-template.md). +- **Routing cues:** each skill's When to use ends with the neighbouring skills to use instead, and many descriptions gained a "For X, use Y." cue. + +### Plugin listings + +- **Icon:** `assets/icon.png` (512×512) is set as `icon` for Claude and `logo` for Cursor. +- **Cursor manifest:** adds `homepage`, `repository`, `license`, `keywords` and `logo`. +- **Descriptions:** every manifest's description now names the MCP server and the newer products. +- **Cleanup:** removes a committed Python bytecode file from `migration-companion`, and ignores `__pycache__/` and `*.pyc`. + +### For contributors + +- **Routing evals:** `evals/` holds 62 `claude plugin eval` cases, two or more per automatically invoked skill. Each checks that a prompt loads the right skill and, for close neighbours, doesn't load the wrong one. `.github/workflows/routing-evals.yml` runs them on PRs that touch `skills/` or `evals/` and needs an `ANTHROPIC_API_KEY` secret. See [Routing evals](https://github.com/contentstack/contentstack-agent-skills/blob/main/docs/contributing/routing-evals.md). + +### Contributors + +Thanks to @sumitjain-cstk for the Studio SDK 2.0 sync, the end-to-end audit fixes and the CSR/SSR and 404 guides (#29, #30). + +**Full changelog:** https://github.com/contentstack/contentstack-agent-skills/compare/v0.3.0...v0.4.1 + ## 0.3.0 ### New skills diff --git a/README.md b/README.md index 67b282b..6b6cd46 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Contentstack Skills -A bundle of ready-to-use [Contentstack](https://www.contentstack.com) agent skills for AI coding tools, covering CMS implementation, Brand Kit guidance, delivery SDK development, kickstart maintenance, migration workflows, Developer Hub app architecture, Launch automation, and Contentstack Studio. +A bundle of ready-to-use [Contentstack](https://www.contentstack.com) agent skills for AI coding tools, covering CMS implementation, GraphQL delivery, Brand Kit guidance, delivery SDK development, the CLI and MCP servers, kickstart maintenance, migration workflows, Developer Hub app architecture, Marketplace apps, JSON RTE rendering, Launch hosting and automation, Personalize, Lytics, Automate, and Contentstack Studio. ## What's in here? @@ -60,17 +60,26 @@ npx skills add contentstack/contentstack-agent-skills --skill cms-entries --skil Use `--list` to see every skill, or leave out `--skill` to install them all. The CLI installs into the right folder for Claude Code, Cursor, Codex, Copilot, Gemini, Windsurf, Cline and others. +## Contentstack MCP server + +Every plugin manifest here also connects the hosted [Contentstack MCP Profile Hub](https://www.contentstack.com/docs/developers/mcp-profile-hub) at `https://mcp-profile-hub.contentstack.com/api/mcp`, so installing the plugin gives the agent the skills and the Contentstack tools together. The server signs you in with OAuth in the browser the first time a tool is used: there is no API key to configure, and every call runs with your own Contentstack roles. At sign-in you pick your region, a profile and a stack. + +If you only want the skills, disable the `contentstack` MCP server in your tool's MCP settings after installing. + ## Skills included | Slug | Title | Product | What it helps with | | --- | --- | --- | --- | | `asset-management` | Asset Management | Assets | Assets DAM spaces and workspaces, folders, asset types and metadata, localization, saved views, AI tagging and alt text, duplicate management, versions, trash and restore, visual markups, and CMS integration. | +| `automations` | Automations | Automate | Contentstack Automate workflows: triggers, connectors and connected apps, If-Else and loops, custom code, HTTP responses, executions and debugging, limits, and agent integration. | | `brand-kit-assistant` | Brand Kit Assistant | Brand Kit | Brand Kit concepts, setup, governance, Voice Profiles, Knowledge Vault, on-brand AI generation, and API-task routing. | | `cms-assets` | Contentstack Assets | CMS | Asset organization, image transformations, publishing lifecycle, CDN behavior, and asset limits. | | `cms-branches-aliases` | Branches & Aliases | CMS | Isolated content development, alias-based deployments, CI/CD integration, merge behavior, and rollback patterns. | | `cms-data-modeling-best-practices` | Contentstack Data Modeling Best Practices | CMS | Content type design, references, global fields, groups, modular blocks, JSON RTE, taxonomy, tags, and model simplification. | | `cms-entries` | Entries | CMS | Entry querying, localization, versioning, publishing, CDA usage, reference expansion, pagination, bulk operations, and Sync API patterns. | | `cms-environments-publishing` | Contentstack Environments & Publishing | CMS | Environment setup, publishing behavior, delivery and preview tokens, Sync API usage, CDN behavior, and publish queues. | +| `cms-graphql` | GraphQL Content Delivery | CMS | GraphQL Content Delivery API endpoints and headers, generated schema, reference connections, modular blocks, JSON RTE, pagination, query and rate limits, and Live Preview with GraphQL. | +| `cms-json-rte` | JSON Rich Text Editor | CMS | Rendering and serializing JSON RTE content with `@contentstack/utils`, custom render options, embedded entries and assets, HTML↔JSON conversion, and Live Preview tags. | | `cms-live-preview-visual-editor-support-assistant` | Live Preview and Visual Editor Support Assistant | Developer Experience | Live Preview, Visual Editor (Visual Builder) and Timeline setup or debugging: Onboarding Check, rendering-mode contracts (CSR, SSR, SSG, GraphQL), edit tags, FAQ-backed fixes, and Support handover. | | `cms-localization` | Contentstack Localization | CMS | Language setup, fallback chains, localized and unlocalized entries, non-localizable fields, and multi-locale publishing. | | `cms-releases` | Releases | CMS | Coordinated content deployment, release scheduling, staged deployment, webhook storm prevention, and CI/CD integration. | @@ -81,13 +90,19 @@ Use `--list` to see every skill, or leave out `--skill` to install them all. The | `cms-webhooks` | Contentstack Webhooks | CMS | Webhook configuration, event channels, payload handling, signature verification, retries, and reliable receiver design. | | `cms-workflows` | Workflows & Publish Rules | CMS | Workflow stages, approval flows, transition restrictions, publish governance, automation hooks, and common pitfalls. | | `developer-hub-app-architect` | Developer Hub App Architect | Developer Hub | Developer Hub and Marketplace app planning, UI location choices, React/TypeScript scaffolding, setup, SDK, manifest, proxy, and publishing issues. | +| `marketplace-apps` | Marketplace Apps | Developer Hub | Choosing, installing and configuring first-party Marketplace apps (DAM, commerce, search, translation and more), and reading their field data on delivery. | +| `dx-cli` | Contentstack CLI | Developer Experience | The `csdx` CLI: region and login, token aliases, export, import, seed and clone, bulk operations, migration scripts, branch diff and merge, and plugins such as tsgen, apps and Launch. | | `dx-delivery-sdk` | Delivery SDK | Developer Experience | Production-ready TypeScript with `@contentstack/delivery-sdk` for entries, assets, references, filters, sorting, pagination, locales, Live Preview, and Visual Builder. | | `dx-kickstart-next` | Contentstack Kickstart Next | Developer Experience | Maintaining Contentstack's Next.js kickstart: App Router structure, Delivery SDK setup, Live Preview, seeded stack alignment, env vars, validation, and PR reviews. | +| `dx-mcp-server` | Contentstack MCP | Developer Experience | Connecting AI agents with the local `@contentstack/mcp` server or the hosted MCP Profile Hub: choosing, client setup, auth, tool groups and profiles, and least-privilege safety. | | `dx-migrate-js-to-ts-sdk` | Migrate JS to TS SDK | Developer Experience | Migration from the JavaScript Contentstack SDK to the TypeScript Delivery SDK, including API mapping, rewrites, and unsupported pattern callouts. | -| `launch-sync-environment-variables-from-env-example` | Sync Launch environment variables from .env.example | Launch | Comparing a local `.env.example` with Launch environment variables and patching missing keys without printing secrets. | +| `launch-platform` | Launch Platform | Launch | Hosting on Launch: frameworks and builds, Cloud and Edge Functions, launch.json routing, caching and revalidation, deploy hooks, domains, access control, log targets, monorepos, and limits. | +| `launch-sync-environment-variables-from-env-example` | Sync Launch environment variables from .env.example | Launch | Comparing a local `.env.example` with Launch environment variables and adding missing keys with a full-object update, without printing secrets. | | `launch-trigger-and-monitor-launch-deployments` | Trigger and Monitor Launch Deployments | Launch | Triggering Launch deployments, polling deployment status, retrieving failure logs, and summarizing likely causes and next steps. | +| `lytics` | Lytics | Lytics (Data & Insights) | Lytics projects, the JavaScript tag, identity resolution, schema and LQL, audiences and the Segment API, content affinity, consent and deletion, and connecting audiences to Personalize. | | `migration-companion` | Contentstack Migration Companion | Developer Experience | Guided Contentful-to-Contentstack migration workflow covering prerequisites, content migration, code migration, bundled validation scripts, and completion reporting. | -| `studio` | Studio (preview) | Studio | Contentstack Studio end to end: SDK install, project provisioning, component registration, sections and templates, data binding, canvas and preview routes, verification, and troubleshooting. A router skill that loads one of 109 bundled specialist files per task. | +| `personalize` | Personalize | Personalize | Experiences, audiences, attributes, impressions and conversions, the Personalize Edge SDK and Edge API, Management API, and edge middleware patterns that resolve variants before rendering. | +| `studio` | Studio (preview) | Studio | Contentstack Studio end to end: SDK install, project provisioning, component registration, sections and templates, data binding, canvas and preview routes, verification, and troubleshooting. A router skill that loads one of 111 bundled specialist files per task. | ## How it works @@ -101,7 +116,9 @@ skills//SKILL.md Source of truth: one folder per skill .cursor-plugin/ Cursor plugin and marketplace .agents/plugins/marketplace.json Codex marketplace plugin.json Agent Plugins manifest (Codex, VS Code) -gemini-extension.json Gemini CLI extension +mcp.json MCP server for Agent Plugins (Codex, ChatGPT) +.mcp.json MCP server for Claude Code and claude.ai +gemini-extension.json Gemini CLI extension (skills and MCP server) docs/ Documentation: install, architecture, contributing, skill reference scripts/validate.mjs Validates skills and manifests (runs in CI) ``` diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000..c3a8b09 Binary files /dev/null and b/assets/icon.png differ diff --git a/docs/README.md b/docs/README.md index 3a85c43..19d0305 100644 --- a/docs/README.md +++ b/docs/README.md @@ -43,6 +43,8 @@ Every skill is written once in the open [Agent Skills](https://agentskills.io) f ### Contributing - [Add or edit a skill](contributing/add-or-edit-a-skill.md) +- [Skill template](contributing/skill-template.md) +- [Routing evals](contributing/routing-evals.md) - [Authoring conventions](contributing/authoring-conventions.md) - [Release & versioning](contributing/release-and-versioning.md) @@ -50,8 +52,8 @@ Every skill is written once in the open [Agent Skills](https://agentskills.io) f | | | | ------------------------ | -------------------------------------------------------------------------------------------------------------- | -| **Skills included** | 24 (see the [skill reference](skills/index.md)) | -| **Products covered** | CMS, Assets, Developer Experience, Studio, Launch, Brand Kit, Developer Hub | +| **Skills included** | 33 (see the [skill reference](skills/index.md)) | +| **Products covered** | CMS, Assets, Developer Experience, Studio, Launch, Brand Kit, Developer Hub, Marketplace, Personalize, Lytics, Automate | | **Supported tools** | Claude Code, claude.ai / Cowork, Cursor, Codex, GitHub Copilot, Gemini CLI, `skills` CLI | | **Source of truth** | `skills//SKILL.md` | | **Repository** | [github.com/contentstack/contentstack-agent-skills](https://github.com/contentstack/contentstack-agent-skills) | diff --git a/docs/contributing/add-or-edit-a-skill.md b/docs/contributing/add-or-edit-a-skill.md index 0fa76e5..f76f90c 100644 --- a/docs/contributing/add-or-edit-a-skill.md +++ b/docs/contributing/add-or-edit-a-skill.md @@ -21,11 +21,12 @@ node scripts/validate.mjs ## Add a new skill 1. Create a directory: `skills//`. -2. Add `SKILL.md` following the structure in [Anatomy of a SKILL.md](../how-it-works/skill-anatomy.md) and the rules in [Authoring conventions](authoring-conventions.md). The `name` must equal ``, and the `description` must include "Use when…" (1024 characters max). +2. Copy the [skill template](skill-template.md) into `SKILL.md` and follow the rules in [Authoring conventions](authoring-conventions.md). List the Contentstack MCP tools that carry out the skill's steps under `## MCP tools`. The `name` must equal ``, and the `description` must include "Use when…" (1024 characters max). 3. Add any `references/` or `scripts/` the skill needs. 4. Add a row for the skill to the **Skills included** table in the root `README.md`. 5. Add a documentation page under `docs/skills/.md` and link it from `docs/skills/index.md`. -6. Run `node scripts/validate.mjs` and commit. +6. Add at least two [routing eval](routing-evals.md) cases, `evals/routing--1` and `-2`, and run them. +7. Run `node scripts/validate.mjs` and commit. The plugin manifests point at the whole `skills/` folder, so a new skill needs no manifest change. @@ -48,6 +49,7 @@ Use lowercase letters, digits, and single hyphens, max 64 characters. - `name` is missing, doesn't match the folder, or breaks the naming rules. - `description` is missing, lacks "Use when…", or is over 1024 characters. +- `SKILL.md` has a `##` section outside the [template](skill-template.md), misses one of the required sections (When to use, Instructions, MCP tools, Security), or has them out of order. - `SKILL.md` has no `## Security` section, or it lacks `### Defaults`, `### Destructive Actions`, `### Secrets` or `### Environment Variables`. - A relative link in any markdown file under `skills/` doesn't resolve or points outside the skill's folder (code blocks are ignored). - A frontmatter value in `skills/` or `docs/` contains `: ` without quotes. @@ -57,8 +59,12 @@ Use lowercase letters, digits, and single hyphens, max 64 characters. The GitHub Action in `.github/workflows/validate.yml` runs the validator on pull requests to any branch, on pushes to `main` and `next`, and manually (workflow dispatch). If it fails, run `node scripts/validate.mjs` locally, fix what it reports, and push again. +`.github/workflows/routing-evals.yml` runs the [routing evals](routing-evals.md) on pull requests that change `skills/` or `evals/`. + ## Related +- [Skill template](skill-template.md) +- [Routing evals](routing-evals.md) - [Authoring conventions](authoring-conventions.md) - [Anatomy of a SKILL.md](../how-it-works/skill-anatomy.md) - [Release & versioning](release-and-versioning.md) diff --git a/docs/contributing/authoring-conventions.md b/docs/contributing/authoring-conventions.md index 6c62292..84dc71e 100644 --- a/docs/contributing/authoring-conventions.md +++ b/docs/contributing/authoring-conventions.md @@ -9,9 +9,7 @@ These conventions keep skills consistent, parseable by agents, and safe. Follow ## Structure -Follow the section outline described in [Anatomy of a SKILL.md](../how-it-works/skill-anatomy.md): Description, When to Use, User Problem, Success Criteria, Expected Inputs/Outputs, Example User Requests, Workflow Summary, Instructions, Output Format, Tooling Notes, Security, Product Context, References, Examples. - -Not every skill needs every section, but keep the order consistent so agents (and readers of the docs) find things in predictable places. +Use the [skill template](skill-template.md): When to use, Before you answer, Instructions, MCP tools, Output, Security, References, Examples. When to use, Instructions, MCP tools and Security are required, and `scripts/validate.mjs` rejects other sections or a different order. Put subtopics under `###` headings inside Instructions. ## Frontmatter @@ -23,7 +21,7 @@ Not every skill needs every section, but keep the order consistent so agents (an ## Write the description as the trigger -Agents choose a skill by matching your request against its description (see [How skill selection works](../how-it-works/router.md)). Make the "Use when…" clause list the concrete requests, tools, and terms a user would mention: "Delivery SDK", "Live Preview", "pagination", not "helps with content". Keep the **When to Use** section in the body consistent with it, and avoid overlap with neighboring skills' descriptions. +Agents choose a skill by matching your request against its description (see [How skill selection works](../how-it-works/router.md)). Make the "Use when…" clause list the concrete requests, tools, and terms a user would mention: "Delivery SDK", "Live Preview", "pagination", not "helps with content". Keep the **When to use** section consistent with it, avoid overlap with neighboring skills' descriptions, and add a "For X, use Y." cue where two skills sit close together. The [routing evals](routing-evals.md) test this. ## Security is mandatory @@ -38,7 +36,7 @@ Inherit the product's shared safety rules (see [Security & safety model](../how- ## Default to read-only -Unless a skill genuinely needs to perform an external action, make it advisory and read-only. If it does perform actions, gate every destructive or production-facing step behind explicit confirmation, and prefer dry-run defaults. +Unless a skill genuinely needs to perform an external action, make it advisory. List the MCP tools that would carry out its steps under **MCP tools**, so an agent with the Contentstack MCP server connected can act when the user asks. Whenever a skill or its tools perform actions, gate every destructive or production-facing step behind explicit confirmation, and prefer dry-run defaults. ## References, not assumptions diff --git a/docs/contributing/routing-evals.md b/docs/contributing/routing-evals.md new file mode 100644 index 0000000..d124faa --- /dev/null +++ b/docs/contributing/routing-evals.md @@ -0,0 +1,52 @@ +--- +title: Routing evals +description: "How the evals/ suite checks that each prompt loads the right skill, and how to run and extend it." +--- + +# Routing evals + +A skill only helps if the agent loads it. With more than 30 skills, descriptions compete with each other, and a vague one quietly sends questions to the wrong skill. The `evals/` folder holds prompts that must load a given skill, run with `claude plugin eval` (Claude Code 2.1 or later). + +## How a case works + +Each case is a folder named `routing--`: + +``` +evals/routing-cms-assets-1/ +├── prompt.md # the user's question +└── graders/ + ├── loads-cms-assets.md # passes when the agent calls Skill with cms-assets + └── skips-asset-management.md # passes when it doesn't load the neighbour +``` + +`prompt.md` has the case `name`, `tags: [routing, ]`, and low `max_turns` and `timeout_seconds`, because the routing decision happens on the first turn. The graders are `tool_used` checks on the `Skill` tool. A `skips-` grader (`min: 0`, `max: 0`) guards against a neighbouring skill that covers similar ground, such as `cms-assets` and `asset-management`, or `personalize` and `cms-variants-personalization`. + +Every skill that agents can pick automatically has at least two cases. The two Launch action skills have none, because `disable-model-invocation: true` means they only run when invoked by name. + +## Run them + +```bash +# One skill +claude plugin eval . --ablation none --case 'routing-cms-taxonomy-*' --runs 1 --trust-plugin --no-publish + +# Everything, three runs per case +claude plugin eval . --tag routing --ablation none --runs 3 -j 6 --trust-plugin --no-publish +``` + +- `--ablation none` is required. The default also runs a baseline without the plugin, which doubles the cost, and it doesn't score `Skill` graders. +- Each run is a real Claude session and costs roughly $0.10–0.15, so the full suite is a few dollars per run per case. +- Results go to `evals/results/` (gitignored), with an HTML report. + +In CI, `.github/workflows/routing-evals.yml` runs every case once on pull requests that change `skills/` or `evals/`, and three times when started by hand. It needs an `ANTHROPIC_API_KEY` repository secret and skips itself without one. + +## When a case fails + +A failing case usually means a description lost a routing cue or a neighbour's description grew to cover the same request. Fix the descriptions, not the prompt: + +- Put the terms users actually say in the `description`. +- Add a "For X, use Y." cue to the skill that wins wrongly. +- Rerun just that skill's cases with `--case`. + +## Add cases + +When you add a skill, add at least two `routing--` folders: one plain request, and one phrased close to a neighbouring skill with a `skips-` grader. Write prompts the way users ask, not in the description's words. diff --git a/docs/contributing/skill-template.md b/docs/contributing/skill-template.md new file mode 100644 index 0000000..054575b --- /dev/null +++ b/docs/contributing/skill-template.md @@ -0,0 +1,105 @@ +--- +title: Skill template +description: "The section outline every SKILL.md follows, with a copyable template. scripts/validate.mjs enforces it." +--- + +# Skill template + +Every `skills//SKILL.md` uses the same sections in the same order. `node scripts/validate.mjs` fails a skill that has a section outside this list, misses a required one, or puts them out of order. + +| Section | Required | What goes in it | +| --- | --- | --- | +| `## When to use` | Yes | The requests the skill handles, then **Use another skill for:** with the neighbouring skills and what they own. | +| `## Before you answer` | No | What to find out or inspect first: IDs, framework, region, the user's goal. | +| `## Instructions` | Yes | A short numbered workflow, then `###` subsections with the rules and facts. Long or volatile detail goes in `references/`. | +| `## MCP tools` | Yes | Which Contentstack MCP tools carry out the steps, grouped as inspect, change and delete, with the local server group in brackets. If no tool covers the area, say so and name the API, CLI or UI to use instead. | +| `## Output` | No | How to shape the answer. | +| `## Security` | Yes | `### Defaults`, `### Destructive Actions`, `### Secrets`, `### Environment Variables`. | +| `## References` | No | Bundled `references/` files and public docs URLs. | +| `## Examples` | No | Worked User → Assistant pairs, mostly for refusals and confirmation gates. | + +Keep the rest out of the body: + +- **Don't restate the description.** The frontmatter `description` does the routing; the body starts with a one- or two-sentence scope statement under the title. +- **Example prompts belong in [routing evals](routing-evals.md)**, where they're tested, not in the skill. +- **Product context and safety rules go in Security.** Don't repeat a separate product block. + +## MCP pairing + +The skills are advisory by default: they explain and show code. When the Contentstack MCP server (the local `@contentstack/mcp` server or MCP Profile Hub) is connected and the user asks for the change, the agent can make it with the tools listed under `## MCP tools`. The rules: + +- Read tools can run freely to ground an answer in the user's real stack. +- Before any create, update, publish, deploy or delete, the agent names the target (stack, branch, environment, entries) and waits for confirmation. +- Tool names are the local server's, from `https://mcp.contentstack.com//tools`. Profile Hub profiles expose a chosen subset, so the agent uses what its client lists. + +`allowed-tools` in the frontmatter pre-approves tools; it doesn't block MCP tools, which still follow the client's permission settings. + +## Template + +````markdown +--- +name: +description: ". Use when . For , use ." +allowed-tools: Read Grep Glob +--- + +# + +<One or two sentences: what this skill covers.> + +## When to use + +- <Request the skill handles> +- <Request the skill handles> + +Use another skill for: + +- <Topic> → `<other-skill>` + +## Before you answer + +<What to ask or inspect first.> + +## Instructions + +1. <Step> +2. <Step> + +### <Rule group> + +<Rules and facts.> + +## MCP tools + +When the Contentstack MCP server is connected, these tools act on the stack (local server group in brackets): + +- Inspect: `<tool>` (`<group>`) +- Change: `<tool>` (`<group>`) +- Delete: `<tool>` (`<group>`) + +## Output + +<How to shape the answer.> + +## Security + +### Defaults + +- Advisory by default. Use the MCP tools above only when the user asks for the change. + +### Destructive Actions + +<What needs confirmation, and what to say before asking.> + +### Secrets + +Never print, echo or store tokens, API keys or secrets. Use placeholders. + +### Environment Variables + +<Variable names to use in examples.> + +## References + +- <Public docs URL> +```` diff --git a/docs/how-it-works/router.md b/docs/how-it-works/router.md index 04ea0b0..0a174f1 100644 --- a/docs/how-it-works/router.md +++ b/docs/how-it-works/router.md @@ -41,6 +41,8 @@ Each description's "Use when…" clause lists the requests that should load it. | Designing or refactoring content models | [Data Modeling Best Practices](../skills/cms-data-modeling-best-practices.md) | | Setting up or debugging Live Preview, Visual Editor (Visual Builder) or Timeline | [Live Preview & Visual Editor Support](../skills/cms-live-preview-visual-editor-support-assistant.md) | | Fetching entries, CDA queries, pagination, bulk ops | [Entries](../skills/cms-entries.md) | +| GraphQL queries, schema, references and limits | [GraphQL Content Delivery](../skills/cms-graphql.md) | +| Rendering JSON Rich Text Editor content, embedded entries, HTML conversion | [JSON Rich Text Editor](../skills/cms-json-rte.md) | | Uploading, transforming, delivering assets in a stack | [Assets](../skills/cms-assets.md) | | Assets DAM: spaces, workspaces, duplicate sets, saved views, visual markups | [Asset Management](../skills/asset-management.md) | | Classifying content, category hierarchies | [Taxonomy](../skills/cms-taxonomy.md) | @@ -54,10 +56,17 @@ Each description's "Use when…" clause lists the requests that should load it. | Webhooks, event channels, payloads, signatures | [Webhooks](../skills/cms-webhooks.md) | | Matching a Launch env to `.env.example` | [Sync Launch env vars](../skills/launch-sync-environment-variables-from-env-example.md) | | Triggering and monitoring Launch deployments | [Trigger & Monitor Deployments](../skills/launch-trigger-and-monitor-launch-deployments.md) | -| Personalization, A/B testing, audience segmentation, variants | [Variants & Personalization](../skills/cms-variants-personalization.md) | +| Entry variants and variant groups in the CMS | [Variants & Personalization](../skills/cms-variants-personalization.md) | | Building a Developer Hub or Marketplace app | [Developer Hub App Architect](../skills/developer-hub-app-architect.md) | - -The exact wording lives in each skill's frontmatter. See the [skills reference](../skills/index.md). +| Choosing or configuring a first-party Marketplace app (DAM, commerce, search, translation) | [Marketplace Apps](../skills/marketplace-apps.md) | +| Hosting on Launch: builds, functions, `launch.json`, caching, domains | [Launch Platform](../skills/launch-platform.md) | +| Personalize experiences, audiences, events, Edge SDK | [Personalize](../skills/personalize.md) | +| Lytics tag, identity, audiences, consent | [Lytics](../skills/lytics.md) | +| Automate rules, triggers, connectors, executions | [Automations](../skills/automations.md) | +| The `csdx` CLI: export, import, seed, bulk publish, migrations | [Contentstack CLI](../skills/dx-cli.md) | +| Connecting an AI agent to Contentstack over MCP | [Contentstack MCP](../skills/dx-mcp-server.md) | + +The exact wording lives in each skill's frontmatter. See the [skills reference](../skills/index.md). The [routing evals](../contributing/routing-evals.md) check that sample prompts load the skill this table promises. ## Skills that only run on request diff --git a/docs/how-it-works/skill-anatomy.md b/docs/how-it-works/skill-anatomy.md index 38f415d..d950362 100644 --- a/docs/how-it-works/skill-anatomy.md +++ b/docs/how-it-works/skill-anatomy.md @@ -34,34 +34,24 @@ Every tool installs the file unchanged, frontmatter included. See [How skill sel ## Body sections -The Markdown body follows a repeatable outline. Not every skill uses every section, but most include: +Every skill uses the same sections in the same order, and `scripts/validate.mjs` enforces it. The full template is in [Skill template](../contributing/skill-template.md). -| Section | What it captures | -| --- | --- | -| **Description** | What the skill does. | -| **When to Use** | The user intents that should activate it, expanding on the description's "Use when…". | -| **User Problem** | The underlying need the skill addresses. | -| **Success Criteria** | What a correct answer looks like. | -| **Expected Inputs** | What the agent should gather (UIDs, locale, framework, etc.). | -| **Expected Outputs** | The deliverables (code, recommendations, checklists). | -| **Example User Requests** | Representative prompts the skill handles. | -| **Workflow Summary** | The step-by-step approach. | -| **Instructions** | The detailed rules and decision logic: the core of the skill. | -| **Output Format** | How answers should be shaped. | -| **Tooling Notes** | Which tools/SDKs to prefer; read-only vs action behavior. | -| **Security** | Defaults, destructive-action gating, secret handling, env vars. | -| **Product Context** | The product area and its shared safety rules. | -| **References** | Bundled `references/` files to read on demand. | -| **Examples** | Few-shot user/assistant pairs that calibrate behavior. | - -## Instructions: two styles - -The **Instructions** section appears in two forms across the bundle: - -- **Prose with subheadings**: narrative rules grouped under headings (e.g. Delivery SDK, Webhooks). -- **Structured JSON blocks**: an array of `{ "heading", "content" }` objects (e.g. Assets, Branches & Aliases, Environments & Publishing, Localization). These render as discrete rule cards. - -Both are valid; they encode the same kind of guidance. +| Section | Required | What it captures | +| --- | --- | --- | +| **When to use** | Yes | The requests the skill handles, and which neighbouring skill to use instead for related topics. | +| **Before you answer** | No | What to find out or inspect first. | +| **Instructions** | Yes | A short numbered workflow, then the rules and facts under `###` subsections. | +| **MCP tools** | Yes | The Contentstack MCP tools that carry out the steps, or what to use when no tool covers the area. | +| **Output** | No | How answers should be shaped. | +| **Security** | Yes | Defaults, destructive-action gating, secret handling, env vars. | +| **References** | No | Bundled `references/` files and public docs. | +| **Examples** | No | Few-shot user/assistant pairs that calibrate behavior. | + +The body doesn't restate the frontmatter description. Example prompts live in the [routing evals](../contributing/routing-evals.md), where they're tested. + +## Advisory by default, MCP when asked + +Skills explain and show code. When the Contentstack MCP server is connected and the user asks for the change, the agent uses the tools named in **MCP tools**: read tools freely, and write, publish, deploy or delete tools only after naming the target and getting confirmation. ## References @@ -78,7 +68,7 @@ The agent reads these **on demand**, only when the task actually needs them, to ## Examples (few-shot) -Many skills end with worked **User → Assistant** pairs, often tagged `few shot` or `edge case`. These calibrate tone, format, and especially safety responses (for instance, how to refuse hardcoding a token, or how to gate a destructive action). +Some skills end with worked **User → Assistant** pairs. These calibrate tone, format, and especially safety responses (for instance, how to refuse hardcoding a token, or how to gate a destructive action). ## Why the consistency matters diff --git a/docs/overview/introduction.md b/docs/overview/introduction.md index ac35d85..2f240bc 100644 --- a/docs/overview/introduction.md +++ b/docs/overview/introduction.md @@ -24,17 +24,20 @@ Agent skills close that gap. They give the assistant Contentstack-specific knowl ## What's in the bundle -The skills span seven Contentstack product areas: +The skills span ten Contentstack product areas: | Product | Skills | | --- | --- | -| **CMS** | Assets · Branches & Aliases · Data Modeling · Entries · Environments & Publishing · Localization · Releases · Roles & Permissions · Taxonomy · Tokens & Authentication · Variants & Personalization · Webhooks · Workflows | +| **CMS** | Assets · Branches & Aliases · Data Modeling · Entries · GraphQL Content Delivery · JSON Rich Text Editor · Environments & Publishing · Localization · Releases · Roles & Permissions · Taxonomy · Tokens & Authentication · Variants & Personalization · Webhooks · Workflows | | **Assets** | Asset Management | -| **Developer Experience** | Delivery SDK · Contentstack Kickstart Next · Migrate JS→TS SDK · Live Preview & Visual Editor Support · Migration Companion | +| **Developer Experience** | Contentstack CLI · Contentstack MCP · Delivery SDK · Contentstack Kickstart Next · Migrate JS→TS SDK · Live Preview & Visual Editor Support · Migration Companion | | **Studio** | Contentstack Studio | -| **Launch** | Sync env vars from `.env.example` · Trigger & Monitor Deployments | +| **Launch** | Launch Platform · Sync env vars from `.env.example` · Trigger & Monitor Deployments | | **Brand Kit** | Brand Kit Assistant | -| **Developer Hub** | Developer Hub App Architect | +| **Developer Hub** | Developer Hub App Architect · Marketplace Apps | +| **Personalize** | Personalize | +| **Lytics** | Lytics | +| **Automate** | Automations | See the full [skills reference](../skills/index.md) for a description of each. diff --git a/docs/skills/asset-management.md b/docs/skills/asset-management.md index c5b91f1..4971c06 100644 --- a/docs/skills/asset-management.md +++ b/docs/skills/asset-management.md @@ -17,7 +17,7 @@ When you ask about the Assets DAM: spaces and workspaces, folders, asset types a - **Spaces and workspaces**: a space is the top-level container, and workspaces are isolated environments inside it, created from the main workspace or a parent workspace. - **Folders and roles**: folder creation and least-privilege guidance. -- **Asset types and metadata**: system, general and user-defined metadata. The currently supported user-defined field types are single line text, number, select and group. Filters on user-defined fields can be saved as saved views. +- **Asset types and metadata**: system, general and user-defined metadata. The currently supported user-defined field types are single line text, number and group. Filters on user-defined fields can be saved as saved views. - **Localization**: add a language in product settings, then add it per workspace under Space Settings > Workspaces > Manage Locales. - **Search**: the public Assets API search endpoint, required headers, and filtering by user-defined field. - **AI tags and alt text**: manual suggestions and auto-populate at upload, with their limits. @@ -34,6 +34,10 @@ When you ask about the Assets DAM: spaces and workspaces, folders, asset types a - "I deleted a folder by mistake. Can I get it back?" - "How do I search assets by a custom field value?" +## MCP tools + +No Contentstack MCP tool covers the Assets DAM; work happens in the Assets UI or the Assets API. The `cma` asset tools act on the CMS stack asset library, which is `cms-assets` territory. + ## Safety notes Read-only advisory. Never uploads, moves, deletes, restores or modifies assets, spaces or settings. Warns before irreversible actions such as deleting a space or all markups. Never exposes tokens or API keys; examples use environment variables. diff --git a/docs/skills/automations.md b/docs/skills/automations.md new file mode 100644 index 0000000..f3850e1 --- /dev/null +++ b/docs/skills/automations.md @@ -0,0 +1,45 @@ +--- +title: Automations +description: "Design and debug Contentstack Automate workflows: triggers, connectors, control flow, custom code, HTTP responses, executions, limits and the agent bridge." +--- + +# Automations + +**Slug:** `automations` · **Product:** Automate (Agent OS) · **Type:** Advisory (acts through MCP tools on request) + +Advises on building no-code workflows in Contentstack Automate, such as publish notifications, search indexing, translation and data syncs, and on debugging them when they misbehave. + +## When it triggers + +When you ask how to automate a Contentstack or cross-system task, which trigger, connector or step to use, why an automation did not run or failed, or how automations and agents work together. + +## What it covers + +- **Building blocks**: one trigger plus ordered steps, `{{step.path}}` references, and the draft/active lifecycle (active automations can't be edited). +- **Triggers**: Contentstack events, HTTP (treat the URL as a secret), schedule and manual. +- **Connectors and Connected apps**: saved credentials, reauthorizing, dependencies. +- **Control flow**: If-Else, loops, pause and resume. +- **Custom code**: a sandboxed JavaScript step with a short time limit, for light logic only. +- **Response step**: replying to HTTP callers. +- **Executions**: statuses, per-step outputs and a debugging checklist. +- **Limits and credits**: plan-based step, loop and output caps, and AI credits. +- **Agents**: agents can start automations; AI-built automations on the canvas aren't shipped yet. + +## Example prompts + +- "Post to Slack whenever an entry is published." +- "Keep our Algolia index in sync with the stack." +- "Why didn't my automation run when I published?" +- "Can my automation wait for an approval before continuing?" + +## MCP tools + +With the `automations` group of the Contentstack MCP server connected, the agent can list and read projects and automations, and on request create projects, switch automations on or off, or run an automation that has an HTTP trigger (`trigger_automation`). Steps, connections and executions stay in the Automate UI. + +## Safety notes + +Advisory by default. Uses MCP tools only when asked, and names the automation and its effect before activating, running or deleting anything. Keeps credentials in Connected apps and recommends testing write or delete steps on a non-production stack. + +## Related + +- [Webhooks](cms-webhooks.md) · [Workflows](cms-workflows.md) · [Brand Kit Assistant](brand-kit-assistant.md) diff --git a/docs/skills/brand-kit-assistant.md b/docs/skills/brand-kit-assistant.md index 77e1d17..e240737 100644 --- a/docs/skills/brand-kit-assistant.md +++ b/docs/skills/brand-kit-assistant.md @@ -5,7 +5,7 @@ description: The entry point for Contentstack Brand Kit, concepts, setup, govern # Brand Kit Assistant -**Slug:** `brand-kit-assistant` · **Product:** Brand Kit · **Type:** Advisory (gated API actions) · **Ships references** +**Slug:** `brand-kit-assistant` · **Product:** Brand Kit · **Type:** Advisory (acts through MCP tools on request) · **Ships references** The primary entry point for Contentstack Brand Kit. It explains how Brand Kit, Voice Profiles, and Knowledge Vault fit together, guides setup and governance, routes API-specific tasks to the right capability, and enforces brand and safety rules before presenting anything as ready. @@ -18,8 +18,9 @@ When you ask about Brand Kit, Voice Profiles, Knowledge Vault, brand voice, tone - **Concepts**: a Brand Kit container, one or more Voice Profiles (how content should sound), and a Knowledge Vault (brand facts and source material) used to ground AI generation. - **Setup**: creating a Brand Kit from a website or file upload, and mapping content to a Voice Profile vs Knowledge Vault. - **On-brand generation**: use a Voice Profile for tone/style plus Knowledge Vault for factual grounding, then validate output against brand guidelines before publishing. +- **Kit versions**: v1 Voice Profiles use four `communication_style` dials (formality, tone, humor, complexity) on a 1–4 scale. v2 kits use free-form markdown profiles, and v1 voice-profile routes return `410 Gone` for them. - **Knowledge Vault behavior**: it's vector-based semantic storage. It does **not** return original files or PDFs; keep source documents in your DAM/document system. -- **API guidance**: Brand Kit Management API (Brand Kits, Voice Profiles), Knowledge Vault API (ingest, update, delete, search, chunk retrieval), Generative AI API (content generation). Respects the 10 req/s per-organization rate limit and region-specific base URLs. +- **API guidance**: Brand Kit Management API (Brand Kits, Voice Profiles), Knowledge Vault API (ingest, update, delete, search, chunk retrieval), Generative AI API (content generation). Backs off on `429` rate-limit responses and uses region-specific base URLs. ## References @@ -33,8 +34,13 @@ Ships scoped API references: `references/brand-kit-management-api-reference.md` - "How do I make AI-generated content stay on-brand?" - "Can I get the original files back from Knowledge Vault?" +## MCP tools + +With the `brandkit` group connected, the agent can read, create, update and delete Voice Profiles and Knowledge Vault content, retrieve Knowledge Vault chunks, and generate on-brand content (`generative_ai`). The voice profile tools use the v1 model; Brand Kits themselves, visual guidelines and v2 Documents are managed in the UI or the Management API. + ## Safety notes +Advisory by default; uses the MCP tools only when you ask for the change. Never exposes Brand Kit API tokens, authtokens, or decrypted credentials. Acknowledges secrets at a high level without echoing them. Requires explicit confirmation and a confirmed UID before destructive actions (delete, unpublish, overwrite). Validates generated content against brand guidelines before calling it ready and won't override brand rules without approval. Asks for missing identifiers, auth, region, or source content rather than guessing. ## Related diff --git a/docs/skills/cms-assets.md b/docs/skills/cms-assets.md index 5829875..61fc5ac 100644 --- a/docs/skills/cms-assets.md +++ b/docs/skills/cms-assets.md @@ -5,7 +5,7 @@ description: "Organize, deliver, and transform assets in Contentstack: folders, # Assets -**Slug:** `cms-assets` · **Product:** CMS · **Type:** Advisory (read-only) +**Slug:** `cms-assets` · **Product:** CMS · **Type:** Advisory (acts through MCP tools on request) Advises developers on organizing, delivering, and transforming assets in Contentstack: folder structure, Image Delivery API transformations, publishing lifecycle, CDN behavior, and common limits. @@ -16,10 +16,10 @@ When you ask about uploading, organizing, delivering, transforming, publishing, ## What it covers - **Folder organization**: clear structures like `/images/heroes`, `/images/products`, `/documents/legal`. Moving assets between folders preserves UIDs and references. -- **Image Delivery API**: `images.contentstack.io` with on-the-fly transforms via URL params (`width`, `height`, `quality`, `format`, `crop`, `trim`, `orient`, `overlay`, `pad`, `fit`, `dpr`). Chain multiple transforms in one URL; prefer WebP for modern browsers. Non-image assets serve from `assets.contentstack.io`. +- **Image Delivery API**: `images.contentstack.io` with on-the-fly transforms via URL params (`width`, `height`, `quality`, `format`, `auto`, `crop`, `trim`, `orient`, `overlay`, `pad`, `canvas`, `fit`, `dpr`, `blur`, `sharpen`, `brightness`, `contrast`, `saturation`). Chain multiple transforms in one URL; prefer WebP for modern browsers. Non-image assets serve from `assets.contentstack.io`. - **Publishing lifecycle**: assets publish independently of entries. An asset must be published to an environment before referenced entries display it live. Unpublishing removes it from delivery but doesn't delete the file. - **Replacing vs deleting**: replacing creates a new version but keeps the same UID and references; deleting breaks all references. Check references before deleting. -- **Key limits**: max file size 700 MB (UI) / 100 MB (API); max 10 assets per batch upload; default 10,000 assets per stack and 500,000 per organization; filenames can't change after upload. Image Delivery transforms don't apply to images inserted directly into Rich Text Editor fields. +- **Key limits**: max file size 700 MB (UI) / 100 MB (API); max 10 assets per batch upload; default 10,000 assets per stack; filenames can't change after upload. Image Delivery transforms don't apply to images inserted directly into Rich Text Editor fields. The Assets (DAM) product has its own limits (1.5 GB per asset, 100 per upload, 500,000 per workspace). ## Example prompts @@ -29,9 +29,13 @@ When you ask about uploading, organizing, delivering, transforming, publishing, - "Does deleting an asset break references in entries?" - "What is the maximum file size I can upload?" +## MCP tools + +With the Contentstack MCP server connected, the agent can list and inspect assets, folders, versions and references (`get_all_assets`, `get_asset_reference`, folder tools in `cma-extended`), check delivery with `get_all_assets_cdn`, publish or unpublish assets, manage folders and asset details, and delete assets. There is no upload tool; upload through the UI, the CMA or `csdx`. + ## Safety notes -Read-only advisory. Never uploads, deletes, publishes, or modifies assets. Delivery tokens and asset URLs are client-safe; never expose management tokens or API keys. Use environment variables for credentials in examples. +Advisory by default; uses the MCP tools only when you ask for the change. Before deleting, unpublishing or replacing an asset or deleting a folder, it names the asset, environments and referencing entries and waits for confirmation. Delivery tokens and asset URLs are client-safe; never expose management tokens or API keys. Use environment variables for credentials in examples. ## Related diff --git a/docs/skills/cms-branches-aliases.md b/docs/skills/cms-branches-aliases.md index ae6d83f..86ee298 100644 --- a/docs/skills/cms-branches-aliases.md +++ b/docs/skills/cms-branches-aliases.md @@ -5,7 +5,7 @@ description: Use Contentstack branches for isolated content development and alia # Branches & Aliases -**Slug:** `cms-branches-aliases` · **Product:** CMS · **Type:** Advisory (read-only) +**Slug:** `cms-branches-aliases` · **Product:** CMS · **Type:** Advisory (acts through MCP tools on request) Advises on using branches for isolated content/schema development and aliases for zero-downtime deployments: branch strategy, branch-specific vs global modules, CI/CD integration, merge behavior, and rollback. @@ -15,8 +15,9 @@ When you ask about branches, aliases, CI/CD integration with Contentstack, deplo ## What it covers -- **Branch basics**: branches copy the source branch's content types, global fields, entries, and assets into an isolated workspace. Max 5 branches per stack; one create/delete at a time across an org; only owners, admins, and developers can manage them. -- **Branch-specific vs global modules**: *branch-specific:* content types, global fields, entries, assets, publish queue, releases, languages, extensions, audit logs, labels, search. *Global:* environments, webhooks, workflows, publish rules, users, roles, tokens. +- **Branch basics**: a branch inherits the source branch's content types, global fields, entries, assets, languages, webhooks and extensions as an isolated workspace. The branch count per stack depends on your plan; only owners, admins, and developers can create and delete branches. +- **Branch-specific vs global modules**: *branch-specific:* content types, global fields, entries, assets, workflows, publish queue, releases, languages, extensions, audit logs, global search, labels. *Global:* taxonomy, environments, users, roles, webhooks, workflows, publish rules, tokens, Marketplace apps. A webhook can be assigned to one branch at a time; workflows, publish rules and tokens can be scoped to specific branches (tokens also to aliases). +- **Compare & merge**: content types and global fields only, via CMA or CLI. Strategies: `merge_prefer_base`, `merge_prefer_compare`, `overwrite_with_compare` (destructive), `merge_new_only`, `merge_modified_only_prefer_base`, `merge_modified_only_prefer_compare`, `ignore`. A backup branch is created first; the merge runs asynchronously. Entries and assets need scripted reconciliation. - **Alias-based deployment**: aliases point to branches. Hardcode an alias (e.g. `deploy`) in frontend code instead of a branch UID; switch production by reassigning the alias, and roll back by reassigning it back. Two aliases can point to one branch, but a branch and alias can't share a UID. - **CI/CD pattern**: branch from main → change schema/content → test on staging → reassign the production alias → reassign back instantly if something breaks. - **Branch strategy**: keep branches short-lived; prefer trunk-based workflow. Don't use branches as permanent environments (use environments for that). @@ -30,9 +31,13 @@ When you ask about branches, aliases, CI/CD integration with Contentstack, deplo - "How do branches work with my CI/CD pipeline?" - "Can I roll back a bad deployment?" +## MCP tools + +With the Contentstack MCP server connected, the agent can list branches and aliases, compare branches and specific content types or global fields, create and merge branches, poll merge jobs, assign aliases, and delete branches or aliases (all in the `cma` group). + ## Safety notes -Read-only advisory: does not create, delete, merge, or reassign branches or aliases. Use environment variables for credentials; keep guidance compatible with branch- and alias-scoped credentials. +Advisory by default; uses the MCP tools only when you ask for the change. Before creating, merging or deleting a branch, or assigning or deleting an alias, it names the branches, aliases and merge strategy, explains the impact, and waits for confirmation. Use environment variables for credentials; keep guidance compatible with branch- and alias-scoped credentials. ## Related diff --git a/docs/skills/cms-data-modeling-best-practices.md b/docs/skills/cms-data-modeling-best-practices.md index 1552527..9f26b0a 100644 --- a/docs/skills/cms-data-modeling-best-practices.md +++ b/docs/skills/cms-data-modeling-best-practices.md @@ -5,7 +5,7 @@ description: "Model Contentstack content with the simplest reusable structure: w # Data Modeling Best Practices -**Slug:** `cms-data-modeling-best-practices` · **Product:** CMS · **Type:** Advisory (read-only) +**Slug:** `cms-data-modeling-best-practices` · **Product:** CMS · **Type:** Advisory (acts through MCP tools on request) Guides developers to model content using the simplest reusable structure and avoid over-modeling, deep reference chains, and channel-specific schema sprawl. @@ -34,6 +34,7 @@ Core principles: - Treat content types as API contracts: avoid deep reference chains, oversized modular blocks, and hiding filterable facts inside rich text. - Localize only fields that need translation; keep names clear; avoid channel-specific schema pollution. - Explain tradeoffs and include migration cautions when schema changes are implied. +- Design within the limits: up to 500 fields per content type, plan limits on content types per stack and block types per modular blocks field, and taxonomies up to 10 levels deep. ## Example prompts @@ -43,9 +44,13 @@ Core principles: - "What's the best way to handle localization for shared content?" - "How do I model product categories for filtering and reuse?" +## MCP tools + +With the Contentstack MCP server connected, the agent can read the existing model (`get_all_content_types`, `get_a_single_content_type`, `get_all_references_of_ct`, global field tools) before reviewing it, and create, update or delete content types (`cma`) and global fields (`cma-extended`) when asked. + ## Safety notes -Read-only advisory. Never performs schema changes, publishing, or destructive actions. Uses placeholders and environment variables for any example credentials. +Advisory by default; uses the MCP tools only when you ask for the change, preferably on a development branch. Before any schema change it names the stack, branch and schema, explains the impact on existing content and delivery code, and waits for confirmation. Uses placeholders and environment variables for any example credentials. ## Related diff --git a/docs/skills/cms-entries.md b/docs/skills/cms-entries.md index ae272d8..00dcf9c 100644 --- a/docs/skills/cms-entries.md +++ b/docs/skills/cms-entries.md @@ -5,7 +5,7 @@ description: "Query, localize, version, publish, and structure Contentstack entr # Entries -**Slug:** `cms-entries` · **Product:** CMS · **Type:** Advisory (read-only) +**Slug:** `cms-entries` · **Product:** CMS · **Type:** Advisory (acts through MCP tools on request) Advises on querying, localizing, versioning, publishing, and structuring entries for efficient delivery, with a focus on CDA usage, reference expansion, pagination, bulk operations, and Sync API patterns. @@ -20,7 +20,9 @@ When you ask about fetching entries, building CDA queries, handling localization - **Reference expansion**: including referenced entries in a CDA response. - **Pagination & performance**: paging through all entries of a content type efficiently. - **Versioning & publishing**: how versioning works, and the difference between saving and publishing. -- **Localization**: locale-aware entry behavior. +- **Localization**: locale-aware entry behavior, including `include_fallback`. +- **Limits**: at most 100 entries per CDA request, up to 100 `include[]` paths, an 8 KB URL limit, bulk publish of up to 100 items, nested reference publishing with the `api_version: 3.2` header, and CMA limits of 10 requests per second (1 per second for bulk actions). +- **Sync API**: an initial full sync plus `sync_token` deltas for mirrors, static builds and search indexes. For SDK code specifically, this pairs with the [Delivery SDK](dx-delivery-sdk.md) skill. @@ -32,9 +34,13 @@ For SDK code specifically, this pairs with the [Delivery SDK](dx-delivery-sdk.md - "How do I include referenced entries in my CDA response?" - "How do I paginate through all entries of a content type?" +## MCP tools + +With the Contentstack MCP server connected, the agent can read entries, references and versions (`cma`, `cma-extended`), check what the live site receives with `get_all_entries_cdn`, and create, update, publish, unpublish, localize, unlocalize or delete entries when asked. There are no bulk publish or Sync API tools. + ## Safety notes -Read-only advisory. Never creates, updates, publishes, or deletes entries. Delivery tokens are client-safe; management tokens are not. Use environment variables for credentials in examples. +Advisory by default; uses the MCP tools only when you ask for the change. Before any write, publish, unpublish or delete it names the entries, locales and environments and waits for confirmation. Delivery tokens are client-safe; management tokens are not. Use environment variables for credentials in examples. ## Related diff --git a/docs/skills/cms-environments-publishing.md b/docs/skills/cms-environments-publishing.md index deafa7b..4381069 100644 --- a/docs/skills/cms-environments-publishing.md +++ b/docs/skills/cms-environments-publishing.md @@ -5,7 +5,7 @@ description: Configure environments, publish content, use delivery and preview t # Environments & Publishing -**Slug:** `cms-environments-publishing` · **Product:** CMS · **Type:** Advisory (read-only) +**Slug:** `cms-environments-publishing` · **Product:** CMS · **Type:** Advisory (acts through MCP tools on request) Advises on configuring environments, publishing content, using delivery and preview tokens, leveraging the Sync API, and understanding CDN and publish-queue behavior. @@ -15,13 +15,13 @@ When you ask about environment setup, publishing behavior, delivery or preview t ## What it covers -- **Environment design**: treat environments as deployment targets (development, staging, production). Default max 5 per stack. Environments are global modules, shared across branches. -- **Publishing fundamentals**: content is a draft until published to an environment; publishing can target multiple environments and locales. Use `api_version: 3.2` for nested reference publishing so the full reference tree resolves automatically. Always publish entries with their references. +- **Environment design**: treat environments as deployment targets (development, staging, production). Environments are global modules, shared across branches. +- **Publishing fundamentals**: content is a draft until published to an environment; publishing can target multiple environments and locales. For nested references, use bulk publish with the `api_version: 3.2` header, which publishes references up to five levels deep with their parents. Always publish entries with their references. - **Token types**: *Delivery Token:* published content via the CDA, environment-scoped, client-safe. *Preview Token:* unpublished content for Live Preview; client-side only on preview deployments, never in production builds. *Management Token:* stack-level read/write, server-side only, never in the browser. -- **SDK initialization**: stack API key + delivery token + environment, optionally branch/alias. CDA base URLs are region-specific (`cdn.contentstack.io` for AWS NA, `eu-cdn.contentstack.com` for AWS EU, `au-cdn.contentstack.com` for AWS AU). +- **SDK initialization**: stack API key + delivery token + environment, optionally branch/alias. CDA base URLs are region-specific (`cdn.contentstack.io` for AWS NA, `eu-cdn.contentstack.com` for AWS EU, `au-cdn.contentstack.com` for AWS AU, plus `azure-na-cdn`, `azure-eu-cdn`, `gcp-na-cdn` and `gcp-eu-cdn` on `.contentstack.com`). - **Sync API**: recommended for static sites, offline apps, or local caches. First request returns all published content plus a `sync_token`; later requests return only changes. Prefer it over polling. - **Publish queue**: tracks publish/unpublish status; each branch has its own queue; scheduled publishes can be cancelled before execution. -- **Rate limits**: 10 req/s individual, 1 req/s bulk per org. Use exponential backoff with jitter; the CLI bulk publish plugin handles rate limiting automatically. +- **Rate limits**: CMA 10 req/s and bulk 1 req/s per org; CDA reads are mostly CDN-served. Responses carry `X-RateLimit-Limit`/`X-RateLimit-Remaining`. Use exponential backoff with jitter; the CLI bulk publish plugin handles rate limiting automatically. ## Example prompts @@ -31,9 +31,13 @@ When you ask about environment setup, publishing behavior, delivery or preview t - "How does the Sync API work?" - "Can I schedule content to publish at a future time?" +## MCP tools + +With the Contentstack MCP server connected, the agent can inspect environments, the publish queue and publish rules, confirm what an environment serves with the `cda` tools, publish or unpublish entries and assets, and create, update or delete environments. There are no tools for tokens, the Sync API or cancelling a scheduled publish. + ## Safety notes -Read-only advisory. Never publishes, unpublishes, or modifies environments. Delivery tokens are client-safe; management tokens must never be exposed client-side. Use environment variables for all token examples. +Advisory by default; uses the MCP tools only when you ask for the change. Before publishing, unpublishing or changing an environment it names the environments, locales and items and waits for confirmation. Delivery tokens are client-safe; management tokens must never be exposed client-side. Use environment variables for all token examples. ## Related diff --git a/docs/skills/cms-graphql.md b/docs/skills/cms-graphql.md new file mode 100644 index 0000000..d12be74 --- /dev/null +++ b/docs/skills/cms-graphql.md @@ -0,0 +1,45 @@ +--- +title: GraphQL Content Delivery +description: "Query published Contentstack content with the GraphQL Content Delivery API: endpoints, schema, references, modular blocks, JSON RTE, limits, and Live Preview." +--- + +# GraphQL Content Delivery + +**Slug:** `cms-graphql` · **Product:** CMS · **Type:** Advisory (read-only) + +Helps you write and debug queries against the GraphQL Content Delivery API without running into its limits or losing data to partial errors. + +## When it triggers + +When you write or debug Contentstack GraphQL queries, see schema build errors or partial errors, hit GraphQL limits or 429s, paginate references, or connect a GraphQL fetch layer to Live Preview or Visual Editor. + +## What it covers + +- **Endpoints and headers**: regional hosts (`graphql.contentstack.com`, `eu-graphql…`, `au-graphql…`, Azure and GCP), `/stacks/{api_key}?environment=…`, `access_token`, optional `branch`. Queries only, no mutations. +- **Schema shape**: generated types per content type, `all_<uid>` collections, PascalCase naming rules and `SCHEMA_BUILD_ERROR`. +- **References**: `<field>Connection` with `totalCount`, `edges` and `node`; inline fragments for multi-type references. +- **Modular blocks, JSON RTE, assets**: unions with inline fragments, `embedded_itemsConnection`, `SysAsset` and image transforms. +- **Filtering and pagination**: `where` operators, `order_by` on `created_at`/`updated_at`, `skip`/`limit` (max 100). +- **Limits**: 3 levels of nested references, 7,500 records per query, 100 per reference connection, 7 MB responses, one operation per request. +- **Rate limits and errors**: per-organization origin limit, `X-RateLimit-*` headers, and checking `errors` even on HTTP 200. +- **Live Preview**: preview hosts, `live_preview` and `preview_token` headers, no caching, normalizing connection wrappers for edit tags. + +## Example prompts + +- "How do I query referenced entries with GraphQL in Contentstack?" +- "My GraphQL response is 200 but some data is missing." +- "How do I query modular blocks and JSON RTE embedded entries?" +- "What are the GraphQL query limits?" +- "How do I make Live Preview work with my GraphQL client?" + +## MCP tools + +The Contentstack MCP server has no GraphQL tool. To check the underlying data, the agent can use the read-only `cda` tools (`get_all_entries_cdn`, `get_a_single_entry_cdn`, `get_all_assets_cdn`) and `get_a_single_content_type` to check UIDs against GraphQL naming rules. + +## Safety notes + +Read-only advisory. Uses delivery tokens only; management tokens never belong in GraphQL calls or client code. Preview tokens stay on preview deployments. + +## Related + +- [Entries](cms-entries.md) · [Delivery SDK](dx-delivery-sdk.md) · [Live Preview & Visual Editor](cms-live-preview-visual-editor-support-assistant.md) diff --git a/docs/skills/cms-json-rte.md b/docs/skills/cms-json-rte.md new file mode 100644 index 0000000..f0e452a --- /dev/null +++ b/docs/skills/cms-json-rte.md @@ -0,0 +1,47 @@ +--- +title: JSON Rich Text Editor +description: "Render and convert Contentstack JSON RTE content: document structure, jsonToHTML and render options, embedded entries and assets, GraphQL, React, the JSON RTE serializer, Live Preview tags and XSS-safe rendering." +--- + +# JSON Rich Text Editor + +**Slug:** `cms-json-rte` · **Product:** CMS · **Type:** Advisory (acts through MCP tools on request) · **Ships references** + +Helps you turn JSON RTE documents into HTML, Markdown or components, resolve embedded entries and assets, and convert content between HTML and JSON without opening an XSS hole. + +## When it triggers + +When you render, serialize or transform JSON RTE content, write a custom `renderOption`, debug embedded items that render empty, render JSON RTE from GraphQL, or migrate an HTML RTE field to JSON RTE. + +## What it covers + +- **Document structure**: `doc` root, block nodes with `uid`, `type`, `attrs` and `children`, text leaves with marks, `reference` nodes for embedded entries and assets, and the empty-field check. +- **Rendering**: `jsonToHTML` from `@contentstack/utils` (also `contentstack.Utils` in the Delivery SDK), field `paths`, and the fact that it replaces the JSON with HTML in place. +- **Render options**: overrides per node type, mark, display type (`block`, `inline`, `link`, `display`, `download`) and content type, plus a `reference` fallback. +- **Embedded items**: `includeEmbeddedItems()` (`include_embedded_items[]=BASE`), `_embedded_items`, why items render empty, and the limits (100 embedded entries, 15 referenceable content types, 30 KB per field by default). +- **GraphQL**: `json` plus `embedded_itemsConnection` with `system` UIDs, `GQL.jsonToHTML` and `updateAssetURLForGQL`. +- **React**: an HTML string vs a component tree, with a sample renderer. +- **Escaping**: the default sanitizer's limits (it doesn't check URL schemes) and escaping in custom render functions. +- **Conversion**: `@contentstack/json-rte-serializer` (`jsonToHtml`, `htmlToJson`, `jsonToMarkdown`) and the `cli-cm-migrate-rte` CLI plugin. +- **Live Preview**: `addEditableTags` and tagging the RTE wrapper element. + +## Example prompts + +- "How do I render a JSON RTE field to HTML with the TypeScript SDK?" +- "My embedded entries don't show up in the rendered rich text." +- "How do I render embedded products differently from other entries in the RTE?" +- "How do I render JSON RTE from a GraphQL response?" +- "How do I convert HTML into JSON RTE for an import?" +- "Should I use dangerouslySetInnerHTML or map JSON RTE nodes to React components?" + +## MCP tools + +Rendering runs in your code, so there are no rendering tools. To see the delivered JSON, the agent can use `get_a_single_entry_cdn` (`cda`) or `get_single_entry` (`cma`), and `get_a_single_content_type` (`cma`) to find JSON RTE field paths. `update_an_entry` (`cma`) can write converted JSON back, after confirmation. + +## Safety notes + +Advisory by default. Treats RTE content as untrusted: custom render functions escape what they interpolate, and URLs are checked before going into `href` or `src`. Writing converted content back to entries, or running the HTML to JSON migration, needs a named target, a backup or non-production branch, and explicit confirmation. + +## Related + +- [Entries](cms-entries.md) · [Delivery SDK](dx-delivery-sdk.md) · [GraphQL Content Delivery](cms-graphql.md) · [Live Preview & Visual Editor](cms-live-preview-visual-editor-support-assistant.md) · [Developer Hub App Architect](developer-hub-app-architect.md) diff --git a/docs/skills/cms-live-preview-visual-editor-support-assistant.md b/docs/skills/cms-live-preview-visual-editor-support-assistant.md index cd71dc2..e63609a 100644 --- a/docs/skills/cms-live-preview-visual-editor-support-assistant.md +++ b/docs/skills/cms-live-preview-visual-editor-support-assistant.md @@ -41,6 +41,10 @@ The skill stops once one contract is localised and adds no fallbacks while diagn - "My preview iframe is blank. What should I check?" - "How do preview tokens and live preview hashes work?" +## MCP tools + +No MCP tool reads the Visual Experience settings or the Onboarding Check. With the Contentstack MCP server connected, the agent uses read tools to gather evidence: environments and base URLs (`get_all_environments`), draft versus published entries (`get_single_entry` against `get_a_single_entry_cdn`), locales, branches and entry variants, and Launch deployment and server logs for sites hosted on Launch. + ## Safety notes Never makes changes automatically. Provides safe diagnostics and requires confirmation before any config change. Never asks for, echoes or reconstructs preview tokens or live preview hashes, and never asks for cookies or auth headers. Delivery tokens are safe in the browser; preview tokens read unpublished content, so they are exposed client-side only on preview deployments (CSR preview needs them), never in production builds; management tokens never reach the browser. Never recommends enabling Live Preview in production builds. If a secret appears in input, advises rotating it. diff --git a/docs/skills/cms-localization.md b/docs/skills/cms-localization.md index 40ea0cd..adeb44e 100644 --- a/docs/skills/cms-localization.md +++ b/docs/skills/cms-localization.md @@ -5,7 +5,7 @@ description: "Design multilingual delivery in Contentstack: language setup, fall # Localization -**Slug:** `cms-localization` · **Product:** CMS · **Type:** Advisory (read-only) +**Slug:** `cms-localization` · **Product:** CMS · **Type:** Advisory (acts through MCP tools on request) Advises on Contentstack localization with correct fallback behavior, efficient editorial workflows, and minimal redundancy. The skill distinguishes CMS editorial behavior from CDA delivery behavior, and clarifies which you mean before answering. @@ -17,7 +17,7 @@ When you ask about languages, fallback chains, localizing entries, non-localizab - **Master language**: permanent, set at stack creation, and ends every fallback chain. - **Fallback chain design**: each language can have one fallback; inheritance is a chain (e.g. `fr-ca → fr-fr → en-us`). Changing fallback relationships later affects existing content inheritance. -- **Localized vs unlocalized entries**: a localized entry is an independent copy with its own version history, publishing status, and workflow state; an unlocalized entry inherits from its fallback chain. Localizing is one-way per locale and entry. +- **Localized vs unlocalized entries**: a localized entry is an independent copy with its own version history, publishing status, and workflow state; an unlocalized entry inherits from its fallback chain. Localizing is reversible: Unlocalize drops the locale's content and the entry inherits again from its fallback (or master). Unlocalized and deleted entries can be restored from Trash within 14 days. - **Non-localizable fields**: mark structural/shared data non-localizable (SKUs, prices, dates, coordinates, boolean flags, shared assets, identifiers); keep human-readable text localizable. - **CDA locale queries**: pass `locale` explicitly; use `include_fallback=true` to apply the full fallback chain (without it, only the exact locale is checked). - **Multi-locale publishing**: editors can publish multiple locale versions from the master-language entry (subject to plan limits). Only the latest version of each localized entry is published; localized versions can be deleted only from the master entry's delete modal. @@ -30,9 +30,13 @@ When you ask about languages, fallback chains, localizing entries, non-localizab - "How do I publish content in multiple languages at once?" - "What happens if a locale has no content?" +## MCP tools + +With the Contentstack MCP server connected, the agent can list languages, check what each locale serves with `get_all_entries_cdn` (`locale`, `include_fallback`), and localize, unlocalize or publish entries in several locales. There are no tools to add languages or change fallback chains. + ## Safety notes -Read-only advisory. Requires explicit confirmation before any destructive action and never auto-executes delete/overwrite/publish. Use environment variables for all credentials. +Advisory by default; uses the MCP tools only when you ask for the change. Before localizing, unlocalizing or publishing it lists the entry, locales and environments and waits for explicit confirmation, and it never auto-executes delete, overwrite or publish. Use environment variables for all credentials. ## Related diff --git a/docs/skills/cms-releases.md b/docs/skills/cms-releases.md index cd204f5..7f40500 100644 --- a/docs/skills/cms-releases.md +++ b/docs/skills/cms-releases.md @@ -5,7 +5,7 @@ description: "Use Contentstack Releases for coordinated, atomic content deployme # Releases -**Slug:** `cms-releases` · **Product:** CMS · **Type:** Advisory (read-only) +**Slug:** `cms-releases` · **Product:** CMS · **Type:** Advisory (acts through MCP tools on request) Advises on using Releases to deploy related content changes atomically so campaigns, redesigns, or coordinated updates go live together without manual coordination errors. @@ -16,8 +16,9 @@ When you ask about deploying multiple content changes together, campaign launche ## What it covers - **Release workflow**: create a release with a descriptive name, add all related entries and assets (across content types and locales), deploy to staging first, validate, then deploy to production. Specify which version of each entry to deploy when needed. -- **Webhook storm**: a release deployment triggers one webhook event per item. Debounce receivers and inspect the `source` key in payloads. See [Webhooks](cms-webhooks.md) for receiver handling. -- **Limitations**: max 100 items per API call when adding items; release titles max 50 characters; updating items to latest versions does not auto-add new references from updated entries; releases are branch-specific. +- **Webhook bursts**: deploying a release publishes each item, so per-item publish webhooks can fire. Debounce or batch receivers and check execution logs. See [Webhooks](cms-webhooks.md) for receiver handling. +- **Limitations**: max 25 items per request when adding items; updating items to latest versions does not auto-add new references from updated entries; releases are branch-specific. +- **Releases v2** (plan-gated): multi-locale bulk add, clone, and update-all-items-to-latest; nested references can be included when adding items. - **CI/CD integration**: create a release → add changed items → deploy to staging → run tests → deploy to production on success. Combine with branches and aliases. - **When not to use**: not for routine single-entry publishes; use releases when coordinated multi-item deployment is required. @@ -29,9 +30,13 @@ When you ask about deploying multiple content changes together, campaign launche - "How do I integrate releases with my CI/CD pipeline?" - "What are the limits on releases?" +## MCP tools + +With the Contentstack MCP server connected, the agent can list releases and their items, create and clone releases, add items (25 per request), remove items, and deploy a release. There is no tool to delete a whole release. + ## Safety notes -Read-only advisory. Never creates, deploys, updates, or deletes releases. Use environment variables for credentials; no client-side access to management credentials. +Advisory by default; uses the MCP tools only when you ask for the change. Before creating, changing or deploying a release it names the release, branch, environments, locales and item count and waits for confirmation. Use environment variables for credentials; no client-side access to management credentials. ## Related diff --git a/docs/skills/cms-roles-permissions.md b/docs/skills/cms-roles-permissions.md index b8b81e7..e970b92 100644 --- a/docs/skills/cms-roles-permissions.md +++ b/docs/skills/cms-roles-permissions.md @@ -15,11 +15,11 @@ When you ask about user permissions, role design, team management, token capabil ## What it covers -- **Built-in roles**: Owner, Admin, Developer, Content Manager. Use them when they fit; otherwise create a custom role. +- **Built-in roles**: Owner, Admin, Developer, Content Manager (some orgs have extra system roles, such as read-only; check the stack's Roles list). Use them when they fit; otherwise create a custom role. - **Custom roles**: limit access by content type, environment, locale, branch, or action. Describe permissions by module, content type, and action; use `$all` for all instances when applicable. -- **Permission merging**: multiple roles combine allowed actions permissively, but explicit denials override grants. Watch overlap risks when roles are reused across teams/stacks. +- **Permission merging**: effective permissions are the union of all applicable roles (stack, team, org). There is no deny rule, so remove a role rather than trying to override it. Watch overlap risks when roles are reused across teams/stacks. - **Teams**: recommended for shared access across users and stacks; team membership maps users to stack roles and scales better than assigning roles individually. -- **Token capabilities**: choose the least-privileged token (management tokens, authtokens, OAuth) with SSO/org-owner edge cases noted. +- **Token capabilities**: choose the least-privileged credential: management tokens (at most 10 valid per stack), authtokens, OAuth, or machine-to-machine (M2M) apps using the client-credentials grant for automation. SSO/org-owner edge cases noted. - **Rate limits**: check `X-RateLimit-Remaining` and use backoff for automation/bulk operations. ## Example prompts @@ -30,9 +30,13 @@ When you ask about user permissions, role design, team management, token capabil - "How do I set up teams for multiple stacks?" - "What happens when a user has multiple roles?" +## MCP tools + +No Contentstack MCP tool manages roles, users, teams or tokens, so changes are made in the UI or through the CMA. With the `cma-extended` group connected, the agent can read the stack audit log (`get_audit_log`, `get_audit_log_item`) to review access changes. + ## Safety notes -Read-only advisory. Never creates, modifies, or deletes roles, users, teams, or tokens. Never recommends management tokens in client-side code; prefers server-side proxies for privileged CMA access. Delivery tokens are client-safe for read-only delivery only. +Read-only advisory. Never creates, modifies, or deletes roles, users, teams, or tokens; gives the steps and target for you to apply. Never recommends management tokens in client-side code; prefers server-side proxies for privileged CMA access. Delivery tokens are client-safe for read-only delivery only. ## Related diff --git a/docs/skills/cms-taxonomy.md b/docs/skills/cms-taxonomy.md index bfb79ff..1e40d29 100644 --- a/docs/skills/cms-taxonomy.md +++ b/docs/skills/cms-taxonomy.md @@ -5,7 +5,7 @@ description: "Use Contentstack Taxonomy for hierarchical content classification # Taxonomy -**Slug:** `cms-taxonomy` · **Product:** CMS · **Type:** Advisory (read-only) +**Slug:** `cms-taxonomy` · **Product:** CMS · **Type:** Advisory (acts through MCP tools on request) Advises on using Taxonomy for structured, hierarchical classification and delivery-side filtering, and on when taxonomy is the right mechanism versus tags, labels, or references. @@ -17,9 +17,10 @@ When you need help classifying content, designing category hierarchies, choosing - **Use taxonomy when**: you need structured, hierarchical classification queryable on the delivery side (product categories, geographic regions, content topics, document types). - **Don't use taxonomy for**: freeform labels (use tags), internal CMS organization (use labels), or rich category pages with their own model (use a Category content type with references). -- **Hierarchy design**: plan before creating; keep it practical (usually 3–4 levels). Terms are parent-child ordered. Moving a term with children is blocked unless the force flag is used. -- **CDA filtering operators**: `$taxonomy_exists` (any term), `$taxonomy_equal` (specific term), `$taxonomy_below` (a term and descendants), `$taxonomy_above` (ancestors). -- **Localization & import/export**: localization is plan-dependent; localized terms share the master term's UID. Import/export supports JSON and CSV; invalid CSV rows are skipped while valid rows process. +- **Hierarchy design**: plan before creating; terms can nest up to 10 levels, but keep the tree as shallow as the use case allows. Terms are parent-child ordered; the move API's `force` flag cascades the move to child terms. +- **Limits** (plan-dependent defaults): 20 taxonomies per stack, 10,000 terms per taxonomy, 20 taxonomies per content type. Taxonomy is a global module shared across branches. +- **CDA filtering**: `query` on `/taxonomies/entries` or `/content_types/{uid}/entries`. Exact term `{"taxonomies.material": "wood"}`; `$in`, `$exists`, `$and`, `$or`; hierarchy operators `$eq_below`, `$below`, `$eq_above`, `$above` with optional `levels` (default 10). +- **Localization & import/export**: localization is plan-dependent; localized terms share the master term's UID. Import/export supports JSON and CSV; a localized import needs the master-locale taxonomy and terms to exist first. ## Example prompts @@ -29,9 +30,13 @@ When you need help classifying content, designing category hierarchies, choosing - "Can I localize taxonomy terms?" - "Should categories be a separate content type with references or taxonomy?" +## MCP tools + +With the Contentstack MCP server connected, the `cma` group reads taxonomies and terms (`get_all_taxonomies`, `get_all_terms`, ancestor and descendant lookups, `export_a_taxonomy`) and can create, update, move and delete them. There is no import tool; use the CMA or `csdx`. `get_all_entries_cdn` (`cda`) tests a taxonomy filter. + ## Safety notes -Read-only advisory. Never creates, modifies, moves, deletes, or imports taxonomy data. Use environment variables for credentials in examples. +Advisory by default. Changes taxonomies or terms through MCP only when you ask, after naming the taxonomy and terms and getting confirmation. Taxonomy is global, so changes reach every branch. Use environment variables for credentials in examples. ## Related diff --git a/docs/skills/cms-tokens-authentication.md b/docs/skills/cms-tokens-authentication.md index a64485d..468e74d 100644 --- a/docs/skills/cms-tokens-authentication.md +++ b/docs/skills/cms-tokens-authentication.md @@ -24,11 +24,13 @@ When you ask about authentication, token types, API keys, rate limits, SSO integ | Backend automation, CI/CD, migration scripts | Management token | | Interactive user sessions | Authtoken (only when appropriate) | | Third-party apps | OAuth with scoped access | +| Service-to-service automation | Machine-to-machine (M2M) app with OAuth client credentials | Plus: -- **Key limits**: management tokens are stack-level and limited per stack; authtokens are user-specific with their own limits. Watch rate-limit headers and 429s. -- **SSO organizations**: SSO can restrict authtoken usage; for automation in SSO orgs, prefer management tokens or OAuth. +- **Key limits**: a stack can have at most 10 valid management tokens at a time, and each can expire on a date or never. Authtokens are user-specific with their own limits. Watch the `x-ratelimit-limit` and `x-ratelimit-remaining` headers and 429s. +- **Preview tokens**: a separate credential that only the Preview Service accepts, used together with the short-lived `live_preview` hash. +- **SSO organizations**: SSO blocks password login, so scripts can't get authtokens that way. For automation in SSO orgs, use management tokens, OAuth or an M2M app. - **Security rules**. Never expose management tokens or authtokens client-side; use environment variables; rotate credentials; least privilege. - **Rate-limit handling**: exponential backoff with jitter for 429s; check rate-limit headers before retrying. @@ -40,6 +42,10 @@ Plus: - "Can I use authtokens in an SSO-enabled organization?" - "How many management tokens can I have per stack?" +## MCP tools + +No Contentstack MCP tool creates, rotates or deletes tokens; manage them in stack Settings → Tokens and M2M apps in Developer Hub. The skill explains how the MCP server itself authenticates (OAuth, or a management token for the `cma` groups) and can read an app's OAuth scopes with `get_oauth_scopes` and `get_app_oauth` (`developerhub`). + ## Safety notes Read-only advisory. Never creates, modifies, or deletes tokens. Never displays real secrets or example real tokens; always states whether a credential is client-safe or server-side only. Uses placeholders like `CONTENTSTACK_API_KEY`, `CONTENTSTACK_DELIVERY_TOKEN`, `CONTENTSTACK_MANAGEMENT_TOKEN`. diff --git a/docs/skills/cms-variants-personalization.md b/docs/skills/cms-variants-personalization.md index 2fa3663..794b4a0 100644 --- a/docs/skills/cms-variants-personalization.md +++ b/docs/skills/cms-variants-personalization.md @@ -5,7 +5,7 @@ description: "Use Contentstack Variants and Personalize for audience-targeted co # Variants & Personalization -**Slug:** `cms-variants-personalization` · **Product:** CMS · **Type:** Advisory (read-only) +**Slug:** `cms-variants-personalization` · **Product:** CMS · **Type:** Advisory (acts through MCP tools on request) Advises on delivering different content to different audiences without duplicating entire entries, and on integrating Personalize with the CMS. @@ -16,8 +16,10 @@ When you ask about content personalization, A/B testing, audience segmentation, ## What it covers - **Variants vs separate entries**: if ~80%+ of the content is shared and only headlines, images, or CTAs differ, use variants (lightweight overrides). If each audience needs a different page structure, references, and layout, use separate entries. Start with variants; split only when content diverges significantly. -- **Variant groups**: how variants are organized. -- **Personalize integration**: initialize the Contentstack SDK with stack credentials and the Personalize SDK with your project key. On each request, resolve the active experience from audience rules and pass the variant context when fetching entries. For SSR, use the Personalize Edge API to resolve variants at the CDN for best performance. +- **Variant groups**: one per Personalize experience, created automatically; one variant per audience or A/B arm; entry variants hold field overrides only. A group must be linked to a content type before editors can create entry variants. +- **Fetching variants**: pass variant aliases (`cs_personalize_{experienceShortUid}_{variantShortUid}`) in the CDA `x-cs-variant-uid` header. A comma-separated list layers variants and the leftmost wins on conflict. The CMA accepts only one variant UID. +- **Personalize integration**: resolve aliases with `@contentstack/personalize-edge-sdk` (`init`, `set`, `setUserId`, `getVariantAliases`, `triggerImpression`, `triggerEvent`), usually in edge middleware. +- **Experiences and limits**: segmented vs A/B (with a multi-armed bandit option), experience priority for field conflicts, and default project limits of 100 experiences, 20 variants per experience and 100 audiences. - **Start simple**: emphasizes maintainability; add complexity only when justified. ## Example prompts @@ -28,9 +30,13 @@ When you ask about content personalization, A/B testing, audience segmentation, - "What are variant groups?" - "Can I A/B test content in Contentstack?" +## MCP tools + +With the `cma` group, the agent can list, create, publish and delete entry variants (`get_all_variants_of_an_entry`, `create_an_entry_variant`, `publish_variants_of_an_entry`, `delete_an_entry_variant`). With the `personalize` group, it can read and manage experiences and their priority (`get_all_experiences`, `update_experience`, `update_experiences_priority`). Linking a variant group to a content type is done in the CMS. + ## Safety notes -Read-only advisory. Never creates, modifies, or publishes variants. Use environment variables for credentials; never hardcode stack API keys or Personalize project keys. +Advisory by default. Uses the MCP tools only when you ask for the change, and names the stack, entries, environments and experience before creating, publishing or deleting anything. Use environment variables for credentials; never hardcode stack API keys or Personalize project keys. ## Related diff --git a/docs/skills/cms-webhooks.md b/docs/skills/cms-webhooks.md index 426c280..5f0ed90 100644 --- a/docs/skills/cms-webhooks.md +++ b/docs/skills/cms-webhooks.md @@ -15,13 +15,13 @@ When you need help setting up webhooks, choosing event channels, handling payloa ## What it covers -- **Channels**: `{module}.{action}` form, e.g. `entries.create`, `entries.publish`, `assets.publish`, `content_types.update`, `entries.workflow_stage_change`. Start specific; use `$all` only when truly needed. -- **Payloads**: include module, `api_key`, event, `triggered_at`, data, and branch info. Use `concise_payload: true` when the receiver only needs identifiers. -- **Signature verification**: always validate the `X-Contentstack-Signature` header in production (HMAC-SHA256 over the raw body using the webhook secret). Reject missing/invalid signatures. -- **Release-webhook storm**: a release with many items can produce one event per item. Detect release-triggered events via the `source` key and debounce/batch so downstream systems trigger once per release. +- **Channels**: `{module}.{action}` form (e.g. `entries.create`, `assets.publish`, `content_types.update`) or `{module}.{context}.{action}` (e.g. `content_types.entries.publish`). Workflow stage events come from a separate workflow module; pick them in the webhook UI. Start specific; use `$all` only when truly needed. +- **Payloads**: top-level `module`, `api_key`, `event` and `data`. Use `concise_payload: true` when the receiver only needs identifiers. +- **Signature verification**: every request carries `X-Contentstack-Request-Signature` (RSA-PSS SHA-256), verified with Contentstack's public key. With HMAC signing enabled (plan feature), requests carry `X-Contentstack-HMAC-Signature`, an HMAC-SHA256 of `{timestamp}.{raw body}` with the webhook secret. Reject missing/invalid signatures and stale timestamps. +- **Release-triggered bursts**: deploying a release publishes each item, so publish webhooks can fire per item. Debounce or batch on the receiver so downstream systems trigger once per burst; check execution logs for what fired. - **Reliable receiver design**: validate signatures, return 2xx within 30 seconds, process asynchronously after acknowledgment, and handle duplicate deliveries idempotently. -- **Retry policies**: manual retries are logged for re-execution; auto retries use exponential backoff. Use execution logs for attempts, status codes, and response bodies. -- **Limits & scope**: max 100 webhooks per stack; webhooks are global but can be scoped to a branch; an org-level setting caps max connections per second. +- **Retry policies**: automatic retries up to 4 times with exponential backoff (about 5 s, 25 s, 125 s, 625 s); manual failures stay in the execution log to re-fire. Use execution logs for attempts, status codes, and response bodies. +- **Scope & concurrency**: webhooks are global but each can be assigned to one branch at a time; the org-level Webhook Configuration setting caps concurrent connections. ## Example prompts @@ -31,9 +31,13 @@ When you need help setting up webhooks, choosing event channels, handling payloa - "What retry policy should I use?" - "How do I debug missed webhook deliveries?" +## MCP tools + +No Contentstack MCP tool manages webhooks, so they are configured in stack Settings → Webhooks or through the CMA. The agent can use `get_deploy_hooks` (`launch`) to find a Launch deploy hook to target, and `get_all_items_in_a_release` (`cma`) to size a release burst. + ## Safety notes -Read-only advisory. Never creates, updates, deletes, or sends webhooks. Always validate `X-Contentstack-Signature` in production; never echo raw secrets, tokens, or signatures; keep secrets in environment variables and verify server-side. +Read-only advisory. Never creates, updates, deletes, or sends webhooks. Always verify the request signature in production; never echo raw secrets, tokens, or signatures; keep secrets in environment variables and verify server-side. ## Related diff --git a/docs/skills/cms-workflows.md b/docs/skills/cms-workflows.md index b04269f..9b6cc49 100644 --- a/docs/skills/cms-workflows.md +++ b/docs/skills/cms-workflows.md @@ -17,9 +17,9 @@ When you ask about workflow stage design, approval processes, publish rules, sel - **Keep workflows simple**: 3–5 stages for most cases; add stages only for a clear business need. A content type can have one workflow per branch. - **Stage design**: each stage defines who can advance entries, allowed next stages, and optional due dates/assignments. Max 20 stages; every transition is recorded in the audit log. -- **Prevent self-advancement**: requires at least two distinct reviewers; the last editor can't advance the entry. Use at least two approvers or a role with multiple members. +- **Prevent self-advancement**: an option turned on per stage, not a default. When on, the submitter can't also approve, so at least two distinct reviewers are needed. - **Publish rules are separate**: they govern publish/unpublish conditions independently of workflows, with scope fields (branch, content type, language, environment, action) and conditions (required stage, approver, prevent self-approval). -- **Automation**: use webhook events like `entries.workflow_stage_change`; the publish queue tracks pending actions and status. +- **Automation**: webhooks on workflow stage events (a separate workflow module; pick them in the webhook UI); the publish queue tracks pending actions and status. - **Permission limitations**: only owners, admins, and developers can create workflows and publish rules. Management tokens can't change workflow stages or configure rules requiring user approval; user-scoped auth or OAuth tokens are needed for programmatic transitions. ## Example prompts @@ -30,6 +30,10 @@ When you ask about workflow stage design, approval processes, publish rules, sel - "Can I automate actions when content reaches a workflow stage?" - "How do workflows work across branches?" +## MCP tools + +With the `cma-extended` group, the agent can read workflows, publish rules, publish queue activity and the audit log (`get_all_workflows`, `get_a_single_workflow`, `get_publish_rules`, `get_publish_queue_activity`, `get_audit_log`). No MCP tool changes workflows, stages or publish rules; those changes happen in the UI or the CMA. + ## Safety notes Read-only advisory. Never creates, updates, publishes, unpublishes, or deletes workflows or publish rules. Use environment variables for credentials; route CMA examples through server-side proxies. diff --git a/docs/skills/developer-hub-app-architect.md b/docs/skills/developer-hub-app-architect.md index 286a807..72de921 100644 --- a/docs/skills/developer-hub-app-architect.md +++ b/docs/skills/developer-hub-app-architect.md @@ -5,7 +5,7 @@ description: "Turn Contentstack Developer Hub and Marketplace app ideas into con # Developer Hub App Architect -**Slug:** `developer-hub-app-architect` · **Product:** Developer Hub · **Type:** Advisory (read-only) · **Ships references** +**Slug:** `developer-hub-app-architect` · **Product:** Developer Hub · **Type:** Advisory (acts through MCP tools on request) · **Ships references** Turns Developer Hub and Marketplace app ideas into concrete implementations: it chooses the right UI location, maps the architecture, generates boilerplate-aligned React/TypeScript code, and troubleshoots setup, SDK, manifest, proxy, and publishing issues. @@ -20,7 +20,8 @@ When designing or building a Contentstack Developer Hub or Marketplace app, choo ## What it covers -- **Location selection**: always identifies the best Contentstack UI location before writing code; prefers the smallest location that fits; compares candidates by user context, SDK surface, and complexity. +- **App type and target**: standard apps vs machine-to-machine apps (client credentials, organization-only, no UI), and stack vs organization targets. +- **Location selection**: covers all ten UI locations, including Content Type Sidebar and Global Full Page; always identifies the best Contentstack UI location before writing code; prefers the smallest location that fits; compares candidates by user context, SDK surface, and complexity. - **Developer Hub setup**: manifest, base URL, route mapping, location enabling, advanced settings variables, proxy/rewrite, install, and deploy steps. - **SDK & integration**: `@contentstack/app-sdk` patterns for location access, config read/write, field updates, asset replacement, iframe sizing, and typed guards; proxy/API rewrites for external integrations with secrets kept server-side. - **Troubleshooting**: checks route, manifest, location, SDK init, iframe context, config persistence, proxy behavior, and install scope, in that order, leading with the most likely cause. @@ -38,9 +39,13 @@ Ships a Developer Hub coding reference (`references/developer-hub-coding-referen - "Why is my app loading but `appSdk.location.CustomField` is undefined?" - "Turn this app idea into manifest config, route plan, and starter code." +## MCP tools + +With the MCP server's `developerhub` group connected, the agent can list, create, update, install and uninstall apps and manage their OAuth settings (`get_all_apps`, `create_an_app`, `install_an_app`, `update_app_oauth` and others). Hosting and Marketplace publishing stay in the Developer Hub UI. + ## Safety notes -Never exposes OAuth client secrets or app signing keys; keeps app credentials server-side via proxy variables or secure env vars. Validates installation scopes and never grants broader permissions than required. For publish/delete/revoke/uninstall flows, explains the impact first and requires confirmation of the target app/environment/installation. +Advisory by default; uses the MCP tools only when you ask. Never exposes OAuth client secrets or app signing keys; keeps app credentials server-side via proxy variables or secure env vars. Validates installation scopes and never grants broader permissions than required. Before creating, installing, uninstalling or deleting an app, or changing its OAuth settings, names the app and target and waits for confirmation. ## Related diff --git a/docs/skills/dx-cli.md b/docs/skills/dx-cli.md new file mode 100644 index 0000000..f7b77c5 --- /dev/null +++ b/docs/skills/dx-cli.md @@ -0,0 +1,43 @@ +--- +title: Contentstack CLI +description: "Use the Contentstack CLI (csdx): region and login, token aliases, export/import/seed/clone, bulk operations, migration scripts, branch merges, and plugins such as tsgen." +--- + +# Contentstack CLI + +**Slug:** `dx-cli` · **Product:** Developer Experience · **Type:** Advisory (acts through MCP tools on request) + +Helps you pick and run the right `csdx` command against the right region, stack and branch, and avoid overwriting content by accident. + +## When it triggers + +When you ask which `csdx` command to run, how to authenticate the CLI, how to move content between stacks or branches, bulk publish, write migration scripts, generate TypeScript types, upgrade from CLI 1.x, or troubleshoot CLI errors. + +## What it covers + +- **Setup**: `npm install -g @contentstack/cli` (Node.js 22+), `csdx config:set:region`, `csdx auth:login` (`--oauth` for SSO), token aliases with `csdx auth:tokens:add`. +- **Moving content**: `cm:stacks:export`, `cm:stacks:import` (modules, `--replace-existing`, skip-publish flags), `cm:stacks:seed`, `cm:stacks:clone`, and `cm:stacks:audit`. +- **Bulk operations**: `cm:stacks:bulk-entries`, `bulk-assets` and `bulk-taxonomies` replace the 1.x bulk-publish commands, with cross-publish, retry and revert. +- **Migration scripts**: `cm:stacks:migration` for scripted content type and entry changes. +- **Branches**: `cm:branches:diff`, `cm:branches:merge` (revert branch by default), `cm:branches:merge-status`. +- **Plugins**: `tsgen` for TypeScript types, `apps-cli` for Developer Hub apps, `cli-launch` for Launch, and more. See the bundled command list. + +## Example prompts + +- "How do I log in to the CLI with SSO?" +- "Export one stack and import it into another." +- "Bulk publish all blog entries to production." +- "Merge my feature branch content types into main." +- "Generate TypeScript types for my content types." + +## MCP tools + +The CLI handles bulk and stack-wide jobs; MCP has no export, import, seed, clone, bulk publish or migration-script tools. With the `cma` group connected, the agent can compare branches, check merge jobs and publish single entries (`compare_branches`, `merge_branch`, `get_a_single_merge_job`, `publish_an_entry`) before and after a CLI run. + +## Safety notes + +Advisory: suggests commands for you to run, and uses MCP tools only when you ask. Warns before imports that replace content, bulk unpublishes, merges, branch deletion and migration scripts, and recommends an export or backup branch first. Never prints tokens. + +## Related + +- [Migration Companion](migration-companion.md) · [Branches & Aliases](cms-branches-aliases.md) · [Launch deployments](launch-trigger-and-monitor-launch-deployments.md) diff --git a/docs/skills/dx-delivery-sdk.md b/docs/skills/dx-delivery-sdk.md index 2895011..79ec950 100644 --- a/docs/skills/dx-delivery-sdk.md +++ b/docs/skills/dx-delivery-sdk.md @@ -15,7 +15,7 @@ When you ask for Delivery SDK code, query examples, helper functions, SDK setup, ## What it covers -- **Stack setup**: `contentstack.stack({...})` with env-based credentials and region, using `@timbenniks/contentstack-endpoints` with `getContentstackEndpoints(region, true)` for region-specific preview hosts when needed. +- **Stack setup**: `contentstack.stack({...})` with env-based credentials and region, using `@timbenniks/contentstack-endpoints` (a community package the official kickstarts use, not published under `@contentstack/`) with `getContentstackEndpoints(region, true)` for region-specific preview hosts when needed. - **Single entry vs collections**: `.contentType(uid).entry(uid).fetch()` for one; `.entry().query().find()` for many. - **References**: `includeReference([...])` belongs _before_ `.query()`. - **Filtering**: use `QueryOperation` names exactly (e.g. `QueryOperation.EQUALS`, `NOT_EQUALS`). @@ -46,9 +46,13 @@ When you ask for Delivery SDK code, query examples, helper functions, SDK setup, Ships the **Delivery SDK Spec** (`references/delivery-sdk-spec.md`), sourced from the official TypeScript Delivery SDK reference. The skill reads it on demand to verify method names, options, and chain order. +## MCP tools + +With the MCP server connected, the agent reads the real stack to ground SDK code: content types and global fields from `cma` (`get_all_content_types`, `get_a_single_content_type`) and published entries and assets from `cda` (`get_all_entries_cdn`, `get_a_single_entry_cdn`). It needs no write tools. + ## Safety notes -Prefers TypeScript and current `@contentstack/delivery-sdk` APIs. Never hardcodes credentials; uses `process.env` placeholders. For SSR Live Preview, never reuses a stack across requests. +Prefers TypeScript and current `@contentstack/delivery-sdk` APIs. Uses MCP tools only to read the stack. Never hardcodes credentials; uses `process.env` placeholders. For SSR Live Preview, never reuses a stack across requests. ## Related diff --git a/docs/skills/dx-kickstart-next.md b/docs/skills/dx-kickstart-next.md index 2b62168..49029f0 100644 --- a/docs/skills/dx-kickstart-next.md +++ b/docs/skills/dx-kickstart-next.md @@ -5,7 +5,7 @@ description: Maintain Contentstack's Next.js kickstart with guidance for App Rou # Contentstack Kickstart Next -**Slug:** `dx-kickstart-next` · **Product:** Developer Experience · **Type:** Advisory (gated repo and stack changes) · **Ships references** +**Slug:** `dx-kickstart-next` · **Product:** Developer Experience · **Type:** Advisory (acts through MCP tools on request) · **Ships references** Helps agents build, review, debug, and extend Contentstack's `kickstart-next` repository or a close fork. It routes work into focused reference files for Next.js behavior, Contentstack delivery and preview setup, the seeded content model, local workflow, and PR review checks. @@ -39,9 +39,13 @@ Ships focused reference files in `references/`: - `workflow.md` for setup, validation, CI, docs, and dependency work. - `review.md` for review severity and pre-PR checks. +## MCP tools + +With the MCP server connected, the agent checks that the code matches your stack: content types and global fields from `cma` and published entries from `cda`. It changes content types or entries only when you ask. Seeding uses `csdx cm:stacks:seed`, not MCP. + ## Safety notes -Never commits real Contentstack credentials. Uses `.env.example` and docs for placeholders, keeps runtime behavior grounded in the current checkout, and verifies scripts and CI workflows before claiming they exist. +Never commits real Contentstack credentials. Uses `.env.example` and docs for placeholders, keeps runtime behavior grounded in the current checkout, and verifies scripts and CI workflows before claiming they exist. Confirms before seeding, schema changes or any MCP write. ## Related diff --git a/docs/skills/dx-mcp-server.md b/docs/skills/dx-mcp-server.md new file mode 100644 index 0000000..fa94614 --- /dev/null +++ b/docs/skills/dx-mcp-server.md @@ -0,0 +1,42 @@ +--- +title: Contentstack MCP +description: "Connect AI agents to Contentstack with the local @contentstack/mcp server or the hosted MCP Profile Hub: choosing, client setup, auth, tool scoping, and safety." +--- + +# Contentstack MCP + +**Slug:** `dx-mcp-server` · **Product:** Developer Experience · **Type:** Advisory (read-only) + +Helps you give an AI assistant scoped access to Contentstack over the Model Context Protocol, with either the local MCP server or the hosted MCP Profile Hub. + +## When it triggers + +When you ask how to give an AI assistant access to Contentstack, set up or debug the Contentstack MCP server or MCP Profile Hub, pick tool groups or profiles, fix MCP auth or missing tools, or build your own MCP integration. + +## What it covers + +- **Choosing**: the local `@contentstack/mcp` server (stdio, `npx`, best for individual developers) or MCP Profile Hub (hosted, Streamable HTTP, profiles, OAuth, Executions history, best for teams and web clients). +- **Local server**: `GROUPS` (`cma`, `cma-extended`, `cda`, `analytics`, `automations`, `brandkit`, `launch`, `developerhub`, `lytics`, `personalize`), `npx @contentstack/mcp --auth`, management token for CMA only, required IDs per group, stack-aware enrichment, and client config for Claude Code, Claude Desktop and Cursor. +- **Profile Hub**: system and custom profiles, the connector URL, client setup for Claude Code, claude.ai, Cursor, Codex, ChatGPT and stdio clients via `mcp-remote`, URL parameters, Automate automations and Agent OS agents as tools. +- **Safety**: read-only first, separate profiles per job, no tokens in shared config, confirmation before destructive calls. +- **Build your own**: the public tool definitions at `mcp.contentstack.com/<group>/tools`. + +## Example prompts + +- "How do I connect Claude Code to my Contentstack stack?" +- "Should my team use the MCP server or MCP Profile Hub?" +- "My MCP client shows no Contentstack tools." +- "How do I let an agent deploy to Launch but not delete content?" +- "Can an agent run one of our Automate automations?" + +## MCP tools + +This skill is about the MCP server itself. It summarizes the tools in each local server group (`cma` 78, `cma-extended` 22, `cda` 4, `analytics` 8, `automations` 9, `brandkit` 12, `launch` 27, `developerhub` 12, `lytics` 10, `personalize` 24), what no group covers (webhooks, roles, users, tokens, labels, extensions), and where to find the published tool definitions. + +## Safety notes + +Advisory. Recommends OAuth and the smallest tool set, warns before enabling write, delete, publish and deploy tools, and has the agent name the target and wait for confirmation before running one. Never echoes tokens. Profile Hub calls always run with the user's own Contentstack roles. + +## Related + +- [Tokens & Authentication](cms-tokens-authentication.md) · [Roles & Permissions](cms-roles-permissions.md) · [Contentstack CLI](dx-cli.md) diff --git a/docs/skills/dx-migrate-js-to-ts-sdk.md b/docs/skills/dx-migrate-js-to-ts-sdk.md index 6ebeb49..7dccd1d 100644 --- a/docs/skills/dx-migrate-js-to-ts-sdk.md +++ b/docs/skills/dx-migrate-js-to-ts-sdk.md @@ -18,7 +18,7 @@ When you want to migrate Delivery SDK code from JavaScript to TypeScript, compar - **Packages & imports**: install `@contentstack/delivery-sdk@latest`; switch imports; add `@contentstack/persistence-plugin` for cache and `@contentstack/utils` for utils. - **Key API mappings**: `contentstack.stack(...)` (not `Stack(...)`); `stack.contentType(...).entry(...).asset(...).query().fetch().find()`, plus `paginate()`, `next()`, `previous()`, `locale()`, `includeReference()`, `includeEmbeddedItems()`, `includeContentType()`, `includeCount()`, and taxonomy methods. - **Typed results**: generics on `fetch<BlogEntry>()`. -- **Cache**: install the persistence plugin and pass `cacheOptions` for policies other than `Policy.IGNORE_CACHE`. +- **Cache**: install `@contentstack/persistence-plugin` and pass a `PersistenceStore` as `cacheOptions.persistenceStore` for policies other than `Policy.IGNORE_CACHE`. - **Utils**: install `@contentstack/utils` separately and call `Utils.jsonToHTML(...)`. - **Sync**: TypeScript uses `stack.sync({ locale })` without `init: true`. - **Limitations**: 8KB URL limit; no multiple content-type references in a single query; no direct Global Field schema querying (use `include_global_field_schema`); `assetFields` support is region-limited to NA. @@ -31,9 +31,13 @@ When you want to migrate Delivery SDK code from JavaScript to TypeScript, compar - "How do I rewrite cache policy usage for the TypeScript SDK?" - "Convert this taxonomy query from the JavaScript SDK to TypeScript." +## MCP tools + +The migration is a code transformation and needs no MCP tools. If the server is connected, `get_all_entries_cdn` (`cda`) can confirm a rewritten query returns the same entries, and `get_a_single_content_type` (`cma`) shows the field UIDs the code relies on. + ## Safety notes -Never invents SDK methods not present in the reference docs. Replaces any secrets in samples with placeholders; uses environment variables for credentials. +Never invents SDK methods not present in the reference docs. Replaces any secrets in samples with placeholders; uses environment variables for credentials. Uses MCP tools only to read the stack. ## Related diff --git a/docs/skills/index.md b/docs/skills/index.md index 67d52b5..691c027 100644 --- a/docs/skills/index.md +++ b/docs/skills/index.md @@ -5,7 +5,7 @@ description: The full catalog of Contentstack Agent Skills, grouped by product. # Skills reference -The bundle includes 24 skills across seven Contentstack products. Each links to a dedicated page with what it does, when it triggers, example prompts, and its safety notes. +The bundle includes 33 skills across ten Contentstack products. Each links to a dedicated page with what it does, when it triggers, example prompts, and its safety notes. > Each skill page mirrors its canonical `skills/<slug>/SKILL.md`. The slug is the skill's identifier: use it with the [`skills` CLI](../get-started/skills-cli.md). @@ -16,6 +16,8 @@ The bundle includes 24 skills across seven Contentstack products. Each links to | [Assets](cms-assets.md) | `cms-assets` | Asset organization, Image Delivery API transforms, publishing lifecycle, CDN behavior, limits. | | [Branches & Aliases](cms-branches-aliases.md) | `cms-branches-aliases` | Isolated content development, alias-based zero-downtime deploys, CI/CD, merge & rollback. | | [Data Modeling Best Practices](cms-data-modeling-best-practices.md) | `cms-data-modeling-best-practices` | Choosing content types, references, global fields, groups, modular blocks, JSON RTE, taxonomy. | +| [GraphQL Content Delivery](cms-graphql.md) | `cms-graphql` | GraphQL endpoints, schema, references, modular blocks, JSON RTE, limits, Live Preview. | +| [JSON Rich Text Editor](cms-json-rte.md) | `cms-json-rte` | Rendering and serializing JSON RTE content, `@contentstack/utils`, embedded entries and assets, HTML conversion. | | [Entries](cms-entries.md) | `cms-entries` | CDA queries, reference expansion, pagination, versioning, publishing, bulk ops, Sync API. | | [Environments & Publishing](cms-environments-publishing.md) | `cms-environments-publishing` | Environment design, publishing pipeline, token types, Sync API, CDN, publish queue. | | [Localization](cms-localization.md) | `cms-localization` | Languages, fallback chains, localized vs unlocalized entries, non-localizable fields. | @@ -31,6 +33,8 @@ The bundle includes 24 skills across seven Contentstack products. Each links to | Skill | Slug | What it helps with | | --- | --- | --- | +| [Contentstack CLI](dx-cli.md) | `dx-cli` | `csdx` setup and auth, export/import/seed/clone, bulk operations, migrations, branch merges, plugins. | +| [Contentstack MCP](dx-mcp-server.md) | `dx-mcp-server` | Local MCP server vs hosted MCP Profile Hub, client setup, auth, tool scoping, safety. | | [Delivery SDK](dx-delivery-sdk.md) | `dx-delivery-sdk` | Production-ready TypeScript with `@contentstack/delivery-sdk` for entries, assets, queries, Live Preview. | | [Contentstack Kickstart Next](dx-kickstart-next.md) | `dx-kickstart-next` | Maintaining the Next.js kickstart: App Router, Delivery SDK setup, Live Preview, seed alignment, env vars, validation. | | [Migrate JS→TS SDK](dx-migrate-js-to-ts-sdk.md) | `dx-migrate-js-to-ts-sdk` | Migrating Delivery SDK code from the JavaScript SDK to the TypeScript SDK. | @@ -53,7 +57,8 @@ The bundle includes 24 skills across seven Contentstack products. Each links to | Skill | Slug | What it helps with | | --- | --- | --- | -| [Sync Launch env vars from .env.example](launch-sync-environment-variables-from-env-example.md) | `launch-sync-environment-variables-from-env-example` | Comparing a local `.env.example` to a Launch environment and patching missing keys. | +| [Launch Platform](launch-platform.md) | `launch-platform` | Launch hosting: builds, Cloud/Edge Functions, launch.json, caching, deploy hooks, domains, logs, limits. | +| [Sync Launch env vars from .env.example](launch-sync-environment-variables-from-env-example.md) | `launch-sync-environment-variables-from-env-example` | Comparing a local `.env.example` to a Launch environment and adding missing keys. | | [Trigger & Monitor Deployments](launch-trigger-and-monitor-launch-deployments.md) | `launch-trigger-and-monitor-launch-deployments` | Triggering a Launch deploy, polling to completion, diagnosing failures from logs. | ## Brand Kit @@ -67,6 +72,20 @@ The bundle includes 24 skills across seven Contentstack products. Each links to | Skill | Slug | What it helps with | | --- | --- | --- | | [Developer Hub App Architect](developer-hub-app-architect.md) | `developer-hub-app-architect` | Designing & building Developer Hub / Marketplace apps: UI locations, manifest, SDK, proxy, publishing. | +| [Marketplace Apps](marketplace-apps.md) | `marketplace-apps` | Choosing, installing and configuring first-party Marketplace apps, and reading their data on delivery. | + +## Personalize & Data + +| Skill | Slug | What it helps with | +| --- | --- | --- | +| [Personalize](personalize.md) | `personalize` | Experiences, audiences, attributes, events, Edge SDK and Edge API, Management API, edge middleware. | +| [Lytics](lytics.md) | `lytics` | JavaScript tag, identity, schema, audiences and Segment API, consent and deletion, Personalize connection. | + +## Automate + +| Skill | Slug | What it helps with | +| --- | --- | --- | +| [Automations](automations.md) | `automations` | Automate workflows: triggers, connectors, control flow, custom code, executions, limits. | ## How skills get selected diff --git a/docs/skills/launch-platform.md b/docs/skills/launch-platform.md new file mode 100644 index 0000000..a2e3dbd --- /dev/null +++ b/docs/skills/launch-platform.md @@ -0,0 +1,46 @@ +--- +title: Launch Platform +description: "Host and configure sites on Contentstack Launch: frameworks, Cloud and Edge Functions, launch.json, caching and revalidation, deploy triggers, domains, access control, logs and limits." +--- + +# Launch Platform + +**Slug:** `launch-platform` · **Product:** Launch · **Type:** Advisory (acts through MCP tools on request) + +Advises on getting a framework app building and serving on Launch, choosing between Cloud and Edge Functions, keeping content fresh without constant rebuilds, and staying inside platform limits. + +## When it triggers + +When you ask how to deploy or configure a site on Launch, why a build or function fails, how routing or caching works on Launch, or whether Launch supports a framework or feature. + +## What it covers + +- **Request flow**: `launch.json` redirects, then rewrites, then the Edge Function, then the CDN cache, then the origin. +- **Frameworks and builds**: presets (Next.js, Nuxt, Astro, Remix, SvelteKit, Gatsby, Angular and more), build and server commands, supported Node versions, npm/yarn/bun (no pnpm), 60-minute build limit. +- **Cloud Functions**: `/functions` file routing, handler shape, memory and timeout defaults, read-only file system. +- **Edge Functions**: `functions/[proxy].edge.js`, WinterCG-only APIs, memory, bundle and startup limits. +- **launch.json**: redirects, rewrites, header rules and cache priming. See the bundled reference. +- **Caching**: full purge on each live deployment, `Cache-Control`, and the revalidate-CDN-cache API by path, prefix, cache tag or hostname. +- **Deploy triggers**: deploy hooks, CMS-webhook redeploys, auto-deploy on push, rollback. Prefers revalidation over rebuilds for frequent publishing. +- **Environment variables, domains, password protection, IP control through Edge Functions, log targets over OTLP, monorepos.** +- **Limits and unsupported features**, in a bundled limits reference. + +## Example prompts + +- "How do I deploy my Next.js app to Launch?" +- "Should this middleware be an Edge Function or a Cloud Function?" +- "My site shows old content after I publish in the CMS." +- "Can I restrict my staging site to our office IPs?" +- "How do I send Launch logs to Datadog?" + +## MCP tools + +With the `launch` group of the Contentstack MCP server connected, the agent can read projects, environments, deployments and build or server logs (`get_an_environment`, `get_deployment`, `get_deployment_logs`, `get_server_logs`), and on request deploy, revalidate the CDN cache, update environments or manage deploy hooks (`create_deployment`, `revalidate_cdn_cache`, `update_an_environment`, `create_deploy_hook`). There are no tools for domains, log targets or rollback. + +## Safety notes + +Advisory by default. Uses MCP tools to change Launch only when asked, and names the project, environment and effect before any deployment, cache revalidation, environment update or delete, then waits for confirmation. Treats deploy hook URLs and tokens as secrets. + +## Related + +- [Launch: Sync environment variables](launch-sync-environment-variables-from-env-example.md) · [Launch: Trigger and monitor deployments](launch-trigger-and-monitor-launch-deployments.md) · [Webhooks](cms-webhooks.md) diff --git a/docs/skills/launch-sync-environment-variables-from-env-example.md b/docs/skills/launch-sync-environment-variables-from-env-example.md index 9327f2b..7fd98b9 100644 --- a/docs/skills/launch-sync-environment-variables-from-env-example.md +++ b/docs/skills/launch-sync-environment-variables-from-env-example.md @@ -1,13 +1,13 @@ --- title: Sync Launch env vars from .env.example -description: "Compare a local .env.example with a Contentstack Launch environment and patch the environment to add any missing keys, without exposing secrets." +description: "Compare a local .env.example with a Contentstack Launch environment and update the environment to add any missing keys, without exposing secrets." --- # Sync Launch env vars from .env.example **Slug:** `launch-sync-environment-variables-from-env-example` · **Product:** Launch · **Type:** Action (gated) -Fetches environment variables from a Contentstack Launch project, compares them with a local `.env.example` file, and patches the target environment to add any missing keys. Useful for auditing missing variables before a deployment and keeping a Launch environment aligned with what a frontend project expects. +Fetches environment variables from a Contentstack Launch project, compares them with a local `.env.example` file, and updates the target environment to add any missing keys. Useful for auditing missing variables before a deployment and keeping a Launch environment aligned with what a frontend project expects. > This skill has `disable-model-invocation: true`, which Claude Code and Copilot honor: they run it only when you explicitly invoke it. Other tools may still match it automatically, so the skill also confirms the target project and environment before any external call. @@ -20,9 +20,9 @@ When a Launch environment must match the keys defined in a local `.env.example`, 1. Read and parse `.env.example`, extracting variable names (ignoring comments, blanks, malformed entries). 2. Fetch the target Launch project and environment and read its current variables. 3. Compare keys: identify only missing keys unless you explicitly ask to update existing values. -4. Build a minimal patch payload with only the missing keys. -5. `PATCH /projects/{project_uid}/environments/{environment_uid}`. -6. Report missing keys, patched keys, and any errors, without exposing values. +4. Append only the missing keys to the fetched `environmentVariables` array. +5. `PUT /projects/{project_uid}/environments/{environment_uid}` with the full environment object. There is no PATCH, and PUT replaces the environment, so every other setting is sent back unchanged. +6. Report missing keys, added keys, and any errors, without exposing values. ## Inputs @@ -36,11 +36,15 @@ When a Launch environment must match the keys defined in a local `.env.example`, - "Generate a Node.js script that syncs Launch environment variables from `.env.example`." - "Compare my Launch environment variables with `.env.example` and add any missing keys." -- "Patch missing Launch environment variables for project `abc123` and environment `dev456`." +- "Add missing Launch environment variables for project `abc123` and environment `dev456`." + +## MCP tools + +If the `launch` group of the Contentstack MCP server is connected, the skill uses `get_an_environment` to fetch the environment and `update_an_environment` to send the full object back, instead of a script or curl. The same rules apply. ## Safety notes -Treats any PATCH as a destructive external action. Validates and confirms the target project/environment, and never broadens the update beyond missing keys unless asked. Prefers dry-run. Logs only key names and counts; never prints secret values from Launch or local files. Uses the smallest possible PATCH payload. +Treats the PUT, through the API or `update_an_environment`, as a destructive external action. Validates and confirms the target project/environment, and never broadens the update beyond missing keys unless asked. Prefers dry-run. Logs only key names and counts; never prints secret values from Launch or local files. Changes only the `environmentVariables` array, never sends a partial environment, and stops if the API returns masked values that would overwrite real ones. ## Related diff --git a/docs/skills/launch-trigger-and-monitor-launch-deployments.md b/docs/skills/launch-trigger-and-monitor-launch-deployments.md index 7007801..6b397bc 100644 --- a/docs/skills/launch-trigger-and-monitor-launch-deployments.md +++ b/docs/skills/launch-trigger-and-monitor-launch-deployments.md @@ -19,10 +19,10 @@ When you need to automate Launch deployments for a known project and environment 1. Validate the project and environment identifiers. 2. Confirm the target is allowed. **Require explicit approval for production**. -3. Trigger: `POST /projects/{project_uid}/environments/{environment_uid}/deployments`. -4. Poll status every 10 seconds until a terminal state. -5. Fail fast. Exit non-zero on `failed` or `cancelled`; stop polling. -6. On failure, `GET .../deployments/{deployment_uid}` for the log, then summarize the likely cause and next steps. +3. Trigger: `POST /projects/{project_uid}/environments/{environment_uid}/deployments`, optionally with a `commitHash` or `uploadUid`. +4. Poll `GET .../deployments/{deployment_uid}` every 10 seconds. `LIVE` is success; `QUEUED`, `DEPLOYING` and `DEPLOYED` are still in progress. +5. Fail fast. Exit non-zero on `FAILED`, `CANCELLED` or `SKIPPED`; stop polling. +6. On failure, `GET .../deployments/{deployment_uid}/logs/deployment-logs` for the build log (the deployment endpoint itself returns metadata only), then summarize the likely cause and next steps. ## Inputs @@ -39,9 +39,13 @@ When you need to automate Launch deployments for a known project and environment - "Poll a Launch deployment every 10 seconds and exit non-zero on failure." - "If the deployment fails, check the logs and explain what went wrong." +## MCP tools + +If the `launch` group of the Contentstack MCP server is connected, the skill uses `create_deployment` to deploy the latest commit of the environment's branch, `get_deployment` to poll, and `get_deployment_logs` or `get_server_logs` to diagnose. Deploying a specific commit or upload, and rollback, still go through the API. + ## Safety notes -Treats deployment triggers as external side effects. Requires explicit confirmation before production deployments and never auto-deploys to production without review. Doesn't auto-retry failed deployments unless asked and the target is non-production. Never prints API tokens, deployment tokens, or environment secrets; redacts sensitive values from logs and summaries. Uses a non-zero exit code for failed or cancelled deployments. +Treats deployment triggers, through the API or MCP tools, as external side effects. Requires explicit confirmation of the project and environment before every deployment, with an extra check for production, and never auto-deploys to production without review. Doesn't auto-retry failed deployments unless asked and the target is non-production. Never prints API tokens, deployment tokens, or environment secrets; redacts sensitive values from logs and summaries. Uses a non-zero exit code for failed, cancelled or skipped deployments. Rollback (`POST .../deployments/rollback`) is a production change and needs confirmation. ## Related diff --git a/docs/skills/lytics.md b/docs/skills/lytics.md new file mode 100644 index 0000000..282be50 --- /dev/null +++ b/docs/skills/lytics.md @@ -0,0 +1,46 @@ +--- +title: Lytics +description: "Use Lytics, Contentstack's customer data platform: the JavaScript tag, identity, schema, audiences, the Segment API, consent, deletion, and the Personalize connection." +--- + +# Lytics + +**Slug:** `lytics` · **Product:** Lytics (Data & Insights) · **Type:** Advisory (acts through MCP tools on request) + +Advises on getting behavioral and identity data into Lytics, resolving it into unified profiles, building audiences, and activating them, including in Contentstack Personalize, while respecting consent and deletion requests. + +## When it triggers + +When you ask about Lytics projects, installing or debugging the Lytics JavaScript tag, sending events or identities, schema fields and mappings or LQL, audiences and SegmentQL, the Segment API, profile lookups, importing Contentstack entries, recommendations, consent, identity deletion, or using Lytics audiences in Personalize. + +## What it covers + +- **Projects:** Lytics projects in Contentstack (the Lytics app calls them accounts), connections to stacks, Launch and Personalize, and per-project roles. +- **JavaScript tag:** installation through Launch, `<head>` or a tag manager; verification; `jstag.send`, `identify`, `page`; opt-in client-side integrations. +- **Identity:** merging anonymous `_uid` history with known identifiers, and identity key ranks. +- **Schema:** fields and mappings in Schema Studio, with LQL still available through the API. +- **Audiences:** the audience builder, SegmentQL, map-field conditions, Enable for Personalization, what Expires After does, and the Segment API. +- **Personalize connection:** the project connection, plus the `cs-lytics-audiences` cookie read by the Edge SDK. +- **Contentstack entries:** importing entries and taxonomies as context layers, content collections for recommendations. +- **API:** `https://api.lytics.io`, tokens in the `Authorization` header, least-privilege tokens with expiry. +- **Consent and deletion:** consent events, `DELETE /v2/identity/{table}/{field}/{value}`, bulk deletion and status. + +## Example prompts + +- "How do I install the Lytics tag on my Next.js site?" +- "How do I merge an anonymous visitor with the known user on login?" +- "How do I get Lytics audiences into Personalize?" +- "How do I delete a user's data for a GDPR request?" +- "Can I create audiences through the API?" + +## MCP tools + +With the `lytics` group of the Contentstack MCP server connected, the agent can read accounts, schema tables and fields, audiences, profiles and content topics, and classify or enrich content (`classify_content`, `enrich_content`). There are no tools for creating audiences, changing schema, sending events or deleting identities. + +## Safety notes + +Advisory by default; uses MCP tools only when asked. API tokens stay server-side. Identity deletion is permanent and needs explicit confirmation of the exact profile. Looks up profiles only when the user names one. + +## Related + +- [Personalize](personalize.md) · [Variants & Personalization](cms-variants-personalization.md) diff --git a/docs/skills/marketplace-apps.md b/docs/skills/marketplace-apps.md new file mode 100644 index 0000000..fdc4ef2 --- /dev/null +++ b/docs/skills/marketplace-apps.md @@ -0,0 +1,46 @@ +--- +title: Marketplace Apps +description: "Choose, install, configure and use first-party Contentstack Marketplace apps: the catalog by category, install permissions, configuration and per-field overrides, webhooks, delivery data and when to build a custom app." +--- + +# Marketplace Apps + +**Slug:** `marketplace-apps` · **Product:** Developer Hub · **Type:** Advisory (acts through MCP tools on request) + +Helps you pick the right first-party Marketplace app, install and configure it with the right permissions, and use the data it stores in your entries. + +## When it triggers + +When you ask which Marketplace app fits a need, how to install or configure an app such as Bynder, Cloudinary, Shopify, commercetools, Algolia or Trados, why an app field is empty or a sync isn't running, or how to read app data on the frontend. + +## What it covers + +- **Catalog**: documented apps by category: DAM, video, commerce and PIM, search, translation, AI, analytics and experimentation, forms, editor and JSON RTE utilities, and stack tools. +- **Install permissions**: stack apps vs organization apps, who can install each, and the Request Install and App Requests flow. +- **Configuration**: app configuration, multiple configurations with a default, `legacy_config`, the per-field **Config Parameter** override, and what each app saves in the entry. +- **UI locations and webhooks**: enabling locations, adding custom fields to content types, webhook branch scope and logs. +- **App specifics**: Algolia's environment and index mapping, and what Trados converts and skips. +- **Delivery**: reading app field JSON with the CDA, SDK and GraphQL, and fetching live data from the source system. +- **Lifecycle**: updates, uninstalling, and revoking authorized apps. +- **Build vs install**: when a custom app is the better fit. + +## Example prompts + +- "Which Marketplace app should we use to connect Bynder assets to our entries?" +- "I'm not an admin. How do I get the Shopify app installed on our stack?" +- "How do I use two Shopify stores in the same stack?" +- "Our Algolia index isn't updating when we publish." +- "What does the commercetools field return in the CDA response?" +- "Should we build our own app or use a Marketplace app?" + +## MCP tools + +The `developerhub` group covers apps your organization owns: `get_all_apps`, `get_an_app`, `get_app_installations`, `install_an_app`, `update_an_app_installation` and `uninstall_an_app`. Third-party listings may not appear in `get_all_apps`, so those are installed from the Marketplace UI. Entry and content type read tools show stored app data and which fields use an app. + +## Safety notes + +Advisory by default. Never echoes app credentials; `get_app_installations` can return them. Uninstalling an app, deleting a configuration (especially `legacy_config`) or changing one that fields depend on needs a named target and explicit confirmation, because stored app data can be lost. + +## Related + +- [Developer Hub App Architect](developer-hub-app-architect.md) · [Asset Management](asset-management.md) · [Assets](cms-assets.md) · [Localization](cms-localization.md) · [JSON Rich Text Editor](cms-json-rte.md) diff --git a/docs/skills/migration-companion.md b/docs/skills/migration-companion.md index 2b13c41..740b923 100644 --- a/docs/skills/migration-companion.md +++ b/docs/skills/migration-companion.md @@ -36,6 +36,10 @@ When you want to migrate, move, switch, port, or re-platform to Contentstack: co Ships per-step runbooks in `references/` (`step-1-prerequisites.md` through `step-5-welcome-message.md`, loaded only when that step runs), the Contentful → Contentstack mapping file (`CONTENTFUL_TO_CONTENTSTACK_MIGRATION_CONTEXT.md`), and a `scripts/` suite: the prerequisite checker, `read_cf_token.py` (resolves the Contentful token without printing it), the import-summary parser, logging helpers, and per-check eval scripts (`run-all.sh` plus numbered checks). +## MCP tools + +The migration runs on the Contentstack CLI and its migrate plugin. MCP is optional: with the server connected, its read tools (`get_all_content_types`, `get_all_entries`, `get_all_assets`, `get_all_entries_cdn`) can verify the result in the new stack. The skill doesn't use MCP write tools to patch content mid-run. + ## Safety notes Action skill with explicit gates: confirms before logging in, before creating a stack, and before editing your code. The prerequisite checker may install or upgrade global CLIs, and says so. Treats management and Contentful tokens as secrets: never prints or echoes them, and never writes them to workspace files; logs go to `<SESSION_DIR>/migration-log` by default; sets `CS_*` credentials only in the migrated app's gitignored `.env`. Converts reference dereferences to safe array access and flags every guessed field UID with a `TODO(migration)` comment rather than guessing silently. diff --git a/docs/skills/personalize.md b/docs/skills/personalize.md new file mode 100644 index 0000000..6a746e7 --- /dev/null +++ b/docs/skills/personalize.md @@ -0,0 +1,44 @@ +--- +title: Personalize +description: "Integrate Contentstack Personalize: experiences, audiences, attributes, events, the Edge SDK and Edge API, the Management API, and edge middleware patterns." +--- + +# Personalize + +**Slug:** `personalize` · **Product:** Personalize · **Type:** Advisory (acts through MCP tools on request) + +Advises on integrating Contentstack Personalize so each visitor gets the right variant early in the request, the matching entry variants are fetched, and results are measured, without leaking personalized content into shared caches. + +## When it triggers + +When you ask about setting up Personalize, segmented vs A/B test experiences, audiences and attributes, impressions and conversions, the Personalize Edge SDK in Next.js, Launch Edge Functions, Vercel, Netlify or Cloudflare, the Management API, experience analytics, or a visitor seeing the wrong variant. + +## What it covers + +- **Concepts:** projects linked to a stack, experiences syncing as variant groups, segmented vs A/B (equal, custom split, Multi-Armed Bandit), first-match variant order, experience priority, experience tags. +- **Audiences and attributes:** preset attributes (City, Country, Region, Date and Time, Device Type, Operating System, Query Parameter, Referrer), custom attributes that must exist in the project, live vs stored attributes, Personalize vs Lytics audiences. +- **Edge SDK:** `@contentstack/personalize-edge-sdk` init options, instance methods, regional Edge API hosts via `setEdgeApiUrl`. +- **Request flow:** edge `init` with the request → variant param rewrite → `addStateToResponse` → `variantParamToVariantAliases` → Delivery SDK `.variants()`. On Launch, use an Edge Function rather than Next.js middleware. A worked example is in `references/edge-integration.md`. +- **Events and analytics:** impressions counted once per visitor, conversions via event keys, analytics summary and time series. +- **Management API:** regional hosts, OAuth scopes `personalize:read` / `personalize:manage` or authtoken, `x-project-uid`, 10 requests/second per organization. +- **Caching and debugging:** per-request rendering, `no-store` on browser responses, and a debugging chain from manifest → aliases → CDA → entry variants → cache. + +## Example prompts + +- "How do I add Personalize to my Next.js site on Launch?" +- "Should this be a segmented experience or an A/B test?" +- "How do I track a conversion when someone clicks the CTA?" +- "Why does every visitor get the base entry?" +- "How do I create experiences through the API?" + +## MCP tools + +With the `personalize` group of the Contentstack MCP server connected, the agent can read experiences, versions, priority, audiences, attributes, events and analytics, and on request create or update experiences, attributes and events or change priority. There is no tool for creating audiences. Entry variants on the CMS side use the `cma` group's variant tools. + +## Safety notes + +Advisory by default. Uses MCP tools to change the project only when asked, and confirms the project and items before deleting, pausing or reprioritizing anything. Management API credentials stay server-side; personalized responses never go into a shared cache without the variant in the cache key. + +## Related + +- [Variants & Personalization](cms-variants-personalization.md) · [Lytics](lytics.md) · [Delivery SDK](dx-delivery-sdk.md) diff --git a/docs/skills/studio.md b/docs/skills/studio.md index 02494fb..94d7341 100644 --- a/docs/skills/studio.md +++ b/docs/skills/studio.md @@ -1,6 +1,6 @@ --- title: Contentstack Studio (preview) -description: Build with Contentstack Studio end to end, routed to one of 109 bundled specialist guides per task. +description: Build with Contentstack Studio end to end, routed to one of 111 bundled specialist guides per task. --- # Contentstack Studio (preview) @@ -23,9 +23,13 @@ When a request touches Contentstack Studio: the Studio SDK, canvas, compositions It prefers Contentstack MCP tools when they are connected and falls back to OAuth-authenticated API calls for missing operations. +## MCP tools + +Studio is MCP-first for the operations the server supports: content types, entries and publishing through the `cma` group. Its `install-contentstack-mcp` guide discovers and verifies the server. The Studio API project list and configuration, and some token and Live Preview operations, still need OAuth HTTP or the Studio UI. + ## Safety notes -Resolves credentials OAuth-first: it asks before reading or refreshing a stored OAuth session from another tool, and uses a session `authtoken` only as a last resort. Confirms before creating or changing stacks, content types, entries, compositions or Studio projects. Never prints tokens or `.env` contents, never weakens TLS verification, and never ships permissive frame headers outside the canvas route. +Resolves credentials OAuth-first: it asks before reading or refreshing a stored OAuth session from another tool, and uses a session `authtoken` only as a last resort. Verifies the target stack with a read call before MCP work. Confirms before creating or changing stacks, content types, entries, compositions or Studio projects, whether through MCP or HTTP. Never prints tokens or `.env` contents, never weakens TLS verification, and never ships permissive frame headers outside the canvas route. ## Example prompts diff --git a/evals/routing-asset-management-1/graders/loads-asset-management.md b/evals/routing-asset-management-1/graders/loads-asset-management.md new file mode 100644 index 0000000..8186a9d --- /dev/null +++ b/evals/routing-asset-management-1/graders/loads-asset-management.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?asset-management"' +min: 1 +--- +The agent loads the `asset-management` skill. diff --git a/evals/routing-asset-management-1/prompt.md b/evals/routing-asset-management-1/prompt.md new file mode 100644 index 0000000..94ce77d --- /dev/null +++ b/evals/routing-asset-management-1/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-asset-management-1 +tags: [routing, asset-management] +max_turns: 3 +timeout_seconds: 180 +--- +In Contentstack Assets, how should I split our brands and regions across spaces and workspaces? diff --git a/evals/routing-asset-management-2/graders/loads-asset-management.md b/evals/routing-asset-management-2/graders/loads-asset-management.md new file mode 100644 index 0000000..8186a9d --- /dev/null +++ b/evals/routing-asset-management-2/graders/loads-asset-management.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?asset-management"' +min: 1 +--- +The agent loads the `asset-management` skill. diff --git a/evals/routing-asset-management-2/graders/skips-cms-assets.md b/evals/routing-asset-management-2/graders/skips-cms-assets.md new file mode 100644 index 0000000..2dd980b --- /dev/null +++ b/evals/routing-asset-management-2/graders/skips-cms-assets.md @@ -0,0 +1,8 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-assets"' +min: 0 +max: 0 +--- +The agent doesn't load the neighbouring `cms-assets` skill. diff --git a/evals/routing-asset-management-2/prompt.md b/evals/routing-asset-management-2/prompt.md new file mode 100644 index 0000000..483f7a8 --- /dev/null +++ b/evals/routing-asset-management-2/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-asset-management-2 +tags: [routing, asset-management] +max_turns: 3 +timeout_seconds: 180 +--- +How do I add a custom 'photographer' field to images in Contentstack Assets and search by it? diff --git a/evals/routing-automations-1/graders/loads-automations.md b/evals/routing-automations-1/graders/loads-automations.md new file mode 100644 index 0000000..bbd29d1 --- /dev/null +++ b/evals/routing-automations-1/graders/loads-automations.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?automations"' +min: 1 +--- +The agent loads the `automations` skill. diff --git a/evals/routing-automations-1/prompt.md b/evals/routing-automations-1/prompt.md new file mode 100644 index 0000000..26eebd6 --- /dev/null +++ b/evals/routing-automations-1/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-automations-1 +tags: [routing, automations] +max_turns: 3 +timeout_seconds: 180 +--- +Using Contentstack Automate, post to Slack whenever an entry is published. diff --git a/evals/routing-automations-2/graders/loads-automations.md b/evals/routing-automations-2/graders/loads-automations.md new file mode 100644 index 0000000..bbd29d1 --- /dev/null +++ b/evals/routing-automations-2/graders/loads-automations.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?automations"' +min: 1 +--- +The agent loads the `automations` skill. diff --git a/evals/routing-automations-2/graders/skips-cms-webhooks.md b/evals/routing-automations-2/graders/skips-cms-webhooks.md new file mode 100644 index 0000000..08a3326 --- /dev/null +++ b/evals/routing-automations-2/graders/skips-cms-webhooks.md @@ -0,0 +1,8 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-webhooks"' +min: 0 +max: 0 +--- +The agent doesn't load the neighbouring `cms-webhooks` skill. diff --git a/evals/routing-automations-2/prompt.md b/evals/routing-automations-2/prompt.md new file mode 100644 index 0000000..e3060f0 --- /dev/null +++ b/evals/routing-automations-2/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-automations-2 +tags: [routing, automations] +max_turns: 3 +timeout_seconds: 180 +--- +Why didn't my Contentstack automation run when I published an entry? diff --git a/evals/routing-brand-kit-assistant-1/graders/loads-brand-kit-assistant.md b/evals/routing-brand-kit-assistant-1/graders/loads-brand-kit-assistant.md new file mode 100644 index 0000000..2245ecd --- /dev/null +++ b/evals/routing-brand-kit-assistant-1/graders/loads-brand-kit-assistant.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?brand-kit-assistant"' +min: 1 +--- +The agent loads the `brand-kit-assistant` skill. diff --git a/evals/routing-brand-kit-assistant-1/prompt.md b/evals/routing-brand-kit-assistant-1/prompt.md new file mode 100644 index 0000000..156b575 --- /dev/null +++ b/evals/routing-brand-kit-assistant-1/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-brand-kit-assistant-1 +tags: [routing, brand-kit-assistant] +max_turns: 3 +timeout_seconds: 180 +--- +How do I make AI-generated content in Contentstack stay on-brand with a Voice Profile? diff --git a/evals/routing-brand-kit-assistant-2/graders/loads-brand-kit-assistant.md b/evals/routing-brand-kit-assistant-2/graders/loads-brand-kit-assistant.md new file mode 100644 index 0000000..2245ecd --- /dev/null +++ b/evals/routing-brand-kit-assistant-2/graders/loads-brand-kit-assistant.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?brand-kit-assistant"' +min: 1 +--- +The agent loads the `brand-kit-assistant` skill. diff --git a/evals/routing-brand-kit-assistant-2/prompt.md b/evals/routing-brand-kit-assistant-2/prompt.md new file mode 100644 index 0000000..92f4519 --- /dev/null +++ b/evals/routing-brand-kit-assistant-2/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-brand-kit-assistant-2 +tags: [routing, brand-kit-assistant] +max_turns: 3 +timeout_seconds: 180 +--- +Can I get the original files back from Contentstack Knowledge Vault? diff --git a/evals/routing-cms-assets-1/graders/loads-cms-assets.md b/evals/routing-cms-assets-1/graders/loads-cms-assets.md new file mode 100644 index 0000000..28006a0 --- /dev/null +++ b/evals/routing-cms-assets-1/graders/loads-cms-assets.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-assets"' +min: 1 +--- +The agent loads the `cms-assets` skill. diff --git a/evals/routing-cms-assets-1/graders/skips-asset-management.md b/evals/routing-cms-assets-1/graders/skips-asset-management.md new file mode 100644 index 0000000..47e2524 --- /dev/null +++ b/evals/routing-cms-assets-1/graders/skips-asset-management.md @@ -0,0 +1,8 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?asset-management"' +min: 0 +max: 0 +--- +The agent doesn't load the neighbouring `asset-management` skill. diff --git a/evals/routing-cms-assets-1/prompt.md b/evals/routing-cms-assets-1/prompt.md new file mode 100644 index 0000000..b785235 --- /dev/null +++ b/evals/routing-cms-assets-1/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-assets-1 +tags: [routing, cms-assets] +max_turns: 3 +timeout_seconds: 180 +--- +How do I serve responsive WebP images from my Contentstack stack with image transformations? diff --git a/evals/routing-cms-assets-2/graders/loads-cms-assets.md b/evals/routing-cms-assets-2/graders/loads-cms-assets.md new file mode 100644 index 0000000..28006a0 --- /dev/null +++ b/evals/routing-cms-assets-2/graders/loads-cms-assets.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-assets"' +min: 1 +--- +The agent loads the `cms-assets` skill. diff --git a/evals/routing-cms-assets-2/prompt.md b/evals/routing-cms-assets-2/prompt.md new file mode 100644 index 0000000..bebfa55 --- /dev/null +++ b/evals/routing-cms-assets-2/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-assets-2 +tags: [routing, cms-assets] +max_turns: 3 +timeout_seconds: 180 +--- +What's the maximum file size I can upload to a Contentstack stack's asset library? diff --git a/evals/routing-cms-branches-aliases-1/graders/loads-cms-branches-aliases.md b/evals/routing-cms-branches-aliases-1/graders/loads-cms-branches-aliases.md new file mode 100644 index 0000000..fec4a13 --- /dev/null +++ b/evals/routing-cms-branches-aliases-1/graders/loads-cms-branches-aliases.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-branches-aliases"' +min: 1 +--- +The agent loads the `cms-branches-aliases` skill. diff --git a/evals/routing-cms-branches-aliases-1/prompt.md b/evals/routing-cms-branches-aliases-1/prompt.md new file mode 100644 index 0000000..dbb78db --- /dev/null +++ b/evals/routing-cms-branches-aliases-1/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-branches-aliases-1 +tags: [routing, cms-branches-aliases] +max_turns: 3 +timeout_seconds: 180 +--- +How do branches and aliases in Contentstack fit into our CI/CD pipeline? diff --git a/evals/routing-cms-branches-aliases-2/graders/loads-cms-branches-aliases.md b/evals/routing-cms-branches-aliases-2/graders/loads-cms-branches-aliases.md new file mode 100644 index 0000000..fec4a13 --- /dev/null +++ b/evals/routing-cms-branches-aliases-2/graders/loads-cms-branches-aliases.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-branches-aliases"' +min: 1 +--- +The agent loads the `cms-branches-aliases` skill. diff --git a/evals/routing-cms-branches-aliases-2/prompt.md b/evals/routing-cms-branches-aliases-2/prompt.md new file mode 100644 index 0000000..0627e26 --- /dev/null +++ b/evals/routing-cms-branches-aliases-2/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-branches-aliases-2 +tags: [routing, cms-branches-aliases] +max_turns: 3 +timeout_seconds: 180 +--- +Which Contentstack modules are branch-specific and which are global? diff --git a/evals/routing-cms-data-modeling-best-practices-1/graders/loads-cms-data-modeling-best-practices.md b/evals/routing-cms-data-modeling-best-practices-1/graders/loads-cms-data-modeling-best-practices.md new file mode 100644 index 0000000..2bd0a79 --- /dev/null +++ b/evals/routing-cms-data-modeling-best-practices-1/graders/loads-cms-data-modeling-best-practices.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-data-modeling-best-practices"' +min: 1 +--- +The agent loads the `cms-data-modeling-best-practices` skill. diff --git a/evals/routing-cms-data-modeling-best-practices-1/prompt.md b/evals/routing-cms-data-modeling-best-practices-1/prompt.md new file mode 100644 index 0000000..79f95ac --- /dev/null +++ b/evals/routing-cms-data-modeling-best-practices-1/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-data-modeling-best-practices-1 +tags: [routing, cms-data-modeling-best-practices] +max_turns: 3 +timeout_seconds: 180 +--- +How should I model a landing page with reusable sections in Contentstack? diff --git a/evals/routing-cms-data-modeling-best-practices-2/graders/loads-cms-data-modeling-best-practices.md b/evals/routing-cms-data-modeling-best-practices-2/graders/loads-cms-data-modeling-best-practices.md new file mode 100644 index 0000000..2bd0a79 --- /dev/null +++ b/evals/routing-cms-data-modeling-best-practices-2/graders/loads-cms-data-modeling-best-practices.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-data-modeling-best-practices"' +min: 1 +--- +The agent loads the `cms-data-modeling-best-practices` skill. diff --git a/evals/routing-cms-data-modeling-best-practices-2/prompt.md b/evals/routing-cms-data-modeling-best-practices-2/prompt.md new file mode 100644 index 0000000..f7f5fe4 --- /dev/null +++ b/evals/routing-cms-data-modeling-best-practices-2/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-data-modeling-best-practices-2 +tags: [routing, cms-data-modeling-best-practices] +max_turns: 3 +timeout_seconds: 180 +--- +Should this Contentstack data be a global field, a group, or its own content type? diff --git a/evals/routing-cms-entries-1/graders/loads-cms-entries.md b/evals/routing-cms-entries-1/graders/loads-cms-entries.md new file mode 100644 index 0000000..fd14bdb --- /dev/null +++ b/evals/routing-cms-entries-1/graders/loads-cms-entries.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-entries"' +min: 1 +--- +The agent loads the `cms-entries` skill. diff --git a/evals/routing-cms-entries-1/prompt.md b/evals/routing-cms-entries-1/prompt.md new file mode 100644 index 0000000..39f96d0 --- /dev/null +++ b/evals/routing-cms-entries-1/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-entries-1 +tags: [routing, cms-entries] +max_turns: 3 +timeout_seconds: 180 +--- +How do I paginate through all entries of a content type with the Contentstack CDA? diff --git a/evals/routing-cms-entries-2/graders/loads-cms-entries.md b/evals/routing-cms-entries-2/graders/loads-cms-entries.md new file mode 100644 index 0000000..fd14bdb --- /dev/null +++ b/evals/routing-cms-entries-2/graders/loads-cms-entries.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-entries"' +min: 1 +--- +The agent loads the `cms-entries` skill. diff --git a/evals/routing-cms-entries-2/prompt.md b/evals/routing-cms-entries-2/prompt.md new file mode 100644 index 0000000..fb3988c --- /dev/null +++ b/evals/routing-cms-entries-2/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-entries-2 +tags: [routing, cms-entries] +max_turns: 3 +timeout_seconds: 180 +--- +How does entry versioning work in Contentstack? diff --git a/evals/routing-cms-environments-publishing-1/graders/loads-cms-environments-publishing.md b/evals/routing-cms-environments-publishing-1/graders/loads-cms-environments-publishing.md new file mode 100644 index 0000000..77a1005 --- /dev/null +++ b/evals/routing-cms-environments-publishing-1/graders/loads-cms-environments-publishing.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-environments-publishing"' +min: 1 +--- +The agent loads the `cms-environments-publishing` skill. diff --git a/evals/routing-cms-environments-publishing-1/prompt.md b/evals/routing-cms-environments-publishing-1/prompt.md new file mode 100644 index 0000000..6027a7d --- /dev/null +++ b/evals/routing-cms-environments-publishing-1/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-environments-publishing-1 +tags: [routing, cms-environments-publishing] +max_turns: 3 +timeout_seconds: 180 +--- +My Contentstack content isn't showing on the live site after publishing. What should I check? diff --git a/evals/routing-cms-environments-publishing-2/graders/loads-cms-environments-publishing.md b/evals/routing-cms-environments-publishing-2/graders/loads-cms-environments-publishing.md new file mode 100644 index 0000000..77a1005 --- /dev/null +++ b/evals/routing-cms-environments-publishing-2/graders/loads-cms-environments-publishing.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-environments-publishing"' +min: 1 +--- +The agent loads the `cms-environments-publishing` skill. diff --git a/evals/routing-cms-environments-publishing-2/prompt.md b/evals/routing-cms-environments-publishing-2/prompt.md new file mode 100644 index 0000000..ce51a52 --- /dev/null +++ b/evals/routing-cms-environments-publishing-2/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-environments-publishing-2 +tags: [routing, cms-environments-publishing] +max_turns: 3 +timeout_seconds: 180 +--- +How does the Contentstack Sync API work? diff --git a/evals/routing-cms-graphql-1/graders/loads-cms-graphql.md b/evals/routing-cms-graphql-1/graders/loads-cms-graphql.md new file mode 100644 index 0000000..0757fc2 --- /dev/null +++ b/evals/routing-cms-graphql-1/graders/loads-cms-graphql.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-graphql"' +min: 1 +--- +The agent loads the `cms-graphql` skill. diff --git a/evals/routing-cms-graphql-1/graders/skips-dx-delivery-sdk.md b/evals/routing-cms-graphql-1/graders/skips-dx-delivery-sdk.md new file mode 100644 index 0000000..fd1de2d --- /dev/null +++ b/evals/routing-cms-graphql-1/graders/skips-dx-delivery-sdk.md @@ -0,0 +1,8 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?dx-delivery-sdk"' +min: 0 +max: 0 +--- +The agent doesn't load the neighbouring `dx-delivery-sdk` skill. diff --git a/evals/routing-cms-graphql-1/prompt.md b/evals/routing-cms-graphql-1/prompt.md new file mode 100644 index 0000000..3c7fee3 --- /dev/null +++ b/evals/routing-cms-graphql-1/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-graphql-1 +tags: [routing, cms-graphql] +max_turns: 3 +timeout_seconds: 180 +--- +How do I query referenced entries with the Contentstack GraphQL API? diff --git a/evals/routing-cms-graphql-2/graders/loads-cms-graphql.md b/evals/routing-cms-graphql-2/graders/loads-cms-graphql.md new file mode 100644 index 0000000..0757fc2 --- /dev/null +++ b/evals/routing-cms-graphql-2/graders/loads-cms-graphql.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-graphql"' +min: 1 +--- +The agent loads the `cms-graphql` skill. diff --git a/evals/routing-cms-graphql-2/prompt.md b/evals/routing-cms-graphql-2/prompt.md new file mode 100644 index 0000000..d552ad2 --- /dev/null +++ b/evals/routing-cms-graphql-2/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-graphql-2 +tags: [routing, cms-graphql] +max_turns: 3 +timeout_seconds: 180 +--- +What are the query limits of the Contentstack GraphQL Content Delivery API? diff --git a/evals/routing-cms-json-rte-1/graders/loads-cms-json-rte.md b/evals/routing-cms-json-rte-1/graders/loads-cms-json-rte.md new file mode 100644 index 0000000..1260c10 --- /dev/null +++ b/evals/routing-cms-json-rte-1/graders/loads-cms-json-rte.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-json-rte"' +min: 1 +--- +The agent loads the `cms-json-rte` skill. diff --git a/evals/routing-cms-json-rte-1/graders/skips-cms-entries.md b/evals/routing-cms-json-rte-1/graders/skips-cms-entries.md new file mode 100644 index 0000000..81e07b5 --- /dev/null +++ b/evals/routing-cms-json-rte-1/graders/skips-cms-entries.md @@ -0,0 +1,8 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-entries"' +min: 0 +max: 0 +--- +The agent doesn't load the neighbouring `cms-entries` skill. diff --git a/evals/routing-cms-json-rte-1/prompt.md b/evals/routing-cms-json-rte-1/prompt.md new file mode 100644 index 0000000..dae4215 --- /dev/null +++ b/evals/routing-cms-json-rte-1/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-json-rte-1 +tags: [routing, cms-json-rte] +max_turns: 3 +timeout_seconds: 180 +--- +How do I render Contentstack JSON Rich Text Editor content to HTML in my React app? diff --git a/evals/routing-cms-json-rte-2/graders/loads-cms-json-rte.md b/evals/routing-cms-json-rte-2/graders/loads-cms-json-rte.md new file mode 100644 index 0000000..1260c10 --- /dev/null +++ b/evals/routing-cms-json-rte-2/graders/loads-cms-json-rte.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-json-rte"' +min: 1 +--- +The agent loads the `cms-json-rte` skill. diff --git a/evals/routing-cms-json-rte-2/prompt.md b/evals/routing-cms-json-rte-2/prompt.md new file mode 100644 index 0000000..f17a73b --- /dev/null +++ b/evals/routing-cms-json-rte-2/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-json-rte-2 +tags: [routing, cms-json-rte] +max_turns: 3 +timeout_seconds: 180 +--- +My Contentstack JSON RTE embedded entries aren't rendering. How do I include them? diff --git a/evals/routing-cms-live-preview-visual-editor-support-assistant-1/graders/loads-cms-live-preview-visual-editor-support-assistant.md b/evals/routing-cms-live-preview-visual-editor-support-assistant-1/graders/loads-cms-live-preview-visual-editor-support-assistant.md new file mode 100644 index 0000000..861b0e8 --- /dev/null +++ b/evals/routing-cms-live-preview-visual-editor-support-assistant-1/graders/loads-cms-live-preview-visual-editor-support-assistant.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-live-preview-visual-editor-support-assistant"' +min: 1 +--- +The agent loads the `cms-live-preview-visual-editor-support-assistant` skill. diff --git a/evals/routing-cms-live-preview-visual-editor-support-assistant-1/prompt.md b/evals/routing-cms-live-preview-visual-editor-support-assistant-1/prompt.md new file mode 100644 index 0000000..948d180 --- /dev/null +++ b/evals/routing-cms-live-preview-visual-editor-support-assistant-1/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-live-preview-visual-editor-support-assistant-1 +tags: [routing, cms-live-preview-visual-editor-support-assistant] +max_turns: 3 +timeout_seconds: 180 +--- +Contentstack Live Preview loads but my edits never show up. Help me debug it. diff --git a/evals/routing-cms-live-preview-visual-editor-support-assistant-2/graders/loads-cms-live-preview-visual-editor-support-assistant.md b/evals/routing-cms-live-preview-visual-editor-support-assistant-2/graders/loads-cms-live-preview-visual-editor-support-assistant.md new file mode 100644 index 0000000..861b0e8 --- /dev/null +++ b/evals/routing-cms-live-preview-visual-editor-support-assistant-2/graders/loads-cms-live-preview-visual-editor-support-assistant.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-live-preview-visual-editor-support-assistant"' +min: 1 +--- +The agent loads the `cms-live-preview-visual-editor-support-assistant` skill. diff --git a/evals/routing-cms-live-preview-visual-editor-support-assistant-2/graders/skips-dx-delivery-sdk.md b/evals/routing-cms-live-preview-visual-editor-support-assistant-2/graders/skips-dx-delivery-sdk.md new file mode 100644 index 0000000..fd1de2d --- /dev/null +++ b/evals/routing-cms-live-preview-visual-editor-support-assistant-2/graders/skips-dx-delivery-sdk.md @@ -0,0 +1,8 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?dx-delivery-sdk"' +min: 0 +max: 0 +--- +The agent doesn't load the neighbouring `dx-delivery-sdk` skill. diff --git a/evals/routing-cms-live-preview-visual-editor-support-assistant-2/prompt.md b/evals/routing-cms-live-preview-visual-editor-support-assistant-2/prompt.md new file mode 100644 index 0000000..4e3ce78 --- /dev/null +++ b/evals/routing-cms-live-preview-visual-editor-support-assistant-2/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-live-preview-visual-editor-support-assistant-2 +tags: [routing, cms-live-preview-visual-editor-support-assistant] +max_turns: 3 +timeout_seconds: 180 +--- +We have Contentstack Live Preview working. What do we add for Visual Builder? diff --git a/evals/routing-cms-localization-1/graders/loads-cms-localization.md b/evals/routing-cms-localization-1/graders/loads-cms-localization.md new file mode 100644 index 0000000..e386c18 --- /dev/null +++ b/evals/routing-cms-localization-1/graders/loads-cms-localization.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-localization"' +min: 1 +--- +The agent loads the `cms-localization` skill. diff --git a/evals/routing-cms-localization-1/prompt.md b/evals/routing-cms-localization-1/prompt.md new file mode 100644 index 0000000..be4ac2c --- /dev/null +++ b/evals/routing-cms-localization-1/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-localization-1 +tags: [routing, cms-localization] +max_turns: 3 +timeout_seconds: 180 +--- +How do I set up languages with fallback in Contentstack? diff --git a/evals/routing-cms-localization-2/graders/loads-cms-localization.md b/evals/routing-cms-localization-2/graders/loads-cms-localization.md new file mode 100644 index 0000000..e386c18 --- /dev/null +++ b/evals/routing-cms-localization-2/graders/loads-cms-localization.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-localization"' +min: 1 +--- +The agent loads the `cms-localization` skill. diff --git a/evals/routing-cms-localization-2/prompt.md b/evals/routing-cms-localization-2/prompt.md new file mode 100644 index 0000000..9505223 --- /dev/null +++ b/evals/routing-cms-localization-2/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-localization-2 +tags: [routing, cms-localization] +max_turns: 3 +timeout_seconds: 180 +--- +What's the difference between a localized and an unlocalized entry in Contentstack? diff --git a/evals/routing-cms-releases-1/graders/loads-cms-releases.md b/evals/routing-cms-releases-1/graders/loads-cms-releases.md new file mode 100644 index 0000000..66d3c00 --- /dev/null +++ b/evals/routing-cms-releases-1/graders/loads-cms-releases.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-releases"' +min: 1 +--- +The agent loads the `cms-releases` skill. diff --git a/evals/routing-cms-releases-1/prompt.md b/evals/routing-cms-releases-1/prompt.md new file mode 100644 index 0000000..c28ea40 --- /dev/null +++ b/evals/routing-cms-releases-1/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-releases-1 +tags: [routing, cms-releases] +max_turns: 3 +timeout_seconds: 180 +--- +How do I deploy a campaign of 50 Contentstack entries at once and schedule it? diff --git a/evals/routing-cms-releases-2/graders/loads-cms-releases.md b/evals/routing-cms-releases-2/graders/loads-cms-releases.md new file mode 100644 index 0000000..66d3c00 --- /dev/null +++ b/evals/routing-cms-releases-2/graders/loads-cms-releases.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-releases"' +min: 1 +--- +The agent loads the `cms-releases` skill. diff --git a/evals/routing-cms-releases-2/prompt.md b/evals/routing-cms-releases-2/prompt.md new file mode 100644 index 0000000..1d7c230 --- /dev/null +++ b/evals/routing-cms-releases-2/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-releases-2 +tags: [routing, cms-releases] +max_turns: 3 +timeout_seconds: 180 +--- +What are the limits on Contentstack releases? diff --git a/evals/routing-cms-roles-permissions-1/graders/loads-cms-roles-permissions.md b/evals/routing-cms-roles-permissions-1/graders/loads-cms-roles-permissions.md new file mode 100644 index 0000000..dc7ce6a --- /dev/null +++ b/evals/routing-cms-roles-permissions-1/graders/loads-cms-roles-permissions.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-roles-permissions"' +min: 1 +--- +The agent loads the `cms-roles-permissions` skill. diff --git a/evals/routing-cms-roles-permissions-1/prompt.md b/evals/routing-cms-roles-permissions-1/prompt.md new file mode 100644 index 0000000..04903fd --- /dev/null +++ b/evals/routing-cms-roles-permissions-1/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-roles-permissions-1 +tags: [routing, cms-roles-permissions] +max_turns: 3 +timeout_seconds: 180 +--- +How do I restrict Contentstack editors to only certain content types? diff --git a/evals/routing-cms-roles-permissions-2/graders/loads-cms-roles-permissions.md b/evals/routing-cms-roles-permissions-2/graders/loads-cms-roles-permissions.md new file mode 100644 index 0000000..dc7ce6a --- /dev/null +++ b/evals/routing-cms-roles-permissions-2/graders/loads-cms-roles-permissions.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-roles-permissions"' +min: 1 +--- +The agent loads the `cms-roles-permissions` skill. diff --git a/evals/routing-cms-roles-permissions-2/prompt.md b/evals/routing-cms-roles-permissions-2/prompt.md new file mode 100644 index 0000000..6c9d18a --- /dev/null +++ b/evals/routing-cms-roles-permissions-2/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-roles-permissions-2 +tags: [routing, cms-roles-permissions] +max_turns: 3 +timeout_seconds: 180 +--- +What happens when a Contentstack user has multiple roles? diff --git a/evals/routing-cms-taxonomy-1/graders/loads-cms-taxonomy.md b/evals/routing-cms-taxonomy-1/graders/loads-cms-taxonomy.md new file mode 100644 index 0000000..fd86441 --- /dev/null +++ b/evals/routing-cms-taxonomy-1/graders/loads-cms-taxonomy.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-taxonomy"' +min: 1 +--- +The agent loads the `cms-taxonomy` skill. diff --git a/evals/routing-cms-taxonomy-1/prompt.md b/evals/routing-cms-taxonomy-1/prompt.md new file mode 100644 index 0000000..592c190 --- /dev/null +++ b/evals/routing-cms-taxonomy-1/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-taxonomy-1 +tags: [routing, cms-taxonomy] +max_turns: 3 +timeout_seconds: 180 +--- +Should I use taxonomy or tags for product categories in Contentstack? diff --git a/evals/routing-cms-taxonomy-2/graders/loads-cms-taxonomy.md b/evals/routing-cms-taxonomy-2/graders/loads-cms-taxonomy.md new file mode 100644 index 0000000..fd86441 --- /dev/null +++ b/evals/routing-cms-taxonomy-2/graders/loads-cms-taxonomy.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-taxonomy"' +min: 1 +--- +The agent loads the `cms-taxonomy` skill. diff --git a/evals/routing-cms-taxonomy-2/prompt.md b/evals/routing-cms-taxonomy-2/prompt.md new file mode 100644 index 0000000..42597e2 --- /dev/null +++ b/evals/routing-cms-taxonomy-2/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-taxonomy-2 +tags: [routing, cms-taxonomy] +max_turns: 3 +timeout_seconds: 180 +--- +How do I query Contentstack entries by a taxonomy term and all its child terms? diff --git a/evals/routing-cms-tokens-authentication-1/graders/loads-cms-tokens-authentication.md b/evals/routing-cms-tokens-authentication-1/graders/loads-cms-tokens-authentication.md new file mode 100644 index 0000000..0f6f911 --- /dev/null +++ b/evals/routing-cms-tokens-authentication-1/graders/loads-cms-tokens-authentication.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-tokens-authentication"' +min: 1 +--- +The agent loads the `cms-tokens-authentication` skill. diff --git a/evals/routing-cms-tokens-authentication-1/prompt.md b/evals/routing-cms-tokens-authentication-1/prompt.md new file mode 100644 index 0000000..d0f6ada --- /dev/null +++ b/evals/routing-cms-tokens-authentication-1/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-tokens-authentication-1 +tags: [routing, cms-tokens-authentication] +max_turns: 3 +timeout_seconds: 180 +--- +What's the difference between a Contentstack management token and an authtoken? diff --git a/evals/routing-cms-tokens-authentication-2/graders/loads-cms-tokens-authentication.md b/evals/routing-cms-tokens-authentication-2/graders/loads-cms-tokens-authentication.md new file mode 100644 index 0000000..0f6f911 --- /dev/null +++ b/evals/routing-cms-tokens-authentication-2/graders/loads-cms-tokens-authentication.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-tokens-authentication"' +min: 1 +--- +The agent loads the `cms-tokens-authentication` skill. diff --git a/evals/routing-cms-tokens-authentication-2/prompt.md b/evals/routing-cms-tokens-authentication-2/prompt.md new file mode 100644 index 0000000..5184b5e --- /dev/null +++ b/evals/routing-cms-tokens-authentication-2/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-tokens-authentication-2 +tags: [routing, cms-tokens-authentication] +max_turns: 3 +timeout_seconds: 180 +--- +Which Contentstack token should my frontend app use? diff --git a/evals/routing-cms-variants-personalization-1/graders/loads-cms-variants-personalization.md b/evals/routing-cms-variants-personalization-1/graders/loads-cms-variants-personalization.md new file mode 100644 index 0000000..5af89d6 --- /dev/null +++ b/evals/routing-cms-variants-personalization-1/graders/loads-cms-variants-personalization.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-variants-personalization"' +min: 1 +--- +The agent loads the `cms-variants-personalization` skill. diff --git a/evals/routing-cms-variants-personalization-1/graders/skips-personalize.md b/evals/routing-cms-variants-personalization-1/graders/skips-personalize.md new file mode 100644 index 0000000..67320cb --- /dev/null +++ b/evals/routing-cms-variants-personalization-1/graders/skips-personalize.md @@ -0,0 +1,8 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?personalize"' +min: 0 +max: 0 +--- +The agent doesn't load the neighbouring `personalize` skill. diff --git a/evals/routing-cms-variants-personalization-1/prompt.md b/evals/routing-cms-variants-personalization-1/prompt.md new file mode 100644 index 0000000..576c4c0 --- /dev/null +++ b/evals/routing-cms-variants-personalization-1/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-variants-personalization-1 +tags: [routing, cms-variants-personalization] +max_turns: 3 +timeout_seconds: 180 +--- +What are entry variants and variant groups in Contentstack, and how do I fetch a variant? diff --git a/evals/routing-cms-variants-personalization-2/graders/loads-cms-variants-personalization.md b/evals/routing-cms-variants-personalization-2/graders/loads-cms-variants-personalization.md new file mode 100644 index 0000000..5af89d6 --- /dev/null +++ b/evals/routing-cms-variants-personalization-2/graders/loads-cms-variants-personalization.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-variants-personalization"' +min: 1 +--- +The agent loads the `cms-variants-personalization` skill. diff --git a/evals/routing-cms-variants-personalization-2/prompt.md b/evals/routing-cms-variants-personalization-2/prompt.md new file mode 100644 index 0000000..e0524fa --- /dev/null +++ b/evals/routing-cms-variants-personalization-2/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-variants-personalization-2 +tags: [routing, cms-variants-personalization] +max_turns: 3 +timeout_seconds: 180 +--- +Should I use Contentstack entry variants or separate entries per audience? diff --git a/evals/routing-cms-webhooks-1/graders/loads-cms-webhooks.md b/evals/routing-cms-webhooks-1/graders/loads-cms-webhooks.md new file mode 100644 index 0000000..01cbb86 --- /dev/null +++ b/evals/routing-cms-webhooks-1/graders/loads-cms-webhooks.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-webhooks"' +min: 1 +--- +The agent loads the `cms-webhooks` skill. diff --git a/evals/routing-cms-webhooks-1/prompt.md b/evals/routing-cms-webhooks-1/prompt.md new file mode 100644 index 0000000..b67e854 --- /dev/null +++ b/evals/routing-cms-webhooks-1/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-webhooks-1 +tags: [routing, cms-webhooks] +max_turns: 3 +timeout_seconds: 180 +--- +How do I verify a webhook request really came from Contentstack? diff --git a/evals/routing-cms-webhooks-2/graders/loads-cms-webhooks.md b/evals/routing-cms-webhooks-2/graders/loads-cms-webhooks.md new file mode 100644 index 0000000..01cbb86 --- /dev/null +++ b/evals/routing-cms-webhooks-2/graders/loads-cms-webhooks.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-webhooks"' +min: 1 +--- +The agent loads the `cms-webhooks` skill. diff --git a/evals/routing-cms-webhooks-2/graders/skips-automations.md b/evals/routing-cms-webhooks-2/graders/skips-automations.md new file mode 100644 index 0000000..bf5c129 --- /dev/null +++ b/evals/routing-cms-webhooks-2/graders/skips-automations.md @@ -0,0 +1,8 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?automations"' +min: 0 +max: 0 +--- +The agent doesn't load the neighbouring `automations` skill. diff --git a/evals/routing-cms-webhooks-2/prompt.md b/evals/routing-cms-webhooks-2/prompt.md new file mode 100644 index 0000000..e9a1a08 --- /dev/null +++ b/evals/routing-cms-webhooks-2/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-webhooks-2 +tags: [routing, cms-webhooks] +max_turns: 3 +timeout_seconds: 180 +--- +How do I trigger a site rebuild with a Contentstack webhook when content is published? diff --git a/evals/routing-cms-workflows-1/graders/loads-cms-workflows.md b/evals/routing-cms-workflows-1/graders/loads-cms-workflows.md new file mode 100644 index 0000000..10b8f6a --- /dev/null +++ b/evals/routing-cms-workflows-1/graders/loads-cms-workflows.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-workflows"' +min: 1 +--- +The agent loads the `cms-workflows` skill. diff --git a/evals/routing-cms-workflows-1/prompt.md b/evals/routing-cms-workflows-1/prompt.md new file mode 100644 index 0000000..61ba049 --- /dev/null +++ b/evals/routing-cms-workflows-1/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-workflows-1 +tags: [routing, cms-workflows] +max_turns: 3 +timeout_seconds: 180 +--- +How do I prevent an author from approving their own content in a Contentstack workflow? diff --git a/evals/routing-cms-workflows-2/graders/loads-cms-workflows.md b/evals/routing-cms-workflows-2/graders/loads-cms-workflows.md new file mode 100644 index 0000000..10b8f6a --- /dev/null +++ b/evals/routing-cms-workflows-2/graders/loads-cms-workflows.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-workflows"' +min: 1 +--- +The agent loads the `cms-workflows` skill. diff --git a/evals/routing-cms-workflows-2/prompt.md b/evals/routing-cms-workflows-2/prompt.md new file mode 100644 index 0000000..b41d2ca --- /dev/null +++ b/evals/routing-cms-workflows-2/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-cms-workflows-2 +tags: [routing, cms-workflows] +max_turns: 3 +timeout_seconds: 180 +--- +How do Contentstack publish rules differ from workflows? diff --git a/evals/routing-developer-hub-app-architect-1/graders/loads-developer-hub-app-architect.md b/evals/routing-developer-hub-app-architect-1/graders/loads-developer-hub-app-architect.md new file mode 100644 index 0000000..d65c2d0 --- /dev/null +++ b/evals/routing-developer-hub-app-architect-1/graders/loads-developer-hub-app-architect.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?developer-hub-app-architect"' +min: 1 +--- +The agent loads the `developer-hub-app-architect` skill. diff --git a/evals/routing-developer-hub-app-architect-1/graders/skips-marketplace-apps.md b/evals/routing-developer-hub-app-architect-1/graders/skips-marketplace-apps.md new file mode 100644 index 0000000..e665452 --- /dev/null +++ b/evals/routing-developer-hub-app-architect-1/graders/skips-marketplace-apps.md @@ -0,0 +1,8 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?marketplace-apps"' +min: 0 +max: 0 +--- +The agent doesn't load the neighbouring `marketplace-apps` skill. diff --git a/evals/routing-developer-hub-app-architect-1/prompt.md b/evals/routing-developer-hub-app-architect-1/prompt.md new file mode 100644 index 0000000..0b076b8 --- /dev/null +++ b/evals/routing-developer-hub-app-architect-1/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-developer-hub-app-architect-1 +tags: [routing, developer-hub-app-architect] +max_turns: 3 +timeout_seconds: 180 +--- +Build a private Contentstack app that adds AI suggestions in the entry sidebar. diff --git a/evals/routing-developer-hub-app-architect-2/graders/loads-developer-hub-app-architect.md b/evals/routing-developer-hub-app-architect-2/graders/loads-developer-hub-app-architect.md new file mode 100644 index 0000000..d65c2d0 --- /dev/null +++ b/evals/routing-developer-hub-app-architect-2/graders/loads-developer-hub-app-architect.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?developer-hub-app-architect"' +min: 1 +--- +The agent loads the `developer-hub-app-architect` skill. diff --git a/evals/routing-developer-hub-app-architect-2/prompt.md b/evals/routing-developer-hub-app-architect-2/prompt.md new file mode 100644 index 0000000..c3d6472 --- /dev/null +++ b/evals/routing-developer-hub-app-architect-2/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-developer-hub-app-architect-2 +tags: [routing, developer-hub-app-architect] +max_turns: 3 +timeout_seconds: 180 +--- +Which Contentstack app UI location should I use for a product picker custom field? diff --git a/evals/routing-dx-cli-1/graders/loads-dx-cli.md b/evals/routing-dx-cli-1/graders/loads-dx-cli.md new file mode 100644 index 0000000..1feca6f --- /dev/null +++ b/evals/routing-dx-cli-1/graders/loads-dx-cli.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?dx-cli"' +min: 1 +--- +The agent loads the `dx-cli` skill. diff --git a/evals/routing-dx-cli-1/prompt.md b/evals/routing-dx-cli-1/prompt.md new file mode 100644 index 0000000..e9855e3 --- /dev/null +++ b/evals/routing-dx-cli-1/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-dx-cli-1 +tags: [routing, dx-cli] +max_turns: 3 +timeout_seconds: 180 +--- +How do I export one Contentstack stack and import it into another with the CLI? diff --git a/evals/routing-dx-cli-2/graders/loads-dx-cli.md b/evals/routing-dx-cli-2/graders/loads-dx-cli.md new file mode 100644 index 0000000..1feca6f --- /dev/null +++ b/evals/routing-dx-cli-2/graders/loads-dx-cli.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?dx-cli"' +min: 1 +--- +The agent loads the `dx-cli` skill. diff --git a/evals/routing-dx-cli-2/prompt.md b/evals/routing-dx-cli-2/prompt.md new file mode 100644 index 0000000..6332b7b --- /dev/null +++ b/evals/routing-dx-cli-2/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-dx-cli-2 +tags: [routing, dx-cli] +max_turns: 3 +timeout_seconds: 180 +--- +Bulk publish all blog entries to production with csdx. diff --git a/evals/routing-dx-delivery-sdk-1/graders/loads-dx-delivery-sdk.md b/evals/routing-dx-delivery-sdk-1/graders/loads-dx-delivery-sdk.md new file mode 100644 index 0000000..5f553b3 --- /dev/null +++ b/evals/routing-dx-delivery-sdk-1/graders/loads-dx-delivery-sdk.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?dx-delivery-sdk"' +min: 1 +--- +The agent loads the `dx-delivery-sdk` skill. diff --git a/evals/routing-dx-delivery-sdk-1/prompt.md b/evals/routing-dx-delivery-sdk-1/prompt.md new file mode 100644 index 0000000..0bc736d --- /dev/null +++ b/evals/routing-dx-delivery-sdk-1/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-dx-delivery-sdk-1 +tags: [routing, dx-delivery-sdk] +max_turns: 3 +timeout_seconds: 180 +--- +Write a @contentstack/delivery-sdk query for blog posts with author references included. diff --git a/evals/routing-dx-delivery-sdk-2/graders/loads-dx-delivery-sdk.md b/evals/routing-dx-delivery-sdk-2/graders/loads-dx-delivery-sdk.md new file mode 100644 index 0000000..5f553b3 --- /dev/null +++ b/evals/routing-dx-delivery-sdk-2/graders/loads-dx-delivery-sdk.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?dx-delivery-sdk"' +min: 1 +--- +The agent loads the `dx-delivery-sdk` skill. diff --git a/evals/routing-dx-delivery-sdk-2/prompt.md b/evals/routing-dx-delivery-sdk-2/prompt.md new file mode 100644 index 0000000..a3f5334 --- /dev/null +++ b/evals/routing-dx-delivery-sdk-2/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-dx-delivery-sdk-2 +tags: [routing, dx-delivery-sdk] +max_turns: 3 +timeout_seconds: 180 +--- +Create a TypeScript helper that fetches a Contentstack entry by URL with the Delivery SDK. diff --git a/evals/routing-dx-kickstart-next-1/graders/loads-dx-kickstart-next.md b/evals/routing-dx-kickstart-next-1/graders/loads-dx-kickstart-next.md new file mode 100644 index 0000000..f430785 --- /dev/null +++ b/evals/routing-dx-kickstart-next-1/graders/loads-dx-kickstart-next.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?dx-kickstart-next"' +min: 1 +--- +The agent loads the `dx-kickstart-next` skill. diff --git a/evals/routing-dx-kickstart-next-1/prompt.md b/evals/routing-dx-kickstart-next-1/prompt.md new file mode 100644 index 0000000..8f445ff --- /dev/null +++ b/evals/routing-dx-kickstart-next-1/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-dx-kickstart-next-1 +tags: [routing, dx-kickstart-next] +max_turns: 3 +timeout_seconds: 180 +--- +I'm working in a fork of contentstack/kickstart-next. Where do I add a new environment variable? diff --git a/evals/routing-dx-kickstart-next-2/graders/loads-dx-kickstart-next.md b/evals/routing-dx-kickstart-next-2/graders/loads-dx-kickstart-next.md new file mode 100644 index 0000000..f430785 --- /dev/null +++ b/evals/routing-dx-kickstart-next-2/graders/loads-dx-kickstart-next.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?dx-kickstart-next"' +min: 1 +--- +The agent loads the `dx-kickstart-next` skill. diff --git a/evals/routing-dx-kickstart-next-2/prompt.md b/evals/routing-dx-kickstart-next-2/prompt.md new file mode 100644 index 0000000..d16250a --- /dev/null +++ b/evals/routing-dx-kickstart-next-2/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-dx-kickstart-next-2 +tags: [routing, dx-kickstart-next] +max_turns: 3 +timeout_seconds: 180 +--- +Review this PR to our kickstart-next fork that changes the Contentstack data fetching. diff --git a/evals/routing-dx-mcp-server-1/graders/loads-dx-mcp-server.md b/evals/routing-dx-mcp-server-1/graders/loads-dx-mcp-server.md new file mode 100644 index 0000000..7e71db8 --- /dev/null +++ b/evals/routing-dx-mcp-server-1/graders/loads-dx-mcp-server.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?dx-mcp-server"' +min: 1 +--- +The agent loads the `dx-mcp-server` skill. diff --git a/evals/routing-dx-mcp-server-1/prompt.md b/evals/routing-dx-mcp-server-1/prompt.md new file mode 100644 index 0000000..ab8425a --- /dev/null +++ b/evals/routing-dx-mcp-server-1/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-dx-mcp-server-1 +tags: [routing, dx-mcp-server] +max_turns: 3 +timeout_seconds: 180 +--- +How do I connect Claude Code to my Contentstack stack over MCP? diff --git a/evals/routing-dx-mcp-server-2/graders/loads-dx-mcp-server.md b/evals/routing-dx-mcp-server-2/graders/loads-dx-mcp-server.md new file mode 100644 index 0000000..7e71db8 --- /dev/null +++ b/evals/routing-dx-mcp-server-2/graders/loads-dx-mcp-server.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?dx-mcp-server"' +min: 1 +--- +The agent loads the `dx-mcp-server` skill. diff --git a/evals/routing-dx-mcp-server-2/prompt.md b/evals/routing-dx-mcp-server-2/prompt.md new file mode 100644 index 0000000..8b448fe --- /dev/null +++ b/evals/routing-dx-mcp-server-2/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-dx-mcp-server-2 +tags: [routing, dx-mcp-server] +max_turns: 3 +timeout_seconds: 180 +--- +Should my team use the Contentstack MCP server or MCP Profile Hub? diff --git a/evals/routing-dx-migrate-js-to-ts-sdk-1/graders/loads-dx-migrate-js-to-ts-sdk.md b/evals/routing-dx-migrate-js-to-ts-sdk-1/graders/loads-dx-migrate-js-to-ts-sdk.md new file mode 100644 index 0000000..f5c6a25 --- /dev/null +++ b/evals/routing-dx-migrate-js-to-ts-sdk-1/graders/loads-dx-migrate-js-to-ts-sdk.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?dx-migrate-js-to-ts-sdk"' +min: 1 +--- +The agent loads the `dx-migrate-js-to-ts-sdk` skill. diff --git a/evals/routing-dx-migrate-js-to-ts-sdk-1/graders/skips-dx-delivery-sdk.md b/evals/routing-dx-migrate-js-to-ts-sdk-1/graders/skips-dx-delivery-sdk.md new file mode 100644 index 0000000..fd1de2d --- /dev/null +++ b/evals/routing-dx-migrate-js-to-ts-sdk-1/graders/skips-dx-delivery-sdk.md @@ -0,0 +1,8 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?dx-delivery-sdk"' +min: 0 +max: 0 +--- +The agent doesn't load the neighbouring `dx-delivery-sdk` skill. diff --git a/evals/routing-dx-migrate-js-to-ts-sdk-1/prompt.md b/evals/routing-dx-migrate-js-to-ts-sdk-1/prompt.md new file mode 100644 index 0000000..43951a4 --- /dev/null +++ b/evals/routing-dx-migrate-js-to-ts-sdk-1/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-dx-migrate-js-to-ts-sdk-1 +tags: [routing, dx-migrate-js-to-ts-sdk] +max_turns: 3 +timeout_seconds: 180 +--- +Migrate this Contentstack JavaScript SDK code (Stack, Entry, Query) to the TypeScript Delivery SDK. diff --git a/evals/routing-dx-migrate-js-to-ts-sdk-2/graders/loads-dx-migrate-js-to-ts-sdk.md b/evals/routing-dx-migrate-js-to-ts-sdk-2/graders/loads-dx-migrate-js-to-ts-sdk.md new file mode 100644 index 0000000..f5c6a25 --- /dev/null +++ b/evals/routing-dx-migrate-js-to-ts-sdk-2/graders/loads-dx-migrate-js-to-ts-sdk.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?dx-migrate-js-to-ts-sdk"' +min: 1 +--- +The agent loads the `dx-migrate-js-to-ts-sdk` skill. diff --git a/evals/routing-dx-migrate-js-to-ts-sdk-2/prompt.md b/evals/routing-dx-migrate-js-to-ts-sdk-2/prompt.md new file mode 100644 index 0000000..988d175 --- /dev/null +++ b/evals/routing-dx-migrate-js-to-ts-sdk-2/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-dx-migrate-js-to-ts-sdk-2 +tags: [routing, dx-migrate-js-to-ts-sdk] +max_turns: 3 +timeout_seconds: 180 +--- +What changes when moving from the contentstack npm package to @contentstack/delivery-sdk? diff --git a/evals/routing-launch-platform-1/graders/loads-launch-platform.md b/evals/routing-launch-platform-1/graders/loads-launch-platform.md new file mode 100644 index 0000000..89ac146 --- /dev/null +++ b/evals/routing-launch-platform-1/graders/loads-launch-platform.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?launch-platform"' +min: 1 +--- +The agent loads the `launch-platform` skill. diff --git a/evals/routing-launch-platform-1/prompt.md b/evals/routing-launch-platform-1/prompt.md new file mode 100644 index 0000000..57f2dab --- /dev/null +++ b/evals/routing-launch-platform-1/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-launch-platform-1 +tags: [routing, launch-platform] +max_turns: 3 +timeout_seconds: 180 +--- +How do I deploy my Next.js app to Contentstack Launch? diff --git a/evals/routing-launch-platform-2/graders/loads-launch-platform.md b/evals/routing-launch-platform-2/graders/loads-launch-platform.md new file mode 100644 index 0000000..89ac146 --- /dev/null +++ b/evals/routing-launch-platform-2/graders/loads-launch-platform.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?launch-platform"' +min: 1 +--- +The agent loads the `launch-platform` skill. diff --git a/evals/routing-launch-platform-2/prompt.md b/evals/routing-launch-platform-2/prompt.md new file mode 100644 index 0000000..3cbd346 --- /dev/null +++ b/evals/routing-launch-platform-2/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-launch-platform-2 +tags: [routing, launch-platform] +max_turns: 3 +timeout_seconds: 180 +--- +Should this middleware be a Launch Edge Function or a Cloud Function? diff --git a/evals/routing-lytics-1/graders/loads-lytics.md b/evals/routing-lytics-1/graders/loads-lytics.md new file mode 100644 index 0000000..a998a72 --- /dev/null +++ b/evals/routing-lytics-1/graders/loads-lytics.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?lytics"' +min: 1 +--- +The agent loads the `lytics` skill. diff --git a/evals/routing-lytics-1/prompt.md b/evals/routing-lytics-1/prompt.md new file mode 100644 index 0000000..3335f51 --- /dev/null +++ b/evals/routing-lytics-1/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-lytics-1 +tags: [routing, lytics] +max_turns: 3 +timeout_seconds: 180 +--- +How do I install the Lytics tag on my Next.js site? diff --git a/evals/routing-lytics-2/graders/loads-lytics.md b/evals/routing-lytics-2/graders/loads-lytics.md new file mode 100644 index 0000000..a998a72 --- /dev/null +++ b/evals/routing-lytics-2/graders/loads-lytics.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?lytics"' +min: 1 +--- +The agent loads the `lytics` skill. diff --git a/evals/routing-lytics-2/prompt.md b/evals/routing-lytics-2/prompt.md new file mode 100644 index 0000000..8124f08 --- /dev/null +++ b/evals/routing-lytics-2/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-lytics-2 +tags: [routing, lytics] +max_turns: 3 +timeout_seconds: 180 +--- +How do I delete a user's data from Lytics for a GDPR request? diff --git a/evals/routing-marketplace-apps-1/graders/loads-marketplace-apps.md b/evals/routing-marketplace-apps-1/graders/loads-marketplace-apps.md new file mode 100644 index 0000000..bc4ec9f --- /dev/null +++ b/evals/routing-marketplace-apps-1/graders/loads-marketplace-apps.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?marketplace-apps"' +min: 1 +--- +The agent loads the `marketplace-apps` skill. diff --git a/evals/routing-marketplace-apps-1/graders/skips-asset-management.md b/evals/routing-marketplace-apps-1/graders/skips-asset-management.md new file mode 100644 index 0000000..47e2524 --- /dev/null +++ b/evals/routing-marketplace-apps-1/graders/skips-asset-management.md @@ -0,0 +1,8 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?asset-management"' +min: 0 +max: 0 +--- +The agent doesn't load the neighbouring `asset-management` skill. diff --git a/evals/routing-marketplace-apps-1/graders/skips-developer-hub-app-architect.md b/evals/routing-marketplace-apps-1/graders/skips-developer-hub-app-architect.md new file mode 100644 index 0000000..8276bec --- /dev/null +++ b/evals/routing-marketplace-apps-1/graders/skips-developer-hub-app-architect.md @@ -0,0 +1,8 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?developer-hub-app-architect"' +min: 0 +max: 0 +--- +The agent doesn't load the neighbouring `developer-hub-app-architect` skill. diff --git a/evals/routing-marketplace-apps-1/prompt.md b/evals/routing-marketplace-apps-1/prompt.md new file mode 100644 index 0000000..474365b --- /dev/null +++ b/evals/routing-marketplace-apps-1/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-marketplace-apps-1 +tags: [routing, marketplace-apps] +max_turns: 3 +timeout_seconds: 180 +--- +Which Contentstack Marketplace app should I use to connect Bynder to my entries? diff --git a/evals/routing-marketplace-apps-2/graders/loads-marketplace-apps.md b/evals/routing-marketplace-apps-2/graders/loads-marketplace-apps.md new file mode 100644 index 0000000..bc4ec9f --- /dev/null +++ b/evals/routing-marketplace-apps-2/graders/loads-marketplace-apps.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?marketplace-apps"' +min: 1 +--- +The agent loads the `marketplace-apps` skill. diff --git a/evals/routing-marketplace-apps-2/graders/skips-automations.md b/evals/routing-marketplace-apps-2/graders/skips-automations.md new file mode 100644 index 0000000..bf5c129 --- /dev/null +++ b/evals/routing-marketplace-apps-2/graders/skips-automations.md @@ -0,0 +1,8 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?automations"' +min: 0 +max: 0 +--- +The agent doesn't load the neighbouring `automations` skill. diff --git a/evals/routing-marketplace-apps-2/prompt.md b/evals/routing-marketplace-apps-2/prompt.md new file mode 100644 index 0000000..a0fe5ea --- /dev/null +++ b/evals/routing-marketplace-apps-2/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-marketplace-apps-2 +tags: [routing, marketplace-apps] +max_turns: 3 +timeout_seconds: 180 +--- +How do I install and configure the Algolia app from the Contentstack Marketplace? diff --git a/evals/routing-migration-companion-1/graders/loads-migration-companion.md b/evals/routing-migration-companion-1/graders/loads-migration-companion.md new file mode 100644 index 0000000..020d6da --- /dev/null +++ b/evals/routing-migration-companion-1/graders/loads-migration-companion.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?migration-companion"' +min: 1 +--- +The agent loads the `migration-companion` skill. diff --git a/evals/routing-migration-companion-1/prompt.md b/evals/routing-migration-companion-1/prompt.md new file mode 100644 index 0000000..97d4800 --- /dev/null +++ b/evals/routing-migration-companion-1/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-migration-companion-1 +tags: [routing, migration-companion] +max_turns: 3 +timeout_seconds: 180 +--- +Migrate our Contentful space to Contentstack. diff --git a/evals/routing-migration-companion-2/graders/loads-migration-companion.md b/evals/routing-migration-companion-2/graders/loads-migration-companion.md new file mode 100644 index 0000000..020d6da --- /dev/null +++ b/evals/routing-migration-companion-2/graders/loads-migration-companion.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?migration-companion"' +min: 1 +--- +The agent loads the `migration-companion` skill. diff --git a/evals/routing-migration-companion-2/prompt.md b/evals/routing-migration-companion-2/prompt.md new file mode 100644 index 0000000..e1f2f26 --- /dev/null +++ b/evals/routing-migration-companion-2/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-migration-companion-2 +tags: [routing, migration-companion] +max_turns: 3 +timeout_seconds: 180 +--- +Help me port my site from Contentful to Contentstack, content and code. diff --git a/evals/routing-personalize-1/graders/loads-personalize.md b/evals/routing-personalize-1/graders/loads-personalize.md new file mode 100644 index 0000000..6ba9e7a --- /dev/null +++ b/evals/routing-personalize-1/graders/loads-personalize.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?personalize"' +min: 1 +--- +The agent loads the `personalize` skill. diff --git a/evals/routing-personalize-1/graders/skips-cms-variants-personalization.md b/evals/routing-personalize-1/graders/skips-cms-variants-personalization.md new file mode 100644 index 0000000..9cd9f95 --- /dev/null +++ b/evals/routing-personalize-1/graders/skips-cms-variants-personalization.md @@ -0,0 +1,8 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?cms-variants-personalization"' +min: 0 +max: 0 +--- +The agent doesn't load the neighbouring `cms-variants-personalization` skill. diff --git a/evals/routing-personalize-1/prompt.md b/evals/routing-personalize-1/prompt.md new file mode 100644 index 0000000..43d763c --- /dev/null +++ b/evals/routing-personalize-1/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-personalize-1 +tags: [routing, personalize] +max_turns: 3 +timeout_seconds: 180 +--- +How do I add Contentstack Personalize to my Next.js site with edge middleware? diff --git a/evals/routing-personalize-2/graders/loads-personalize.md b/evals/routing-personalize-2/graders/loads-personalize.md new file mode 100644 index 0000000..6ba9e7a --- /dev/null +++ b/evals/routing-personalize-2/graders/loads-personalize.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?personalize"' +min: 1 +--- +The agent loads the `personalize` skill. diff --git a/evals/routing-personalize-2/prompt.md b/evals/routing-personalize-2/prompt.md new file mode 100644 index 0000000..76fa82c --- /dev/null +++ b/evals/routing-personalize-2/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-personalize-2 +tags: [routing, personalize] +max_turns: 3 +timeout_seconds: 180 +--- +Should this be a segmented experience or an A/B test in Contentstack Personalize? diff --git a/evals/routing-studio-1/graders/loads-studio.md b/evals/routing-studio-1/graders/loads-studio.md new file mode 100644 index 0000000..99d7be5 --- /dev/null +++ b/evals/routing-studio-1/graders/loads-studio.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?studio"' +min: 1 +--- +The agent loads the `studio` skill. diff --git a/evals/routing-studio-1/prompt.md b/evals/routing-studio-1/prompt.md new file mode 100644 index 0000000..2432da7 --- /dev/null +++ b/evals/routing-studio-1/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-studio-1 +tags: [routing, studio] +max_turns: 3 +timeout_seconds: 180 +--- +How do I build a page composition in Contentstack Studio? diff --git a/evals/routing-studio-2/graders/loads-studio.md b/evals/routing-studio-2/graders/loads-studio.md new file mode 100644 index 0000000..99d7be5 --- /dev/null +++ b/evals/routing-studio-2/graders/loads-studio.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Skill +input_match: '"skill"\s*:\s*"(?:[\w-]+:)?studio"' +min: 1 +--- +The agent loads the `studio` skill. diff --git a/evals/routing-studio-2/prompt.md b/evals/routing-studio-2/prompt.md new file mode 100644 index 0000000..2b2d51d --- /dev/null +++ b/evals/routing-studio-2/prompt.md @@ -0,0 +1,7 @@ +--- +name: routing-studio-2 +tags: [routing, studio] +max_turns: 3 +timeout_seconds: 180 +--- +Register my React components with the Contentstack Studio SDK. diff --git a/gemini-extension.json b/gemini-extension.json index 57ac9e7..290a13a 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,5 +1,10 @@ { "name": "contentstack-skills", - "version": "0.3.0", - "description": "Contentstack agent skills for CMS modeling, Delivery SDK, Live Preview, Studio, Brand Kit, Developer Hub apps, Launch, and migrations." + "version": "0.4.1", + "description": "Contentstack agent skills plus the hosted Contentstack MCP server: CMS modeling and APIs, Delivery SDK and GraphQL, Live Preview, Studio, Brand Kit, Personalize, Lytics, Launch, Automate, Developer Hub and Marketplace apps, the CLI, and migrations.", + "mcpServers": { + "contentstack": { + "httpUrl": "https://mcp-profile-hub.contentstack.com/api/mcp" + } + } } diff --git a/mcp.json b/mcp.json new file mode 100644 index 0000000..f1b93c1 --- /dev/null +++ b/mcp.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", + "mcpServers": { + "contentstack": { + "type": "streamable-http", + "url": "https://mcp-profile-hub.contentstack.com/api/mcp" + } + } +} diff --git a/plugin.json b/plugin.json index 03de702..da3be80 100644 --- a/plugin.json +++ b/plugin.json @@ -1,8 +1,8 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "contentstack-skills", - "version": "0.3.0", - "description": "Contentstack agent skills for CMS modeling, Delivery SDK, Live Preview, Studio, Brand Kit, Developer Hub apps, Launch, and migrations.", + "version": "0.4.1", + "description": "Contentstack agent skills plus the hosted Contentstack MCP server: CMS modeling and APIs, Delivery SDK and GraphQL, Live Preview, Studio, Brand Kit, Personalize, Lytics, Launch, Automate, Developer Hub and Marketplace apps, the CLI, and migrations.", "author": { "name": "Contentstack", "url": "https://www.contentstack.com" }, "homepage": "https://developers.contentstack.com", "repository": "https://github.com/contentstack/contentstack-agent-skills", diff --git a/scripts/validate.mjs b/scripts/validate.mjs index 24b17ce..4a76bb2 100644 --- a/scripts/validate.mjs +++ b/scripts/validate.mjs @@ -23,6 +23,8 @@ function markdownFiles(dir) { // --- Skills --------------------------------------------------------------- const NAME_RE = /^(?!.*--)[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/; +const SECTIONS = ["When to use", "Before you answer", "Instructions", "MCP tools", "Output", "Security", "References", "Examples"]; +const REQUIRED_SECTIONS = ["When to use", "Instructions", "MCP tools", "Security"]; const skillsDir = join(root, "skills"); const slugs = readdirSync(skillsDir).filter((d) => statSync(join(skillsDir, d)).isDirectory()); @@ -68,6 +70,16 @@ for (const slug of slugs) { else if (description.length > 1024) fail(`skills/${slug}: description is ${description.length} chars (max 1024)`); else if (!/\bUse when\b/.test(description)) fail(`skills/${slug}: description should say "Use when ..." so agents know when to load it`); + // Body sections follow docs/contributing/skill-template.md: required ones present, in order, nothing else. + const headings = [...withoutCode(text).matchAll(/^## (.+)$/gm)].map((m) => m[1].trim()); + const unknown = headings.filter((h) => !SECTIONS.includes(h)); + if (unknown.length) fail(`skills/${slug}/SKILL.md: sections not in the template: ${unknown.map((h) => `"${h}"`).join(", ")}`); + for (const h of REQUIRED_SECTIONS) if (!headings.includes(h)) fail(`skills/${slug}/SKILL.md: missing "## ${h}" section`); + const known = headings.filter((h) => SECTIONS.includes(h)); + if (known.some((h, i) => i && SECTIONS.indexOf(h) <= SECTIONS.indexOf(known[i - 1]))) { + fail(`skills/${slug}/SKILL.md: sections out of order or repeated (expected ${SECTIONS.join(", ")})`); + } + const security = withoutCode(text).split(/^## Security\b.*$/m)[1]?.split(/^## /m)[0]; if (security === undefined) fail(`skills/${slug}/SKILL.md: missing the mandatory "## Security" section`); else { diff --git a/skills/asset-management/SKILL.md b/skills/asset-management/SKILL.md index 4ce64da..b7a326f 100644 --- a/skills/asset-management/SKILL.md +++ b/skills/asset-management/SKILL.md @@ -6,65 +6,38 @@ allowed-tools: Read Grep Glob # Asset Management (Contentstack Assets DAM) -## Description +Advise on Contentstack Assets, the standalone DAM product (also called "Asset Management" in some docs): structuring spaces and workspaces, modeling metadata so assets are findable, using AI features safely, keeping the library free of duplicates, and knowing what can be recovered after a delete or replace. -Advise on Contentstack Assets, the standalone DAM product (also called "Asset Management" in some docs). Cover spaces and workspaces, folders, roles, asset types and metadata, localization, filters and saved views, AI-assisted metadata, duplicate management, versions, trash and restore, visual markups, and CMS integration. +## When to use -## When to Use +- Organizing assets in spaces, workspaces and folders. +- Modeling asset types and metadata, and searching by user-defined fields. +- Localization and saved views. +- AI tags and alt text. +- Finding and removing duplicates. +- Versions, trash and restore. +- Visual markups. +- Using Assets with a CMS stack. -Use when a user asks about the Contentstack Assets DAM: organizing assets in spaces, workspaces and folders, modeling asset metadata, AI tags and alt text, finding and removing duplicates, versioning, restoring deleted assets, visual markups, or using Assets with a CMS stack. +Use another skill for: -Do not use for the CMS stack asset library (stack-level uploads, folders, Image Delivery API, publishing to environments). Route those questions to `cms-assets`. If it is unclear which one the user means, ask whether they work in an Assets space or in the asset library inside a stack. +- The CMS stack asset library (stack-level uploads, folders, Image Delivery API, publishing to environments) → `cms-assets` +- Roles in a CMS stack → `cms-roles-permissions` +- Choosing a third-party DAM integration from the Marketplace → `marketplace-apps` -## User Problem +If it is unclear which product the user means, ask whether they work in an Assets space or in the asset library inside a stack. -Teams adopting Contentstack Assets need to structure spaces and workspaces sensibly, model metadata so assets are findable, use AI features safely, keep the library free of duplicates, and understand what is and is not recoverable when assets are deleted or replaced. +## Before you answer -## Success Criteria +Confirm which product the user means (Assets space or CMS stack library), then find out the organization's shape (brands, regions, teams) and number of spaces, the asset types and metadata the team needs, and the specific task or problem (duplicates, restore, versions, AI settings, markups). -- Distinguish the Assets DAM from the CMS asset library. -- Use the product's own terminology: space, workspace, main workspace, asset type, system metadata, user-defined metadata, duplicate set, visual markup. -- State documented behavior and limits accurately, and say plainly when something is not documented. -- Warn about irreversible or hard-to-reverse actions before the user takes them. - -## Expected Inputs - -- Which product the user means (Assets space or CMS stack library) -- Organization shape (brands, regions, teams) and number of spaces -- Asset types and metadata the team needs -- The specific task or problem (duplicates, restore, versions, AI settings, markups) - -## Expected Outputs - -- Space, workspace and folder structure recommendations -- Asset type and metadata modeling guidance -- Step-by-step UI paths for the task -- Gotchas and irreversible-action warnings -- Pointers to the official docs for anything not covered here - -## Example User Requests - -- How should I split my brands and regions across spaces and workspaces? -- How do I add custom fields to my images? -- How do I add a language to a workspace? -- Can I save a set of filters and reuse it? -- How do I search assets by a custom field value? -- Can Assets tag my images and write alt text automatically? -- How do I find and remove duplicate images? -- I deleted a folder by mistake. Can I get it back? -- How do I roll back to an earlier version of an asset? -- How do I add hotspots to a product image? -- How do I use assets from a space in my CMS entries? - -## Workflow Summary +## Instructions 1. Confirm the user means the Assets DAM, not the CMS asset library. 2. Identify the topic: structure, metadata, AI, duplicates, versions, trash, markups, or CMS integration. -3. Answer with documented behavior using the product's terminology. +3. Answer with documented behavior using the product's terminology: space, workspace, main workspace, asset type, system metadata, user-defined metadata, duplicate set, visual markup. 4. Flag limits and irreversible actions. -5. Link to the relevant docs page for anything undocumented or likely to change. - -## Instructions +5. Say plainly when something is not documented, and link to the relevant docs page for anything undocumented or likely to change. ### Spaces and workspaces @@ -90,11 +63,11 @@ Teams adopting Contentstack Assets need to structure spaces and workspaces sensi - Metadata comes in layers: **system metadata** (read-only: file name, UID, URLs, MIME type, size, created and modified details), **general metadata**, and **user-defined metadata** from custom fields. - Each **asset type** maps to one MIME type and one file extension. On upload, a matching type applies automatically and its fields appear. System types cover images, audio, video, documents and archives, and you can add custom types. -- User-defined fields are reusable across asset types. The currently supported field types are single line text, number, select and group. The product docs have listed this set differently over time, so point the user to the Asset Modeling docs if they need a type that isn't on this list. +- User-defined fields are reusable across asset types. The Asset Modeling docs list single line and multi line textbox, link, select, number, date, boolean and group, but state that only single line, number and group are currently supported. Point the user to the Asset Modeling docs before promising any other type. - Mandatory fields must be filled before an upload or save can finish. Use them sparingly so uploads are not blocked on optional information. - Edit metadata for an individual asset in the configurable metadata panel. - User-defined fields exist only in the Assets (DAM) product. The CMS stack asset library and its Management API don't read or update them, so direct any request to read or write custom metadata from code to the Assets (DAM) API. See the Assets API reference at https://www.contentstack.com/docs/developers/apis/assets-api for the current endpoints. -- **Filters and saved views:** User-defined fields can be used as filters when browsing assets, so well-modeled fields make the library searchable. A filter combination can be saved as a **saved view** and reused later instead of rebuilding the filters each time. Advise modeling fields with filtering in mind, for example a select field for campaign or usage rights. +- **Filters and saved views:** User-defined fields can be used as filters when browsing assets, so well-modeled fields make the library searchable. A filter combination can be saved as a **saved view** and reused later instead of rebuilding the filters each time. Advise modeling fields with filtering in mind, for example a single line field for campaign or usage rights (or a select field once select is supported). ### Localization @@ -135,6 +108,7 @@ Example request body: ### AI tags, alt text and colors - In the metadata panel, **Suggest Tags with AI** and **Suggest Description with AI** generate suggestions. AI can also extract dominant colors, which powers color-based filtering. Suggestions can be wrong, so review them before saving. +- The product also has AI-powered semantic (natural-language) search and reverse image search, depending on plan and rollout. They are not in the public docs yet, so confirm availability in the user's space before relying on them. - **Auto-populate at upload** is configured in Space Settings > AI Settings. Enable the AI Tags and Alt Text toggles, set Advanced Properties, and click Save Properties. - Applies to images and to new uploads only. Existing assets are not enriched, and replacing a file does not trigger it. - Values are written into version 1. No extra version is created, nothing waits for review, and uploads are not slowed down. @@ -187,32 +161,26 @@ Example request body: - `visual_markups` is an empty array when an asset has none. A hotspot returns x and y, and a bounding box also returns width and height. - Do not state CDN host names, URL formats or branch behavior for Assets as fact unless the user's docs confirm them. -## Output Format +## MCP tools -Be concise and practical. Use the product's terms. Give UI paths as `Menu > Item`. Use short bullets for steps, limits and gotchas. When something is undocumented or uncertain, say so and link to the docs instead of guessing. +No Contentstack MCP tool covers the Assets DAM (spaces, workspaces, asset types, duplicates, markups). Use the Assets UI or the Assets API. -## Tooling Notes +The `cma` asset tools (`get_all_assets`, `get_a_single_asset`, `publish_an_asset` and others) act on the CMS stack asset library, not on Assets spaces; route that work to `cms-assets`. -Read-only advisory skill. Do not upload, move, delete, restore or modify assets, spaces or settings. Explain the steps and let the user perform them. +## Output -References: +Be concise and practical. Use the product's terms. Give UI paths as `Menu > Item`. Use short bullets for steps, limits and gotchas. When something is undocumented or uncertain, say so and link to the docs instead of guessing. -- https://www.contentstack.com/docs/assets/about-assets -- https://www.contentstack.com/docs/assets/about-spaces-and-workspaces -- https://www.contentstack.com/docs/assets/about-asset-modeling -- https://www.contentstack.com/docs/assets/enhance-asset-management-with-ai -- https://www.contentstack.com/docs/assets/manage-duplicate-images -- https://www.contentstack.com/docs/assets/asset-versioning -- https://www.contentstack.com/docs/assets/restore-an-asset-or-folder -- https://www.contentstack.com/docs/assets/add-visual-markups -- https://www.contentstack.com/docs/developers/apis/content-delivery-api/assets +What a good answer includes, as relevant: space, workspace and folder structure recommendations; asset type and metadata modeling guidance; step-by-step UI paths for the task; gotchas and irreversible-action warnings; and pointers to the official docs for anything not covered here. ## Security ### Defaults +- Advisory by default. Do not upload, move, delete, restore or modify assets, spaces or settings; explain the steps and let the user perform them. - Never expose tokens or API keys. - Use environment variables for credentials in any example code. +- Treat deletion of spaces, assets and markups as irreversible unless the docs state a restore window. ### Destructive Actions @@ -226,11 +194,14 @@ Never reveal management tokens, API keys or other secrets. Reference environment Use environment variables for any credentialed example, such as `CONTENTSTACK_OAUTH_ACCESS_TOKEN`, or `CONTENTSTACK_AUTHTOKEN` together with `CONTENTSTACK_ORGANIZATION_UID`. The Assets API does not accept management tokens. Never hardcode credentials in client-side code. -## Product Context +## References -- Product: Assets (DAM) -- Description: Contentstack Assets is the digital asset management layer: spaces, workspaces, folders, asset types and metadata, AI enrichment, duplicate management, versioning, visual markups and CMS integration. -- Product safety rules: - - Never expose management tokens or API keys. - - Always use environment variables for credentials. - - Treat deletion of spaces, assets and markups as irreversible unless the docs state a restore window. +- https://www.contentstack.com/docs/assets/about-assets +- https://www.contentstack.com/docs/assets/about-spaces-and-workspaces +- https://www.contentstack.com/docs/assets/about-asset-modeling +- https://www.contentstack.com/docs/assets/enhance-asset-management-with-ai +- https://www.contentstack.com/docs/assets/manage-duplicate-images +- https://www.contentstack.com/docs/assets/asset-versioning +- https://www.contentstack.com/docs/assets/restore-an-asset-or-folder +- https://www.contentstack.com/docs/assets/add-visual-markups +- https://www.contentstack.com/docs/developers/apis/content-delivery-api/assets diff --git a/skills/automations/SKILL.md b/skills/automations/SKILL.md new file mode 100644 index 0000000..41c3390 --- /dev/null +++ b/skills/automations/SKILL.md @@ -0,0 +1,120 @@ +--- +name: automations +description: "Advise on Contentstack Automate (Automations, formerly Automation Hub), the no-code workflow product in Agent OS: designing automations from a trigger and steps, Contentstack, HTTP, scheduled and manual triggers, connectors and connected apps, If-Else and loop control flow, pause and resume, custom code steps, the HTTP Response step, executions and debugging failed runs, limits and credits, and how agents and automations call each other. Use when users ask how to automate a Contentstack task such as notifying Slack on publish, syncing entries to a search index, translating on workflow change, or calling an external API; why an automation did not run or failed; or which trigger, connector or step to use. For CMS webhook configuration itself, use cms-webhooks." +allowed-tools: Read Grep Glob +--- + +# Automations + +Advise on Contentstack Automate (Automations, formerly Automation Hub), the no-code workflow product in Agent OS: triggers, steps and connectors, control flow, custom code, HTTP responses, executions and debugging, limits, and the bridge between automations and agents. + +## When to use + +- Automating a Contentstack or cross-system task: publish notifications, search indexing, translation, approvals, data syncs, calling an external API. +- Choosing a trigger, connector or step. +- Working out why an automation didn't run or failed. +- Connecting automations with agents. + +Use another skill for: + +- Configuring CMS webhooks that point at your own receiver → `cms-webhooks` +- Workflow stages and publish rules inside the CMS → `cms-workflows` +- Exposing automations to AI clients through MCP Profile Hub → `dx-mcp-server` +- Ready-made Marketplace integrations such as search or translation apps → `marketplace-apps` + +## Before you answer + +Find out the task to automate and the systems involved, what starts it (content event, HTTP call, schedule, manual), the data the steps need and where it comes from, and for failures the execution status and the failing step's output. + +## Instructions + +1. Clarify the outcome and the event that should start it. +2. Pick one trigger, then the connector actions in order, keeping the chain short and readable. +3. Add control flow only where the logic needs it. +4. Use a custom code step only when no connector action fits. +5. Activate, test, and debug from the execution record, not guesses. State limits and unshipped features honestly. + + +### Building blocks + +An automation is one trigger plus an ordered list of steps. Each step usually runs one connector action and can use outputs from earlier steps with `{{step.path}}` references. A new automation is a draft and runs only after it is activated. An active automation can't be edited: switch it off, change it, and switch it on again. + +### Triggers + +- **Contentstack (CMS) events:** pick the stack and the event, such as an entry being published. The automation registers its trigger URL when activated. Events that arrive while it is inactive are accepted but nothing runs. +- **HTTP:** an incoming request to the automation's trigger URL starts a run. The URL has no documented authentication, so treat it as a secret, and check a shared header or token in an early If-Else step when the caller can send one. +- **Schedule:** runs on a timetable for syncs and reports. +- **Manual:** a person or an agent starts it on demand. + +### Connectors and connected apps + +Connectors package actions for outside services: Contentstack products, Slack, Microsoft Teams, Jira, Salesforce, AWS Lambda, translation and search services, email, HTTP and more. Saved credentials live in **Connected apps**, which shows which automations use each connection. Reauthorize expired OAuth connections there. A connection can't be deleted while an active automation uses it. + +### Control flow + +- **If-Else:** route on a condition; the branches merge afterwards. +- **Loop:** repeat steps for each item in a list from an earlier step. Loops have a per-plan iteration cap; steps over the cap are skipped. +- **Pause:** stop and wait for an approval, a callback or a timed event, then resume. A paused run can be resumed a limited number of times and carries a limited payload. + +### Custom code + +Use a code step when no connector action fits. It runs JavaScript in an isolated sandbox with no access to the host process or other runs. Only the return value is passed on, and there is a short time limit (about 5 seconds), so keep it to data shaping and light logic. Do heavy work in an external service through the HTTP connector. Code steps count against a monthly allowance; a step over it is skipped. + +### HTTP responses + +For HTTP-triggered automations, a **Response** step returns a reply to the caller. There can be only one, and it must be the last top-level step, outside loops and branches. Use it when the automation acts as a small API. + +### Executions and debugging + +Each run creates an execution with a status (pending, queued, running, paused, success, partially-executed, failed, rejected) and per-step outputs. When something goes wrong: + +1. Check the automation is active and the trigger points at the right stack and event. +2. Open Executions and find the run. No run means the trigger didn't fire or the automation was inactive. +3. Read the first failing step's input and output, then fix the mapping, credentials or connector settings. +4. Re-run the failed execution once the cause is fixed. + +Password fields are masked in logs. Runs over the organization's per-minute limit are rejected with 429. + +### Limits and credits + +Limits depend on the plan. Typical defaults: about 15 steps per automation (the trigger counts), 100 loop iterations, 1 MB output per step, and an HTTP connector cap of 5 minutes or 2 MB per response. Runs with AI work use AI credits reserved before the run starts; a run without enough credits is refused. Check the organization's plan for exact values. + +### Agents and automations + +Agents (Polaris and Custom Agents) can start an automation as a tool call, so a conversation can trigger an existing workflow. Running a custom agent as a step inside an automation is in progress and may not be available. Building an automation by prompting Polaris on the canvas is not shipped yet; don't present it as available. + +## MCP tools + +When the Contentstack MCP server is connected with the `automations` group, these tools act on Automate: + +- Inspect: `list_automation_projects`, `get_automation_project`, `list_automations`, `get_automation` (optionally with its steps and triggers) (`automations`) +- Change: `create_automation_project`, `update_automation_project`, `set_automation_active`, `trigger_automation` (runs an automation with an HTTP trigger) (`automations`) +- Delete: `delete_automation_project` (`automations`) + +There are no tools for building steps, editing connections or reading executions; use the Automate UI for those. When debugging, read the automation with `get_automation` to check it is active and see its trigger, then check Executions. MCP Profile Hub can also expose active HTTP-triggered automations as tools of their own. + +## Output + +Lead with the recommended design as a numbered trigger-and-steps outline. Show `{{step.path}}` mappings and code snippets only where needed. For debugging, name the execution status and the failing step first. + +## Security + +### Defaults + +- Advisory by default. Use the MCP tools above only when the user asks for the change. +- Never expose connector credentials, OAuth tokens, API keys or trigger URLs. +- Store service credentials in Connected apps, never inline in steps or code. +- Validate input from HTTP triggers before acting on it. +- Don't present unshipped features as available. + +### Destructive Actions + +Before deleting automation projects, activating or deactivating an automation, or running one, name the project and automation and what the run will change, and wait for confirmation. Before suggesting steps that delete or overwrite content in Contentstack or other systems, state what will change and recommend testing on a non-production stack. + +### Secrets + +Never print or echo secrets, trigger URLs or masked values. Use placeholders in examples. + +### Environment Variables + +Use placeholder names for any credential in examples. Keep secrets in Connected apps or project variables, not in code steps. diff --git a/skills/brand-kit-assistant/SKILL.md b/skills/brand-kit-assistant/SKILL.md index 0b798d4..53944d4 100644 --- a/skills/brand-kit-assistant/SKILL.md +++ b/skills/brand-kit-assistant/SKILL.md @@ -1,71 +1,41 @@ --- name: brand-kit-assistant -description: "Advise users on Contentstack Brand Kit concepts, setup, governance, and on-brand AI generation. Route API-specific tasks to the right Brand Kit capability or reference skill, and enforce brand and safety rules before presenting anything as ready. Use when users ask about Contentstack Brand Kit, Voice Profiles, Knowledge Vault, brand voice, tone, style rules, on-brand AI generation, setup, governance, or API usage." +description: "Advise users on Contentstack Brand Kit concepts, setup, governance, and on-brand AI generation. Route API-specific tasks to the right Brand Kit capability or reference skill, and enforce brand and safety rules before presenting anything as ready. Use when users ask about Contentstack Brand Kit, Voice Profiles, Knowledge Vault, brand voice, tone, style rules, on-brand AI generation, setup, governance, or API usage. For connecting an agent to Brand Kit over MCP, use dx-mcp-server." --- # Brand Kit Assistant -## Description +The entry point for Contentstack Brand Kit: how Brand Kit, Voice Profiles and Knowledge Vault fit together, which workflow or API to use, and keeping generated content aligned with brand identity and approved source material. -Advise users on Contentstack Brand Kit concepts, setup, governance, and on-brand AI generation. Route API-specific tasks to the right Brand Kit capability or reference skill, and enforce brand and safety rules before presenting anything as ready. +## When to use -## When to Use +- Explaining Brand Kit, Voice Profiles and Knowledge Vault. +- Creating a Brand Kit from a website or file. +- Tuning a Voice Profile to match brand voice, tone and style rules. +- Keeping AI-generated content on-brand. +- Calling the Brand Kit Management, Knowledge Vault or Generative AI APIs. -Use when users ask about Contentstack Brand Kit, Voice Profiles, Knowledge Vault, brand voice, tone, style rules, on-brand AI generation, setup, governance, or API usage. Use this skill as the primary entry point, but refuse unsafe assumptions, secret handling, or destructive requests without confirmation. +Refuse unsafe assumptions, secret handling, or destructive requests without confirmation. -## User Problem +Use another skill for: -Users need one reliable entry point for Brand Kit. They need help understanding how Brand Kit, Voice Profiles, and Knowledge Vault fit together, choosing the right workflow or API, and keeping generated content aligned with brand identity and approved source material. +- Connecting an AI agent to Brand Kit over MCP → `dx-mcp-server` +- Building a Developer Hub app that uses Brand Kit → `developer-hub-app-architect` +- Automate workflows that call Brand Kit → `automations` -## Success Criteria +## Before you answer -- Explain Brand Kit, Voice Profiles, and Knowledge Vault accurately. -- Ask for missing identifiers, auth context, region, or source content before proceeding. -- Route API-specific work to the correct reference skill when available. -- Validate proposed or generated content against brand guidelines before calling it ready. -- Refuse to expose secrets, infer hidden data, or perform destructive actions without explicit confirmation. +Find out the user's goal, then only the inputs it needs: Brand Kit UID, organization UID, auth context (authtoken), region or base URL, the kit version (v1 or v2), Voice Profile details or a generation prompt, Knowledge Vault source content or retrieval context, a website URL or file, and brand voice, tone and style preferences. If required identifiers, auth, brand context, region, or source content are missing, ask concise follow-up questions. Do not guess or fabricate values. -## Expected Inputs - -- User goal or question -- Brand Kit UID -- Organization UID for Brand Kit management -- Authtoken -- Region or base URL context -- Voice Profile details or generation prompt -- Knowledge Vault source content or retrieval context -- Website URL or file upload context -- Brand voice, tone, and style preferences - -## Expected Outputs - -- Plain-language explanation of Brand Kit concepts -- Routing to the correct Brand Kit capability or reference skill -- Step-by-step setup or usage guidance -- API request guidance or field mapping when needed -- Clarifying questions when required inputs are missing -- Warnings about auth, rate limits, destructive actions, and Knowledge Vault behavior - -## Example User Requests - -- What is Contentstack Brand Kit and what can I use it for? -- How do I create a Brand Kit from our website? -- Can you update this Voice Profile to match our brand voice? -- How do I make AI-generated content stay on-brand? -- Can I get the original files back from Knowledge Vault? -- What do I need to call the Brand Kit API? - -## Workflow Summary +## Instructions 1. Identify whether the request is conceptual, setup-related, or API/action-oriented. 2. Check for missing identifiers, auth, region, brand context, or source content. 3. Refuse unsafe assumptions, secret disclosure, and destructive actions without confirmation. 4. Distinguish Brand Kit, Voice Profile, and Knowledge Vault behavior. -5. Route API-specific work to the relevant reference skill when available. +5. Route API-specific work to the relevant reference file. 6. Validate the answer against brand rules and call out constraints, rate limits, and risks. -## Instructions - ### Primary role Act as the main Brand Kit orchestrator and knowledge source. Explain Brand Kit concepts, determine the correct capability, and route API-specific tasks to reference skills when available. @@ -74,14 +44,6 @@ Act as the main Brand Kit orchestrator and knowledge source. Explain Brand Kit c If required identifiers, auth, brand context, region, or source content are missing, ask concise follow-up questions before proceeding. Do not guess or fabricate values. -### Secret handling - -Never expose, repeat, transform, or infer Brand Kit API tokens, authtokens, API keys, OAuth secrets, or decrypted credentials. If the user provides secrets, acknowledge receipt at a high level and do not echo them back. - -### Destructive actions - -Do not delete, unpublish, overwrite, or make irreversible updates without explicit user confirmation and clear identification of the target resource. Confirm the Brand Kit UID, Voice Profile UID, or Knowledge Vault content UID before proceeding. - ### Brand governance Validate generated or proposed content against brand guidelines before presenting it as ready. Do not override brand rules without explicit approval. If the request conflicts with brand guidance, say so and offer the closest compliant alternative. @@ -90,33 +52,49 @@ Validate generated or proposed content against brand guidelines before presentin Treat Knowledge Vault as vector-based semantic storage. Do not imply original files, binary assets, or exact source documents can be retrieved from it. If the user asks for originals, explain the limitation directly. +Knowledge Vault ingests PDF and TXT files, crawled websites and Contentstack entries, chunks and embeds them, and serves semantic or hybrid (embedding plus keyword) search. Items can sit in folders, and retrieval can be scoped to a folder. Set an `external_uid` on items you sync from another system so re-ingesting upserts instead of duplicating. + +### Brand Kit versions and surfaces + +- **v1 kits** use Voice Profiles with four `communication_style` dials, each on a 1–4 scale: formality, tone, humor and complexity. Profiles also carry `insights` and `sample_content`. +- **v2 kits** replace the dials with free-form markdown profiles ("Documents"), optionally started from a profile template. The API version is sent as a request header, not a path prefix, and v1 voice-profile routes return `410 Gone` for a v2 kit. Ask which version the kit uses before mapping a request. +- Other surfaces you may be asked about: visual guidelines (logos, palettes, fonts, icons), image profiles, translation profiles, LLM configs (Contentstack-managed or customer-managed credentials), brand consistency scoring of an entry, a Playground for trying a profile, and Quick Start onboarding that drafts profiles from a URL or file for review. +- Brand Kit supplies context to other AI surfaces, such as the AI Assistant app, Polaris and the Contentstack MCP server. It is not a generator on its own. + ### Safety and constraints -Call out rate limits, region-specific base URLs, and API constraints when relevant. If a request is ambiguous, unsafe, or incomplete, pause and ask for the minimum additional information needed. +Call out rate limits, region-specific base URLs, and API constraints when relevant. The APIs are rate limited: on a `429`, back off and retry after a delay instead of retrying immediately. If a request is ambiguous, unsafe, or incomplete, pause and ask for the minimum additional information needed. + +### APIs + +Use Brand Kit Management API for Brand Kits and Voice Profiles. Use Knowledge Vault API for ingest, update, delete, search, and chunk retrieval. Use Generative AI API for content generation. Prefer reference skills for specific API collections when they exist. Respect rate limits (back off on `429`) and region-specific base URLs. Brand Kit and Voice Profile endpoints take `authtoken` and `organization_uid`, with the Brand Kit UID in the path for Voice Profile calls. Knowledge Vault endpoints take `organization_uid` and `brand_kit_uid` headers; confirm the auth header in the current API docs. Never surface tokens in examples, logs, or summaries. + +## MCP tools + +When the Contentstack MCP server is connected with the `brandkit` group (it needs `CONTENTSTACK_BRAND_KIT_ID`), these tools act on the Brand Kit: -## Output Format +- Inspect: `get_all_voice_profiles`, `get_a_single_voice_profile`, `get_all_contents_in_knowledge_vault`, `get_a_single_content_in_knowledge_vault`, `get_data_chunks_from_knowledge_vault` (`brandkit`) +- Generate: `generative_ai` (`brandkit`) +- Change: `create_a_voice_profile`, `update_a_voice_profile`, `create_an_content_in_knowledge_vault`, `update_a_content_in_knowledge_vault` (`brandkit`) +- Delete: `delete_a_voice_profile`, `delete_a_content_from_knowledge_vault` (`brandkit`) -Be concise and practical. -State the relevant Brand Kit concept, workflow, or API scope up front. -Ask only for the missing inputs needed to proceed. -Use bullets or short steps for setup and API guidance. -Do not include secrets, speculative values, or unsupported claims. -Warn clearly about destructive actions, rate limits, and Knowledge Vault limitations. +The voice profile tools call the v1 Voice Profile routes, which return `410 Gone` for a v2 kit. No tool manages Brand Kits themselves, visual guidelines or v2 Documents; use the Brand Kit UI or the Management API. -## Tooling Notes +## Output -Use Brand Kit Management API for Brand Kits and Voice Profiles. Use Knowledge Vault API for ingest, update, delete, search, and chunk retrieval. Use Generative AI API for content generation. Prefer reference skills for specific API collections when they exist. Respect the 10 requests/second per organization rate limit and region-specific base URLs. Never surface tokens in examples, logs, or summaries. +Be concise and practical. State the relevant Brand Kit concept, workflow, or API scope up front, and ask only for the missing inputs needed to proceed. Use bullets or short steps for setup and API guidance. Do not include secrets, speculative values, or unsupported claims. Warn clearly about destructive actions, rate limits, and Knowledge Vault limitations. ## Security ### Defaults -Never expose Brand Kit API tokens. -Validate generated content against brand guidelines before publishing. -Do not override brand rules without explicit approval. -Treat Knowledge Vault content as brand-governed source material, not a public data source. -If auth, region, or identifiers are missing, ask for them instead of guessing. -If a request is ambiguous, incomplete, or unsafe, stop and clarify before acting. +- Advisory by default. Use the MCP tools above only when the user asks for the change. +- Never expose Brand Kit API tokens. +- Validate generated content against brand guidelines before publishing. +- Do not override brand rules without explicit approval. +- Treat Knowledge Vault content as brand-governed source material, not a public data source. +- If auth, region, or identifiers are missing, ask for them instead of guessing. +- If a request is ambiguous, incomplete, or unsafe, stop and clarify before acting. ### Destructive Actions diff --git a/skills/brand-kit-assistant/references/brand-kit-management-api-reference.md b/skills/brand-kit-assistant/references/brand-kit-management-api-reference.md index 0270709..8a8f000 100644 --- a/skills/brand-kit-assistant/references/brand-kit-management-api-reference.md +++ b/skills/brand-kit-assistant/references/brand-kit-management-api-reference.md @@ -13,17 +13,18 @@ Operate Contentstack Brand Kit and Voice Profile APIs with correct headers, payl - Target resource: Brand Kit or Voice Profile - Region / base URL - organization_uid and authtoken -- brand_kit_uid for Voice Profile operations +- brand_kit_uid (path parameter) for Voice Profile operations - brand_kit_uid or voice_profile_uid for targeted get/update/delete operations - Name, description, linked stack API keys, communication_style, insights, and sample content as needed ## Behavior - Select the correct regional base URL. - Include authtoken and organization_uid for Brand Kit endpoints. -- Include authtoken and brand_kit_uid for Voice Profile endpoints. +- Include authtoken and organization_uid for Voice Profile endpoints too, with brand_kit_uid in the path (`brand-kits/{brand_kit_uid}/voice-profiles`). +- Check whether the kit is v1 or v2. The API version is a request header. v2 kits use markdown profiles ("Documents"), and v1 voice-profile routes return `410 Gone` for them. - Validate required IDs and payload fields before calling the API. - For create/update, support brand_kit.name, optional brand_kit.description, and optional brand_kit.api_keys. -- For Voice Profiles, validate communication_style values are integers from 1 to 5 and include insights and sample_content when available. +- For v1 Voice Profiles, validate the four communication_style dials (formality, tone, humor, complexity) are integers from 1 to 4, and include insights and sample_content when available. - If deleting a Voice Profile, ask for explicit confirmation before proceeding. - If the API returns 429, report rate-limit status and recommend retrying after a short delay. diff --git a/skills/brand-kit-assistant/references/knowledge-vault-api-reference-skill.md b/skills/brand-kit-assistant/references/knowledge-vault-api-reference-skill.md index 4e988d9..6c25a78 100644 --- a/skills/brand-kit-assistant/references/knowledge-vault-api-reference-skill.md +++ b/skills/brand-kit-assistant/references/knowledge-vault-api-reference-skill.md @@ -18,9 +18,11 @@ Operate Contentstack Knowledge Vault through the API and mapped MCP tools with r ## Behavior - Select the correct regional base URL. -- Include authtoken and brand_kit_uid headers. +- Include organization_uid and brand_kit_uid headers, plus the auth header the current API docs require. - Validate required IDs and payload fields before calling the API. - Accept text intended for semantic storage and include useful metadata when available. +- Use `external_uid` as an upsert key when syncing from another system, so re-ingesting updates instead of duplicating. +- Supported sources include PDF and TXT files, crawled websites and Contentstack entries. Items can be organized in folders, and search can be semantic or hybrid. - For delete, request explicit confirmation and verify brand_kit_uid and content_uid before proceeding. - If the API returns 429, report rate-limit status and recommend retrying after a short delay. - Support mapped MCP operations where available. diff --git a/skills/cms-assets/SKILL.md b/skills/cms-assets/SKILL.md index 37b1985..6363cff 100644 --- a/skills/cms-assets/SKILL.md +++ b/skills/cms-assets/SKILL.md @@ -6,82 +6,82 @@ allowed-tools: Read Grep Glob # Contentstack Assets -## Description +Advise on organizing, delivering, transforming and publishing files in a stack's asset library, and on the limits and reference risks that come with them. -Advise developers on organizing, delivering, and transforming assets in Contentstack. Cover folder structure, Image Delivery API transformations, publishing lifecycle, CDN behavior, and common limits. +## When to use -## When to Use +- Organizing assets into folders and naming them. +- Serving responsive or optimized images with the Image Delivery API. +- Publishing, unpublishing, replacing or deleting assets, and what that does to references. +- Upload limits and other asset library gotchas. -Use when developers ask about uploading, organizing, delivering, transforming, publishing, or troubleshooting images and other media files in a stack's asset library. +Use another skill for: -For the standalone Assets DAM product (spaces, workspaces, duplicate sets, saved views, visual markups), use the `asset-management` skill instead. +- The standalone Assets DAM (spaces, workspaces, duplicate sets, saved views, visual markups) and any reading or updating of user-defined fields → `asset-management` +- Images embedded in JSON Rich Text Editor content → `cms-json-rte` +- Fetching assets in code with the Delivery SDK → `dx-delivery-sdk` +- Asset fields in a GraphQL query → `cms-graphql` +- Environments and the publishing model in general → `cms-environments-publishing` +- Third-party DAM connectors such as Bynder or Cloudinary → `marketplace-apps` -This skill does not cover reading or updating user-defined fields (custom metadata). The stack asset library doesn't support them. Route those requests to the `asset-management` skill, which points to the Assets (DAM) API. +## Before you answer -## User Problem +Find out the asset type and use case, any image optimization requirements, how assets are organized today, and whether the question is about publishing or CDN delivery. -Developers need to organize assets effectively, serve optimized images, and understand how asset publishing and CDN delivery work. They also need to avoid broken references and common asset-management mistakes. +## Instructions -## Success Criteria +1. Identify the asset type and use case. +2. Recommend a folder organization if needed. +3. For images, explain the Image Delivery API transforms. +4. Describe publishing and CDN behavior. +5. Flag limits, gotchas and reference risks. -Recommend clear folder structures and naming patterns. -Explain Image Delivery API transformations and responsive image delivery. -Describe asset publishing, unpublishing, and replacement behavior correctly. -Call out relevant limits, gotchas, and reference-breaking actions. +### Folder organization -## Expected Inputs +Recommend a clear folder structure early in the project, such as `/images/heroes`, `/images/products` and `/documents/legal`. Moving assets between folders preserves UIDs and references. -- Asset type and use case -- Image optimization requirements -- Folder organization needs -- Publishing or CDN questions +### Image Delivery API -## Expected Outputs +Use `images.contentstack.io` for image delivery, with on-the-fly transforms through URL parameters such as `width`, `height`, `quality`, `format`, `auto`, `crop`, `trim`, `orient`, `overlay`, `pad`, `canvas`, `fit`, `dpr`, `blur`, `sharpen`, `brightness`, `contrast` and `saturation`. Recommend responsive delivery with width/height and dpr, WebP for modern browsers, and quality tuning for file size. Chain multiple transforms in one URL. Non-image assets serve from `assets.contentstack.io`. -- Folder structure recommendations -- Image Delivery API transformation guidance -- Publishing lifecycle explanations -- Performance optimization tips +### Publishing lifecycle -## Example User Requests +Assets have their own publishing lifecycle, independent of entries. An asset must be published to an environment before referenced entries display it on the live site. Unpublishing removes it from delivery for that environment but does not delete the file. -- How do I serve responsive images from Contentstack? -- What image transformations does Contentstack support? -- How should I organize my assets into folders? -- Does deleting an asset break references in entries? -- What is the maximum file size I can upload? +### Replacing vs deleting -## Workflow Summary +Replacing an asset creates a new version while keeping the same UID and references intact. Deleting an asset breaks all references to it in entries. Advise checking references before deletion. -Identify the asset type and use case. -Recommend folder organization if needed. -Explain Image Delivery API transforms for images. -Describe publishing and CDN behavior. -Flag limits, gotchas, and reference risks. +### Key limits -## Instructions +State the main stack asset library limits clearly: max file size is 700 MB via the UI and 100 MB via the API, max 10 assets per batch upload, 10,000 assets per stack by default, and filenames cannot be changed after upload. Image Delivery API transforms do not apply to images inserted directly into Rich Text Editor fields. The standalone Assets (DAM) product has different limits (up to 1.5 GB per asset, 100 assets per upload, 500,000 assets per workspace); route those questions to `asset-management`. -[{"heading":"Folder organization","content":"Recommend a clear folder structure early in the project, such as /images/heroes, /images/products, and /documents/legal. Moving assets between folders preserves UIDs and references."},{"heading":"Image Delivery API","content":"Use images.contentstack.io for image delivery and support on-the-fly transforms with URL parameters such as width, height, quality, format, crop, trim, orient, overlay, pad, fit, and dpr. Recommend responsive delivery with width/height and dpr, WebP for modern browsers, and quality tuning for file size. Chain multiple transforms in one URL. Non-image assets serve from assets.contentstack.io."},{"heading":"Publishing lifecycle","content":"Explain that assets have their own publishing lifecycle independent of entries. An asset must be published to an environment before referenced entries display it on the live site. Unpublishing removes it from delivery for that environment but does not delete the file."},{"heading":"Replacing vs deleting","content":"Explain that replacing an asset creates a new version while keeping the same UID and references intact. Deleting an asset breaks all references to it in entries. Advise checking references before deletion."},{"heading":"Key limits","content":"State the main limits clearly: max file size is 700 MB via UI and 100 MB via API, max 10 assets per batch upload, default 10,000 assets per stack and 500,000 per organization, and filenames cannot be changed after upload. Note that Image Delivery API transforms do not apply to images inserted directly into Rich Text Editor fields."}], +## MCP tools -## Output Format +When the Contentstack MCP server is connected, these tools act on the stack (local server group in brackets): -Be concise and practical. Show Image Delivery API parameters inline when helpful. Prefer direct answers with short bullets when listing options or limits. +- Inspect: `get_all_assets`, `get_a_single_asset`, `get_asset_reference` (`cma`); `get_assets_of_a_specific_folder`, `get_assets_and_subfolders_of_a_parent_folder`, `get_all_versions_of_an_asset` (`cma-extended`); `get_all_assets_cdn`, `get_a_single_asset_cdn` (`cda`) +- Change: `publish_an_asset`, `unpublish_an_asset` (`cma`); `update_asset_details`, `create_asset_folder`, `update_asset_folder`, `set_asset_version_name` (`cma-extended`) +- Delete: `delete_an_asset` (`cma`); `delete_asset_folder`, `delete_asset_version_name` (`cma-extended`) -## Tooling Notes +There is no upload tool; upload through the UI, the CMA or `csdx`. Run `get_asset_reference` before deleting an asset to see which entries use it. -Read-only advisory skill. Do not upload, delete, publish, or modify assets. Use only read-oriented guidance. +## Output + +Be concise and practical. Show Image Delivery API parameters inline when helpful. Prefer direct answers with short bullets when listing options or limits. ## Security ### Defaults +- Advisory by default. Use the MCP tools above only when the user asks for the change. - Never expose tokens or API keys. - Delivery tokens and asset URLs are safe for client-side code. -- Use environment variables for credentials in example code. +- Use environment variables for credentials in example code, and route CMA calls through a server-side proxy in browser apps. ### Destructive Actions -Do not perform destructive asset actions. If the user asks to delete, unpublish, or replace assets, explain the implications and recommend safe checks, but do not execute changes. +Before deleting, unpublishing or replacing an asset, or deleting a folder, name the asset, the environments and the entries that reference it, explain the impact (a deleted asset breaks every reference to it), and wait for confirmation. ### Secrets @@ -90,14 +90,3 @@ Never reveal management tokens, API keys, or other secrets. If code examples nee ### Environment Variables Use environment variables for any credentialed examples. Never hardcode stack API keys, management tokens, or delivery tokens in client-side code. - -## Product Context - -- - Product: CMS -- - Description: Contentstack headless CMS: content types, entries, assets, environments, publishing, workflows, webhooks, and the Content Management API (CMA). -- - Product safety rules: - Never expose management tokens or API keys. -- Always use environment variables for credentials. -- Route all CMA calls through server-side proxies in browser apps. -- Never hardcode stack API keys in client-side code. -- - Default tools: ["CMA API", "Content Types", "Entries", "Assets", "Workflows", "Webhooks", "Environments", "Releases", "Publish Queue"] -- - Default connectors: ["CMA Proxy", "Webhooks"] \ No newline at end of file diff --git a/skills/cms-branches-aliases/SKILL.md b/skills/cms-branches-aliases/SKILL.md index 44202ba..cd8e9d2 100644 --- a/skills/cms-branches-aliases/SKILL.md +++ b/skills/cms-branches-aliases/SKILL.md @@ -1,85 +1,93 @@ --- name: cms-branches-aliases -description: "Advise developers on using Contentstack branches for isolated content development and aliases for zero-downtime content deployments. Cover branch strategy, branch-specific vs global modules, CI/CD integration, merge behavior, and rollback patterns. Use when developers ask about branches, aliases, CI/CD integration with Contentstack, deployment strategies, or branch-specific vs global module behavior." +description: "Advise developers on using Contentstack branches for isolated content development and aliases for zero-downtime content deployments. Cover branch strategy, branch-specific vs global modules, CI/CD integration, merge behavior, and rollback patterns. Use when developers ask about branches, aliases, CI/CD integration with Contentstack, deployment strategies, or branch-specific vs global module behavior. For running csdx branch commands, use dx-cli." --- # Branches & Aliases -## Description +Advise on branches for isolated schema and content work, and aliases for zero-downtime deployment and rollback. -Advise developers on using Contentstack branches for isolated content development and aliases for zero-downtime content deployments. Cover branch strategy, branch-specific vs global modules, CI/CD integration, merge behavior, and rollback patterns. +## When to use -## When to Use +- Choosing a branch strategy for schema and content development. +- Which modules are branch-specific and which are global. +- Comparing and merging branches. +- Alias-based deployment, rollback and CI/CD pipelines. -Use when developers ask about branches, aliases, CI/CD integration with Contentstack, deployment strategies, or branch-specific vs global module behavior. +Use another skill for: -## User Problem +- Running `csdx` branch, export and import commands → `dx-cli` +- Deploying a set of entries together → `cms-releases` +- Environments, publishing and token scoping → `cms-environments-publishing` +- Designing the content types you change on a branch → `cms-data-modeling-best-practices` -Developers need to make schema and content changes safely without affecting the live site, then deploy changes with zero downtime and easy rollback. +## Before you answer -## Success Criteria +Find out the development workflow or CI/CD pipeline, the deployment and rollback requirements, and how the team collaborates. -Recommends an appropriate branch strategy. -Explains branch-specific vs global modules accurately. -Gives practical alias-based deployment patterns. -Provides safe CI/CD integration guidance. -Includes clear merge and rollback advice. +## Instructions -## Expected Inputs +1. Understand the development and deployment workflow. +2. Recommend a branch strategy. +3. Explain branch-specific vs global modules. +4. Guide alias-based deployment. +5. Cover merge, rollback and CI/CD integration. -- Development workflow or CI/CD pipeline -- Deployment strategy requirements -- Team structure and collaboration needs -- Rollback requirements +### Branch basics -## Expected Outputs +A new branch inherits the source branch's content types, global fields, entries, assets, languages, webhooks and extensions as an isolated workspace. The number of branches per stack depends on the plan. Only owners, admins and developers can create and delete branches. -- Branch strategy recommendations -- Global vs branch-specific module explanations -- Alias-based deployment patterns -- CI/CD integration guidance -- Merge and rollback advice +### Branch-specific vs global modules -## Example User Requests +Branch-specific: content types, global fields, entries, assets, workflows, publish queue, releases, languages, extensions, audit logs, global search and labels. Global: taxonomy, environments, users, roles, webhooks, workflows, publish rules, tokens and Marketplace apps. Some global modules can be scoped: a webhook can be assigned to one branch at a time, workflows and publish rules can apply to all branches or specific ones, and delivery and management tokens can be limited to specific branches or aliases. A content type on a development branch does not exist on main until merged. -- How do I use branches for content schema development? -- What is the difference between branch-specific and global modules? -- How do I deploy content changes with zero downtime? -- How do branches work with my CI/CD pipeline? -- Can I roll back a bad deployment? +### Compare and merge -## Workflow Summary +Compare and merge covers only content types and global fields, and runs through the CMA or the CLI. Merge strategies are `merge_prefer_base`, `merge_prefer_compare`, `overwrite_with_compare`, `merge_new_only`, `merge_modified_only_prefer_base`, `merge_modified_only_prefer_compare` and `ignore` (per item). Warn that `overwrite_with_compare` replaces the base and loses anything that exists only there. A backup branch is created before the merge runs, and the merge is asynchronous, so poll the merge queue for its status. Entries, assets and other modules have no merge tool; reconcile them with scripts. -Understand the development and deployment workflow. -Recommend a branch strategy. -Explain branch-specific vs global modules. -Guide alias-based deployment. -Cover merge, rollback, and CI/CD integration. +### Alias-based deployment -## Instructions +Aliases point to branches. Hardcode an alias such as `deploy` in frontend code instead of a branch UID. To switch production to a new branch, reassign the alias. Frontend code does not change. For rollback, reassign the alias back. Two aliases can point to the same branch, but a branch and alias cannot share the same UID. -[{"heading":"Branch basics","content":"Branches copy the source branch's content types, global fields, entries, and assets into an isolated workspace. Max 5 branches per stack. Only one branch can be created or deleted at a time across an organization. Only owners, admins, and developers can manage branches."},{"heading":"Branch-specific vs global modules","content":"Branch-specific: content types, global fields, entries, assets, publish queue, releases, languages, extensions, audit logs, labels, search. Global: environments, webhooks, workflows, publish rules, users, roles, tokens. A webhook created on main is visible everywhere but can be scoped to a specific branch. A content type on a development branch does not exist on main until merged."},{"heading":"Alias-based deployment","content":"Aliases point to branches. Hardcode an alias such as deploy in frontend code instead of a branch UID. To switch production to a new branch, reassign the alias. Frontend code does not change. For rollback, reassign the alias back. Two aliases can point to the same branch, but a branch and alias cannot share the same UID."},{"heading":"CI/CD pattern","content":"Create a branch from main, make schema and content changes, test on staging, reassign the production alias to the new branch, and reassign back immediately if something goes wrong."},{"heading":"Branch strategy","content":"Keep branch lifetimes short. Long-lived branches drift and create merge conflicts. Prefer trunk-based workflow with short-lived feature branches merged back to main quickly. Do not use branches as permanent environments; use environments for that."},{"heading":"SDK initialization","content":"Pass the branch or alias ID explicitly, for example branch: 'deploy'. If no branch header is passed, main is used by default. Always pass the branch header explicitly in scripts, even when targeting main."}], +### CI/CD pattern -## Output Format +Create a branch from main, make schema and content changes, test on staging, reassign the production alias to the new branch, and reassign back immediately if something goes wrong. -Provide concise branch and deployment guidance. Favor practical CI/CD patterns over abstract explanations. +### Branch strategy + +Keep branch lifetimes short. Long-lived branches drift and create merge conflicts. Prefer a trunk-based workflow with short-lived feature branches merged back to main quickly. Do not use branches as permanent environments; use environments for that. + +### SDK initialization + +Pass the branch or alias ID explicitly, for example `branch: 'deploy'`. If no branch header is passed, main is used by default. Always pass the branch header explicitly in scripts, even when targeting main. + +## MCP tools -## Tooling Notes +When the Contentstack MCP server is connected, these tools act on the stack (local server group in brackets): -Read-only advisory. Do not create, delete, or merge branches. +- Inspect: `get_all_branches`, `get_a_single_branch`, `get_all_branch_aliases`, `get_a_single_branch_alias`, `compare_branches`, `compare_content_types_between_branches`, `compare_global_fields_between_branches`, `compare_specific_ct_between_branches`, `compare_specific_gf_between_branches`, `get_all_merge_jobs`, `get_a_single_merge_job` (`cma`) +- Change: `create_a_branch`, `merge_branch`, `assign_a_branch_alias` (`cma`) +- Delete: `delete_a_branch`, `delete_a_branch_alias` (`cma`) + +Run a compare before `merge_branch`, and poll `get_a_single_merge_job` until the merge finishes. Set `CONTENTSTACK_BRANCH` (or the Profile Hub `branch` URL parameter) so other tools act on the intended branch. + +## Output + +Provide concise branch and deployment guidance. Favor practical CI/CD patterns over abstract explanations. ## Security ### Defaults +- Advisory by default. Use the MCP tools above only when the user asks for the change. - Never expose tokens or API keys. -- Use environment variables for credentials in example code. +- Use environment variables for credentials in example code, and route CMA calls through a server-side proxy in browser apps. - Do not recommend hardcoding secrets in client-side code. - Keep guidance compatible with branch- and alias-scoped credentials. ### Destructive Actions -Do not create, delete, merge, or reassign branches or aliases. Provide guidance only. +Before creating, merging or deleting a branch, or assigning or deleting an alias, name the stack, the branches and aliases involved and the merge strategy, explain the impact (reassigning a production alias switches the live site; `overwrite_with_compare` loses base-only items; deleting a branch deletes its content), and wait for confirmation. ### Secrets @@ -88,14 +96,3 @@ Never expose management tokens, delivery tokens, API keys, or other credentials. ### Environment Variables Use environment variables for all credentials and secret values. Never hardcode stack API keys, tokens, or branch-scoped credentials in examples. - -## Product Context - -- - Product: CMS -- - Description: Contentstack headless CMS: content types, entries, assets, environments, publishing, workflows, webhooks, and the Content Management API (CMA). -- - Product safety rules: - Never expose management tokens or API keys. -- Always use environment variables for credentials. -- Route all CMA calls through server-side proxies in browser apps. -- Never hardcode stack API keys in client-side code. -- - Default tools: ["CMA API", "Content Types", "Entries", "Assets", "Workflows", "Webhooks", "Environments", "Releases", "Publish Queue"] -- - Default connectors: ["CMA Proxy", "Webhooks"] \ No newline at end of file diff --git a/skills/cms-data-modeling-best-practices/SKILL.md b/skills/cms-data-modeling-best-practices/SKILL.md index 791d763..bb7641a 100644 --- a/skills/cms-data-modeling-best-practices/SKILL.md +++ b/skills/cms-data-modeling-best-practices/SKILL.md @@ -1,57 +1,34 @@ --- name: cms-data-modeling-best-practices -description: "Guide developers to model content in Contentstack using the simplest reusable structure. The skill explains when to use content types, references, global fields, groups, modular blocks, JSON RTE, taxonomy, and tags, and helps avoid over-modeling, deep reference chains, and channel-specific schema sprawl. Use when designing, reviewing, or refactoring Contentstack content models before creating or changing schemas." +description: "Guide developers to model content in Contentstack using the simplest reusable structure. The skill explains when to use content types, references, global fields, groups, modular blocks, JSON RTE, taxonomy, and tags, and helps avoid over-modeling, deep reference chains, and channel-specific schema sprawl. Use when designing, reviewing, or refactoring Contentstack content models before creating or changing schemas. For taxonomy hierarchies and CDA taxonomy filters, use cms-taxonomy." allowed-tools: Read Grep Glob --- # Contentstack Data Modeling Best Practices -## Description +Help pick the simplest Contentstack construct that fits, so editors work efficiently, delivery code stays simple, and schemas stay reusable, governed and easy to query. -Guide developers to model content in Contentstack using the simplest reusable structure. The skill explains when to use content types, references, global fields, groups, modular blocks, JSON RTE, taxonomy, and tags, and helps avoid over-modeling, deep reference chains, and channel-specific schema sprawl. +## When to use -## When to Use +- Designing a new content model, or reviewing and simplifying an existing one. +- Choosing between a content type, reference, global field, group, modular block, JSON RTE, taxonomy, tag or plain field. +- Planning localization and governance at the schema level. +- Checking a model against query impact and stack limits. -Use when designing, reviewing, or refactoring Contentstack content models before creating or changing schemas. +Use another skill for: -## User Problem +- Taxonomy hierarchy design and taxonomy filters on the CDA → `cms-taxonomy` +- Rendering JSON RTE content once it's modeled → `cms-json-rte` +- Personalization variants of an entry → `cms-variants-personalization` +- Locales and fallback chains → `cms-localization` +- Making schema changes safely on a branch → `cms-branches-aliases` +- Querying the entries a model produces → `cms-entries` -Developers need a practical way to choose the right Contentstack construct so editors can work efficiently, delivery code stays simple, and schemas stay reusable, governed, and easy to query. +## Before you answer -## Success Criteria +Gather the business goal, the current or proposed model, target channels and delivery needs, localization, reuse and governance requirements, sample content, and any performance or query constraints. -Recommend the simplest valid model, explain tradeoffs clearly, preserve editorial usability, avoid unnecessary abstraction, and keep the schema stable, shallow, and aligned with localization and governance needs. - -## Expected Inputs - -- Business goal or use case -- Current or proposed model -- Target channels and delivery needs -- Localization requirements -- Reuse and governance requirements -- Sample content or entries -- Performance or query constraints - -## Expected Outputs - -- Recommended modeling approach -- Construct-by-construct guidance -- Tradeoff explanations -- Warnings about anti-patterns -- Localization and governance recommendations -- Query and performance considerations -- Optional sample model or decision summary -- Migration cautions when schema changes are implied - -## Example User Requests - -- How should I model a landing page with reusable sections in Contentstack? -- Should this data be a global field, group, or content type? -- Review this content model and tell me what to simplify. -- What is the best way to handle localization for shared content? -- How do I model product categories for filtering and reuse? - -## Workflow Summary +## Instructions 1. Identify the domain concept, editorial workflow, delivery channels, localization needs, reuse requirements, and query constraints. 2. Choose the simplest fitting construct: content type, reference, global field, group, modular block, JSON RTE, taxonomy, tag, or plain field. @@ -61,8 +38,6 @@ Recommend the simplest valid model, explain tradeoffs clearly, preserve editoria 6. Call out anti-patterns and suggest simpler alternatives. 7. Return a concise recommendation with migration cautions if needed. -## Instructions - ### Understand the goal Identify the domain concept, editorial workflow, delivery channels, localization needs, reuse requirements, and query constraints before recommending changes. @@ -95,18 +70,31 @@ Give a concise recommendation, compare alternatives only when useful, and includ Use a content type for a real domain concept with its own lifecycle. Use a reference for reusable content with independent ownership. Use a global field for the same nested field set across multiple content types. Use a group for parent-owned nested data inside one content type. Use modular blocks for page-local composition. Use JSON RTE for narrative content. Use taxonomy for governed classification. Use tags for lightweight internal labels. -## Output Format +### Limits to design around + +- A content type schema can have up to 500 fields. A content type UID can be up to 200 characters. +- The number of content types per stack and the number of block types in one modular blocks field are plan limits. Product defaults are around 100 content types per stack and 10 block types per modular blocks field, so check the plan before designing a model that depends on more. +- Taxonomies can nest up to 10 levels deep. + +## MCP tools + +When the Contentstack MCP server is connected, these tools act on the stack (local server group in brackets): -Use concise, structured, instruction-oriented prose. Prefer bullets and short sections. State the recommended choice first when comparing options. Include warnings for anti-patterns and migration concerns when relevant. Do not expose secrets, API keys, or management tokens. +- Inspect: `get_all_content_types`, `get_a_single_content_type`, `get_all_references_of_ct`, `export_content_type`, `get_all_global_fields`, `get_a_single_global_field` (`cma`); `export_global_field` (`cma-extended`) +- Change: `create_a_content_type`, `update_content_type` (`cma`); `create_a_global_field`, `update_a_global_field` (`cma-extended`) +- Delete: `delete_content_type` (`cma`); `delete_a_global_field` (`cma-extended`) -## Tooling Notes +Read the existing model with the inspect tools before reviewing it. Make schema changes on a development branch first, and run `get_all_references_of_ct` before changing or deleting a content type that other types reference. -Read-only advisory skill. Prefer default CMS knowledge and documentation sources. If tools are used, restrict to read-only inspection and documentation lookup. Do not perform schema changes, publishing, or destructive actions. +## Output + +Use concise, structured, instruction-oriented prose. Prefer bullets and short sections. State the recommended choice first when comparing options. Include warnings for anti-patterns and migration cautions when relevant, and optionally a sample model or decision summary. ## Security ### Defaults +- Advisory by default. Use the MCP tools above only when the user asks for the change. - Never expose management tokens or API keys. - Never ask users to paste secrets into the prompt. - Use environment variables for credentials in any example code. @@ -116,7 +104,7 @@ Read-only advisory skill. Prefer default CMS knowledge and documentation sources ### Destructive Actions -Do not perform destructive actions. Do not delete, publish, unpublish, or modify Contentstack resources. Provide guidance only. +Before creating, updating or deleting a content type or global field, name the stack, branch and schema, explain the impact (removing a field or content type can remove the content stored in it, and every schema change is an API contract change for delivery code), and wait for confirmation. ### Secrets @@ -125,14 +113,3 @@ Treat all credentials as sensitive. Never request or display management tokens, ### Environment Variables Use environment variables for any credentialed examples or integrations. Prefer placeholders such as CONTENTSTACK_API_KEY, CONTENTSTACK_MANAGEMENT_TOKEN, and CONTENTSTACK_DELIVERY_TOKEN. Never hardcode secrets in examples or instructions. - -## Product Context - -- - Product: CMS -- - Description: Contentstack headless CMS: content types, entries, assets, environments, publishing, workflows, webhooks, and the Content Management API (CMA). -- - Product safety rules: - Never expose management tokens or API keys. -- Always use environment variables for credentials. -- Route all CMA calls through server-side proxies in browser apps. -- Never hardcode stack API keys in client-side code. -- - Default tools: ["CMA API", "Content Types", "Entries", "Assets", "Workflows", "Webhooks", "Environments", "Releases", "Publish Queue"] -- - Default connectors: ["CMA Proxy", "Webhooks"] \ No newline at end of file diff --git a/skills/cms-entries/SKILL.md b/skills/cms-entries/SKILL.md index 330153e..3c21d95 100644 --- a/skills/cms-entries/SKILL.md +++ b/skills/cms-entries/SKILL.md @@ -1,89 +1,103 @@ --- name: cms-entries -description: "Advise developers on querying, localizing, versioning, publishing, and structuring Contentstack entries for efficient delivery. Focus on CDA usage, reference expansion, pagination, bulk operations, and Sync API patterns. Use when developers ask about fetching entries, building CDA queries, handling localization, publishing workflows, versioning behavior, bulk operations, or entry-related performance issues." +description: "Advise developers on querying, localizing, versioning, publishing, and structuring Contentstack entries for efficient delivery. Focus on CDA usage, reference expansion, pagination, bulk operations, and Sync API patterns. Use when developers ask about fetching entries, building CDA queries, handling localization, publishing workflows, versioning behavior, bulk operations, or entry-related performance issues. For SDK code, use dx-delivery-sdk; for GraphQL, use cms-graphql." allowed-tools: Read Grep Glob --- # Entries -## Description +Advise on querying, publishing, versioning and paging through entries so delivery is efficient and editorial workflows behave as expected. -Advise developers on querying, localizing, versioning, publishing, and structuring Contentstack entries for efficient delivery. Focus on CDA usage, reference expansion, pagination, bulk operations, and Sync API patterns. +## When to use -## When to Use +- Building CDA queries: filters, references, field projection, sorting, locales. +- Paginating through every entry of a content type. +- Versioning, saving vs publishing, and bulk publish or unpublish. +- Choosing the Sync API over polling. +- Entry-related performance problems. -Use when developers ask about fetching entries, building CDA queries, handling localization, publishing workflows, versioning behavior, bulk operations, or entry-related performance issues. +Use another skill for: -## User Problem +- Writing the query with `@contentstack/delivery-sdk` → `dx-delivery-sdk` +- GraphQL queries → `cms-graphql` +- Rendering JSON RTE fields and embedded items → `cms-json-rte` +- Filtering by taxonomy terms → `cms-taxonomy` +- Locales and fallback chains in depth → `cms-localization` +- Environments, tokens and the publish queue → `cms-environments-publishing` +- Entry variants and personalization → `cms-variants-personalization` +- Bulk export, import or migration scripts with `csdx` → `dx-cli` -Developers need to query, publish, and structure entries so frontend delivery is efficient and editorial workflows behave as expected. +## Before you answer -## Success Criteria +Find out the query or filtering requirement, the content type structure, the locales involved, any publishing or scheduling needs, and any performance or pagination concerns. -Write correct CDA queries and explain the response shape. -Clarify versioning, publishing, and localization behavior. -Show how to expand references and paginate results efficiently. -Flag common mistakes, especially using the CMA for frontend reads. +## Instructions -## Expected Inputs +1. Confirm whether the user needs CDA or CMA guidance. +2. Give the correct query syntax, publishing approach or delivery pattern. +3. Explain versioning, localization and reference expansion as needed. +4. Flag anti-patterns and recommend best practices. -- Query or filtering requirements -- Content type structure -- Localization or locale requirements -- Publishing or scheduling needs -- Performance or pagination concerns +### CDA or CMA -## Expected Outputs +Say which API the answer is for. Use the Content Delivery API (CDA) with a delivery token for anything a site or app reads. Use the Content Management API (CMA) only for authoring, migration and automation on a server. Never use the CMA for frontend reads: it needs a management token, it isn't cached at the CDN, and it shares the organization's rate limit. -- Query syntax guidance with operators and examples -- Reference expansion patterns -- Publishing and versioning explanations -- Pagination and performance recommendations -- Warnings about common mistakes +### Query parameters -## Example User Requests +Filter with the `query` parameter, a JSON object matched against field values, for example `query={"title":"Welcome"}`. Common companions: -- How do I query entries filtered by a field inside Modular Blocks? -- How does entry versioning work in Contentstack? -- What is the difference between publishing and saving? -- How do I include referenced entries in my CDA response? -- How do I paginate through all entries of a content type? +- `include[]` resolves reference fields inline, up to 100 reference paths per request. Dot paths such as `include[]=section.author` go deeper, but each level multiplies the work behind the request, so keep includes shallow and only ask for the paths the page renders. +- `only[]` and `except[]` trim the response to the fields you need. +- `asc` and `desc` sort the results. +- `skip` and `limit` page through results. A request returns at most 100 entries. +- `include_count` returns the total so you know how many pages there are. +- `locale` picks the language. Add `include_fallback=true` to fall back along the locale chain when an entry isn't localized. +- `include_branch`, `include_publish_details` and `include_metadata` add branch, publish and metadata details to each entry. -## Workflow Summary +Request URLs are capped at 8 KB, and longer ones fail with `414 URI Too Long`. When a filter grows past that, simplify it: fewer `$in` values, fewer `include[]` paths, or several smaller requests. -Confirm whether the user needs CDA guidance or CMA guidance. -Provide the correct query syntax, publishing approach, or delivery pattern. -Explain versioning, localization, and reference expansion as needed. -Flag anti-patterns and recommend best practices. -Keep the answer concise and actionable. +### Pagination -## Instructions +To read every entry of a content type, request with `include_count=true` and `limit=100`, then loop with `skip` increasing by 100 until you have `count` entries. Keep the sort order fixed while paging so results don't shift between pages. To mirror a whole environment, use the Sync API instead of paging. + +### Versioning + +Every save increments a `_version` counter per entry and locale, starting at 1. Old versions are kept. Restoring an older version creates a new version with those values, so history is never rewritten. Publishing is separate from saving: a saved change stays a draft until it is published to an environment. + +### Publishing and bulk operations + +Publish entries together with the entries they reference. Bulk publish and bulk unpublish accept up to 100 items per request. To publish nested references along with their parents, call the bulk publish endpoint with the `api_version: 3.2` header, which publishes references up to five levels deep. Bulk actions are limited to 1 request per second per organization, and other CMA calls to 10 requests per second per organization, so scripts should back off on `429` responses. -[] +### Sync API -## Output Format +Use the Sync API to keep a full local copy of an environment, for static builds, search indexes or offline apps. The first call returns everything published plus a `sync_token`. Later calls pass that token and receive only what changed: publishes, unpublishes, deletes, and content type and locale changes, with a fresh token each time. Prefer it over polling the CDA. -Be concise and practical. -State whether the guidance applies to CDA or CMA. -Show inline query syntax when helpful. -Avoid unnecessary background unless it prevents a common mistake. +## MCP tools -## Tooling Notes +When the Contentstack MCP server is connected, these tools act on the stack (local server group in brackets): -Read-only advisory skill. -Do not create, update, publish, or delete entries. +- Inspect: `get_all_entries`, `get_single_entry`, `get_references_of_an_entry` (`cma`); `get_all_versions_of_an_entry` (`cma-extended`); `get_all_entries_cdn`, `get_a_single_entry_cdn` (`cda`, to check what the live site receives) +- Change: `create_an_entry`, `update_an_entry`, `publish_an_entry` (can schedule), `localize_an_entry` (`cma`); `set_entry_version_name` (`cma-extended`) +- Delete or withdraw: `delete_an_entry`, `unpublish_an_entry`, `unlocalize_an_entry` (`cma`); `delete_entry_version_name` (`cma-extended`) + +There are no bulk publish or Sync API tools; use the CMA bulk endpoints or `csdx` for bulk work, and the Sync API from code. + +## Output + +Be concise and practical. State whether the guidance applies to the CDA or the CMA, show inline query syntax when helpful, and skip background unless it prevents a common mistake. ## Security ### Defaults -Never expose tokens or API keys. -Delivery tokens are safe for client-side code; management tokens are not. -Use environment variables for credentials in example code. +- Advisory by default. Use the MCP tools above only when the user asks for the change. +- Never expose tokens or API keys. +- Delivery tokens are safe for client-side code; management tokens are not. +- Use environment variables for credentials in example code, and route CMA calls through a server-side proxy in browser apps. ### Destructive Actions -Do not perform destructive actions. Do not delete, unpublish, or modify entries. Provide guidance only. +Before creating, updating, publishing, unpublishing, unlocalizing or deleting entries, name the stack, branch, content type, entries, locales and environments, explain the impact (unpublishing removes content from the live site), and wait for confirmation. Run `get_references_of_an_entry` before a delete to see which entries point to it. ### Secrets @@ -92,14 +106,3 @@ Never reveal management tokens, API keys, or other secrets. Prefer environment v ### Environment Variables Use environment variables for all credentials in sample code. Never hardcode delivery tokens, management tokens, or stack identifiers in client-side examples. - -## Product Context - -- - Product: CMS -- - Description: Contentstack headless CMS: content types, entries, assets, environments, publishing, workflows, webhooks, and the Content Management API (CMA). -- - Product safety rules: - Never expose management tokens or API keys. -- Always use environment variables for credentials. -- Route all CMA calls through server-side proxies in browser apps. -- Never hardcode stack API keys in client-side code. -- - Default tools: ["CMA API", "Content Types", "Entries", "Assets", "Workflows", "Webhooks", "Environments", "Releases", "Publish Queue"] -- - Default connectors: ["CMA Proxy", "Webhooks"] \ No newline at end of file diff --git a/skills/cms-environments-publishing/SKILL.md b/skills/cms-environments-publishing/SKILL.md index bcd6bb5..6ec7424 100644 --- a/skills/cms-environments-publishing/SKILL.md +++ b/skills/cms-environments-publishing/SKILL.md @@ -1,90 +1,98 @@ --- name: cms-environments-publishing -description: "Advise developers on configuring environments, publishing content, using delivery and preview tokens, leveraging the Sync API, and understanding CDN and publish queue behavior in Contentstack. Use when developers ask about environment setup, publishing behavior, delivery or preview tokens, the Sync API, scheduling, or CDN and caching configuration in Contentstack." +description: "Advise developers on configuring environments, publishing content, using delivery and preview tokens, leveraging the Sync API, and understanding CDN and publish queue behavior in Contentstack. Use when developers ask about environment setup, publishing behavior, delivery or preview tokens, the Sync API, scheduling, or CDN and caching configuration in Contentstack. For Launch hosting environments, use launch-platform." allowed-tools: Read Grep Glob --- # Contentstack Environments & Publishing -## Description +Advise on how content moves from draft to live: environments, publishing and scheduling, the token for each job, the Sync API, the publish queue and CDN behavior. -Advise developers on configuring environments, publishing content, using delivery and preview tokens, leveraging the Sync API, and understanding CDN and publish queue behavior in Contentstack. +## When to use -## When to Use +- Designing environments for a deployment pipeline. +- Content that doesn't appear on the live site after publishing. +- Choosing between delivery, preview and management tokens. +- Scheduling publishes and reading the publish queue. +- Setting up the Sync API, regional CDA hosts and rate limits. -Use when developers ask about environment setup, publishing behavior, delivery or preview tokens, the Sync API, scheduling, or CDN and caching configuration in Contentstack. +Use another skill for: -## User Problem +- Launch hosting environments, builds and deploys → `launch-platform` +- Token types, scopes and auth in depth → `cms-tokens-authentication` +- Publishing many entries together on a schedule → `cms-releases` +- Approval stages and publish rules → `cms-workflows` +- Rebuilding a site when content publishes → `cms-webhooks` +- Entry queries and bulk publish details → `cms-entries` +- Branch and alias deployment → `cms-branches-aliases` -Developers need to understand how content moves from draft to live, how environments and tokens work together, and how to keep frontends in sync. +## Before you answer -## Success Criteria +Find out the deployment pipeline and environments, the frontend framework or static site generator, the token or auth question, and any scheduling or publish queue concern. -Explains the publishing pipeline clearly. -Identifies the correct token type for the scenario. -Recommends the Sync API when appropriate. -Avoids suggesting any unsafe client-side secret handling. +## Instructions -## Expected Inputs +1. Understand the deployment pipeline. +2. Recommend the environment structure. +3. Explain the token types and which one applies. +4. Guide publishing, scheduling and reference publishing. +5. Recommend the Sync API for static sites or local caches. +6. When content is missing after a publish, troubleshoot step by step. -- Deployment pipeline or environment requirements -- Frontend framework or static site generator -- Token or authentication questions -- Scheduling or publish queue concerns +### Environment design -## Expected Outputs +Treat environments as deployment targets such as development, staging and production. Keep them aligned with the real deployment pipeline. Environments are global modules, shared across branches. -- Environment configuration guidance -- Token usage recommendations -- Publishing workflow explanation -- Sync API setup guidance -- CDN and caching explanations +### Publishing fundamentals -## Example User Requests +Content is a draft until explicitly published to an environment. Publishing can target one or more environments and locales. For nested references, use the bulk publish API with the `api_version: 3.2` header, which publishes referenced entries up to five levels deep together with their parents. Always publish entries with their references. -- How many environments should I set up? -- What is the difference between a delivery token and a preview token? -- My content is not showing on the live site after publishing. -- How does the Sync API work? -- Can I schedule content to publish at a future time? +### Token types -## Workflow Summary +Use the correct token in every answer: Delivery Token for published content via the CDA, environment-scoped and safe for client-side use; Preview Token for unpublished draft content in Live Preview; Management Token for stack-level read/write access, server-side only, never exposed client-side. -Understand the deployment pipeline. -Recommend the environment structure. -Explain token types and their use cases. -Guide publishing, scheduling, and reference publishing. -Recommend the Sync API for static sites or local caches. +### SDK initialization -## Instructions +When relevant, mention stack API key plus delivery token plus environment name, and optionally branch or alias ID. CDA base URLs are region-specific: `cdn.contentstack.io` (AWS NA), `eu-cdn.contentstack.com` (AWS EU), `au-cdn.contentstack.com` (AWS AU), `azure-na-cdn.contentstack.com` (Azure NA), `azure-eu-cdn.contentstack.com` (Azure EU), `gcp-na-cdn.contentstack.com` (GCP NA) and `gcp-eu-cdn.contentstack.com` (GCP EU). + +### Sync API + +Recommend the Sync API for static sites, offline apps, or local content caches. The first request returns all published content plus a `sync_token`, and later requests return only changes such as created, updated, deleted, published, and unpublished items. Prefer it over polling when near-real-time updates are needed without excessive API usage. + +### Publish queue + +The publish queue tracks publish and unpublish operations and their status. Each branch has its own queue, and scheduled publishes can be cancelled before execution. -[{"heading":"Environment design","content":"Treat environments as deployment targets such as development, staging, and production. Keep them aligned with the real deployment pipeline. Default max is 5 per stack. Environments are global modules and are shared across branches."},{"heading":"Publishing fundamentals","content":"Explain that content is a draft until explicitly published to an environment. Publishing can target one or more environments and locales. For nested reference publishing, use api_version: 3.2 so the full reference tree is resolved and published automatically. Always publish entries with their references."},{"heading":"Token types","content":"Use the correct token in every answer: Delivery Token for published content via the CDA, environment-scoped and safe for client-side use; Preview Token for unpublished draft content in live preview; Management Token for stack-level read/write access, server-side only, never exposed client-side."},{"heading":"SDK initialization","content":"When relevant, mention Stack API key plus delivery token plus environment name, and optionally branch or alias ID. Note that CDA base URLs are region-specific, such as cdn.contentstack.io for AWS NA, eu-cdn.contentstack.com for AWS EU, and au-cdn.contentstack.com for AWS AU."},{"heading":"Sync API","content":"Recommend the Sync API for static sites, offline apps, or local content caches. Explain that the first request returns all published content plus a sync_token, and later requests return only changes such as created, updated, deleted, published, and unpublished items. Prefer it over polling when near-real-time updates are needed without excessive API usage."},{"heading":"Publish queue","content":"Explain that the publish queue tracks publish and unpublish operations and their status. Mention that each branch has its own queue and that scheduled publishes can be cancelled before execution."},{"heading":"Rate limits","content":"Mention the platform limits when relevant: 10 requests/second individual and 1 request/second bulk per organization. Recommend exponential backoff with jitter for scripts, and note that the CLI bulk publish plugin handles rate limiting automatically."}], +### Rate limits -## Output Format +The CMA allows 10 requests per second per organization, and bulk operations 1 request per second. CDA reads are mostly served from the CDN cache and don't count against the origin limit. Responses carry `X-RateLimit-Limit` and `X-RateLimit-Remaining` headers. Recommend exponential backoff with jitter for scripts, and note that the CLI bulk publish plugin handles rate limiting automatically. -Be concise and practical. -Always specify which token type applies. -Do not recommend client-side use of management tokens. -Prefer step-by-step troubleshooting when content is missing after publish. +## MCP tools -## Tooling Notes +When the Contentstack MCP server is connected, these tools act on the stack (local server group in brackets): -Read-only advisory skill. -Do not publish, unpublish, or modify environments. -If Claude tool restrictions are applied, use Read Grep Glob only. +- Inspect: `get_all_environments`, `get_an_environment`, `get_publish_queue` (`cma`); `get_publish_queue_activity`, `get_publish_rules` (`cma-extended`); `get_all_entries_cdn`, `get_a_single_entry_cdn` (`cda`, to confirm what an environment serves) +- Change: `create_an_environment`, `update_an_environment`, `publish_an_entry` (can schedule), `publish_an_asset`, `publish_variants_of_an_entry` (`cma`) +- Delete or withdraw: `delete_an_environment`, `unpublish_an_entry`, `unpublish_an_asset` (`cma`) + +There are no tools for tokens, the Sync API or cancelling a scheduled publish; use the UI or the CMA. For "published but not live", check `get_publish_queue` for the job's status, then `get_a_single_entry_cdn` with the environment's delivery token. + +## Output + +Be concise and practical. Always say which token type applies, never recommend client-side use of management tokens, and troubleshoot step by step when content is missing after a publish. ## Security ### Defaults -Delivery tokens are safe for client-side code. -Management tokens must never be exposed client-side. -Use environment variables for all token examples. -Never hardcode tokens in frontend code. +- Advisory by default. Use the MCP tools above only when the user asks for the change. +- Delivery tokens are safe for client-side code. +- Management tokens must never be exposed client-side; route CMA calls through a server-side proxy in browser apps. +- Use environment variables for all token examples, and never hardcode tokens in frontend code. ### Destructive Actions -Do not perform publishing, unpublishing, environment changes, or other write actions. Provide guidance only. +Before publishing, unpublishing, or creating, updating or deleting an environment, name the stack, branch, environments, locales and items, explain the impact (publishing to production changes the live site; deleting an environment affects every branch, since environments are global), and wait for confirmation. ### Secrets @@ -93,14 +101,3 @@ Never expose management tokens, API keys, or other secrets. Use environment vari ### Environment Variables Use environment variables for all token and API key examples. Prefer server-side injection for management credentials and client-safe delivery token usage only where appropriate. - -## Product Context - -- - Product: CMS -- - Description: Contentstack headless CMS: content types, entries, assets, environments, publishing, workflows, webhooks, and the Content Management API (CMA). -- - Product safety rules: - Never expose management tokens or API keys. -- Always use environment variables for credentials. -- Route all CMA calls through server-side proxies in browser apps. -- Never hardcode stack API keys in client-side code. -- - Default tools: ["CMA API", "Content Types", "Entries", "Assets", "Workflows", "Webhooks", "Environments", "Releases", "Publish Queue"] -- - Default connectors: ["CMA Proxy", "Webhooks"] \ No newline at end of file diff --git a/skills/cms-graphql/SKILL.md b/skills/cms-graphql/SKILL.md new file mode 100644 index 0000000..bca8c36 --- /dev/null +++ b/skills/cms-graphql/SKILL.md @@ -0,0 +1,115 @@ +--- +name: cms-graphql +description: "Advise developers on querying published Contentstack content with the GraphQL Content Delivery API. Cover regional endpoints, required headers, the generated schema, reference connections, modular blocks, JSON RTE embedded items, assets, filtering, pagination, query limits, rate limits, and Live Preview with GraphQL. Use when developers write or debug Contentstack GraphQL queries, see schema build errors or partial errors, hit GraphQL limits or 429s, paginate references, or wire GraphQL into Live Preview or Visual Editor. For REST CDA queries use cms-entries; for Delivery SDK code use dx-delivery-sdk; for preview failures use cms-live-preview-visual-editor-support-assistant." +allowed-tools: Read Grep Glob +--- + +# GraphQL Content Delivery API + +Advise on querying published content with the GraphQL Content Delivery API: endpoints, headers, schema shape, references, modular blocks, JSON RTE, assets, filtering, pagination, limits and Live Preview, without silently losing data to limits or partial errors. + +## When to use + +- Writing or fixing a GraphQL query against the generated schema. +- `SCHEMA_BUILD_ERROR`, partial errors on HTTP 200, 414s or 429s. +- Paginating references and staying inside query limits. +- Querying modular blocks, JSON RTE embedded items and assets. +- Wiring a GraphQL fetch layer into Live Preview or Visual Editor. + +Use another skill for: + +- REST Content Delivery API queries → `cms-entries` +- Delivery SDK code → `dx-delivery-sdk` +- Rendering the JSON RTE document the query returns → `cms-json-rte` +- Live Preview or Visual Editor failures → `cms-live-preview-visual-editor-support-assistant` +- Designing the content types the schema is generated from → `cms-data-modeling-best-practices` + +## Before you answer + +Find out the stack region, the content type and field UIDs, the query or the error returned, and whether the request runs in the browser, on a server or in a preview session. + +## Instructions + +1. Confirm the region, environment and branch. +2. Build the query against the generated schema. +3. Check depth, record and size limits; paginate where needed. +4. Check `errors` as well as `data`. +5. For preview sessions, switch host and add preview headers. + +### Endpoints and headers + +Send queries to `https://<host>/stacks/{api_key}?environment={environment}`. Hosts: `graphql.contentstack.com` (AWS NA), `eu-graphql.contentstack.com`, `au-graphql.contentstack.com`, `azure-na-graphql.contentstack.com`, `azure-eu-graphql.contentstack.com`, `gcp-na-graphql.contentstack.com`, `gcp-eu-graphql.contentstack.com`. Dedicated infrastructure uses its own host. + +Headers: `access_token` with the environment's delivery token (required), optional `branch` (branch UID or alias; `main` when omitted), and `Content-Type: application/json`. POST a JSON body with `query`, optional `variables` and `operationName`. GET works too, but the URL must stay under 2,000 bytes or the server returns 414. The API supports queries only: no mutations or subscriptions. + +### Schema shape + +Each content type becomes a type with a single-entry field and an `all_<content_type_uid>` collection field. Type names are the PascalCase form of the UID, so UIDs must not start with a number, must match `[_A-Za-z][_0-9A-Za-z]*`, and must not collide after PascalCase conversion. Collisions and invalid names cause `SCHEMA_BUILD_ERROR`. Use introspection in GraphiQL to see the generated schema, and `skip_content_type_uids` to exclude a content type that breaks the build. + +### References + +Query a reference through its connection field: append `Connection` to the field UID and select `totalCount` and `edges { node { ... } }`. For a reference to several content types, use inline fragments on the PascalCase type names and select `__typename`. `skip` and `limit` on a connection only work for fields marked Multiple. Referenced items are paginated whether or not they are published. + +### Modular blocks, JSON RTE and assets + +- **Modular blocks** are unions. Select `__typename` and use inline fragments named `<ContentType><Field><Block>`, for example `... on PageSectionsHero { hero { title } }`. +- **JSON RTE** returns the document in `json`. Fetch embedded entries and assets through `embedded_itemsConnection` with inline fragments, using `... on SysAsset` for assets. Embedded items can't be filtered, and references inside embedded entries can't be fetched. +- **Assets** are `SysAsset`. Use `all_assets` for lists. Image fields accept transform arguments such as `width`, `height`, `format`, `fit` and `quality`. + +### Filtering, sorting and pagination + +Filter with `where`: equality plus `_ne`, `_exists`, `_in`, `_nin`, `_gt`, `_gte`, `_lt`, `_lte`, combined with `AND` and `OR`. Regular expressions aren't supported. `order_by` accepts `created_at` and `updated_at`, ascending or descending. Paginate root collections with `skip` and `limit` (default 25, max 100) and request `total` to know when to stop. + +### Query limits + +- Up to 3 levels of nested references, and 3 levels for reference filters in `where`. +- Up to 7,500 records per query, counting root and referenced entries. +- Up to 100 referenced records per reference connection. +- Up to 100 embedded items per JSON RTE field. +- Responses up to 7 MB, and one operation per request: use aliases instead of batching. +- Up to 3 variant UIDs in `x-cs-variant-uid`. + +When a page needs more, split it into several queries, or fetch the deep part with a separate query keyed by UID. + +### Rate limits and errors + +Uncached requests to the origin are limited per organization (80 per second by default, depending on plan). Requests served from the CDN cache aren't rate-limited but still count toward usage. A 429 means back off; read `X-RateLimit-Limit` and `X-RateLimit-Remaining`. HTTP 200 doesn't guarantee success: partial failures return in `errors` alongside `data`, so always check `errors` before using the result. + +### Live Preview with GraphQL + +When a preview hash is present, swap the host for the region's preview host (`graphql-preview.contentstack.com`, `eu-graphql-preview.contentstack.com` and so on) and add the `live_preview` (the hash) and `preview_token` headers. Keep `access_token`. Under CSR read the hash from `ContentstackLivePreview.hash`; under SSR read it from the `live_preview` query parameter on the request. Never cache preview responses. Edit tags (`data-cslp`) need the response normalized first, because `Connection`, `edges` and `node` wrappers break tag paths. Route anything that still fails to `cms-live-preview-visual-editor-support-assistant`. + +## MCP tools + +The Contentstack MCP server has no GraphQL tool; run queries in GraphiQL or from code. To check the underlying data when a query returns less than expected, use these read-only tools (local server group in brackets): + +- Inspect: `get_all_entries_cdn`, `get_a_single_entry_cdn`, `get_all_assets_cdn` (`cda`, the same published data over REST); `get_a_single_content_type` (`cma`, to check UIDs against the GraphQL naming rules) + +## Output + +Lead with the working query or the fix. Show the endpoint and headers when they matter. State which limit applies when splitting a query. Keep explanations short. + +## Security + +### Defaults + +- Advisory by default. Don't send queries with real credentials or change stack settings unless the user asks. +- Delivery tokens are read-only and environment-scoped; they can be used client-side. +- Preview tokens belong only on preview deployments. +- Never use management tokens with GraphQL or in client code, and never hardcode stack API keys in client-side code. + +### Destructive Actions + +None. This skill only reads published content and advises. + +### Secrets + +Never echo tokens or live preview hashes. Use placeholders in examples. + +### Environment Variables + +Use variables such as `CONTENTSTACK_API_KEY`, `CONTENTSTACK_DELIVERY_TOKEN`, `CONTENTSTACK_ENVIRONMENT` and `CONTENTSTACK_PREVIEW_TOKEN` in examples. + +## References + +- https://www.contentstack.com/docs/developers/apis/graphql-content-delivery-api diff --git a/skills/cms-json-rte/SKILL.md b/skills/cms-json-rte/SKILL.md new file mode 100644 index 0000000..e7dca07 --- /dev/null +++ b/skills/cms-json-rte/SKILL.md @@ -0,0 +1,192 @@ +--- +name: cms-json-rte +description: "Help developers render and convert Contentstack JSON Rich Text Editor (JSON RTE) content. Cover the JSON RTE document structure (doc, blocks, text leaves, marks, attrs, uid), rendering to HTML with @contentstack/utils jsonToHTML and custom renderOption per node type, mark and embedded item, embedded entries and assets (includeEmbeddedItems, _embedded_items, reference nodes), GraphQL embedded_itemsConnection with GQL.jsonToHTML, React rendering, HTML and Markdown conversion with @contentstack/json-rte-serializer, migrating HTML RTE to JSON RTE, Live Preview edit tags for RTE fields, empty-field checks, limits and XSS-safe rendering. Use when developers render, serialize, transform or debug JSON RTE content, embedded items that don't show, or custom node types. For fetching entries use cms-entries or dx-delivery-sdk; for GraphQL query shape use cms-graphql; for building an RTE plugin use developer-hub-app-architect." +allowed-tools: Read Grep Glob +--- + +# Contentstack JSON Rich Text Editor + +Help developers turn JSON RTE documents into HTML, Markdown or components, resolve embedded entries and assets, and convert between HTML and JSON safely. + +## When to use + +- Rendering a JSON RTE field to HTML or to framework components. +- Writing a `renderOption` for custom node types, marks or embedded entries and assets. +- Embedded items that render empty or as placeholders. +- Rendering JSON RTE from a GraphQL response. +- Converting HTML or Markdown to and from JSON RTE, or migrating an HTML RTE field. +- Checking whether a JSON RTE field is empty, or hitting its size and embed limits. + +Use another skill for: + +- Fetching entries, references and pagination with the CDA → `cms-entries` +- Delivery SDK setup, query chains and typing → `dx-delivery-sdk` +- GraphQL endpoints, query shape and limits → `cms-graphql` +- Live Preview or Visual Editor setup and debugging → `cms-live-preview-visual-editor-support-assistant` +- Building a JSON RTE plugin or custom field app → `developer-hub-app-architect` +- Rendering custom elements in Studio → `studio` + +## Before you answer + +Find out how the content is fetched (Delivery SDK, REST CDA or GraphQL), the field UIDs and their paths (top level, inside a group or modular block), whether the field embeds entries or assets, and whether the output is an HTML string or components. + +## Instructions + +1. Fetch the entry with embedded items included. +2. Render with `@contentstack/utils`, adding a `renderOption` only for what the defaults don't cover. +3. Escape anything your own render functions interpolate. +4. Add Live Preview tags to the field's wrapper element if the site uses Live Preview. + +### Document structure + +A JSON RTE value is a tree. The root has `type: "doc"`. Each block node has `uid`, `type` (for example `p`, `h1`–`h6`, `a`, `img`, `embed`, `ol`, `ul`, `li`, `table`, `blockquote`, `code`, `hr`, `reference`), `attrs` and `children`. Leaves are text nodes: `{ "text": "Hello", "bold": true }`. Marks are boolean keys on the leaf: `bold`, `italic`, `underline`, `strikethrough`, `inlineCode`, `subscript`, `superscript`, `break`, plus `classname` and `id`. Text copied from Word or Google Docs carries styling in the leaf's `attrs.style`. + +Embedded entries and assets are `reference` nodes. Their `attrs` include `type` (`entry` or `asset`), `entry-uid` or `asset-uid`, `content-type-uid`, and `display-type`: `block`, `inline` or `link` for entries, and `display`, `download` or `link` for assets. + +An untouched field stores one empty paragraph, not `null`. Treat it as empty when the doc has one child with one child whose `text` is `""`: + +```ts +const isEmpty = (doc?: { children?: { children?: { text?: string }[] }[] }) => + !doc?.children?.length || + (doc.children.length === 1 && + doc.children[0].children?.length === 1 && + doc.children[0].children[0].text === ""); +``` + +### Render with the Delivery SDK + +`@contentstack/delivery-sdk` depends on `@contentstack/utils` and re-exports it as `contentstack.Utils`. Import from `@contentstack/utils` directly if you install it yourself. + +```ts +import contentstack from "@contentstack/delivery-sdk"; +import { jsonToHTML } from "@contentstack/utils"; + +const stack = contentstack.stack({ + apiKey: process.env.CONTENTSTACK_API_KEY!, + deliveryToken: process.env.CONTENTSTACK_DELIVERY_TOKEN!, + environment: process.env.CONTENTSTACK_ENVIRONMENT!, +}); + +const entry = await stack + .contentType("blog_post") + .entry("<ENTRY_UID>") + .includeEmbeddedItems() + .fetch<{ uid: string; body: unknown }>(); + +jsonToHTML({ entry, paths: ["body", "sections.rich_text"], renderOption }); +// entry.body is now an HTML string. +``` + +- `jsonToHTML` **mutates the entry in place**: each field in `paths` is replaced by its HTML string. Keep a copy if you also need the JSON, and don't run it twice on the same object. +- `paths` are field UID paths, with dots for nesting inside groups and modular blocks. It accepts one entry or an array (for example `result.entries` from `.find()`). +- `includeEmbeddedItems()` sends `include_embedded_items[]=BASE`; the embedded items arrive in `entry._embedded_items`. On the REST CDA, add that query parameter yourself. + +### Render options + +A `renderOption` object overrides the defaults by key: + +- Node types: `p: (node, next) => \`<p>${next(node.children)}</p>\``. Always call `next(node.children)` or the children disappear. +- Marks: `bold: (text) => \`<strong>${text}</strong>\``. +- Embedded entries by display type, optionally per content type: `block: { product: (item, metadata) => ..., $default: (item, metadata) => ... }`, and the same for `inline`. +- Embedded assets and links: `display`, `download` and `link` receive `(item, metadata)`; `metadata.attributes` holds the node's `attrs` and `metadata.text` the link text. +- `reference: (node, next) => ...` is used for a reference node when the item isn't in `_embedded_items`, so it's a good place for a fallback. + +See [references/render-options.md](references/render-options.md) for a complete, escaped `renderOption` and a React renderer. + +### Embedded items that don't render + +- **Missing `includeEmbeddedItems()`** (or `include_embedded_items[]=BASE`): the reference node finds no item. With no `reference` render option it renders as an empty string. +- **Wrong content type key**: per-type renderers are keyed by the embedded entry's content type UID, not its title. +- **Unpublished embedded entry**: only items published to the same environment and locale are returned. +- **References inside an embedded entry** aren't expanded. Fetch that entry separately with `includeReference()` if you need them. +- **Limits**: up to 100 embedded entries per JSON RTE, 15 referenceable content types per field, 30 KB of JSON per field by default (support can raise it), up to 100 instances of a multiple JSON RTE field, and up to 15 JSON RTE fields per content type. + +### GraphQL + +Request the field's `json` and its `embedded_itemsConnection`. Include `system { uid content_type_uid }` on every embedded entry fragment and `system { uid }` plus `url` on `SysAsset`, because the utils match embedded items by those UIDs. Then: + +```ts +import { GQL, updateAssetURLForGQL } from "@contentstack/utils"; + +updateAssetURLForGQL(response); // sets asset-link on embedded assets from their url +GQL.jsonToHTML({ entry: response.data.all_blog_post.items, paths: ["body"], renderOption }); +``` + +GraphQL returns at most 100 embedded items per request. Query shape and limits are in `cms-graphql`. + +### React and other component frameworks + +Two options: + +- **HTML string:** run `jsonToHTML` on the server and inject the result (`dangerouslySetInnerHTML` in React). Only do this with render functions that escape what they interpolate, and sanitize the output if editors can paste arbitrary content. +- **Components:** walk the JSON tree yourself and map `type` to components and marks to wrappers. This avoids raw HTML, makes embedded entries real components, and is the better fit for interactive blocks. See the reference file. + +### Escaping and XSS + +The default renderers pass content through a small tag and attribute allowlist (`sanitizeHTML`), but it is regex-based and doesn't validate URL schemes, so `javascript:` links can survive. Your own render functions get no sanitizing at all: `` `<h2>${item.title}</h2>` `` injects whatever the editor typed. + +- Escape text and attribute values in every custom render function. +- Allow only `http:`, `https:`, `mailto:` and relative URLs in `href` and `src`. +- If you inject HTML in the browser, run it through a maintained sanitizer such as DOMPurify. + +### Convert HTML, JSON and Markdown + +`@contentstack/json-rte-serializer`: + +- `jsonToHtml(doc, options?)`: JSON to HTML. `customElementTypes` renders custom block or inline types, `customTextWrapper` renders custom leaf properties, `allowedEmptyAttributes` and `addNbspForEmptyBlocks` tune the output. +- `htmlToJson(element, options?)`: takes a DOM element, not a string. In the browser use `new DOMParser().parseFromString(html, "text/html").body`; in Node use `jsdom`. `customElementTags` and `customTextTags` keys are upper-case tag names (`"SOCIAL-EMBED"`). `allowNonStandardTags: true` keeps tags the editor doesn't support natively. +- `jsonToMarkdown(doc)`: JSON to Markdown. + +Use the serializer to convert content for import, migration or other systems. For rendering delivered entries, prefer `@contentstack/utils`, which resolves embedded items. + +To move an existing HTML RTE field to JSON RTE across entries, use the CLI plugin: `csdx plugins:install @contentstack/cli-cm-migrate-rte`, then `csdx cm:entries:migrate-html-rte`. Both fields must be at the same depth, a "Multiple" JSON RTE can't take content from a single HTML RTE, and both must allow the same referenced content types. See `dx-cli` for CLI auth. + +### Live Preview and Visual Editor + +Call `addEditableTags(entry, contentTypeUid, tagsAsObject, locale)` from `@contentstack/utils` (or `contentstack.Utils`), then put the field's tag on the element that wraps the rendered HTML, for example `<div {...entry.$.body} dangerouslySetInnerHTML={{ __html: entry.body }} />`. Setup and debugging belong to `cms-live-preview-visual-editor-support-assistant`. + +## MCP tools + +When the Contentstack MCP server is connected (local server group in brackets): + +- Inspect: `get_a_single_entry_cdn` and `get_all_entries_cdn` (`cda`) to see the delivered JSON, `get_single_entry` (`cma`) for the draft, `get_a_single_content_type` (`cma`) to find JSON RTE field UIDs, paths and allowed reference content types. +- Change: `update_an_entry` (`cma`) writes a whole JSON RTE value, for example after an HTML to JSON conversion. + +There are no rendering or conversion tools; rendering runs in the user's code. Check whether the `cda` tools accept the embedded items parameter in the client's tool schema before relying on it. + +## Output + +Lead with the cause or the recommended approach, then the code. Name the field paths you assumed and mark placeholders. Show escaping in every custom render function. + +## Security + +### Defaults + +- Advisory by default. Use the MCP write tool above only when the user asks for the change. +- Treat RTE content as untrusted input: escape in custom renderers and sanitize injected HTML. +- Use delivery tokens for rendering. Preview tokens stay on preview deployments, and management tokens never reach the browser. + +### Destructive Actions + +Before writing converted JSON back to entries, or running the HTML to JSON migration, name the stack, branch, content type, fields and number of entries, recommend a backup or a non-production branch, and wait for confirmation. A wrong conversion overwrites content. + +### Secrets + +Never print, echo or store tokens or API keys. Use placeholders and environment variables in examples. + +### Environment Variables + +Use `CONTENTSTACK_API_KEY`, `CONTENTSTACK_DELIVERY_TOKEN`, `CONTENTSTACK_ENVIRONMENT` and, for previews, `CONTENTSTACK_PREVIEW_TOKEN`, with placeholder values. + +## References + +- [references/render-options.md](references/render-options.md): escaped render options, GraphQL query and React renderer. +- https://www.contentstack.com/docs/headless-cms/about-json-rich-text-editor +- https://www.contentstack.com/docs/headless-cms/schema-of-json-rich-text-editor +- https://www.contentstack.com/docs/headless-cms/limitations-for-json-rich-text-editor +- https://www.contentstack.com/docs/developers/sdks/utils-sdk/javascript/get-started-with-javascript-utils-library +- https://www.contentstack.com/docs/developers/sdks/utils-sdk/javascript/use-graphql-queries-with-javascript-utils-sdk-to-render-json-rte-content +- https://www.contentstack.com/docs/headless-cms/render-json-rte-content-using-json-rte-serializer +- https://www.contentstack.com/docs/headless-cms/cli-migrate-content-from-html-rte-to-json-rte +- https://github.com/contentstack/contentstack-utils-javascript +- https://github.com/contentstack/json-rte-serializer diff --git a/skills/cms-json-rte/references/render-options.md b/skills/cms-json-rte/references/render-options.md new file mode 100644 index 0000000..4ae6550 --- /dev/null +++ b/skills/cms-json-rte/references/render-options.md @@ -0,0 +1,188 @@ +# JSON RTE render recipes + +Read this when writing a full `renderOption`, rendering from GraphQL, or rendering JSON RTE as React components. The code targets `@contentstack/utils` 1.9 and `@contentstack/delivery-sdk` 5.x. + +## An escaped renderOption + +Every function that interpolates content escapes it. URLs pass through `safeUrl`, which only allows `http:`, `https:`, `mailto:` and relative URLs. + +```ts +import type { RenderItem, RenderMark, RenderNode, RenderOption } from "@contentstack/utils"; + +// RenderOption values are a union of function types, so wrap each function to type its parameters. +const node = (fn: RenderNode) => fn; +const mark = (fn: RenderMark) => fn; +const item = (fn: RenderItem) => fn; + +const escapeHtml = (value: unknown): string => + String(value ?? "") + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + +const safeUrl = (value: unknown): string => { + const url = String(value ?? "").trim(); + return /^(https?:|mailto:|\/|#)/i.test(url) ? escapeHtml(url) : "#"; +}; + +export const renderOption: RenderOption = { + // Node types: `next` renders the children. Leaf text arrives already rendered. + p: node((n, next) => `<p>${next(n.children)}</p>`), + h2: node((n, next) => `<h2 class="heading">${next(n.children)}</h2>`), + a: node( + (n, next) => + `<a href="${safeUrl(n.attrs.url ?? n.attrs.href)}"${ + n.attrs.target === "_blank" ? ' target="_blank" rel="noopener noreferrer"' : "" + }>${next(n.children)}</a>` + ), + + // Marks + bold: mark((text) => `<strong>${text}</strong>`), + italic: mark((text) => `<em>${text}</em>`), + + // Embedded entries shown as blocks, per content type UID, with a default + block: { + product: item( + (entry) => + `<article class="product"><h3>${escapeHtml(entry.title)}</h3><p>${escapeHtml(entry.price)}</p></article>` + ), + $default: item((entry) => `<div class="embed">${escapeHtml(entry.title)}</div>`), + }, + inline: { + $default: item((entry) => `<span class="embed-inline">${escapeHtml(entry.title)}</span>`), + }, + // Entries or assets inserted as links + link: item( + (entry, metadata) => `<a href="${safeUrl(entry.url)}">${escapeHtml(metadata.text || entry.title)}</a>` + ), + // Assets shown as images + display: item( + (asset, metadata) => + `<img src="${safeUrl(asset.url ?? metadata.attributes["asset-link"])}" alt="${escapeHtml( + metadata.attributes.alt ?? asset.title ?? "" + )}" />` + ), + + // Fallback when an embedded item wasn't returned (not included, or unpublished) + reference: node(() => ""), +}; +``` + +Keys that aren't in your `renderOption` fall back to the package defaults. + +## GraphQL + +Query the `json` and the embedded items, with the `system` fields the utils use for matching: + +```graphql +query BlogPost($url: String!) { + all_blog_post(where: { url: $url }) { + items { + title + body { + json + embedded_itemsConnection { + edges { + node { + ... on Product { + title + price + system { uid content_type_uid } + } + ... on SysAsset { + title + url + system { uid } + } + } + } + } + } + } + } +} +``` + +```ts +import { GQL, updateAssetURLForGQL } from "@contentstack/utils"; + +updateAssetURLForGQL(response); +GQL.jsonToHTML({ entry: response.data.all_blog_post.items, paths: ["body"], renderOption }); +// Each item's body is replaced by its HTML string. +``` + +The GraphQL type name of an embedded content type is generated from its UID (`product` → `Product`). Check the schema if a fragment doesn't match; `cms-graphql` covers the naming rules. + +## React components instead of an HTML string + +Walk the tree and map node types to elements. Embedded items are looked up from `_embedded_items` (REST and SDK) by UID. + +```tsx +import type { ReactNode } from "react"; + +type RteText = { text: string; bold?: boolean; italic?: boolean; underline?: boolean; inlineCode?: boolean }; +type RteNode = { type: string; uid?: string; attrs?: Record<string, any>; children?: Array<RteNode | RteText> }; +type Embedded = { uid: string; _content_type_uid?: string; title?: string; url?: string; [key: string]: any }; + +const isText = (node: RteNode | RteText): node is RteText => "text" in node; + +function renderText(leaf: RteText, key: number): ReactNode { + let out: ReactNode = leaf.text; + if (leaf.inlineCode) out = <code>{out}</code>; + if (leaf.bold) out = <strong>{out}</strong>; + if (leaf.italic) out = <em>{out}</em>; + if (leaf.underline) out = <u>{out}</u>; + return <span key={key}>{out}</span>; +} + +export function JsonRte({ doc, embedded = [] }: { doc: RteNode; embedded?: Embedded[] }) { + const byUid = new Map(embedded.map((item) => [item.uid, item])); + + const renderNode = (node: RteNode | RteText, key: number): ReactNode => { + if (isText(node)) return renderText(node, key); + const children = node.children?.map(renderNode); + const attrs = node.attrs ?? {}; + switch (node.type) { + case "doc": + return <>{children}</>; + case "p": + return <p key={key}>{children}</p>; + case "h2": + return <h2 key={key}>{children}</h2>; + case "ul": + return <ul key={key}>{children}</ul>; + case "ol": + return <ol key={key}>{children}</ol>; + case "li": + return <li key={key}>{children}</li>; + case "a": { + const href = String(attrs.url ?? attrs.href ?? ""); + return ( + <a key={key} href={/^(https?:|mailto:|\/|#)/i.test(href) ? href : "#"}> + {children} + </a> + ); + } + case "reference": { + const item = byUid.get(attrs["entry-uid"] ?? attrs["asset-uid"]); + if (!item) return null; + if (attrs.type === "asset") return <img key={key} src={item.url} alt={attrs.alt ?? item.title ?? ""} />; + return <EmbeddedEntry key={key} entry={item} display={attrs["display-type"]} />; + } + default: + return <div key={key}>{children}</div>; + } + }; + + return <>{renderNode(doc, 0)}</>; +} + +function EmbeddedEntry({ entry, display }: { entry: Embedded; display?: string }) { + if (display === "inline") return <span>{entry.title}</span>; + return <aside data-content-type={entry._content_type_uid}>{entry.title}</aside>; +} +``` + +Pass `embedded={Object.values(entry._embedded_items ?? {}).flat()}`. React escapes text and attribute values, so this path doesn't need `escapeHtml`, but URLs still need the scheme check. Extend the `switch` with the node types your content uses (`h1`–`h6`, `blockquote`, `code`, `table`, `img`, `embed`, `hr`). Before you build a full renderer, check for a maintained community package that fits your framework. diff --git a/skills/cms-live-preview-visual-editor-support-assistant/SKILL.md b/skills/cms-live-preview-visual-editor-support-assistant/SKILL.md index 56886cc..c737807 100644 --- a/skills/cms-live-preview-visual-editor-support-assistant/SKILL.md +++ b/skills/cms-live-preview-visual-editor-support-assistant/SKILL.md @@ -6,24 +6,47 @@ allowed-tools: Read Grep Glob # Live Preview and Visual Editor Support Assistant -## Description - Answer any question about Contentstack Visual Experience: Live Preview, Visual Editor (also called Visual Builder) and Timeline. One flow covers all of it. A question about how something works is answered from the references and the documentation; a new integration follows the official documentation and is verified with the Onboarding Check; a failing one is localised to one broken contract from evidence, then gets the smallest fix. -## When to Use +## When to use Use for any Visual Experience question: how a feature works or behaves, what a setting or check -means, setting up an integration, or an integration that misbehaves. +means, setting up an integration, or an integration that misbehaves. Stay here when the question is +how to wire preview, the canvas or Timeline, or why they misbehave, even if the answer is one +Delivery SDK call. + +Use another skill for: + +- Delivery SDK query code with no preview involved → `dx-delivery-sdk` +- GraphQL queries with no preview involved → `cms-graphql` +- Rendering JSON Rich Text Editor content → `cms-json-rte` +- Work inside the Next.js kickstart repository → `dx-kickstart-next` +- Contentstack Studio compositions and canvas → `studio` +- Variant and Personalize setup → `cms-variants-personalization` or `personalize` +- Hosting and builds on Launch → `launch-platform` + +## Before you answer + +Collected step by step, never all at once. Never ask for delivery tokens, preview tokens, management +tokens, live preview hashes, cookies or auth headers. + +## Instructions -Hand off to `dx-delivery-sdk` when the user wants Delivery SDK query code and preview is not involved. -Stay here when the question is how to wire preview, the canvas or Timeline, or why they misbehave, -even if the answer is one Delivery SDK call. +Report this back ticked. An unticked step is a visible gap. + +- [ ] 1. Identify the product. +- [ ] 2. Question, new integration, never worked, or regression? Questions and new integrations are answered from the references and docs. +- [ ] 3. Read the Onboarding Check. +- [ ] 4. Detect framework, rendering mode and fetch layer from the code. +- [ ] 5. Collect only the evidence steps 3 and 4 did not settle. +- [ ] 6. Localise to one contract. +- [ ] 7. Fix and verify, or hand to Contentstack Support. -## Terminology +### Terminology **Visual Builder and Visual Editor are the same product.** Current documentation and the app UI say Visual Editor; older documentation and marketing say Visual Builder. Never treat the two names as a @@ -34,48 +57,14 @@ discrepancy. Mirror the term the user used. - **`mode: "builder"`** (SDK option) and **`visual_builder.onboarding-setup-visible`** (stack setting) keep their names. Do not rename them in an answer. -## User Problem +### Why a fixed protocol Setup instructions reconstructed from memory drift from the documentation and miss the requirements that differ per rendering mode. In debugging, "edits do not show" can be a fetch host problem, a cache, a locale, or a role, and only evidence separates them. Guessing produces a long thread of speculative fixes. -## Success Criteria - -- Works the steps in order and reports the checklist back ticked -- For a new integration, links the documentation page for the framework and mode instead of - inventing steps, and does not ask diagnostic questions about code that does not exist yet -- Reads the Onboarding Check before any other diagnostic question -- Never asks for what the code or earlier evidence already answers -- Names one broken contract, with evidence, before proposing a change -- Recognises causes outside the application and hands them to Contentstack Support - -## Expected Inputs - -Collected step by step, never all at once. Never ask for delivery tokens, preview tokens, management -tokens, live preview hashes, cookies or auth headers. - -## Expected Outputs - -- For a new integration: the documentation page, the requirements that apply, and how to verify -- For a failing one: the broken contract and the evidence for it -- The smallest correct change, or a plain statement that no application-side fix exists -- One verification the user runs -- A Support handover note when the cause is outside the application - -## Example User Requests - -- "What does the Onboarding Check actually verify?" -- "How do I set up Live Preview in our Next.js App Router site?" -- "We have Live Preview working. What do we add for Visual Builder?" -- "Live Preview loads but my edits never show up." -- "The preview pane is blank / says it could not connect." -- "Visual Builder shows Setup Complete but I can't click anything." -- "Preview worked yesterday and now shows published content." -- "Timeline always shows the current version." - -## Rules +### Rules Follow these on every turn that diagnoses a failure. @@ -91,20 +80,6 @@ Follow these on every turn that diagnoses a failure. 6. **No resilience while diagnosing.** Add no fallbacks, retries or caches. A fallback to published content hides the exact signal being debugged. -## Workflow Summary - -Report this back ticked. An unticked step is a visible gap. - -- [ ] 1. Identify the product. -- [ ] 2. Question, new integration, never worked, or regression? Questions and new integrations are answered from the references and docs. -- [ ] 3. Read the Onboarding Check. -- [ ] 4. Detect framework, rendering mode and fetch layer from the code. -- [ ] 5. Collect only the evidence steps 3 and 4 did not settle. -- [ ] 6. Localise to one contract. -- [ ] 7. Fix and verify, or hand to Contentstack Support. - -## Instructions - ### Step 1. Which product? Ask explicitly. Users say "preview" for all of them, and each has its own check and failure set. @@ -246,7 +221,33 @@ Some causes sit outside the application: provisioning, plan gating, browser poli fixes them. See [references/contact-support.md](references/contact-support.md) and produce its handover note from what steps 1 to 6 already collected. -## Output Format +### Inspecting code + +Read-only inspection with Read, Grep and Glob. Useful greps: `init(`, `addEditableTags`, +`onEntryChange`, `live_preview`, `preview_token`, hostname constants, cache configuration. + +Minimum versions: Live Preview Utils v3.0+ for Visual Editor (legacy `contentstack` JavaScript SDK +v3.20.3+); Live Preview Utils v4.4.4+ for `setPageContext()`. + +Without repository access, run the same steps against described code and screenshots. Do not claim +to have inspected code. + +## MCP tools + +No MCP tool reads the stack's Visual Experience settings or the Onboarding Check; read those in +*Settings → Visual Experience*. When the Contentstack MCP server is connected, these read tools help +gather evidence for steps 5 and 6 (local server group in brackets): + +- Environments and their base URLs: `get_all_environments`, `get_an_environment` (`cma`) +- Draft versus published content: `get_single_entry` (`cma`) against `get_a_single_entry_cdn` (`cda`) +- Locales and branches: `get_all_languages`, `get_all_branches` (`cma`) +- Entry variants: `get_all_variants_of_an_entry` (`cma`) +- Sites hosted on Launch: `get_deployment_logs`, `get_server_logs` (`launch`) + +Use them only to read. Changing environments, entries or settings stays behind the confirmation +rule in Destructive Actions. + +## Output New integration: follow "Reply shape" in [references/setup-docs.md](references/setup-docs.md). @@ -259,16 +260,23 @@ say what each answer would mean. Say whether a behaviour applies to preview only or to production too. Preview output reaching real visitors is a production incident. -## Tooling Notes +What a good answer delivers: -Read-only inspection with Read, Grep and Glob. Useful greps: `init(`, `addEditableTags`, -`onEntryChange`, `live_preview`, `preview_token`, hostname constants, cache configuration. +- For a new integration: the documentation page, the requirements that apply, and how to verify +- For a failing one: the broken contract and the evidence for it +- The smallest correct change, or a plain statement that no application-side fix exists +- One verification the user runs +- A Support handover note when the cause is outside the application -Minimum versions: Live Preview Utils v3.0+ for Visual Editor (legacy `contentstack` JavaScript SDK -v3.20.3+); Live Preview Utils v4.4.4+ for `setPageContext()`. +Checks before replying: -Without repository access, run the same steps against described code and screenshots. Do not claim -to have inspected code. +- Works the steps in order and reports the checklist back ticked +- For a new integration, links the documentation page for the framework and mode instead of + inventing steps, and does not ask diagnostic questions about code that does not exist yet +- Reads the Onboarding Check before any other diagnostic question +- Never asks for what the code or earlier evidence already answers +- Names one broken contract, with evidence, before proposing a change +- Recognises causes outside the application and hands them to Contentstack Support ## Security @@ -282,6 +290,11 @@ to have inspected code. - Validate preview target hosts before suggesting header or CSP changes, and scope any relaxation to the preview deployment. +### Destructive Actions + +Do not change stack settings, roles, tokens, cache or environment configuration automatically. +Propose the change, state the blast radius, and require confirmation. + ### Secrets Never reveal, echo or reconstruct preview tokens, delivery tokens, management tokens, live preview @@ -292,8 +305,3 @@ recommend rotating it. Ask for environment variable names, never their values. Use placeholder examples such as `CONTENTSTACK_PREVIEW_TOKEN=<your_preview_token>`. - -### Destructive Actions - -Do not change stack settings, roles, tokens, cache or environment configuration automatically. -Propose the change, state the blast radius, and require confirmation. diff --git a/skills/cms-localization/SKILL.md b/skills/cms-localization/SKILL.md index f4aa019..d791be1 100644 --- a/skills/cms-localization/SKILL.md +++ b/skills/cms-localization/SKILL.md @@ -1,80 +1,91 @@ --- name: cms-localization -description: "Advise developers on Contentstack localization: language setup, fallback chains, localized vs unlocalized entries, non-localizable fields, and multi-locale publishing. Use when developers ask about languages, fallback chains, localizing entries, non-localizable fields, or multi-locale publishing. Clarify whether the question concerns the CMS editorial experience or CDA delivery behavior." +description: "Advise developers on Contentstack localization: language setup, fallback chains, localized vs unlocalized entries, non-localizable fields, and multi-locale publishing. Use when developers ask about languages, fallback chains, localizing entries, non-localizable fields, or multi-locale publishing. Clarify whether the question concerns the CMS editorial experience or CDA delivery behavior. For translation apps such as Trados or Smartling, use marketplace-apps." allowed-tools: Read Grep Glob --- # Contentstack Localization -## Description +Advise on multilingual content: the master language, fallback chains, localized vs unlocalized entries, non-localizable fields, CDA locale queries and multi-locale publishing. -Advise developers on Contentstack localization: language setup, fallback chains, localized vs unlocalized entries, non-localizable fields, and multi-locale publishing. +## When to use -## When to Use +- Setting up languages and fallback chains. +- Localized vs unlocalized entries, and Unlocalize. +- Deciding which fields are non-localizable. +- Querying locales on the CDA and publishing in several locales. -Use when developers ask about languages, fallback chains, localizing entries, non-localizable fields, or multi-locale publishing. Clarify whether the question concerns the CMS editorial experience or CDA delivery behavior. +Use another skill for: -## User Problem +- Translation and localization apps from the Marketplace → `marketplace-apps` +- Localizing taxonomy terms → `cms-taxonomy` +- Locale-aware queries in SDK code → `dx-delivery-sdk` +- Modeling which content is shared across locales → `cms-data-modeling-best-practices` -Developers need to design multilingual content delivery with correct fallback behavior, efficient editorial workflows, and minimal redundancy. +## Before you answer -## Success Criteria +Find out the target languages and markets, the fallback requirements, the translation workflow, which content is shared across locales, and whether the question is about the CMS editorial experience or CDA delivery behavior. If that last point is unclear, ask. -Language hierarchy is correct and master language constraints are stated. -Fallback behavior is explained clearly, including chain traversal. -Field-level localization choices are practical and consistent. -Multi-locale publishing guidance is accurate and actionable. -Advice distinguishes editorial UI behavior from CDA behavior. +## Instructions -## Expected Inputs +1. Confirm the target locales and markets, and whether the question is about CMS or CDA behavior. +2. Explain the master language constraints and design the fallback chain. +3. Recommend which fields stay non-localizable. +4. Describe localized vs unlocalized entry behavior and its editorial impact. +5. Give CDA locale query guidance and a multi-locale publishing strategy. -- Target languages and markets -- Fallback requirements -- Editorial workflow for translations -- Content shared across locales +### Master language -## Expected Outputs +State upfront that the master language is permanent, set at stack creation, and ends every fallback chain. -- Language hierarchy recommendations -- Fallback chain design -- Non-localizable field guidance -- Publishing strategy for multiple locales -- CDA locale query guidance +### Fallback chain design -## Example User Requests +Each language can have one fallback language. Inheritance is a chain such as `fr-ca` -> `fr-fr` -> `en-us` (master). Warn that changing fallback relationships later affects existing content inheritance. -- How do I set up languages with fallback in Contentstack? -- What is the difference between a localized and unlocalized entry? -- Which fields should I mark as non-localizable? -- How do I publish content in multiple languages at once? -- What happens if a locale has no content? +### Localized vs unlocalized entries -## Workflow Summary +A localized entry is an independent copy with its own version history, publishing status and workflow state. An unlocalized entry inherits from its fallback chain. Localizing is reversible: Unlocalize removes the locale's own content and the entry goes back to inheriting from its fallback, or from the master language if the fallback has no content. Unlocalized and deleted entries can be restored from Trash within 14 days. -Confirm target locales, markets, and whether the question is about CMS or CDA behavior. -Explain master language constraints and design the fallback chain. -Recommend which fields should remain non-localizable. -Describe localized vs unlocalized entry behavior and editorial impact. -Give CDA locale query guidance and multi-locale publishing strategy. +### Non-localizable fields -## Instructions +Recommend marking structural or shared data as non-localizable, such as SKUs, prices, dates, coordinates, boolean flags, shared assets and identifiers. Keep human-readable text fields localizable. + +### CDA locale queries + +Tell developers to pass `locale` explicitly in CDA requests. Use `include_fallback=true` when they want the fallback chain to apply; without it, only the exact locale is checked. + +### Multi-locale publishing + +Editors can publish multiple locale versions from the master language entry, subject to plan limits. Only the latest version of each localized entry is published, and localized entry versions can be deleted only from the master language entry's delete modal. + +## MCP tools -[{"heading":"Master language","content":"State upfront that the master language is permanent, set at stack creation, and ends every fallback chain."},{"heading":"Fallback chain design","content":"Explain that each language can have one fallback language. Describe inheritance as a chain such as fr-ca -> fr-fr -> en-us (master). Warn that changing fallback relationships later affects existing content inheritance."},{"heading":"Localized vs unlocalized entries","content":"Clarify that a localized entry is an independent copy with its own version history, publishing status, and workflow state. An unlocalized entry inherits from its fallback chain. Localizing is a one-way operation per locale and entry."},{"heading":"Non-localizable fields","content":"Recommend marking structural or shared data as non-localizable, such as SKUs, prices, dates, coordinates, boolean flags, shared assets, and identifiers. Keep human-readable text fields localizable."},{"heading":"CDA locale queries","content":"Tell developers to pass locale explicitly in CDA requests. Use include_fallback=true when they want the full fallback chain to apply; without it, only the exact locale is checked."},{"heading":"Multi-locale publishing","content":"Explain that editors can publish multiple locale versions from the master language entry, subject to plan limits. Note that only the latest version of each localized entry is published, and localized entry versions can be deleted only from the master language entry's delete modal."}], +When the Contentstack MCP server is connected, these tools act on the stack (local server group in brackets): + +- Inspect: `get_all_languages` (`cma`); `get_all_entries_cdn`, `get_a_single_entry_cdn` with `locale` and `include_fallback` (`cda`, to see what each locale serves) +- Change: `localize_an_entry`, `publish_an_entry` (several locales at once) (`cma`) +- Remove: `unlocalize_an_entry` (`cma`) + +There are no tools to add languages or change fallback chains; do that in the stack's language settings or with the CMA. + +## Output + +State whether the answer covers the editorial UI or CDA behavior. Lead with the recommendation, and show the fallback chain or the query inline. ## Security ### Defaults +- Advisory by default. Use the MCP tools above only when the user asks for the change. - Never expose secrets, tokens, or API keys in output or logs. -- Always use environment variables for credentials. -- Never hardcode secrets in scripts, examples, or instructions. +- Always use environment variables for credentials, and never hardcode secrets in scripts, examples, or instructions. +- Route CMA calls through server-side proxies in browser apps, and never hardcode stack API keys in client-side code. - Do not claim access to tools or connectors that are not explicitly available. - Clearly separate verified facts from assumptions. ### Destructive Actions -- Require explicit user confirmation before any destructive action. -- List exactly what will be changed or deleted before proceeding. +- Before localizing, unlocalizing or publishing, list the entry, locales and environments affected and wait for explicit confirmation. Unlocalize removes the locale's own content. - Never auto-execute delete, overwrite, or publish operations. ### Secrets @@ -90,14 +101,3 @@ Give CDA locale query guidance and multi-locale publishing strategy. - Provide example values without real credentials. - Validate presence of required env vars before execution. - Never use default fallback values for secrets. - -## Product Context - -- - Product: CMS -- - Description: Contentstack headless CMS: content types, entries, assets, environments, publishing, workflows, webhooks, and the Content Management API (CMA). -- - Product safety rules: - Never expose management tokens or API keys. -- Always use environment variables for credentials. -- Route all CMA calls through server-side proxies in browser apps. -- Never hardcode stack API keys in client-side code. -- - Default tools: ["CMA API", "Content Types", "Entries", "Assets", "Workflows", "Webhooks", "Environments", "Releases", "Publish Queue"] -- - Default connectors: ["CMA Proxy", "Webhooks"] \ No newline at end of file diff --git a/skills/cms-releases/SKILL.md b/skills/cms-releases/SKILL.md index a2c6242..335ebab 100644 --- a/skills/cms-releases/SKILL.md +++ b/skills/cms-releases/SKILL.md @@ -1,74 +1,55 @@ --- name: cms-releases -description: "Advise developers on using Contentstack Releases for coordinated, atomic content deployment. Cover release creation, item management, staged deployment, webhook storm prevention, and CI/CD integration. Use when developers ask about deploying multiple content changes together, campaign launches, coordinated content updates, release scheduling, or CI/CD content deployment." +description: "Advise developers on using Contentstack Releases for coordinated, atomic content deployment. Cover release creation, item management, staged deployment, webhook storm prevention, and CI/CD integration. Use when developers ask about deploying multiple content changes together, campaign launches, coordinated content updates, release scheduling, or CI/CD content deployment. For single-entry publishing, use cms-environments-publishing." allowed-tools: Read Grep Glob --- # Releases -## Description +Advise on Releases for deploying related entries and assets together, so campaigns and coordinated updates go live at once without manual coordination errors. -Advise developers on using Contentstack Releases for coordinated, atomic content deployment. Cover release creation, item management, staged deployment, webhook storm prevention, and CI/CD integration. +## When to use -## When to Use +- Deploying many entries and assets together, such as a campaign launch. +- Scheduling a release. +- A static site rebuilding many times during one release deployment. +- Release limits, and Releases in a CI/CD pipeline. -Use when developers ask about deploying multiple content changes together, campaign launches, coordinated content updates, release scheduling, or CI/CD content deployment. +Use another skill for: -## User Problem +- Routine single-entry publishing and scheduling → `cms-environments-publishing` +- Debouncing webhook receivers → `cms-webhooks` +- Branches and alias-based deployment → `cms-branches-aliases` +- Approvals before publishing → `cms-workflows` -Developers need to deploy related content changes atomically so campaigns, redesigns, or coordinated updates go live together without manual coordination errors. +## Before you answer -## Success Criteria - -Provides practical release workflow guidance. -Explains webhook storm risks and mitigation. -Shows CI/CD integration patterns when relevant. -Correctly states release limitations and constraints. - -## Expected Inputs - -- Deployment coordination requirements -- Number and type of content items -- CI/CD pipeline details -- Scheduling needs - -## Expected Outputs - -- Release workflow recommendations -- Webhook storm prevention guidance -- CI/CD integration patterns -- Limitation awareness - -## Example User Requests - -- How do I deploy a campaign with 50 entries at once? -- Can I schedule a release for a future date? -- My static site rebuilds hundreds of times per release deployment. -- How do I integrate releases with my CI/CD pipeline? -- What are the limits on releases? - -## Workflow Summary - -Understand the coordination requirements. -Recommend release creation and item grouping. -Advise staging validation before production. -Warn about webhook storms and receiver debouncing. -Cover CI/CD integration if applicable. -State release limitations and when not to use releases. +Find out what has to go live together, how many items and of which types and locales, the CI/CD pipeline, and any scheduling need. ## Instructions +1. Understand the coordination requirements. +2. Recommend release creation and item grouping. +3. Advise staging validation before production. +4. Warn about webhook bursts and receiver debouncing. +5. Cover CI/CD integration if applicable. +6. State release limitations and when not to use releases. + ### Release workflow Create a release with a descriptive name. Add all related entries and assets, including items from multiple content types and locales. Deploy to staging first, validate, then deploy to production. Specify which version of each entry to deploy when needed. -### Webhook storm +### Webhook bursts -A release deployment triggers one webhook event per item. Warn users to debounce webhook receivers and inspect the source key in payloads when present. Refer to the Webhooks skill for receiver-specific handling. +Deploying a release publishes each item, so per-item publish webhooks can fire. Warn users to debounce or batch on the receiver so a rebuild runs once per deployment, and to check the webhook execution logs for what actually fired. Refer to the Webhooks skill for receiver-specific handling. ### Limitations -Max 100 items per single API call when adding items to a release. Release titles max out at 50 characters. Updating release items to latest versions does not automatically add new references from updated entries. Releases are branch-specific. +Add at most 25 items (entries or assets) per request when adding items to a release. Updating release items to their latest versions does not automatically add new references from the updated entries. Releases are branch-specific: a release deploys items from the branch it was created on. + +### Releases v2 + +Releases v2 is plan-gated. It adds multi-locale bulk add of items, cloning a release, and updating all items to their latest versions in one call. Nested references can be included when items are added. ### CI/CD integration @@ -78,30 +59,32 @@ Use a pipeline that creates a release, adds changed items, deploys to staging, r Do not recommend releases for routine single-entry publishes. Use them when coordinated deployment across multiple content pieces is required. -## Output Format +## MCP tools -Be concise and practical. -Emphasize the webhook storm gotcha when relevant. -Prefer bullets or short steps over long prose. -State limitations clearly when they affect the recommendation. +When the Contentstack MCP server is connected, these tools act on the stack (local server group in brackets): -## Tooling Notes +- Inspect: `get_all_releases`, `get_a_single_release`, `get_all_items_in_a_release` (`cma`) +- Change: `create_a_release`, `add_items_to_a_release`, `clone_a_release`, `deploy_a_release` (`cma`) +- Delete: `delete_items_from_a_release` (`cma`) -Read-only advisory. -Do not create, deploy, or modify releases. -Use read-only tools only. +`add_items_to_a_release` follows the 25-items-per-request limit, so batch larger releases. There is no tool to delete a whole release; use the UI or the CMA. + +## Output + +Be concise and practical. Emphasize the webhook burst gotcha when relevant, prefer bullets or short steps, and state limitations when they affect the recommendation. ## Security ### Defaults -Never expose tokens or API keys. -Use environment variables for credentials in example code. -Do not suggest client-side access to management credentials. +- Advisory by default. Use the MCP tools above only when the user asks for the change. +- Never expose tokens or API keys. +- Use environment variables for credentials in example code. +- Do not suggest client-side access to management credentials; route CMA calls through a server-side proxy in browser apps. ### Destructive Actions -Do not perform or instruct destructive or irreversible release operations. This skill is advisory only and must not create, deploy, update, or delete releases. +Before creating, changing or deploying a release, name the release, the branch, the target environments and locales, and the number of items, and wait for confirmation. Deploying to production changes the live site and can fire a webhook for every item. ### Secrets @@ -110,14 +93,3 @@ Never reveal management tokens, API keys, or other secrets. If code is shown, us ### Environment Variables Use environment variables for all credentials in examples and integration guidance. Never hardcode secrets in sample code. - -## Product Context - -- - Product: CMS -- - Description: Contentstack headless CMS: content types, entries, assets, environments, publishing, workflows, webhooks, and the Content Management API (CMA). -- - Product safety rules: - Never expose management tokens or API keys. -- Always use environment variables for credentials. -- Route all CMA calls through server-side proxies in browser apps. -- Never hardcode stack API keys in client-side code. -- - Default tools: ["CMA API", "Content Types", "Entries", "Assets", "Workflows", "Webhooks", "Environments", "Releases", "Publish Queue"] -- - Default connectors: ["CMA Proxy", "Webhooks"] \ No newline at end of file diff --git a/skills/cms-roles-permissions/SKILL.md b/skills/cms-roles-permissions/SKILL.md index fd3ab96..241a104 100644 --- a/skills/cms-roles-permissions/SKILL.md +++ b/skills/cms-roles-permissions/SKILL.md @@ -1,66 +1,43 @@ --- name: cms-roles-permissions -description: "Advise developers on designing roles, permissions, teams, and token access in Contentstack. Explain built-in roles, custom roles, permission merging, team-based access, and token capabilities with least-privilege guidance. Use when developers ask about user permissions, role design, team management, token capabilities, access control, or automation access in Contentstack." +description: "Advise developers on designing roles, permissions, teams, and token access in Contentstack. Explain built-in roles, custom roles, permission merging, team-based access, and token capabilities with least-privilege guidance. Use when developers ask about user permissions, role design, team management, token capabilities, access control, or automation access in Contentstack. For choosing and storing tokens and API auth flows, use cms-tokens-authentication." allowed-tools: Read Grep Glob --- # Roles & Permissions -## Description +Advise on giving users, teams and automation the right level of access in Contentstack, with least privilege and without security gaps. -Advise developers on designing roles, permissions, teams, and token access in Contentstack. Explain built-in roles, custom roles, permission merging, team-based access, and token capabilities with least-privilege guidance. +## When to use -## When to Use +- Explaining built-in roles or designing custom roles. +- Restricting access by content type, environment, locale, branch or action. +- Working out effective permissions when a user has several roles. +- Setting up teams across stacks. +- Choosing the least-privileged credential for automation. -Use when developers ask about user permissions, role design, team management, token capabilities, access control, or automation access in Contentstack. +Use another skill for: -## User Problem +- Token types, storage, rotation and auth flows → `cms-tokens-authentication` +- Workflow stages and publish rules (who can approve or publish) → `cms-workflows` +- Branch-scoped access and aliases → `cms-branches-aliases` +- App permissions and OAuth scopes for Developer Hub apps → `developer-hub-app-architect` -Developers need to grant the right level of access to users, teams, and automation without over-privileging or creating security gaps. +## Before you answer -## Success Criteria - -Recommends the principle of least privilege. -Explains built-in roles, custom role design, permission merging, teams, and token capabilities clearly. -Distinguishes safe client-side tokens from server-side credentials. -Flags rate-limit and SSO edge cases when relevant. - -## Expected Inputs - -- Team structure and user types -- Access control requirements -- Automation or integration needs -- SSO or compliance requirements - -## Expected Outputs - -- Role design recommendations -- Custom role configuration guidance -- Team setup guidance -- Token selection recommendations -- Permission merging explanations - -## Example User Requests - -- What built-in roles does Contentstack have? -- How do I restrict editors to only certain content types? -- What can management tokens do vs authtokens? -- How do I set up teams for multiple stacks? -- What happens when a user has multiple roles? - -## Workflow Summary - -Understand the team structure and access needs. -Recommend built-in roles or custom roles as appropriate. -Explain permission merging if multiple roles apply. -Guide on teams for larger organizations. -Recommend the right token type for automation. +Find out the team structure and user types, the access the requirement needs, any automation or integration that needs credentials, and any SSO or compliance requirements. ## Instructions +1. Understand the team structure and access needs. +2. Recommend built-in roles or custom roles as appropriate. +3. Explain permission merging if several roles apply. +4. Recommend teams for larger organizations. +5. Recommend the right token type for automation. + ### Built-in roles -Explain the default roles first: Owner, Admin, Developer, and Content Manager. Use them when they meet the need; otherwise recommend a custom role. +Explain the default roles first: Owner, Admin, Developer, and Content Manager. Some organizations have additional system roles, such as a read-only role, so check the stack's Roles list. Use built-in roles when they meet the need; otherwise recommend a custom role. ### Custom roles @@ -68,7 +45,7 @@ Use custom roles when access must be limited by content type, environment, local ### Permission merging -If a user has multiple roles, combine allowed actions permissively. Explicit denials override grants. Call out overlap risks when roles are reused across teams or stacks. +A user's effective permissions are the union of every role that applies: direct stack roles, team roles and organization roles. There is no deny rule, so a role cannot take away access that another role grants. Call out overlap risks when roles are reused across teams or stacks, and remove a role rather than trying to override it. ### Teams @@ -76,38 +53,35 @@ Recommend teams for shared access across users and stacks. Explain that team mem ### Token capabilities -Choose the least-privileged token that fits the use case. Explain management tokens, authtokens, and OAuth tokens, and note SSO/org-owner edge cases when relevant. +Choose the least-privileged credential that fits the use case. Explain management tokens (stack-scoped, at most 10 valid per stack at a time), authtokens, OAuth tokens, and machine-to-machine (M2M) apps, which use the OAuth client-credentials grant for service-to-service automation. Note SSO and org-owner edge cases when relevant. ### Rate limits Mention Contentstack rate limits when users ask about automation or bulk operations. Advise checking X-RateLimit-Remaining and using backoff. -## Output Format +## MCP tools -Be concise and advisory. -Favor bullet points over long prose. -State the recommended access model first. -Call out security and token placement constraints explicitly when relevant. +No Contentstack MCP tool manages roles, users, teams or tokens. Make these changes in the Contentstack UI (stack Settings → Roles, organization Teams) or with the Content Management API's roles endpoints. -## Tooling Notes +To review who changed access, `get_audit_log` and `get_audit_log_item` (`cma-extended`) read the stack audit log. -Read-only advisory skill. -Do not create, modify, or delete roles, users, teams, or tokens. -Do not use write-capable tooling unless the user explicitly asks for a non-advisory workflow. +## Output + +Lead with the recommended access model, then the details. Keep it concise and favor bullet points over long prose. Call out security and token placement constraints explicitly when relevant. ## Security ### Defaults -Never expose tokens or API keys. -Never recommend placing management tokens in client-side code. -Use environment variables for credentials. -Prefer server-side proxies for privileged CMA access in browser apps. -Treat delivery tokens as client-safe only for read-only delivery use cases. +- Advisory by default. This skill does not create, modify, or delete roles, users, teams, or tokens. +- Recommend least privilege. +- Never expose tokens or API keys, and never recommend placing management tokens in client-side code. +- Prefer server-side proxies for privileged CMA access in browser apps. +- Treat delivery tokens as client-safe only for read-only delivery use cases. ### Destructive Actions -Do not perform destructive actions. This skill is advisory only and must not delete, revoke, or modify access controls. +Do not delete, revoke, or modify access controls. If the user wants a change made, give the steps and the target (stack, role, team) for them to apply. ### Secrets @@ -116,14 +90,3 @@ Never print, infer, or request secrets in plain text. If credentials are needed, ### Environment Variables Recommend environment variables for all secrets and credentials. Do not hardcode stack API keys, management tokens, or auth tokens in examples. - -## Product Context - -- - Product: CMS -- - Description: Contentstack headless CMS: content types, entries, assets, environments, publishing, workflows, webhooks, and the Content Management API (CMA). -- - Product safety rules: - Never expose management tokens or API keys. -- Always use environment variables for credentials. -- Route all CMA calls through server-side proxies in browser apps. -- Never hardcode stack API keys in client-side code. -- - Default tools: ["CMA API", "Content Types", "Entries", "Assets", "Workflows", "Webhooks", "Environments", "Releases", "Publish Queue"] -- - Default connectors: ["CMA Proxy", "Webhooks"] \ No newline at end of file diff --git a/skills/cms-taxonomy/SKILL.md b/skills/cms-taxonomy/SKILL.md index 2eccdd8..8778bac 100644 --- a/skills/cms-taxonomy/SKILL.md +++ b/skills/cms-taxonomy/SKILL.md @@ -6,60 +6,32 @@ allowed-tools: Read Grep Glob # Contentstack Taxonomy -## Description +Advise on taxonomy for structured, hierarchical classification that is filtered on the delivery side, and on when tags, labels or references fit better. -Advise developers on using Contentstack Taxonomy for structured, hierarchical content classification and delivery-side filtering. Covers taxonomy vs tags, labels, and references; hierarchy design; CDA query operators; localization; and import/export. +## When to use -## When to Use +- Choosing between taxonomy, tags, labels and a Category content type with references. +- Designing a term hierarchy. +- Filtering entries by term with the Content Delivery API (CDA). +- Localizing terms, or importing and exporting taxonomies. -Use when developers need help classifying content, designing category hierarchies, choosing between taxonomy and other classification approaches, or querying entries by category on the delivery side. +Use another skill for: -## User Problem +- Overall content model design → `cms-data-modeling-best-practices` +- General entry queries, pagination and references → `cms-entries` +- Locales and fallback chains → `cms-localization` -Developers need a governed way to classify content so it can be filtered, navigated, and organized in delivery experiences. They also need guidance on when taxonomy is the right mechanism versus tags, labels, or references. +## Before you answer -## Success Criteria - -Recommends taxonomy only when it fits the use case -Explains hierarchy design and term relationships clearly -Uses CDA taxonomy operators for filtering examples -Distinguishes taxonomy from tags, labels, and references -Covers localization and import/export when relevant - -## Expected Inputs - -- Classification or filtering requirements -- Hierarchy depth and structure -- Delivery-side query needs -- Localization requirements -- Migration or import needs - -## Expected Outputs - -- Taxonomy vs alternatives recommendation -- Hierarchy design guidance -- CDA query operator examples -- Localization guidance -- Import/export advice - -## Example User Requests - -- Should I use taxonomy or tags for product categories? -- How do I query entries by taxonomy term on the CDA? -- How do I design a taxonomy hierarchy for my site? -- Can I localize taxonomy terms? -- Should categories be a separate content type with references or taxonomy? - -## Workflow Summary - -Understand the classification need and delivery requirements. -Recommend the right mechanism: taxonomy, tags, labels, or references. -If taxonomy fits, guide hierarchy design and term modeling. -Show CDA taxonomy query operators for filtering. -Cover localization and import/export if relevant. +Find out what the classification drives (navigation, filtering, internal organization), how deep the hierarchy needs to be, and whether terms must be localized. ## Instructions +1. Decide whether taxonomy fits. +2. If it does, design the hierarchy. +3. Show the CDA query for the filter the user needs. +4. Cover localization or import/export if they're relevant. + ### Use taxonomy when Use taxonomy for structured, hierarchical classification that must be queried on the delivery side. Good fits include product categories, geographic regions, content topics, and document types. @@ -70,41 +42,54 @@ Do not use taxonomy for freeform labels, internal CMS organization, or rich cate ### Hierarchy design -Plan the hierarchy before creating it. Keep it practical, usually 3-4 levels max. Terms are parent-child ordered. Moving a term with children is blocked by default unless the force flag is explicitly used. +Plan the hierarchy before creating it. Terms can nest up to 10 levels deep, but shallow trees are easier for editors and queries, so keep it as flat as the use case allows. Terms are parent-child ordered. The move-term API takes a `force` flag that cascades the move to child terms. + +### Limits + +Default limits are plan-dependent: 20 taxonomies per stack, 10,000 terms per taxonomy, 10 levels of depth, and 20 taxonomies per content type. Taxonomy is a global module, so it is shared across branches. ### CDA filtering -Use taxonomy query operators instead of manual filtering: $taxonomy_exists for any term, $taxonomy_equal for a specific term, $taxonomy_below for a term and descendants, and $taxonomy_above for ancestors. +Filter entries by taxonomy with the `query` parameter on `/taxonomies/entries` (across content types) or `/content_types/{content_type_uid}/entries`: + +- Exact term: `{"taxonomies.material": "wood"}` +- Any of several terms: `{"taxonomies.material": {"$in": ["wood", "bamboo"]}}` +- Any term applied: `{"taxonomies.material": {"$exists": true}}` +- Term and its descendants: `{"taxonomies.color": {"$eq_below": "blue", "levels": 2}}` +- Descendants only: `$below`. Term and its ancestors: `$eq_above`. Ancestors only: `$above`. +- Combine conditions with `$and` and `$or`. + +`levels` limits the depth for the hierarchical operators and defaults to 10. ### Localization and import/export -Note that taxonomy localization is plan-dependent. Localized terms share the same UID as the master term. Import/export supports JSON and CSV; invalid CSV rows are skipped while valid rows are processed. +Taxonomy localization is plan-dependent. Localized terms share the same UID as the master term, so entry links stay valid across locales while the label changes. Taxonomies import and export as JSON or CSV. A localized import needs the master-locale taxonomy and terms to exist first. + +## MCP tools + +When the Contentstack MCP server is connected, these tools act on the stack (local server group in brackets): -## Output Format +- Inspect: `get_all_taxonomies`, `get_a_single_taxonomy`, `get_all_terms`, `get_a_single_term`, `get_all_terms_across_all_taxonomies`, `get_all_ancestors_of_a_term`, `get_all_descendants_of_a_term`, `export_a_taxonomy` (`cma`) +- Change: `create_a_taxonomy`, `update_a_taxonomy`, `create_a_term`, `update_a_term`, `move_a_term` (`cma`) +- Delete: `delete_a_taxonomy`, `delete_a_term` (`cma`) -Be concise and practical. -State the taxonomy-vs-alternatives decision first. -Show CDA query operators inline. -Use examples only when they clarify the recommendation. +There is no import tool; use the CMA or `csdx` for imports. To test a CDA filter, use `get_all_entries_cdn` (`cda`) with the `query` above. -## Tooling Notes +## Output -Read-only advisory skill. -Do not create, modify, or delete taxonomies or terms. -Restrict tool use to read/search tools when possible. +Lead with the taxonomy-vs-alternatives decision, then the hierarchy or the query. Show CDA operators inline and keep examples to the ones that clarify the recommendation. ## Security ### Defaults -Never expose tokens or API keys. -Use environment variables for credentials in example code. -Do not perform destructive actions. -Avoid suggesting client-side access to management credentials. +- Advisory by default. Use the MCP tools above only when the user asks for the change. +- Never expose tokens or API keys; use environment variables in example code. +- Keep management credentials out of client-side code. ### Destructive Actions -Do not create, update, move, delete, or import taxonomy data. Provide guidance only. +Before creating, moving or deleting taxonomies or terms, name the taxonomy, the terms and the entries affected, and wait for confirmation. Taxonomy is a global module, so a change reaches every branch. ### Secrets @@ -114,13 +99,7 @@ Never reveal management tokens, API keys, or other secrets. Use placeholders and Use environment variables for credentials in examples and implementation guidance. Never hardcode secrets in client-side code. -## Product Context +## References -- - Product: CMS -- - Description: Contentstack headless CMS: content types, entries, assets, environments, publishing, workflows, webhooks, and the Content Management API (CMA). -- - Product safety rules: - Never expose management tokens or API keys. -- Always use environment variables for credentials. -- Route all CMA calls through server-side proxies in browser apps. -- Never hardcode stack API keys in client-side code. -- - Default tools: ["CMA API", "Content Types", "Entries", "Assets", "Workflows", "Webhooks", "Environments", "Releases", "Publish Queue"] -- - Default connectors: ["CMA Proxy", "Webhooks"] \ No newline at end of file +- https://www.contentstack.com/docs/developers/taxonomy +- https://www.contentstack.com/docs/developers/apis/content-delivery-api#taxonomy diff --git a/skills/cms-tokens-authentication/SKILL.md b/skills/cms-tokens-authentication/SKILL.md index f6890ed..c0c72c5 100644 --- a/skills/cms-tokens-authentication/SKILL.md +++ b/skills/cms-tokens-authentication/SKILL.md @@ -1,6 +1,6 @@ --- name: cms-tokens-authentication -description: "Advise developers on choosing the right Contentstack authentication method and token type for frontend, backend, automation, and third-party app use cases. Include security guidance, rate-limit awareness, and SSO considerations. Use when developers ask about authentication, token types, API keys, rate limits, SSO integration, or credential security." +description: "Advise developers on choosing the right Contentstack authentication method and token type for frontend, backend, automation, and third-party app use cases. Include security guidance, rate-limit awareness, and SSO considerations. Use when developers ask about authentication, token types, API keys, rate limits, SSO integration, or credential security. For role and team design, use cms-roles-permissions." allowed-tools: Read Grep Glob context: fork agent: general-purpose @@ -8,70 +8,56 @@ agent: general-purpose # Tokens & Authentication -## Description +Recommend the right Contentstack authentication method and token type for the use case, and the safest way to use it. -Advise developers on choosing the right Contentstack authentication method and token type for frontend, backend, automation, and third-party app use cases. Include security guidance, rate-limit awareness, and SSO considerations. +## When to use -## When to Use +- Choosing a token for a frontend, backend script, CI/CD pipeline, migration, third-party app or interactive session. +- Comparing management tokens, authtokens, delivery and preview tokens, OAuth and machine-to-machine (M2M) apps. +- Handling rate limits and 429 responses. +- Authenticating in an SSO-enforced organization. +- Keeping credentials safe. -Use when developers ask about authentication, token types, API keys, rate limits, SSO integration, or credential security. +Use another skill for: -## User Problem +- Roles, custom roles, teams and effective permissions → `cms-roles-permissions` +- Environments, delivery tokens per environment and publishing → `cms-environments-publishing` +- Live Preview setup and preview token troubleshooting → `cms-live-preview-visual-editor-support-assistant` +- OAuth scopes and M2M setup inside a Developer Hub app → `developer-hub-app-architect` +- Authenticating the `csdx` CLI → `dx-cli`; authenticating MCP clients → `dx-mcp-server` -Developers need to authenticate correctly for their use case without exposing sensitive credentials or violating access rules. They need a clear recommendation for the right token type and the safest way to use it. +## Before you answer -## Success Criteria +Find out the use case and where the code runs (browser, server, CI, script), whether the organization enforces SSO, any rate-limit concerns, and security requirements. -Recommends the correct token type for the scenario. -Explains what is safe for client-side versus server-side use. -Includes practical security guidance and credential-handling rules. -Calls out rate-limit implications when relevant. -Addresses SSO-specific constraints when applicable. - -## Expected Inputs - -- Use case: frontend, backend script, CI/CD, migration, third-party app, or interactive session -- SSO requirements -- Rate limit concerns -- Security requirements +## Instructions -## Expected Outputs +1. Identify the use case and runtime environment. +2. Recommend the authentication method or token type. +3. State what is safe for client-side versus server-side use. +4. Add security and credential-handling guidance. +5. Explain rate limits and backoff if relevant. +6. Include SSO constraints when applicable. -- Token type recommendation -- Security best practices -- Rate limit guidance -- SSO-specific advice +### Token decision rules -## Example User Requests +Frontend reads of published content: use a delivery token. Live preview of draft content: use a preview token. Backend automation, CI/CD, and migration scripts: use a management token. Interactive user sessions: use an authtoken only when appropriate. Third-party apps: prefer OAuth with scoped access. Service-to-service automation: prefer a machine-to-machine (M2M) app. -- What token should I use in my frontend app? -- What is the difference between a management token and an authtoken? -- How do I handle rate limits in my script? -- Can I use authtokens in an SSO-enabled organization? -- How many management tokens can I have per stack? +### Key limits -## Workflow Summary +A stack can have at most 10 valid management tokens at a time. A management token can expire on a set date or never, so prefer an expiry date and rotate. Authtokens are user-specific and have their own limits. Responses carry `x-ratelimit-limit` and `x-ratelimit-remaining` headers; mention them and a retry strategy when the user asks about throughput or 429 errors. -Identify the use case and runtime environment. -Recommend the appropriate authentication method or token type. -State what is safe for client-side versus server-side use. -Add security and credential-handling guidance. -Explain rate limits and backoff behavior if relevant. -Include SSO constraints when applicable. +### Machine-to-machine apps -## Instructions +For service-to-service automation with no human session, recommend a machine-to-machine (M2M) app. It uses the OAuth client-credentials grant, is created at the organization level, and can be scoped to specific stacks. Prefer it over long-lived personal credentials for new integrations. -### Token decision rules +### Preview tokens -Frontend reads of published content: use a delivery token. Live preview of draft content: use a preview token. Backend automation, CI/CD, and migration scripts: use a management token. Interactive user sessions: use an authtoken only when appropriate. Third-party apps: prefer OAuth with scoped access. - -### Key limits - -Management tokens are stack-level and limited per stack. Authtokens are user-specific and have their own limits. Mention rate-limit headers and retry strategy when the user asks about throughput or 429 errors. +The preview token is a separate credential that only the Preview Service accepts. It reads draft content for Live Preview and only works together with the short-lived `live_preview` hash. Keep it out of production builds. ### SSO organizations -Call out that SSO can restrict authtoken usage. For automation in SSO orgs, recommend management tokens or OAuth instead of authtokens. +When an organization enforces SSO, password-based login is blocked for its users, so scripts can't obtain authtokens that way. For automation in SSO orgs, recommend management tokens, OAuth or an M2M app. ### Security rules @@ -81,32 +67,29 @@ Never expose management tokens or authtokens in client-side code. Use environmen Recommend exponential backoff with jitter for 429 responses. Advise checking rate-limit headers before retrying. Keep guidance practical and concise. -## Output Format +## MCP tools -Be concise and direct. -Always state whether a credential is safe for client-side or server-side use. -Use bullets when comparing token types. -Do not include raw secrets or example real tokens. +No Contentstack MCP tool creates, rotates or deletes tokens. Manage delivery, preview and management tokens in the stack's Settings → Tokens, and M2M apps in Developer Hub. -## Tooling Notes +The MCP server itself authenticates with OAuth (`npx @contentstack/mcp --auth`, or the Profile Hub sign-in) or, for the `cma` groups only, a management token; see `dx-mcp-server`. For OAuth app scopes, `get_oauth_scopes` and `get_app_oauth` (`developerhub`) read what an app requests. -Read-only advisory skill. -Do not create, modify, or delete tokens. -Use documentation sources when needed. -Prefer read-only tools only. +## Output + +Be concise and direct. Always state whether a credential is safe for client-side or server-side use. Use bullets when comparing token types. Do not include raw secrets or example real tokens. ## Security ### Defaults -Never expose management tokens, authtokens, API keys, or OAuth secrets. -Use placeholders and environment variables only. -Never suggest bypassing access controls or security policies. -Prefer least-privilege access and token rotation. +- Advisory by default. This skill does not create, modify, rotate or delete tokens. +- Never expose management tokens, authtokens, API keys, or OAuth secrets. +- Use placeholders and environment variables only. +- Never suggest bypassing access controls or security policies. +- Prefer least-privilege access and token rotation. ### Destructive Actions -Do not perform destructive or credential-changing actions. This skill only advises; it must not create, revoke, rotate, or delete tokens. +Do not perform destructive or credential-changing actions: do not create, revoke, rotate, or delete tokens. Give the steps and the target stack for the user to apply. ### Secrets @@ -115,14 +98,3 @@ Never request, display, or store real secrets. If a secret is needed in an examp ### Environment Variables Recommend environment variables for all credentials. Use descriptive names such as CONTENTSTACK_API_KEY, CONTENTSTACK_DELIVERY_TOKEN, and CONTENTSTACK_MANAGEMENT_TOKEN. - -## Product Context - -- - Product: CMS -- - Description: Contentstack headless CMS: content types, entries, assets, environments, publishing, workflows, webhooks, and the Content Management API (CMA). -- - Product safety rules: - Never expose management tokens or API keys. -- Always use environment variables for credentials. -- Route all CMA calls through server-side proxies in browser apps. -- Never hardcode stack API keys in client-side code. -- - Default tools: ["CMA API", "Content Types", "Entries", "Assets", "Workflows", "Webhooks", "Environments", "Releases", "Publish Queue"] -- - Default connectors: ["CMA Proxy", "Webhooks"] \ No newline at end of file diff --git a/skills/cms-variants-personalization/SKILL.md b/skills/cms-variants-personalization/SKILL.md index e73b778..23f063d 100644 --- a/skills/cms-variants-personalization/SKILL.md +++ b/skills/cms-variants-personalization/SKILL.md @@ -1,87 +1,106 @@ --- name: cms-variants-personalization -description: "Advise developers on using Contentstack Variants and Personalize for audience-targeted content. Explain when to use variants versus separate entries, how variant groups work, and how to integrate Personalize with the CMS. Use when developers ask about content personalization, A/B testing, audience segmentation, variant creation, or integrating Personalize with the CMS." +description: "Advise developers on using Contentstack Variants and Personalize for audience-targeted content. Explain when to use variants versus separate entries, how variant groups work, and how to integrate Personalize with the CMS. Use when developers ask about content personalization, A/B testing, audience segmentation, variant creation, or integrating Personalize with the CMS. For Personalize projects, audiences, attributes, events and analytics in depth, use personalize." allowed-tools: Read Grep Glob --- # Variants & Personalization -## Description +Advise on delivering different content to different audiences with entry variants and Personalize, without duplicating entries or building hard-to-maintain structures. -Advise developers on using Contentstack Variants and Personalize for audience-targeted content. Explain when to use variants versus separate entries, how variant groups work, and how to integrate Personalize with the CMS. +## When to use -## When to Use +- Deciding between entry variants and separate entries per audience. +- Explaining variant groups, variants and entry variants. +- Fetching variant content from the CDA with variant aliases. +- The basic Personalize integration that resolves variants for a page. -Use when developers ask about content personalization, A/B testing, audience segmentation, variant creation, or integrating Personalize with the CMS. +Use another skill for: -## User Problem +- Personalize projects, audiences, attributes, events, priority and analytics in depth → `personalize` +- Lytics audiences and identity → `lytics` +- General entry fetching and references → `cms-entries` +- Delivery SDK code → `dx-delivery-sdk` +- Content model design → `cms-data-modeling-best-practices` -Developers need to deliver different content to different audiences without duplicating entire entries or creating hard-to-maintain content structures. +## Before you answer -## Success Criteria +Find out the personalization requirement, how the audiences are segmented, the frontend framework, and how much of the content varies per audience. -Clearly recommend variants vs separate entries. -Provide practical integration guidance. -Emphasize starting simple and adding complexity only when justified. +## Instructions -## Expected Inputs +1. Understand the personalization requirement. +2. Recommend variants or separate entries. +3. Explain the Personalize integration pattern. +4. Start simple, and add complexity only when it's justified. +5. Cover publishing and performance considerations. -- Personalization requirements -- Audience segmentation needs -- Frontend framework -- Content variation scope +### Start simple -## Expected Outputs +Recommend variants when most of an entry is shared and only some fields change per audience, such as headlines, images or calls to action. Recommend separate entries when an audience needs a different structure, different references or a different layout. Start with variants and split into separate entries only when the content diverges wholesale. -- Variants vs separate entries recommendation -- Integration pattern guidance -- SDK initialization instructions -- Complexity management advice +### How variants are organized -## Example User Requests +- A **variant group** corresponds one to one with a Personalize experience. Creating an experience in Personalize creates the matching variant group in the connected stack automatically. +- Each **variant** inside the group corresponds to one audience (segmented experience) or one arm of an A/B test. +- An **entry variant** holds field overrides for one entry. Fields it doesn't override fall back to the base entry, so there is no duplication. Entry variants are created when an editor first overrides a field for that variant. +- A variant group must be linked to a content type before editors can create entry variants for its entries. Creating the experience doesn't link it to any content type. -- How do I show different content to different user segments? -- Should I use variants or create separate entries per audience? -- How do I integrate Personalize with my frontend? -- What are variant groups? -- Can I A/B test content in Contentstack? +### Fetching variant content -## Workflow Summary +Pass variant aliases to the CDA in the `x-cs-variant-uid` header. An alias has the form `cs_personalize_{experienceShortUid}_{variantShortUid}`, for example `cs_personalize_a_0`. Pass several aliases as a comma-separated list to layer variants from different experiences. When two variants override the same field, the leftmost alias wins. -Understand the personalization requirements. -Recommend variants vs separate entries. -Explain the Personalize integration pattern. -Emphasize starting simple. -Cover publishing and performance considerations. +The CMA accepts only one variant UID in that header. A comma-separated list there returns the base entry, not a merge. -## Instructions +With `include_publish_details`, each entry carries `publish_details.variants`, the alias that was applied. Use it to fire impressions only for content that was actually shown. Attribution is per entry, not per modular block or JSON RTE block inside an entry. + +### Personalize integration + +Resolve variants with the Personalize Edge SDK, `@contentstack/personalize-edge-sdk`, usually in edge middleware such as Next.js middleware, Vercel, Netlify or Cloudflare Workers, so the page renders with the right content on the first request. The core flow: + +1. `Personalize.init(projectUid, options)`. +2. `Personalize.set(attributes)` with the user attributes your audiences use, and `Personalize.setUserId(userId)` for a stable identity across sessions. +3. `Personalize.getVariantAliases()`, then pass the aliases to the CDA. +4. `Personalize.triggerImpression(experienceShortUid)` when content is shown and `Personalize.triggerEvent(eventKey)` on conversions, so A/B results populate. + +### Experiences + +- **Segmented experiences** serve the first variant whose audience rules match. +- **A/B tests** split traffic by percentage. A user is bucketed deterministically, so they see the same variant across sessions. Multi-armed bandit mode rebalances traffic toward the better-performing variant. +- When a user qualifies for several experiences that override the same field, experience priority decides which wins. Set priority deliberately in the Personalize project. -[] +### Lifecycle and limits -## Output Format +Entry variants publish, unpublish and schedule like their base entry. Pausing an experience stops its variants from being selected. Deleting an experience only unlinks its variant group; delete the group in the CMS afterwards to remove it and its entry variants. -Be concise and advisory. -Prefer direct recommendations over long explanations. -Include a clear variants-vs-separate-entries decision. -Mention simplicity and maintainability first. +Default limits per Personalize project, raisable by plan: 100 experiences (up to 1,000), 20 variants per experience (up to 50), and 100 audiences. -## Tooling Notes +## MCP tools -Read-only advisory skill. -Do not create, modify, or publish variants. -Use Read, Grep, and Glob only. +When the Contentstack MCP server is connected, these tools act on the stack and the Personalize project (local server group in brackets): + +- Inspect: `get_all_variants_of_a_content_type`, `get_a_single_variant`, `get_all_variants_of_an_entry`, `get_single_entry_variant` (`cma`); `get_all_experiences`, `get_single_experience`, `get_experiences_priority`, `get_all_audiences` (`personalize`) +- Change: `create_an_entry_variant`, `publish_variants_of_an_entry` (`cma`); `create_experience`, `update_experience`, `update_experiences_priority` (`personalize`) +- Delete: `delete_an_entry_variant` (`cma`); `delete_experience` (`personalize`) + +No tool links a variant group to a content type; do that in the CMS. Deleting an experience only unlinks its variant group. + +## Output + +Be concise and advisory, with direct recommendations over long explanations. Include a clear variants-vs-separate-entries decision, and put simplicity and maintainability first. ## Security ### Defaults -Never expose tokens or API keys. -Use environment variables for credentials in example code. -Do not recommend hardcoding secrets in client-side code. +- Advisory by default. Use the MCP tools above only when the user asks for the change. +- Never expose tokens or API keys. +- Use environment variables for credentials in example code, and route CMA calls through a server-side proxy in browser apps. +- Do not recommend hardcoding secrets in client-side code. ### Destructive Actions -Do not perform create, update, publish, or delete actions. Provide guidance only. +Before creating, publishing or deleting entry variants, or creating, reprioritizing or deleting experiences, name the stack, branch, entries, environments and experience affected, and wait for confirmation. ### Secrets @@ -91,17 +110,6 @@ Never reveal management tokens, API keys, or Personalize credentials. Use placeh Use environment variables for all credentials in code examples. Do not hardcode stack API keys or project keys. -## Product Context - -- - Product: CMS -- - Description: Contentstack headless CMS: content types, entries, assets, environments, publishing, workflows, webhooks, and the Content Management API (CMA). -- - Product safety rules: - Never expose management tokens or API keys. -- Always use environment variables for credentials. -- Route all CMA calls through server-side proxies in browser apps. -- Never hardcode stack API keys in client-side code. -- - Default tools: ["CMA API", "Content Types", "Entries", "Assets", "Workflows", "Webhooks", "Environments", "Releases", "Publish Queue"] -- - Default connectors: ["CMA Proxy", "Webhooks"] - ## Examples ### Variants vs separate entries (few shot *) @@ -116,6 +124,6 @@ Use environment variables for all credentials in code examples. Do not hardcode **User:** How do I integrate Personalize with my Next.js app? -**Assistant:** Initialize the Contentstack SDK with your stack credentials and the Personalize SDK with your Personalize project key. On each page request, call Personalize to determine the active experience for the current user based on audience rules. Pass the resolved variant context to the Contentstack SDK when fetching entries. For server-side rendering, use the Personalize Edge API to resolve variants at the CDN level for best performance. +**Assistant:** Resolve variants in Next.js middleware with `@contentstack/personalize-edge-sdk`: call `Personalize.init(projectUid)`, set any user attributes, then `Personalize.getVariantAliases()`. Pass the aliases to the page (for example as a URL parameter) and send them to the CDA in the `x-cs-variant-uid` header, comma-separated. Call `triggerImpression` when variant content is shown and `triggerEvent` on conversions. Make sure each variant group is linked to the content types you personalize, or editors can't create entry variants. -*Demonstrates the standard SDK flow and SSR guidance.* \ No newline at end of file +*Demonstrates the standard SDK flow and SSR guidance.* diff --git a/skills/cms-webhooks/SKILL.md b/skills/cms-webhooks/SKILL.md index 692c4f1..ca0390a 100644 --- a/skills/cms-webhooks/SKILL.md +++ b/skills/cms-webhooks/SKILL.md @@ -1,122 +1,95 @@ --- name: cms-webhooks -description: "Advise developers on configuring and consuming Contentstack webhooks for real-time event notifications. Cover event channels, payload structure, signature verification, retry behavior, release-triggered webhook storms, and reliable receiver design. Use when developers need help setting up webhooks, choosing event channels, handling webhook payloads, verifying signatures, debugging delivery issues, or integrating Contentstack with external systems such as site rebuilds, search indexes, Slack, or CI/CD." +description: "Advise developers on configuring and consuming Contentstack webhooks for real-time event notifications. Cover event channels, payload structure, signature verification, retry behavior, release-triggered webhook storms, and reliable receiver design. Use when developers need help setting up webhooks, choosing event channels, handling webhook payloads, verifying signatures, debugging delivery issues, or integrating Contentstack with external systems such as site rebuilds, search indexes, Slack, or CI/CD. For no-code event workflows, use automations." allowed-tools: Read Grep Glob --- # Contentstack Webhooks -## Description +Advise on connecting Contentstack to external systems with webhooks, and on the operational edge cases: duplicate deliveries, retries, release-triggered bursts and failed deliveries. -Advise developers on configuring and consuming Contentstack webhooks for real-time event notifications. Cover event channels, payload structure, signature verification, retry behavior, release-triggered webhook storms, and reliable receiver design. +## When to use -## When to Use +- Setting up a webhook and choosing event channels. +- Handling webhook payloads. +- Verifying webhook signatures. +- Designing a reliable receiver and choosing a retry policy. +- Debugging missed or failing deliveries. +- Triggering site rebuilds, search indexing, Slack or CI/CD from content events. -Use when developers need help setting up webhooks, choosing event channels, handling webhook payloads, verifying signatures, debugging delivery issues, or integrating Contentstack with external systems such as site rebuilds, search indexes, Slack, or CI/CD. +Use another skill for: -## User Problem +- No-code event workflows with triggers and connectors → `automations` +- Launch deploy hooks and rebuilds → `launch-platform` +- Releases and what deploying one publishes → `cms-releases` +- Workflow stages and publish rules → `cms-workflows` +- Webhooks for Developer Hub app lifecycle events → `developer-hub-app-architect` -Developers need to connect Contentstack to external systems through webhooks and handle operational edge cases such as duplicate deliveries, retries, release-triggered event storms, and failed deliveries. +## Before you answer -## Success Criteria - -Webhook configuration matches the intended event sources and integration target. -Receiver validates signatures and handles duplicate deliveries idempotently. -Release-triggered webhook storms are detected and collapsed into a single downstream action. -Delivery failures are debugged using execution logs and response details. -Guidance stays read-only and does not suggest creating, modifying, or deleting webhooks. - -## Expected Inputs - -- Integration target (site rebuild, search index, Slack, CI/CD, etc.) -- Event types needed -- Reliability and latency requirements -- Payload details or sample requests -- Debugging information for delivery issues - -## Expected Outputs - -- Webhook configuration guidance -- Event channel selection advice -- Payload handling patterns -- Signature verification instructions -- Release-storm prevention advice -- Debugging recommendations - -## Example User Requests - -- How do I trigger a site rebuild when content is published? -- How do I verify a webhook is genuinely from Contentstack? -- My webhook receiver is getting hundreds of calls when I deploy a release. -- What retry policy should I use? -- How do I debug missed webhook deliveries? - -## Workflow Summary - -Identify the integration target and required events. -Recommend the narrowest event channels first, then expand only if needed. -Explain payload shape, signature verification, and secret handling. -Describe receiver reliability patterns: fast 2xx response, async processing, and idempotency. -Warn about release-triggered webhook storms and suggest debouncing by release source. -Use execution logs to diagnose retries, status codes, and response bodies. +Find out the integration target (site rebuild, search index, Slack, CI/CD), the events needed, reliability and latency requirements, and, when debugging, a sample payload or the execution log details. ## Instructions +1. Identify the integration target and required events. +2. Recommend the narrowest event channels first, and expand only if needed. +3. Explain payload shape, signature verification and secret handling. +4. Describe receiver reliability: fast 2xx response, async processing and idempotency. +5. Warn about release-triggered bursts and suggest debouncing. +6. Use execution logs to diagnose retries, status codes and response bodies. + ### Channels -Use event channels in the form {module}.{action}, such as entries.create, entries.update, entries.publish, entries.unpublish, assets.publish, content_types.update, and entries.workflow_stage_change. Use $all only when the integration truly needs every event. Start specific and expand later. +Event channels use the form `{module}.{action}`, such as `entries.create`, `entries.publish`, `assets.publish` and `content_types.update`, or the longer `{module}.{context}.{action}` form, such as `content_types.entries.publish` or `entries.environments.publish`. Workflow stage changes come from a separate workflow module, not from `entries`; select those events in the webhook UI rather than typing a channel name. Use `$all` only when the integration truly needs every event. Start specific and expand later. ### Payloads -Webhook payloads include module, api_key, event, triggered_at, data, and branch information. Recommend concise_payload: true when the receiver only needs identifiers. Use full payloads only when the integration requires complete entry or asset data. +The top-level payload fields are `module`, `api_key`, `event` and `data`. Recommend `concise_payload: true` when the receiver only needs identifiers, and full payloads only when the integration needs complete entry or asset data. Do not promise other top-level fields; inspect a real delivery in the execution log if the receiver depends on one. ### Signature verification -Always validate the X-Contentstack-Signature header in production. Treat it as an HMAC-SHA256 signature over the raw request body using the webhook secret. Reject requests with missing or invalid signatures. +Every webhook request carries an `X-Contentstack-Request-Signature` header, an RSA-PSS SHA-256 signature that the receiver verifies with Contentstack's public key. Point users to the current webhook documentation for the public key and the exact verification steps. Where the plan supports it, a webhook can instead be set to HMAC signing: requests then carry `X-Contentstack-HMAC-Signature`, an HMAC-SHA256 of `{timestamp}.{raw body}` computed with the webhook secret. Verify against the raw body, reject old timestamps to block replays, and reject requests with a missing or invalid signature. -### Release-webhook storm +### Release-triggered bursts -A release with many items can produce one webhook event per item. Detect release-triggered events using the source key in the payload when present. Debounce or batch these events so downstream systems trigger once per release instead of once per item. +Deploying a release publishes every item in it, so publish webhooks can fire once per item. Do not rely on a payload field to identify release traffic. Debounce or batch on the receiver so downstream systems such as site rebuilds run once per burst, and use the execution logs to see what actually fired. ### Reliable receiver design -Recommend four receiver behaviors: validate signatures, return 2xx within 30 seconds, process asynchronously after acknowledgment, and handle duplicate deliveries idempotently. +Recommend four receiver behaviors: verify the signature, return 2xx within 30 seconds, process asynchronously after acknowledging, and handle duplicate deliveries idempotently. A non-2xx response or a 30-second timeout counts as a failed delivery. ### Retry policies -Explain the difference between manual retries and auto retries. Manual retries are logged for re-execution; auto retries use exponential backoff. Direct users to execution logs for delivery attempts, HTTP status codes, and response bodies. +The automatic retry policy retries a failed delivery up to 4 times with exponential backoff, at roughly 5 s, 25 s, 125 s and 625 s. With the manual policy, failed events stay in the execution log and can be re-fired from there or through the API. Direct users to the execution logs for attempts, status codes and response bodies. -### Limits and scope +### Scope and concurrency -Mention the max of 100 webhooks per stack. Note that webhooks are global modules but can be scoped to a branch. Mention the organization-level Webhook Configuration setting for max connections per second when relevant. +Webhooks are a global module, but each webhook can be assigned to one branch at a time. The organization-level Webhook Configuration setting controls the number of concurrent connections to receivers, not a per-second rate. -## Output Format +## MCP tools -Be concise and practical. -Prioritize configuration guidance, verification, and reliability patterns. -Call out the release-storm issue whenever release-triggered events are involved. -Do not suggest creating, modifying, or deleting webhooks. +No Contentstack MCP tool creates, lists, sends or deletes webhooks. Configure them in stack Settings → Webhooks or with the Content Management API's webhooks endpoints, and read execution logs there. -## Tooling Notes +Related tools: to point a webhook at a Launch rebuild, `get_deploy_hooks` (`launch`) returns a project's deploy hooks. To see what a release deploy will publish, `get_all_items_in_a_release` (`cma`) lists its items. -Read-only advisory skill. -Allowed tools should be limited to Read, Grep, and Glob. -Do not perform mutations or external actions. -Use docs and execution logs for troubleshooting guidance only. +## Output + +Be concise and practical. Prioritize configuration guidance, signature verification and reliability patterns. Call out the release burst issue whenever release-triggered events are involved. ## Security ### Defaults +- Advisory by default. This skill does not create, modify, send or delete webhooks. - Never expose webhook secrets. -- Always validate X-Contentstack-Signature in production. +- Always verify the request signature (`X-Contentstack-Request-Signature`, or `X-Contentstack-HMAC-Signature` when HMAC signing is on) in production. - Use environment variables for all secrets. - Do not echo raw secrets, tokens, or signatures in examples. - Prefer server-side verification and processing patterns. ### Destructive Actions -Do not create, update, delete, or send webhooks. This skill is advisory only. +Do not create, update, delete, or send webhooks. If the user wants a change made, give the steps and the target stack and branch for them to apply. ### Secrets @@ -125,14 +98,3 @@ Never reveal webhook secrets or any credential material. If a secret is needed, ### Environment Variables Store webhook secrets and related credentials in environment variables. Never hardcode secrets in examples or client-side code. - -## Product Context - -- - Product: CMS -- - Description: Contentstack headless CMS: content types, entries, assets, environments, publishing, workflows, webhooks, and the Content Management API (CMA). -- - Product safety rules: - Never expose management tokens or API keys. -- Always use environment variables for credentials. -- Route all CMA calls through server-side proxies in browser apps. -- Never hardcode stack API keys in client-side code. -- - Default tools: ["CMA API", "Content Types", "Entries", "Assets", "Workflows", "Webhooks", "Environments", "Releases", "Publish Queue"] -- - Default connectors: ["CMA Proxy", "Webhooks"] \ No newline at end of file diff --git a/skills/cms-workflows/SKILL.md b/skills/cms-workflows/SKILL.md index 7a88b62..b72821c 100644 --- a/skills/cms-workflows/SKILL.md +++ b/skills/cms-workflows/SKILL.md @@ -6,59 +6,36 @@ allowed-tools: Read Grep Glob # Workflows & Publish Rules -## Description +Advise on content governance with workflows and publish rules: stage design, transition restrictions, approvals, publish governance, automation hooks and common pitfalls. -Advise developers on designing and configuring Contentstack workflows and publish rules for content governance. Cover stage design, transition restrictions, approval flows, publish governance, automation hooks, and common pitfalls. +## When to use -## When to Use +- Designing workflow stages and transitions. +- Setting up approval flows and preventing self-approval. +- Choosing between workflows and publish rules. +- Automating actions when content reaches a stage. +- Understanding how workflows behave across branches. -Use when developers ask about workflow stage design, approval processes, publish rules, self-approval prevention, transition restrictions, or automation triggered by workflow changes. +Use another skill for: -## User Problem +- Roles and who can do what → `cms-roles-permissions` +- Webhook receivers for workflow events → `cms-webhooks` +- No-code automation on workflow events → `automations` +- Scheduling and bundling publishes → `cms-releases` +- Environments and publishing mechanics → `cms-environments-publishing` -Developers need to design content governance flows that match their review process without adding unnecessary complexity or creating permission gaps. +## Before you answer -## Success Criteria - -Recommend the simplest workflow that satisfies the business requirement. -Explain how stages, transitions, approvals, and publish rules differ. -Call out limits, scope, and permission constraints clearly. -Warn against over-engineering and unsupported assumptions. -Suggest integration patterns when automation is needed. - -## Expected Inputs - -- Content approval process requirements -- Team roles and responsibilities -- Compliance or audit requirements -- Branch or multi-environment publishing needs - -## Expected Outputs - -- Workflow stage design recommendations -- Publish rule configuration guidance -- Approval flow patterns -- Integration recommendations using webhook events -- Warnings about complexity and permission limitations - -## Example User Requests - -- How many workflow stages should I have? -- How do I prevent an author from approving their own content? -- How do publish rules differ from workflows? -- Can I automate actions when content reaches a workflow stage? -- How do workflows work across branches? - -## Workflow Summary - -Understand the approval process and team structure. -Design the simplest workflow that meets the requirement. -Add publish rules only when additional governance is needed. -Describe integration points for automation and auditability. -Warn about complexity, scope, and permission limitations. +Find out the approval process, the team structure and who approves, the content types and branches involved, and any automation or audit requirements. ## Instructions +1. Understand the approval process and team structure. +2. Design the simplest workflow that meets the requirement. +3. Add publish rules only when more governance is needed. +4. Describe integration points for automation and auditability. +5. Warn about complexity, scope and permission limitations. + ### Keep workflows simple Use 3 to 5 stages for most cases. Add stages only when there is a clear business need. A content type can be associated with one workflow per branch. @@ -69,7 +46,7 @@ Explain that each stage defines who can advance entries, which stages are allowe ### Prevent self-advancement -Explain that prevent-self-advancement requires at least two distinct reviewers. The last editor cannot advance the entry. Recommend at least two approvers or a role with multiple members. +Prevent-self-advancement is an option you turn on per stage, not a default for every workflow. When it is on, the person who submitted the entry cannot also approve it, so at least two distinct reviewers are needed. Recommend at least two approvers or a role with multiple members for those stages. ### Publish rules are separate @@ -77,40 +54,37 @@ Clarify that publish rules govern publish and unpublish conditions independently ### Automation -Recommend webhook events such as entries.workflow_stage_change for external automation. Mention that the publish queue tracks pending actions and status. +Recommend webhooks on workflow stage events for external automation. These come from the workflow module, so select them in the webhook UI instead of guessing a channel name. Mention that the publish queue tracks pending actions and status. ### Permission limitations State that only owners, admins, and developers can create workflows and publish rules. Management tokens cannot change workflow stages or configure rules that require user approval; user-scoped auth or OAuth tokens are needed for programmatic stage transitions. -## Output Format +## MCP tools + +When the Contentstack MCP server is connected, these tools read the governance setup (local server group in brackets): -Be concise and practical. -Prefer recommendations over theory. -Use bullets when listing options or rules. -Call out constraints and limits explicitly. -Do not provide instructions to create, modify, or delete workflows or publish rules. +- Inspect: `get_all_workflows`, `get_a_single_workflow`, `get_publish_rules`, `get_publish_queue_activity`, `get_audit_log` (`cma-extended`); `get_publish_queue` (`cma`) -## Tooling Notes +No MCP tool creates or changes workflows, stages or publish rules, or moves an entry between stages. Make those changes in stack Settings → Workflows and Publish Rules, or with the Content Management API using user-scoped auth or OAuth. -Read-only advisory. -Use Read, Grep, and Glob only. -Do not create, modify, or delete workflows or publish rules. -Use webhook and publish queue concepts only for explanation. +## Output + +Be concise and practical, with recommendations over theory. Use bullets when listing options or rules, and call out constraints and limits explicitly. ## Security ### Defaults -Never expose tokens or API keys. -Use environment variables for credentials in example code. -Do not perform destructive actions. -Do not recommend hardcoding secrets in client-side code. -Route any CMA-related examples through server-side proxies when relevant. +- Advisory by default. This skill does not create, modify, or delete workflows or publish rules. +- Never expose tokens or API keys. +- Use environment variables for credentials in example code. +- Do not recommend hardcoding secrets in client-side code. +- Route any CMA-related examples through server-side proxies when relevant. ### Destructive Actions -Do not create, update, publish, unpublish, or delete workflows or publish rules. If the user asks for a procedure that would change production configuration, provide advisory guidance only. +Do not create, update, publish, unpublish, or delete workflows or publish rules. If the user asks for a procedure that would change production configuration, give the steps and name the stack, branch and content types affected for them to apply. ### Secrets @@ -119,14 +93,3 @@ Never reveal management tokens, API keys, or auth tokens. Use placeholders and e ### Environment Variables Use environment variables for any credentials or secret values in examples. Prefer server-side access patterns for CMA-related operations. - -## Product Context - -- - Product: CMS -- - Description: Contentstack headless CMS: content types, entries, assets, environments, publishing, workflows, webhooks, and the Content Management API (CMA). -- - Product safety rules: - Never expose management tokens or API keys. -- Always use environment variables for credentials. -- Route all CMA calls through server-side proxies in browser apps. -- Never hardcode stack API keys in client-side code. -- - Default tools: ["CMA API", "Content Types", "Entries", "Assets", "Workflows", "Webhooks", "Environments", "Releases", "Publish Queue"] -- - Default connectors: ["CMA Proxy", "Webhooks"] \ No newline at end of file diff --git a/skills/developer-hub-app-architect/SKILL.md b/skills/developer-hub-app-architect/SKILL.md index fdc0edc..277a575 100644 --- a/skills/developer-hub-app-architect/SKILL.md +++ b/skills/developer-hub-app-architect/SKILL.md @@ -1,6 +1,6 @@ --- name: developer-hub-app-architect -description: "Turn Contentstack Developer Hub and Marketplace app ideas into concrete implementations. Choose the right UI location, map the architecture, generate boilerplate-aligned React/TypeScript code, and troubleshoot setup, SDK, manifest, proxy, and publishing issues. Use when a user needs help designing or building a Contentstack Developer Hub or Marketplace app." +description: "Turn Contentstack Developer Hub and Marketplace app ideas into concrete implementations. Choose the right UI location, map the architecture, generate boilerplate-aligned React/TypeScript code, and troubleshoot setup, SDK, manifest, proxy, and publishing issues. Use when a user needs help designing or building a Contentstack Developer Hub or Marketplace app. To choose or configure an existing first-party Marketplace app, use marketplace-apps." allowed-tools: Read Grep Glob context: fork agent: plan @@ -8,68 +8,36 @@ agent: plan # Developer Hub App Architect -## Description +Turn Developer Hub and Marketplace app ideas into working apps: pick the UI location, map the architecture, write boilerplate-aligned React/TypeScript, and debug setup, SDK, manifest, proxy and publishing issues. -Turn Contentstack Developer Hub and Marketplace app ideas into concrete implementations. Choose the right UI location, map the architecture, generate boilerplate-aligned React/TypeScript code, and troubleshoot setup, SDK, manifest, proxy, and publishing issues. +## When to use -## When to Use +- Designing or building a Developer Hub or Marketplace app. +- Choosing the right UI location for a feature. +- Generating app code from the marketplace boilerplate or a similar starter. +- Explaining manifest, setup, proxy, OAuth or publishing steps. +- Debugging app loading, SDK, iframe, route or location issues. -Use when a user needs help designing or building a Contentstack Developer Hub or Marketplace app. -Use when choosing the right UI location for a feature. -Use when generating app code from the marketplace boilerplate or a similar starter. -Use when explaining manifest, setup, proxy, OAuth, or publishing steps. -Use when debugging app loading, SDK, iframe, route, or location issues. +Use another skill for: -## User Problem +- Choosing or configuring an existing first-party Marketplace app → `marketplace-apps` +- Rendering JSON RTE content, or the data an RTE plugin produces → `cms-json-rte` +- Management and delivery token types → `cms-tokens-authentication` +- Webhooks on stack events (not app lifecycle webhooks) → `cms-webhooks` -Users need to turn app ideas into working Contentstack apps without guessing at UI locations, Developer Hub setup, or SDK integration details. They also need help debugging broken app assumptions quickly and consistently. +## Before you answer -## Success Criteria +Gather the app idea or feature, the candidate UI locations, the user's boilerplate structure or repo conventions, manifest and route details, and any proxy, config or OAuth needs. For debugging, ask for error messages, screenshots or runtime symptoms. -Selects the best UI location(s) with clear tradeoffs when multiple options fit. -Produces implementation-ready React/TypeScript guidance aligned to the user’s boilerplate. -Includes the necessary Developer Hub, manifest, route, proxy, and install steps. -Avoids exposing secrets and keeps credentials server-side. -Uses a repeatable troubleshooting checklist for broken apps. - -## Expected Inputs - -- App idea or feature description -- Target Contentstack UI location or candidate locations -- Boilerplate structure or repo conventions -- Manifest or route details -- Proxy, config, or OAuth requirements -- Error messages, screenshots, or runtime symptoms for debugging - -## Expected Outputs - -- Recommended UI location(s) and rationale -- Architecture and route map -- Manifest and Developer Hub setup checklist -- Code scaffold or concrete code changes -- Config schema and proxy guidance -- Test, install, and deployment checklist -- Troubleshooting diagnosis and next steps - -## Example User Requests - -- Build a private Contentstack app that adds AI suggestions in the entry sidebar. -- Which UI location should I use for a product taxonomy picker? -- Generate an app configuration page and save API credentials through proxy variables. -- Create a marketplace-ready app from our boilerplate with dashboard + custom field routes. -- Why is my app loading but appSdk.location.CustomField is undefined? -- Turn this app idea into manifest config, route plan, and starter code. - -## Workflow Summary +## Instructions -Understand the app idea, constraints, and required integrations. -Choose the best UI location(s) and explain tradeoffs. -Map the app architecture to the boilerplate and route structure. -Generate code, config, and Developer Hub setup steps. -Add test, install, and publishing checks. -Debug issues using route, manifest, location, SDK, config, and proxy assumptions. +1. Understand the app idea, constraints and required integrations. +2. Choose the best UI location(s) and explain tradeoffs. +3. Map the architecture to the boilerplate and route structure. +4. Generate code, config and Developer Hub setup steps. +5. Add test, install and publishing checks. +6. Debug using route, manifest, location, SDK, config and proxy assumptions. -## Instructions ### Operating modes @@ -81,7 +49,7 @@ Use the provided references before inventing new patterns. Prefer the user’s b ### Location selection -Always identify the best Contentstack UI location before writing code. Prefer the smallest location that fits the use case. If multiple locations fit, compare them by user context, available SDK surface, and implementation complexity, then recommend one. Consult the location-selection reference before answering. +The UI locations are App Configuration, Custom Field, Entry Sidebar, Content Type Sidebar, Asset Sidebar, Stack Dashboard, Full Page, Global Full Page (organization apps), Field Modifier (plan-gated) and RTE plugin. Always identify the best Contentstack UI location before writing code. Prefer the smallest location that fits the use case. If multiple locations fit, compare them by user context, available SDK surface, and implementation complexity, then recommend one. Check location mapping in [the coding reference](references/developer-hub-coding-reference.md) before answering. ### Code generation @@ -89,47 +57,54 @@ Generate production-ready React + TypeScript code when asked. Include loading st ### Developer Hub setup -Include manifest, base URL, route mapping, location enabling, advanced settings variables, proxy/rewrite, install, and deploy steps whenever they affect the implementation. Explain how the app is wired in Developer Hub and what must be configured before testing. Use the Developer Hub setup reference for exact steps. +Include manifest, base URL, route mapping, location enabling, advanced settings variables, proxy/rewrite, install, and deploy steps whenever they affect the implementation. Explain how the app is wired in Developer Hub and what must be configured before testing. Use the setup section of [the coding reference](references/developer-hub-coding-reference.md) for exact steps. + +### App type and target + +Pick the app type first. Standard apps have UI locations, webhooks, OAuth and hosting. Machine-to-machine apps use OAuth client credentials only, are organization-targeted, have no UI and are never installable; use them for backend automation. Standard apps target a stack or an organization, which decides which locations and webhook events are available. ### SDK and integration patterns -Use @contentstack/app-sdk patterns for location access, config read/write, field updates, asset replacement, iframe sizing, and typed guards. Prefer proxy/API rewrite patterns for external integrations and keep secrets server-side. Consult the SDK patterns reference for canonical usage. +Use @contentstack/app-sdk patterns for location access, config read/write, field updates, asset replacement, iframe sizing, and typed guards. Prefer proxy/API rewrite patterns for external integrations and keep secrets server-side: developer defaults as `{{var.NAME}}`, per-installation secrets in `serverConfiguration` referenced as `{{map.KEY}}`. Use the SDK section of [the coding reference](references/developer-hub-coding-reference.md) for canonical usage. ### Troubleshooting -Debug by checking route, manifest, location, SDK init, iframe context, config persistence, proxy behavior, and install scope assumptions in that order. Lead with the most likely cause and the fastest verification step. Use the troubleshooting reference to confirm likely failure modes. +Debug by checking route, manifest, location, SDK init, iframe context, config persistence, proxy behavior, and install scope assumptions in that order. Lead with the most likely cause and the fastest verification step. Use the troubleshooting section of [the coding reference](references/developer-hub-coding-reference.md) to confirm likely failure modes. ### Publishing and safety -For marketplace or publishing questions, include readiness checks for scopes, permissions, credentials, versioning, and install flow. Never expose OAuth client secrets or app signing keys. Validate scopes and avoid broader permissions than required. Use the marketplace publishing reference when relevant. +For marketplace or publishing questions, include readiness checks for scopes, permissions, credentials, versioning, and install flow. Never expose OAuth client secrets or app signing keys. Validate scopes and avoid broader permissions than required. The coding reference covers app visibility (private, public, public unlisted) but not the review process, so point to the Developer Hub publishing docs for that. + +## MCP tools -## Output Format +When the Contentstack MCP server is connected, these tools act on Developer Hub apps (local server group in brackets): -Be concise and actionable. -Use bullets for recommendations and checklists. -When code is requested, provide production-ready code or concrete edits, not pseudo-code. -When a decision is unclear, explain the tradeoff and make a recommendation. -When debugging, lead with the most likely cause and the fastest verification step. +- Inspect: `get_all_apps`, `get_an_app`, `get_app_installations`, `get_oauth_scopes`, `get_app_oauth` (`developerhub`) +- Change: `create_an_app`, `update_an_app`, `update_app_oauth`, `install_an_app`, `update_an_app_installation` (`developerhub`) +- Delete: `delete_an_app`, `uninstall_an_app` (`developerhub`) -## Tooling Notes +`get_app_oauth` returns the app's client credentials: don't repeat the client secret in the answer. There are no tools for app hosting or Marketplace publishing; use the Developer Hub UI. -Primary tools: Developer Hub API, Apps, Installations, OAuth, UI Locations, App Hosting, Marketplace. -Use Developer Hub Proxy and OAuth Provider when integrations require secure server-side handling. -Prefer marketplace and app-hosting guidance only when relevant to publishing or deployment. -Consult reference files for location selection, boilerplate conventions, SDK patterns, setup, troubleshooting, and publishing notes. +## Output + +Be concise and actionable. Use bullets for recommendations and checklists. Deliverables, as the task needs them: recommended UI location(s) and rationale, architecture and route map, manifest and Developer Hub setup checklist, code scaffold or concrete edits, config schema and proxy guidance, a test, install and deployment checklist, or a troubleshooting diagnosis with next steps. + +- When code is requested, provide production-ready code or concrete edits, not pseudo-code. +- When a decision is unclear, explain the tradeoff and make a recommendation. +- When debugging, lead with the most likely cause and the fastest verification step. ## Security ### Defaults +- Advisory by default. Use the MCP tools above only when the user asks for the change. - Never expose OAuth client secrets or app signing keys. -- Validate all app installation scopes. -- Never grant broader permissions than required. -- Keep app credentials server-side only. +- Validate all app installation scopes, and never grant broader permissions than required. +- Keep app credentials server-side only, through the Developer Hub proxy and OAuth provider. ### Destructive Actions -Do not perform destructive actions unless the user explicitly requests them and confirms the target app, environment, or installation. For publish, delete, revoke, or uninstall flows, explain the impact first and require confirmation. +Before creating, updating, installing, uninstalling or deleting an app, or changing its OAuth settings, name the app, the target stack or organization and the installation, explain the impact, and wait for confirmation. The same applies to publish and revoke flows. ### Secrets @@ -139,24 +114,11 @@ Never reveal, print, or embed secrets in frontend code, examples, logs, or confi Use environment variables for server-side configuration only. Do not place secrets in client-visible settings. Document required variables clearly and distinguish public config from private credentials. -## Product Context - -- - Product: Developer Hub -- - Description: Contentstack Developer Hub: marketplace apps, custom app development, OAuth app registration, app installations, hosting, UI locations, and the Developer Hub API. -- - Product safety rules: - Never expose OAuth client secrets or app signing keys. -- Validate all app installation scopes. -- Never grant broader permissions than required. -- Keep app credentials server-side only. -- - Default tools: ["Developer Hub API", "Apps", "Installations", "OAuth", "UI Locations", "App Hosting", "Marketplace"] -- - Default connectors: ["Developer Hub Proxy", "OAuth Provider", "Marketplace"] - ## References -This skill ships reference material in the `references/` folder. Read those files on demand when their content is relevant to the task — do not assume their contents from the summaries below. +Read these on demand when their content is relevant; don't assume their contents from the summaries. -### [Developer Hub coding reference](references/developer-hub-coding-reference.md) (playbook, required) -- Summary: Contentstack Developer Hub & Custom Apps Guide -- Read: `references/developer-hub-coding-reference.md` +- [Developer Hub coding reference](references/developer-hub-coding-reference.md): the Developer Hub and custom apps guide, covering location mapping, SDK usage, setup, troubleshooting and publishing. ## Examples diff --git a/skills/developer-hub-app-architect/references/developer-hub-coding-reference.md b/skills/developer-hub-app-architect/references/developer-hub-coding-reference.md index f99d4d5..fe209a9 100644 --- a/skills/developer-hub-app-architect/references/developer-hub-coding-reference.md +++ b/skills/developer-hub-app-architect/references/developer-hub-coding-reference.md @@ -17,9 +17,14 @@ Complete guide for building custom apps and extensions for Contentstack Develope ### Types of Apps -1. **Marketplace Apps**: Public apps available to all Contentstack users -2. **Private Apps**: Organization-specific apps for internal use -3. **Machine-to-Machine Apps**: Apps that interact with Contentstack APIs without user interface +1. **Standard apps**: support UI locations, webhooks, OAuth and hosting. Visibility is private, public (listed in the Marketplace after review) or public unlisted (installable by direct link). +2. **Machine-to-machine (M2M) apps**: OAuth client-credentials only, organization-targeted, no UI locations or webhooks, never installable or publishable. Use them for service-to-service automation. + +Standard apps target either a **stack** (installed into one stack; can subscribe to app and stack events) or an **organization** (installed at org level; app events only; can use org-level App Config and Global Full Page). + +### App lifecycle webhooks + +Apps can subscribe to installation events (`install`, `update`, `uninstall`, `upgrade`) to provision or clean up external resources. Stack-target apps can also receive stack events such as entry and global field changes. --- @@ -51,6 +56,10 @@ UI locations are specific places in Contentstack's interface where your app can | **Full Page** | `/full-page` | Standalone pages | Standalone full-page app | | **Field Modifier** | `/field-modifier` | Modify field values | Modifies field values programmatically | | **RTE Location** | `/json-rte.js` | Rich text extensions | Rich text editor extensions | +| **Content Type Sidebar** | `/content-type-sidebar` | Schema tools | Sidebar in the content type builder (stack apps only) | +| **Global Full Page** | `/global-full-page` | Org-level pages | Full-page app at organization level (organization apps only) | + +The route paths are the boilerplate's conventions; the actual paths are whatever you set in the manifest. Field Modifier is plan-gated and its SDK object is `FieldModifierLocation` (named `EntryFieldLocation` before App SDK 2.0). The SDK objects for the two newer locations are `ContentTypeSidebarWidget` and `GlobalFullPageLocation`. ### App SDK @@ -577,6 +586,10 @@ Configure URL rewrites in Developer Hub Advanced Settings: This allows you to use relative paths in your code while Contentstack proxies to the actual API. +### Per-installation secrets + +Variables (`{{var.NAME}}`) hold developer-level defaults. For values each installation supplies, such as a customer's own API key, save them in `serverConfiguration` through `appSdk.location.AppConfigWidget.installation.setInstallationData()` and reference them in rewrites as `{{map.KEY}}`. The proxy substitutes them server-side, so they never reach the browser. + ### Complete Example ```typescript @@ -687,6 +700,8 @@ npm run dev Runs on `http://localhost:3000` +The boilerplate is React + TypeScript + Vite with one lazy-loaded route per UI location. It ships plain CSS; add `@contentstack/venus-components` (Contentstack's UI component library) if you want the app to look native in the Contentstack UI. + ### 2. Configure in Developer Hub 1. **Create App**: @@ -697,6 +712,7 @@ Runs on `http://localhost:3000` 2. **Set Base URL**: + - In the Hosting tab, choose Launch (Contentstack-managed) or custom hosting - Set base URL to `http://localhost:3000` (for development) - Or use tunneling service (ngrok, Cloudflare Tunnel) for HTTPS diff --git a/skills/dx-cli/SKILL.md b/skills/dx-cli/SKILL.md new file mode 100644 index 0000000..65464b2 --- /dev/null +++ b/skills/dx-cli/SKILL.md @@ -0,0 +1,139 @@ +--- +name: dx-cli +description: "Guide developers through the Contentstack CLI (csdx, @contentstack/cli 2.x): install, region setup, login with password or OAuth/SSO, token aliases, stack export, import, seed and clone, bulk publish and unpublish with cm:stacks:bulk-entries/bulk-assets/bulk-taxonomies, migration scripts, branch diff and merge, audit, and installable plugins such as tsgen, apps-cli and the Launch plugin. Use when developers ask which csdx command to run, how to authenticate the CLI, move content between stacks or branches, bulk publish, write cm:stacks:migration scripts, generate TypeScript types, upgrade from CLI 1.x, or troubleshoot CLI errors. For Contentful-to-Contentstack migrations, use migration-companion; for automated Launch deployments over the API, use the launch-* skills." +allowed-tools: Read Grep Glob +--- + +# Contentstack CLI + +Guide developers through the Contentstack CLI (`csdx`): setup and authentication, moving content between stacks and branches, bulk operations, migration scripts, branch merges, and the installable plugins. + +## When to use + +- Which `csdx` command to run, and with which flags. +- Logging in, regions and token aliases. +- Exporting, importing, seeding or cloning a stack. +- Bulk publishing and unpublishing. +- Scripting content type changes, diffing and merging branches, generating TypeScript types. +- Moving from CLI 1.x to 2.x, or troubleshooting CLI errors. + +Use another skill for: + +- Contentful migrations → `migration-companion` +- Launch deployments driven through the Launch API → `launch-trigger-and-monitor-launch-deployments`, `launch-sync-environment-variables-from-env-example` +- Launch hosting concepts → `launch-platform` +- Branch strategy and aliases → `cms-branches-aliases` +- Building Developer Hub apps → `developer-hub-app-architect` +- Converting HTML RTE content and rendering JSON RTE → `cms-json-rte` + +## Before you answer + +Find out the task (export, import, seed, clone, bulk publish, migration, branch merge, types, Launch), the region, the stack API key or token alias, the source and target branch or environment, and the CLI version (`csdx --version`). The CLI has many plugins and flags, and several changed between 1.x and 2.x. + +## Instructions + +1. Check the CLI version and Node version. +2. Set the region, then authenticate. +3. Pick a command that exists in the current CLI and fill in stack, branch and environment explicitly. +4. Point out what the command overwrites and how to undo it. +5. Give the command and one verification step. + + +### Install and version + +Install with `npm install -g @contentstack/cli`. CLI 2.x needs Node.js 22 or later. Check with `csdx --version`, and list commands with `csdx --help`. Install extra plugins with `csdx plugins:install <package>`. + +### Region and authentication + +Set the region first: `csdx config:set:region AWS-NA` (also `AWS-EU`, `AWS-AU`, `AZURE-NA`, `AZURE-EU`, `GCP-NA`, `GCP-EU`, or custom hosts with `--cma`, `--cda`, `--ui-host` and `--name`). Sessions are tied to a region, so changing it means logging in again. + +Log in with `csdx auth:login` (alias `csdx login`). In SSO organizations use `csdx auth:login --oauth`, which opens the browser. Check the session with `csdx auth:whoami`. + +For scripts and CI, store tokens as aliases instead of using a personal session: `csdx auth:tokens:add --management -a <alias> -k <stack_api_key>` or `--delivery -e <environment>`. Most commands accept `-a <alias>` or `-k <stack_api_key>`. List aliases with `csdx auth:tokens:list`. + +### Export, import, seed and clone + +- `csdx cm:stacks:export -k <key> --data-dir ./export --branch <branch>` exports a stack. Use `--module` for one module and `--content-types` to limit entries. Pass `--branch` explicitly. +- `csdx cm:stacks:import -k <key> --data-dir ./export --branch <branch>` imports into an existing stack. `--module` imports one module. `--replace-existing` overwrites matching modules, so warn before using it. `--skip-entries-publish` and `--skip-assets-publish` skip publishing. `--import-webhook-status disable` keeps imported webhooks off. +- Export and import cover Personalize projects and variant entries (`--module personalize`, `variant-entries`) and Studio compositions alongside the CMS modules. +- `csdx cm:stacks:seed --repo <github_repo>` creates or fills a stack from a starter repository. +- `csdx cm:stacks:clone` copies a stack or branch to another stack: type `a` copies structure only, type `b` copies structure and content. +- `csdx cm:stacks:audit` finds content type and entry issues; `csdx cm:stacks:audit:fix` repairs them. Run it before large imports. + +### Bulk operations + +CLI 2.x replaces the 1.x `cm:stacks:bulk-publish` commands with the bundled bulk-operations plugin: + +- `csdx cm:stacks:bulk-entries --operation publish|unpublish --content-types blog --environments prod --locales en-us -a <alias>` +- `csdx cm:stacks:bulk-assets --operation publish|unpublish ...`, which also moves and deletes Assets (DAM) items with `--space-uid` +- `csdx cm:stacks:bulk-taxonomies` + +`--publish-mode bulk` (default) uses the Bulk Publish API; `single` publishes one item at a time. `--source-env` cross-publishes what is live in one environment to others. Operations write a log folder: rerun failures with `--retry-failed <folder>` and undo with `--revert <folder>`. The CMA allows 1 bulk request per second per organization, so large runs take time. + +### Migration scripts + +`csdx cm:stacks:migration -k <key> --file-path ./migrations/001-add-field.js --branch <branch>` runs a JavaScript migration script that creates or edits content types and transforms entries. `--multiple` runs every script in a folder. Keep scripts in version control and run them on a branch before main. The command executes the script's code, so only run scripts you trust. + +### Branches + +- `csdx cm:branches:create` and `csdx cm:branches:delete` manage branches. +- `csdx cm:branches:diff --base-branch main --compare-branch feature -k <key>` shows content type and global field differences. +- `csdx cm:branches:merge --base-branch main --compare-branch feature -k <key>` merges them. Merge covers content types and global fields only. By default it creates a revert branch first; `--no-revert` skips that, so discourage it. Use `--export-summary-path` to review the plan and `--use-merge-summary` to apply it. +- `csdx cm:branches:merge-status -k <key> --merge-uid <uid>` checks a merge job. + +### Installable plugins + +- **TypeScript types:** `csdx plugins:install contentstack-cli-tsgen`, then `csdx tsgen -a <delivery_alias> --output types/contentstack.d.ts`. Add `--api-type graphql` for GraphQL types and `--include-referenced-entry` for references. +- **Apps:** `@contentstack/apps-cli` adds `csdx app:create`, `app:deploy`, `app:install` and related commands for Developer Hub apps. +- **Launch:** `@contentstack/cli-launch` adds `csdx launch` (deploy from GitHub or a file upload), `csdx launch:logs` (`--type d` for deployment logs) and `csdx launch:functions` (run cloud functions locally). +- Also available: `@contentstack/cli-cm-export-query` (query-based export), `contentstack-cli-content-type` (inspect content types), `@contentstack/cli-cm-regex-validate` and `@contentstack/cli-cm-migrate-rte` (HTML RTE to JSON RTE, opt-in in 2.x). + +See [references/commands.md](references/commands.md) for the command list by plugin. + +### Upgrading from 1.x + +- Node.js 22 or later is required. +- `cm:stacks:bulk-publish` and the related 1.x bulk commands are gone; use `bulk-entries`, `bulk-assets` and `bulk-taxonomies`. +- The HTML-to-JSON RTE migration plugin is no longer bundled; install it if needed. +- Re-check scripts that pass removed flags. + +## MCP tools + +The CLI is the tool for bulk and stack-wide jobs; the MCP server has no export, import, seed, clone, bulk publish or migration-script tools. When the Contentstack MCP server is connected, it covers the single-item and branch operations (local server group in brackets): + +- Inspect: `get_all_branches`, `compare_branches`, `compare_content_types_between_branches`, `compare_global_fields_between_branches`, `get_all_merge_jobs`, `get_a_single_merge_job`, `get_all_content_types`, `export_content_type` (`cma`) +- Change: `create_a_branch`, `merge_branch`, `publish_an_entry`, `unpublish_an_entry` (`cma`) +- Delete: `delete_a_branch` (`cma`) + +Use them to check the state of a stack before and after a CLI run, for example to compare branches before `csdx cm:branches:merge`. + +## Output + +Give the command first, then the setup it needs (region, login, token alias, plugin install), then what it changes and how to verify or undo it. Offer a dry run or read-only check where available, such as `csdx auth:whoami`, `csdx config:get:region` or `csdx cm:branches:diff`. Use placeholders for keys, tokens and aliases. + +## Security + +### Defaults + +- Advisory by default: suggest commands for the user to run, and don't run commands that write to a stack. Use the MCP tools above only when the user asks for the change. +- Prefer token aliases or OAuth over passing tokens on the command line. +- Use management tokens only on trusted machines and in CI secrets. +- Always pass the branch explicitly in write commands. +- Back up or export before destructive operations. + +### Destructive Actions + +Imports with `--replace-existing`, bulk unpublish, branch merges, branch deletion and migration scripts change live data. State the target stack, branch and environment, recommend an export or backup branch first, and require the user to confirm. The same applies before any MCP write, merge or delete. + +### Secrets + +Never print management tokens or authtokens. `csdx auth:tokens:list` shows token values, so don't ask users to paste its output. + +### Environment Variables + +In CI, inject tokens from the secret store and add them with `csdx auth:tokens:add ... --token "$CONTENTSTACK_MANAGEMENT_TOKEN"`. + +## References + +- [Command list by plugin](references/commands.md) +- https://www.contentstack.com/docs/developers/cli diff --git a/skills/dx-cli/references/commands.md b/skills/dx-cli/references/commands.md new file mode 100644 index 0000000..aaf9e04 --- /dev/null +++ b/skills/dx-cli/references/commands.md @@ -0,0 +1,37 @@ +# csdx commands by plugin (CLI 2.x) + +Bundled plugins ship with `@contentstack/cli`. Installable plugins need `csdx plugins:install <package>`. Run `csdx <command> --help` for the full flag list. + +## Bundled + +| Plugin | Commands | +| --- | --- | +| `@contentstack/cli-auth` | `auth:login` (`login`; `--oauth` for SSO), `auth:logout` (`logout`), `auth:whoami` (`whoami`), `auth:tokens:add`, `auth:tokens:list`, `auth:tokens:remove` | +| `@contentstack/cli-config` | `config:set:region`, `config:get:region`, `config:set:base-branch`, `config:get:base-branch`, `config:set:rate-limit`, `config:set:proxy`, `config:set:log`, `config:set:early-access-header` (and matching `get`/`remove` commands) | +| `@contentstack/cli-cm-export` | `cm:stacks:export` | +| `@contentstack/cli-cm-import` | `cm:stacks:import` | +| `@contentstack/cli-cm-seed` | `cm:stacks:seed` | +| `@contentstack/cli-cm-bootstrap` | `cm:bootstrap` (start a project from a starter app) | +| `@contentstack/cli-cm-clone` | `cm:stacks:clone` | +| `@contentstack/cli-cm-export-to-csv` | export entries, taxonomies, terms and organization users to CSV | +| `@contentstack/cli-migration` | `cm:stacks:migration` | +| `@contentstack/cli-cm-branches` | `cm:branches`, `cm:branches:create`, `cm:branches:delete`, `cm:branches:diff`, `cm:branches:merge`, `cm:branches:merge-status` | +| `@contentstack/cli-audit` | `cm:stacks:audit`, `cm:stacks:audit:fix` | +| `@contentstack/cli-bulk-operations` | `cm:stacks:bulk-entries`, `cm:stacks:bulk-assets`, `cm:stacks:bulk-taxonomies` | + +## Installable + +| Package | Commands | +| --- | --- | +| `contentstack-cli-tsgen` | `tsgen` | +| `@contentstack/apps-cli` | `app:create`, `app:get`, `app:update`, `app:delete`, `app:install`, `app:uninstall`, `app:reinstall`, `app:deploy` | +| `@contentstack/cli-launch` | `launch`, `launch:logs`, `launch:functions` | +| `@contentstack/cli-cm-export-query` | query-based export | +| `contentstack-cli-content-type` | inspect content types | +| `@contentstack/cli-cm-regex-validate` | validate regex properties on fields | +| `@contentstack/cli-cm-migrate-rte` | migrate HTML RTE content to JSON RTE | +| `@contentstack/cli-external-migrate` | `migrate:*` commands for migrating from Contentful (see the `migration-companion` skill) | + +## Import and export modules + +`--module` accepts: `stack`, `assets`, `locales`, `environments`, `extensions`, `webhooks`, `global-fields`, `entries`, `content-types`, `custom-roles`, `workflows`, `publishing-rules`, `labels`, `marketplace-apps`, `taxonomies`, `personalize`, `composable-studio`, plus `variant-entries` on import. diff --git a/skills/dx-delivery-sdk/SKILL.md b/skills/dx-delivery-sdk/SKILL.md index 423758d..0508304 100644 --- a/skills/dx-delivery-sdk/SKILL.md +++ b/skills/dx-delivery-sdk/SKILL.md @@ -1,57 +1,35 @@ --- name: dx-delivery-sdk -description: "Help agents write correct, production-ready TypeScript using `@contentstack/delivery-sdk` for Contentstack entries, assets, references, filtering, sorting, pagination, locale, Live Preview, and Visual Builder support. Verify SDK behavior against the Delivery SDK Spec when method names, options, or chain order matter. Use when a user asks for Contentstack Delivery SDK code, query examples, helper functions, SDK setup, stack initialization, reference inclusion, filtering, sorting, pagination, typed entry fetching, asset fetching, Live Preview setup, Visual Builder support, SSR preview handling, or debugging SDK query chains." +description: "Help agents write correct, production-ready TypeScript using `@contentstack/delivery-sdk` for Contentstack entries, assets, references, filtering, sorting, pagination, locale, Live Preview, and Visual Builder support. Verify SDK behavior against the Delivery SDK Spec when method names, options, or chain order matter. Use when a user asks for Contentstack Delivery SDK code, query examples, helper functions, SDK setup, stack initialization, reference inclusion, filtering, sorting, pagination, typed entry fetching, asset fetching, Live Preview setup, Visual Builder support, SSR preview handling, or debugging SDK query chains. For GraphQL, use cms-graphql; for rendering JSON RTE fields, use cms-json-rte; for Live Preview troubleshooting, use cms-live-preview-visual-editor-support-assistant." allowed-tools: Read Grep Glob --- -# dx-delivery-sdk +# Contentstack Delivery SDK -## Description +Write correct, production-ready TypeScript with `@contentstack/delivery-sdk`: entries, assets, references, filtering, sorting, pagination, locale, Live Preview and Visual Builder support. The SDK reads published content, assets and taxonomy from the Content Delivery API. -Help agents write correct, production-ready TypeScript using `@contentstack/delivery-sdk` for Contentstack entries, assets, references, filtering, sorting, pagination, locale, Live Preview, and Visual Builder support. Verify SDK behavior against the Delivery SDK Spec when method names, options, or chain order matter. +## When to use -## When to Use +- Delivery SDK setup and stack initialization. +- Query examples and helper functions: references, filtering, sorting, pagination, field selection, locale. +- Typed entry and asset fetching. +- Live Preview, Visual Builder and SSR preview setup in SDK code. +- Debugging SDK query chains. -Use when a user asks for Contentstack Delivery SDK code, query examples, helper functions, SDK setup, stack initialization, reference inclusion, filtering, sorting, pagination, typed entry fetching, asset fetching, Live Preview setup, Visual Builder support, SSR preview handling, or debugging SDK query chains. +Use another skill for: -## User Problem +- Migrating JavaScript SDK code to TypeScript → `dx-migrate-js-to-ts-sdk` +- GraphQL queries → `cms-graphql` +- Rendering JSON RTE fields and embedded items → `cms-json-rte` +- Diagnosing broken Live Preview or Visual Editor setups → `cms-live-preview-visual-editor-support-assistant` +- Work inside the Next.js kickstart repo → `dx-kickstart-next` +- CDA query semantics beyond the SDK (raw REST, Sync API) → `cms-entries` -Users need current, correct Contentstack Delivery SDK code with the right method names, chain order, and query patterns. The skill should prevent subtle SDK mistakes and default to production-ready TypeScript. +## Before you answer -## Success Criteria +Identify the content type UID, any entry or asset UID, the field UIDs for filters, sorting, selection or references, the locale, the return shape the user wants, and whether SSR or Visual Builder is involved. Ask only for what is missing to build the query correctly. -- Uses current `@contentstack/delivery-sdk` method names and correct chain order. -- Verifies API details against the Delivery SDK Spec when behavior is uncertain. -- Produces TypeScript-first examples with safe environment-based credentials. -- Handles references, filters, sorting, pagination, field selection, assets, locale, and Live Preview correctly. -- Flags or avoids common mistakes such as wrong query methods, missing pagination, or shared SSR Live Preview stacks. - -## Expected Inputs - -- Content type UID -- Entry UID or asset UID when applicable -- Field UIDs for filters, sorting, selection, or references -- Locale when localized content is needed -- Desired return shape or helper function behavior -- SSR or Visual Builder context when Live Preview is involved - -## Expected Outputs - -- TypeScript code snippets -- Reusable helper functions -- Correct SDK query chains -- Live Preview or SSR-safe stack setup -- Debugging guidance for incorrect Contentstack SDK usage - -## Example User Requests - -- Write a Contentstack Delivery SDK query for blog posts with author references included. -- Show me the correct TypeScript setup for Contentstack Live Preview in Next.js SSR. -- Fix this Contentstack query chain; it is not returning the right entries. -- Create a helper to fetch entries by URL with pagination and typing. -- How do I fetch assets and filter by content type in `@contentstack/delivery-sdk`? - -## Workflow Summary +## Instructions 1. Identify the target object: entry, asset, content type, taxonomy, or helper. 2. Check the Delivery SDK Spec when method names, options, or chain order matter. @@ -59,9 +37,8 @@ Users need current, correct Contentstack Delivery SDK code with the right method 4. Choose the correct SDK chain and method order. 5. Add pagination, locale, field selection, and references as needed. 6. Use safe TypeScript types and reusable helpers. -7. Include error handling and common-mistake warnings when relevant. +7. Include error handling and common-mistake warnings when relevant, such as wrong query methods, missing pagination, or shared SSR Live Preview stacks. -## Instructions ### Identify the target @@ -83,37 +60,45 @@ Apply references before `.query()`, use `QueryOperation` for filters, use `order For SSR or Visual Builder, create a new stack per request and apply live preview configuration before fetching. Do not reuse a Live Preview stack across requests. -## Output Format +### Packages and hosts + +- Prefer `@contentstack/delivery-sdk` as the primary SDK. +- Use `@timbenniks/contentstack-endpoints` for region-specific preview hosts when needed. It is a community package (not published under `@contentstack/`) that the official kickstarts use to resolve region hosts. +- Use `getContentstackEndpoints(region, true)` so SDK host values omit `https://`. +- For SSR Live Preview or Visual Editor (formerly Visual Builder), create a new stack per request. +- Cache policies other than `Policy.IGNORE_CACHE` need `@contentstack/persistence-plugin`: pass a `PersistenceStore` as `cacheOptions.persistenceStore`. + +## MCP tools + +When the Contentstack MCP server is connected, use read tools to ground SDK code in the real stack instead of guessing UIDs (local server group in brackets): -Prefer TypeScript. -Use concise, production-ready code blocks. -Verify method-order constraints against the Delivery SDK Spec when they matter. -Do not hardcode credentials. -Call out pagination limits when returning multiple entries. -Include brief notes for Live Preview or SSR safety when relevant. +- Inspect the model: `get_all_content_types`, `get_a_single_content_type`, `get_all_global_fields`, `get_all_languages`, `get_all_environments` (`cma`) +- Check what the SDK will return: `get_all_entries_cdn`, `get_a_single_entry_cdn`, `get_all_assets_cdn`, `get_a_single_asset_cdn` (`cda`) -## Tooling Notes +The `cda` tools read published content with a delivery token, the same data the SDK sees. This skill needs no write tools. -Prefer `@contentstack/delivery-sdk` as the primary SDK. -Use `@timbenniks/contentstack-endpoints` for region-specific preview hosts when needed. -Use `getContentstackEndpoints(region, true)` so SDK host values omit `https://`. -For SSR Live Preview or Visual Builder, create a new stack per request. +## Output + +- Prefer TypeScript, in concise, production-ready code blocks. +- Verify method-order constraints against the Delivery SDK Spec when they matter. +- Do not hardcode credentials. +- Call out pagination limits when returning multiple entries. +- Include brief notes for Live Preview or SSR safety when relevant. ## Security ### Defaults -Never expose deployment tokens or environment secrets. -Validate deployment targets before triggering. -Require confirmation for production deployments. -Never auto-deploy to production without review. -Never hardcode Contentstack credentials in examples. -Avoid sharing preview tokens or other secrets in responses. -For SSR Live Preview, do not reuse a stack instance across requests. +- Advisory by default. Use the MCP tools above only to read the stack. +- Delivery tokens are read-only and safe for client-side code. +- Preview tokens read unpublished content: use them only in preview deployments, never in production builds. +- Never put management tokens in SDK or client code. +- Never hardcode Contentstack credentials in examples, and avoid sharing preview tokens or other secrets in responses. +- For SSR Live Preview, do not reuse a stack instance across requests. ### Destructive Actions -This skill does not perform destructive actions. If a request implies deleting, publishing, or mutating production content, require explicit confirmation and verify the target before proceeding. +This skill does not perform destructive actions. If a request implies deleting, publishing, or mutating content, name the target and get explicit confirmation before proceeding. ### Secrets @@ -123,23 +108,11 @@ Use environment variables for all credentials and tokens. Never print, infer, or Use `process.env` placeholders in examples. Prefer required non-null assertions only when the surrounding code clearly validates configuration. Do not suggest committing secrets to source control. -## Product Context - -- - Product: Developer Experience -- - Description: Developer experience tooling -- - Product safety rules: - Never expose deployment tokens or environment secrets. -- Validate deployment targets before triggering. -- Require confirmation for production deployments. -- Never auto-deploy to production without review. - ## References -This skill ships reference material in the `references/` folder. Read those files on demand when their content is relevant to the task — do not assume their contents from the summaries below. +Read these on demand when their content is relevant; don't assume their contents from the summaries. -### [Delivery SDK Spec](references/delivery-sdk-spec.md) (product doc, required) -- Source: Official Docs (https://www.contentstack.com/docs/developers/sdks/content-delivery-sdk/typescript/reference.md) -- Summary: Contentstack offers the TypeScript Delivery SDK for building applications. Below, is an in-depth guide and valuable resources to initiate your journey with our TypeScript Delivery SDK. Additionally, the SDK supports the creating applications for Node.js and React Native environments. -- Read: `references/delivery-sdk-spec.md` +- [Delivery SDK Spec](references/delivery-sdk-spec.md) (required): the TypeScript Delivery SDK reference for Node.js and React Native. Source: https://www.contentstack.com/docs/developers/sdks/content-delivery-sdk/typescript/reference.md ## Examples diff --git a/skills/dx-kickstart-next/SKILL.md b/skills/dx-kickstart-next/SKILL.md index 8645ba5..6ced2cc 100644 --- a/skills/dx-kickstart-next/SKILL.md +++ b/skills/dx-kickstart-next/SKILL.md @@ -1,13 +1,29 @@ --- name: dx-kickstart-next -description: "Guidance for building, reviewing, debugging, or extending Contentstack's kickstart-next repository. Use when working in contentstack/kickstart-next or a close fork: the Next.js kickstart app, its Contentstack Delivery SDK setup, Live Preview or Visual Builder behavior, seeded stack alignment, environment variables, endpoint or image host configuration, App Router structure, package scripts, local setup, CI expectations, tests, lint/build validation, or PR review checklists. For general Delivery SDK or Live Preview questions outside this repo, use dx-delivery-sdk or cms-live-preview-visual-editor-support-assistant." +description: "Guidance for building, reviewing, debugging, or extending Contentstack's kickstart-next repository. Use when working in contentstack/kickstart-next or a close fork: the Next.js kickstart app, its Contentstack Delivery SDK setup, Live Preview or Visual Builder behavior, seeded stack alignment, environment variables, endpoint or image host configuration, App Router structure, package scripts, local setup, CI expectations, tests, lint/build validation, or PR review checklists. For general Delivery SDK or Live Preview questions outside this repo, use dx-delivery-sdk or cms-live-preview-visual-editor-support-assistant; for rendering JSON RTE fields, use cms-json-rte." --- # Contentstack kickstart-next -Use this skill when working in `contentstack/kickstart-next` or a close fork. Inspect the repository first, then load only the matching reference files. +Build, review, debug and extend `contentstack/kickstart-next` or a close fork. Inspect the repository first, then load only the matching reference files. -## Routing +## When to use + +- Working in `contentstack/kickstart-next` or a close fork: App Router structure, components, Delivery SDK setup, Live Preview and Visual Builder behavior, seeded stack alignment, environment variables, endpoint and image host configuration. +- Package scripts, local setup, CI expectations, tests, lint and build validation. +- PR review checklists for the repo. + +Use another skill for: + +- General Delivery SDK code outside this repo → `dx-delivery-sdk` +- Diagnosing Live Preview or Visual Editor problems in general → `cms-live-preview-visual-editor-support-assistant` +- Rendering JSON RTE fields → `cms-json-rte` +- Content model design → `cms-data-modeling-best-practices` +- Hosting the kickstart on Launch → `launch-platform` + +## Instructions + +### Routing 1. For App Router routes, React components, client/server boundaries, Tailwind, ESLint, TypeScript, images, or `next.config.mjs`, read [references/next.md](references/next.md). 2. For Contentstack delivery, preview, Visual Builder, regions, endpoint overrides, env vars, or seeded stack alignment, read [references/contentstack.md](references/contentstack.md). @@ -15,7 +31,7 @@ Use this skill when working in `contentstack/kickstart-next` or a close fork. In 4. For local setup, Contentstack stack seeding, npm scripts, CI, CODEOWNERS, validation commands, docs updates, or PR prep, read [references/workflow.md](references/workflow.md). 5. For PR/code reviews, severity labels, security checks, or risk checklists, read [references/review.md](references/review.md). -## Default working rules +### Default working rules - Keep guidance grounded in the current checkout. Inspect files before assuming scripts, tests, CI, content types, env vars, or framework conventions exist. - Treat kickstarts as user-facing examples: prefer clarity, minimal moving parts, safe defaults, and copyable setup steps over clever abstractions. @@ -25,7 +41,17 @@ Use this skill when working in `contentstack/kickstart-next` or a close fork. In - When modifying JavaScript or TypeScript files, run `npm run build`; also run `npm run lint` and any `typecheck` script if they exist. - Before claiming CI covers something, verify the workflows. If build, lint, or tests are not wired into CI, tell the user to run them locally. -## Response behavior +## MCP tools + +When the Contentstack MCP server is connected, use read tools to check that the code matches the user's stack (local server group in brackets): + +- Inspect the model: `get_all_content_types`, `get_a_single_content_type`, `get_all_global_fields`, `get_all_environments`, `get_all_languages` (`cma`) +- Check published content: `get_all_entries_cdn`, `get_a_single_entry_cdn` (`cda`) +- Change, only when the user asks: `create_a_content_type`, `update_content_type`, `create_an_entry`, `update_an_entry` (`cma`) + +Seeding a stack uses the CLI (`csdx cm:stacks:seed`), not MCP. + +## Output - For implementation help, give concrete file-level changes and commands. - For reviews, separate blockers from major issues and minor nits. @@ -36,13 +62,14 @@ Use this skill when working in `contentstack/kickstart-next` or a close fork. In ### Defaults +- Advisory by default. Use the MCP tools above only when the user asks, and only read tools unless they ask for a stack change. - Never commit real Contentstack credentials. `.env.example` and docs use placeholders; real values live in a gitignored `.env`. - Delivery tokens are safe in the browser. Preview tokens read unpublished content: expose them client-side only on preview deployments (CSR preview needs them), never in production builds. Management tokens never reach the browser. - Never recommend enabling Live Preview in production builds. ### Destructive Actions -Advisory by default. Before changing a user's stack (seeding, schema changes) or force-pushing, deleting branches or rewriting history in the repo, describe the impact and get explicit confirmation. +Before changing a user's stack (seeding, schema changes, any MCP write) or force-pushing, deleting branches or rewriting history in the repo, describe the impact and get explicit confirmation. ### Secrets diff --git a/skills/dx-mcp-server/SKILL.md b/skills/dx-mcp-server/SKILL.md new file mode 100644 index 0000000..99f943a --- /dev/null +++ b/skills/dx-mcp-server/SKILL.md @@ -0,0 +1,166 @@ +--- +name: dx-mcp-server +description: "Help developers connect AI agents to Contentstack over the Model Context Protocol, with either the local @contentstack/mcp stdio server or the hosted MCP Profile Hub. Cover choosing between them, npx setup, API groups (GROUPS), OAuth and token auth, required IDs per group, client configuration for Claude Code, Claude Desktop, claude.ai, Cursor, Codex and ChatGPT, Profile Hub profiles, connector URLs and URL parameters, Automate automations and Agent OS agents as tools, execution history, the Tool Definitions API, and safe least-privilege setups. Use when developers ask how to give an AI assistant access to Contentstack, set up or debug the Contentstack MCP server or MCP Profile Hub, pick tool groups or profiles, fix MCP auth or missing tools, or build their own MCP integration. Pairs with the other Contentstack skills, which supply the product knowledge the tools need. For the csdx CLI, use dx-cli." +allowed-tools: Read Grep Glob +--- + +# Contentstack MCP + +Help developers give AI agents access to Contentstack through MCP. Contentstack offers two options: the local `@contentstack/mcp` server, which runs on the developer's machine over stdio, and MCP Profile Hub, a hosted server reached over Streamable HTTP that exposes curated tool profiles. + +## When to use + +- Connecting an AI assistant to Contentstack to read or change content, deploy with Launch, query Personalize, Brand Kit, Lytics, Analytics or Automate, or run automations and agents. +- Choosing between the local server and MCP Profile Hub, and picking tool groups or profiles. +- MCP setup failures: no tools appear, auth fails, or the wrong stack or region is used. +- Building your own MCP integration on Contentstack's tool definitions. + +Use another skill for: + +- The `csdx` CLI → `dx-cli` +- What to do with the tools once connected: the product skill for the area, such as `cms-entries`, `cms-branches-aliases`, `launch-platform`, `personalize` or `automations` +- Token types and auth outside MCP → `cms-tokens-authentication` +- Installing Marketplace apps (MCP Profile Hub is one) → `marketplace-apps` +- MCP setup inside a Studio project → `studio` + +## Before you answer + +Find out the MCP client (Claude Code, Claude Desktop, claude.ai, Cursor, Codex, ChatGPT, VS Code or another), the products and tasks the agent needs, the region, stack API key, branch and any project IDs, and whether the setup is personal or shared by a team. The agent should see the right tools for the job, act on the right stack and branch with the user's own permissions, and not load hundreds of tools or leak tokens. + +## Instructions + +1. Choose between the local server and Profile Hub. +2. Pick the smallest set of groups, tools or profile. +3. Configure the client and authenticate. +4. Set stack, branch and project context. +5. Verify with a read-only prompt. + + +### Choose local server or Profile Hub + +- **Local `@contentstack/mcp` server:** runs with `npx` on the developer's machine over stdio. Best for individual development, quick starts, scripted setups, management-token use, and dedicated infrastructure with custom endpoints. Tool selection is per group. +- **MCP Profile Hub:** hosted by Contentstack at `https://mcp-profile-hub.contentstack.com/api/mcp`, reached over Streamable HTTP, so nothing is installed and it also works in web clients such as claude.ai and ChatGPT. Profiles expose only the tools you pick, connections are configured once in the app, sign-in is OAuth, every call runs with the user's Contentstack roles, and calls are logged in an Executions view. Best for teams, non-developers and governed access. It can also expose your Automate automations and Agent OS agents as tools. + +When unsure, recommend Profile Hub for shared or team use and the local server for a single developer working in a repository. + +### Local server: setup + +Run it with `npx -y @contentstack/mcp`; no install is needed. Pick groups with `GROUPS` or `--groups` (comma-separated): `cma`, `cma-extended`, `cda`, `analytics`, `automations`, `brandkit`, `launch`, `developerhub`, `lytics`, `personalize`, or `all`. The default is `cma`. Load `cma-extended` (audit logs, version history, workflow and publish rule inspection, folder and global field writes) only when needed, because every group adds tool definitions to each request. + +Auth and required IDs: + +| Group | Auth | Also needs | +| --- | --- | --- | +| `cma`, `cma-extended` | OAuth, or a management token | `CONTENTSTACK_API_KEY` | +| `cda` | Delivery token | `CONTENTSTACK_API_KEY`, `CONTENTSTACK_DELIVERY_TOKEN` | +| `analytics`, `automations` | OAuth | — | +| `brandkit` | OAuth | `CONTENTSTACK_BRAND_KIT_ID` | +| `launch` | OAuth | `CONTENTSTACK_LAUNCH_PROJECT_ID`, `CONTENTSTACK_ORGANIZATION_UID` | +| `developerhub` | OAuth | `CONTENTSTACK_API_KEY` | +| `lytics` | Lytics access token | `LYTICS_ACCESS_TOKEN` | +| `personalize` | OAuth | `CONTENTSTACK_PERSONALIZE_PROJECT_ID` | + +Run `npx @contentstack/mcp --auth` once to sign in with OAuth. It saves the session and region locally. A management token (`CONTENTSTACK_MANAGEMENT_TOKEN`) works only for `cma` and `cma-extended`, and then `CONTENTSTACK_REGION` must be set (`NA`, `EU`, `AU`, `AZURE_NA`, `AZURE_EU`, `GCP_NA`, `GCP_EU`). Optional defaults: `CONTENTSTACK_ENVIRONMENT`, `CONTENTSTACK_BRANCH` (default `main`) and `CONTENTSTACK_LOCALE`. Since 0.9.0 the server enriches tool schemas with live stack data (content types, environments, locales, branches); set `CONTENTSTACK_MCP_ENRICH_TOOLS=false` to turn it off. Dedicated infrastructure can override hosts with `CONTENTSTACK_*_BASE_URL` variables. Every value can also be passed as a CLI flag (`--stack-api-key`, `--groups` and so on) for clients without environment variable support. + +Client configuration: + +- **Claude Code:** `claude mcp add contentstack --env GROUPS=cma --env CONTENTSTACK_API_KEY=<key> -- npx -y @contentstack/mcp` +- **Claude Desktop:** add an `mcpServers` entry to `claude_desktop_config.json` with `"command": "npx"`, `"args": ["-y", "@contentstack/mcp"]` and an `env` block, then restart. +- **Cursor:** the same `mcpServers` entry in `~/.cursor/mcp.json`. +- Any other stdio MCP client uses the same `npx` command. + +### Profile Hub: setup + +1. Open **MCP Profile Hub** from the App Switcher, or install the Marketplace app (needs a role that can install apps). +2. Use a ready-made system profile (for example `cms` or `launch`), or create a profile: **+ New Profile**, name it, **Browse catalog**, and pick tools from the **Contentstack APIs**, **Automations** or **AI agents** tabs (up to 200 tools per profile). +3. In **Configure**, choose a stack (required) and any Launch, Personalize, Brand Kit, Automate or Lytics connections the tools need. +4. In **Installation**, copy the connector URL or the client snippet: `https://mcp-profile-hub.contentstack.com/api/mcp?profile_id=<PROFILE_ID>&org_uid=<ORG_UID>`. + +Connecting clients: + +- **Claude Code:** `claude mcp add --transport http <name> "<connector URL>"`. Quote the URL. Add `--scope user` to make it available in every project, then run `/mcp` to sign in. +- **claude.ai and Claude Desktop:** **Add to Claude** in the Installation step, or add a custom connector with the URL. On some Claude plans only admins can add custom connectors. +- **Claude Desktop without connectors, and other stdio-only clients:** wrap the URL with `mcp-remote` through `npx`, using the snippet from the Installation step (it pins the version). Needs Node.js. +- **Cursor:** **Add to Cursor**, or `{"mcpServers": {"<name>": {"url": "<connector URL>"}}}` in `.cursor/mcp.json`. +- **Codex:** a `[mcp_servers.<name>]` block with `url = "<connector URL>"` in `~/.codex/config.toml`. +- **ChatGPT:** add a custom connector with the URL, if the ChatGPT plan supports custom connectors. + +Don't add Authorization headers or bearer tokens: they block the OAuth handshake. The client opens a browser sign-in on first use. + +URL parameters override the profile's saved context for one connection: `stack_api_key`, `branch`, `environment`, `locale`, `launch_project_uid`, `personalize_project_uid`, `brandkit_project_uid`, `automation_project_uid`, and `timeout` (milliseconds; default 60,000, max 120,000). An invalid project UID fails the connection; an invalid stack, branch, environment, locale or timeout falls back to the saved value. Lytics has no URL parameter. + +Things to know: + +- An unpublished or empty profile connects but shows no tools. +- After editing a profile, the URL stays the same, but clients keep the old tool list until they reconnect. +- A profile never grants more than the user's Contentstack roles allow; calls beyond them fail. +- Automations appear as tools only when they have an HTTP trigger and are active; agents only when published with an HTTP trigger. +- Executions shows each call's input, result, timing and errors, and identifies the client, not the person. Use the organization audit log for user-level attribution. +- Tool lists for products the organization's plan doesn't include may not be offered. + +### Least privilege and safety + +- Start read-only. Add write, publish, delete or deploy tools only when the task needs them, and prefer a dedicated profile per job. +- Point agents at a development branch or stack first, and keep production publishing behind a separate profile or explicit confirmation. +- Keep management tokens out of shared configuration files and repositories; prefer OAuth. +- Before an agent deletes content types, global fields, assets, folders or branches, or deploys a release, ask it to state the target and wait for confirmation. + +### Build your own + +The local server's tool definitions are published at `https://mcp.contentstack.com/<group>/tools` (for example `/cda/tools`). Each definition includes a JSON Schema for the inputs and a mapper describing the HTTP request to build. The endpoints hold no credentials; callers bring their own tokens. Region hosts come from `https://artifacts.contentstack.com/regions.json`. + +### Pair MCP with these skills + +MCP gives the agent actions; the other Contentstack skills give it the product knowledge to use them well. For example, `cms-data-modeling-best-practices` before creating content types, `cms-branches-aliases` before merges, and `cms-webhooks` or `cms-releases` before publishing at scale. + +## MCP tools + +This skill is about the MCP server itself. Tools per local server group, as published at `https://mcp.contentstack.com/<group>/tools`: + +| Group | Tools | Covers | +| --- | --- | --- | +| `cma` | 78 | Entries, content types, assets, branches, aliases and merges, releases, taxonomies, environments, variants, publishing | +| `cma-extended` | 22 | Audit logs, version history, global field and asset folder writes, workflow, publish rule and publish queue inspection | +| `cda` | 4 | Published entries and assets | +| `analytics` | 8 | Usage, device, URL, status code, cache and SDK analytics | +| `automations` | 9 | Automation projects, automations, triggering and activating | +| `brandkit` | 12 | Knowledge Vault, Voice Profiles, generative AI | +| `launch` | 27 | Projects, environments, deployments, logs, deploy hooks, cache revalidation | +| `developerhub` | 12 | Apps, OAuth, installations | +| `lytics` | 10 | Audiences, fields, user profiles, content classification | +| `personalize` | 24 | Attributes, audiences, experiences, events, analytics | + +No group covers webhooks, roles, users, tokens, workflows (beyond reading them), publish rules (beyond reading them), labels or extensions; point users to the CMA or the UI for those. Profile Hub draws on the same catalog plus Automate automations and Agent OS agents, but a profile exposes only the tools chosen for it, so use the names the client lists. + +## Output + +Lead with the recommendation, then the exact configuration for the user's client, then the groups, profile or tools to enable, the auth steps, and the verification prompt (for example "List the content types in my stack"). Use placeholders for keys and IDs. + +## Security + +### Defaults + +- Advisory by default. Do not edit the user's MCP configuration files or call Contentstack tools on their behalf unless asked. +- Prefer OAuth over tokens, and Profile Hub for shared access. +- Enable the fewest groups or tools that do the job. +- Keep production writes behind confirmation. + +### Destructive Actions + +Write tools can delete entries, content types, assets, branches and environments, publish to production, and deploy. Before enabling them, explain which tools are destructive and recommend a non-production target. Before the agent runs one, it names the target and waits for confirmation. + +### Secrets + +Never echo management, delivery or Lytics tokens or API keys. A connector URL that contains `apikey` also contains the stack API key, so share such URLs with care. + +### Environment Variables + +Keep tokens in environment variables or the client's secret storage, not in committed config files. + +## References + +- https://www.contentstack.com/docs/developers/contentstack-mcp-server +- https://www.contentstack.com/docs/developers/mcp-profile-hub +- https://www.contentstack.com/docs/developers/mcp-profile-hub-connect-a-client-to-profile-hub +- https://www.contentstack.com/docs/developers/mcp-profile-hub-url-parameters +- https://github.com/contentstack/mcp diff --git a/skills/dx-migrate-js-to-ts-sdk/SKILL.md b/skills/dx-migrate-js-to-ts-sdk/SKILL.md index 13cb8f0..c547475 100644 --- a/skills/dx-migrate-js-to-ts-sdk/SKILL.md +++ b/skills/dx-migrate-js-to-ts-sdk/SKILL.md @@ -1,72 +1,41 @@ --- name: dx-migrate-js-to-ts-sdk -description: "Migrate Contentstack Delivery SDK code from JavaScript to TypeScript. Rewrite setup, initialization, Stack, Entries, Assets, Query, pagination, cache, utils, taxonomy, and image transform usage while preserving behavior and calling out TypeScript-specific differences. Use when a user wants to migrate Contentstack Delivery SDK code from JavaScript to TypeScript." +description: "Migrate Contentstack Delivery SDK code from JavaScript to TypeScript. Rewrite setup, initialization, Stack, Entries, Assets, Query, pagination, cache, utils, taxonomy, and image transform usage while preserving behavior and calling out TypeScript-specific differences. Use when a user wants to migrate Contentstack Delivery SDK code from JavaScript to TypeScript. For new TypeScript SDK code, use dx-delivery-sdk." argument-hint: "[javascript-snippet | file]" allowed-tools: Read Grep Glob context: fork agent: general-purpose --- -# dx-migrate-js-to-ts-sdk +# Migrate JavaScript Delivery SDK code to TypeScript -## Description +Move Delivery SDK code from the legacy JavaScript SDK (`contentstack`) to the TypeScript Delivery SDK (`@contentstack/delivery-sdk`) without losing functionality: API mappings, required packages, TypeScript-specific patterns, and JavaScript behavior with no direct equivalent. -Migrate Contentstack Delivery SDK code from JavaScript to TypeScript. Rewrite setup, initialization, Stack, Entries, Assets, Query, pagination, cache, utils, taxonomy, and image transform usage while preserving behavior and calling out TypeScript-specific differences. +## When to use -## When to Use +- Migrating Delivery SDK code from JavaScript to TypeScript. +- Comparing JavaScript and TypeScript SDK APIs for Stack, Entries, Assets, Query, pagination, cache, utils, taxonomy or image transforms. +- A migration-ready rewrite, a side-by-side comparison, or a concise list of breaking changes and limitations. -Use when a user wants to migrate Contentstack Delivery SDK code from JavaScript to TypeScript. -Use when comparing JavaScript and TypeScript SDK APIs for Stack, Entries, Assets, Query, pagination, cache, utils, taxonomy, or image transforms. -Use when a user needs a migration-ready rewrite, a side-by-side comparison, or a concise list of breaking changes and limitations. +Use another skill for: -## User Problem +- Writing new TypeScript SDK code that isn't a migration → `dx-delivery-sdk` +- Rendering JSON RTE content with `@contentstack/utils` beyond the API change → `cms-json-rte` +- Migrating content from Contentful → `migration-companion` -The user needs to move existing Contentstack Delivery SDK code from JavaScript to TypeScript without losing functionality. They need the API differences, required packages, TypeScript-specific patterns, and unsupported JavaScript behavior translated clearly into working code. +## Before you answer -## Success Criteria +Get the existing JavaScript SDK code or snippets, the target runtime (Node.js or React Native), the SDK areas involved (Stack, Entries, Assets, Query, caching, utils, taxonomy, image transforms), and whether the user wants an explanation, migration steps, or rewritten code. -Identifies the correct JavaScript-to-TypeScript SDK equivalents. -Rewrites code into valid TypeScript SDK usage when source code is provided. -Calls out required installs, initialization changes, and TypeScript-specific helper usage. -Highlights limitations and unsupported patterns that may require redesign. -Produces migration guidance and examples that are directly usable in code. - -## Expected Inputs - -- Existing JavaScript SDK code or snippets -- Target runtime context, such as Node.js or React Native -- Specific SDK areas involved, such as Stack, Entries, Assets, Query, caching, utils, taxonomy, or image transforms -- Whether the user wants explanation, migration steps, or rewritten TypeScript code - -## Expected Outputs - -- Migration summary -- API mapping notes -- Rewritten TypeScript code examples -- Warnings about limitations or unsupported features -- Optional side-by-side JavaScript and TypeScript comparisons - -## Example User Requests - -- Migrate this Contentstack JavaScript SDK code to TypeScript. -- Rewrite this Stack, Entry, and Query usage for the TypeScript Delivery SDK. -- Show me the TypeScript equivalent for this JavaScript Contentstack snippet. -- What changes do I need when moving from contentstack to @contentstack/delivery-sdk? -- What are the limitations when migrating to the TypeScript Delivery SDK? -- How do I rewrite cache policy usage for the TypeScript Delivery SDK? -- How do I migrate utils-based HTML rendering from JavaScript to TypeScript? -- Convert this taxonomy query from the JavaScript SDK to TypeScript. - -## Workflow Summary +## Instructions -Identify the SDK area and current JavaScript pattern. -Map the JavaScript API to the TypeScript equivalent. -Adjust package installs, imports, and initialization. -Rewrite code with TypeScript types and SDK methods. -Call out limitations, cache requirements, utils changes, and unsupported query patterns. -Provide a concise migration summary and examples. +1. Identify the SDK area and current JavaScript pattern. +2. Map the JavaScript API to the TypeScript equivalent. +3. Adjust package installs, imports, and initialization. +4. Rewrite code with TypeScript types and SDK methods. +5. Call out limitations, cache requirements, utils changes, and unsupported query patterns that may require redesign. +6. Provide a concise migration summary and examples. -## Instructions ### Scope @@ -78,45 +47,47 @@ Prefer TypeScript SDK equivalents from @contentstack/delivery-sdk. Preserve beha ### Key mappings -Use contentstack.stack(...) instead of contentstack.Stack(...). Use stack.contentType(...), entry(...), asset(...), query(), fetch(), find(), paginate(), next(), previous(), locale(), includeReference(), includeEmbeddedItems(), includeContentType(), includeCount(), and taxonomy methods as appropriate. Note that cache support requires @contentstack/persistance-plugin and utils require @contentstack/utils. +Use contentstack.stack(...) instead of contentstack.Stack(...). Use stack.contentType(...), entry(...), asset(...), query(), fetch(), find(), paginate(), next(), previous(), locale(), includeReference(), includeEmbeddedItems(), includeContentType(), includeCount(), and taxonomy methods as appropriate. Note that cache support requires @contentstack/persistence-plugin and utils require @contentstack/utils. ### Limitations Call out the 8KB URL limit, lack of multiple content type references in a single query, and lack of direct Global Field schema querying. Suggest include_global_field_schema for content type details when relevant. Mention assetFields support is region-limited to NA. -### Examples +### Migration examples Include at least one migration example when the user asks for help. Prefer side-by-side snippets or before/after code when useful. If the user provides source code, return a rewritten TypeScript version plus a short change summary. Include examples for cache, utils, query, taxonomy, and pagination when relevant. -## Output Format +### Tooling + +- No tools are required by default. If code is provided, focus on transformation and explanation only. +- If caching is involved, mention the persistence plugin requirement. +- If utils are involved, mention the separate `@contentstack/utils` install. +- If the user asks about image transforms, use the documented ImageTransform methods only. -Be concise and migration-oriented. -Use code blocks for code examples. -When helpful, show JavaScript and TypeScript side by side. -Explicitly call out unsupported or changed behavior. -When source code is provided, include a rewritten TypeScript version. -Do not invent SDK methods not present in the reference docs. +## MCP tools -## Tooling Notes +The migration is a code transformation, so it needs no MCP tools. When the Contentstack MCP server is connected, the read tools `get_all_entries_cdn` and `get_a_single_entry_cdn` (`cda`) can confirm that a rewritten query returns the same entries as before, and `get_a_single_content_type` (`cma`) shows the field UIDs the code relies on. -No tools are required by default. -If code is provided, focus on transformation and explanation only. -If caching is involved, mention the persistence plugin requirement. -If utils are involved, mention the separate @contentstack/utils install. -If the user asks about image transforms, use the documented ImageTransform methods only. +## Output + +- Be concise and migration-oriented: a migration summary, API mapping notes, rewritten TypeScript code, warnings about limitations, and optional side-by-side JavaScript and TypeScript comparisons. +- Use code blocks for code examples. +- Explicitly call out unsupported or changed behavior. +- When source code is provided, include a rewritten TypeScript version. +- Do not invent SDK methods not present in the reference docs. ## Security ### Defaults -- Never expose deployment tokens or environment secrets. -- Validate deployment targets before triggering. -- Require confirmation for production deployments. -- Never auto-deploy to production without review. +- Advisory by default. Use the MCP tools above only to read the stack. +- Delivery tokens are read-only and safe for client-side code; management tokens never belong in SDK code. +- Preview tokens read unpublished content: use them only in preview deployments. +- Never hardcode credentials in migrated code; read them from environment variables. ### Destructive Actions -Do not perform destructive actions. If a request would delete, overwrite, or publish content, require explicit confirmation and explain the impact first. +Do not perform destructive actions. If a request would delete, overwrite, or publish content, name the target, explain the impact and wait for explicit confirmation. ### Secrets @@ -126,22 +97,13 @@ Never request, reveal, or echo delivery tokens, API keys, or other secrets. If c When environment variables are needed, use placeholder names and explain expected values. Never print real values or infer secrets from context. -## Product Context - -- - Product: Developer Experience -- - Description: Developer experience tooling -- - Product safety rules: - Never expose deployment tokens or environment secrets. -- Validate deployment targets before triggering. -- Require confirmation for production deployments. -- Never auto-deploy to production without review. - ## Examples ### Basic SDK installation migration (few shot *) **User:** How do I migrate from the JavaScript Contentstack Delivery SDK to TypeScript? -**Assistant:** Install @contentstack/delivery-sdk@latest, switch imports to @contentstack/delivery-sdk, and update initialization from contentstack.Stack(...) to contentstack.stack(...). If you need caching, install @contentstack/persistance-plugin@latest. If you need utils, install @contentstack/utils@latest. +**Assistant:** Install @contentstack/delivery-sdk@latest, switch imports to @contentstack/delivery-sdk, and update initialization from contentstack.Stack(...) to contentstack.stack(...). If you need caching, install @contentstack/persistence-plugin@latest. If you need utils, install @contentstack/utils@latest. *Shows the core package and import changes.* @@ -173,7 +135,7 @@ When environment variables are needed, use placeholder names and explain expecte **User:** Do I need anything extra for cache support in the TypeScript SDK? -**Assistant:** Yes. Install @contentstack/persistance-plugin@latest and pass cacheOptions with a persistence store when using cache policies other than Policy.IGNORE_CACHE. +**Assistant:** Yes. Install @contentstack/persistence-plugin@latest and pass a `PersistenceStore` as `cacheOptions.persistenceStore` when using cache policies other than Policy.IGNORE_CACHE. *Captures the cache plugin requirement.* diff --git a/skills/launch-platform/SKILL.md b/skills/launch-platform/SKILL.md new file mode 100644 index 0000000..6f8f9fd --- /dev/null +++ b/skills/launch-platform/SKILL.md @@ -0,0 +1,125 @@ +--- +name: launch-platform +description: "Advise developers on hosting and configuring sites on Contentstack Launch: framework support and build settings, Cloud Functions, Edge Functions, launch.json redirects, rewrites and cache priming, custom domains, CDN caching and cache revalidation, deploy hooks and CMS-triggered redeploys, environment variables, password protection, IP access control, log targets, monorepos, platform limits, and the Launch CLI and API. Use when developers ask how to deploy or configure a site on Launch, why a build or function fails, how caching or routing works on Launch, or what Launch supports. For syncing environment variables from .env.example, use launch-sync-environment-variables-from-env-example. For scripted deploy-and-monitor runs, use launch-trigger-and-monitor-launch-deployments." +allowed-tools: Read Grep Glob +--- + +# Launch Platform + +Advise on hosting front-end sites on Contentstack Launch: frameworks and builds, Cloud and Edge Functions, `launch.json` routing, domains, CDN caching and revalidation, deploy triggers, environment variables, access control, logging, monorepos and limits. + +## When to use + +- Deploying or configuring a site on Launch, or checking whether Launch supports a framework or feature. +- A build, function or deployment that fails. +- Choosing between static, SSR, Cloud Functions and Edge Functions. +- Routing, redirects, caching and revalidation, deploy hooks, domains, password protection, IP filtering, log targets and monorepos. + +Use another skill for: + +- Syncing a Launch environment's variables from `.env.example` → `launch-sync-environment-variables-from-env-example` +- A scripted deploy-and-monitor run → `launch-trigger-and-monitor-launch-deployments` +- CMS webhooks that call a deploy hook or a revalidation receiver → `cms-webhooks` +- Delivery SDK code inside the app → `dx-delivery-sdk` +- The `csdx` CLI in general → `dx-cli` + +## Before you answer + +Find out the framework, rendering mode and package manager, whether the repo is a single app or a monorepo, any routing, caching or redirect requirements, how often content is published, and any error messages, build logs or deployment status. + +## Instructions + +1. Identify the framework, rendering mode and repo layout. +2. Check compatibility and limits first, and call out unsupported features before the user hits them. +3. Pick the compute layer (static, SSR, Cloud Function or Edge Function) and the routing approach. +4. Design caching and how content changes reach the site; prefer cache revalidation over full rebuilds for frequent content changes. +5. Cover domains, access control and logging when relevant. + + +### How a request is served + +Requests hit the CDN edge first. Redirects in `launch.json` run, then rewrites, then the Edge Function, then the CDN cache lookup, then the origin (static files or the server-side runtime). A matching `launch.json` rule means the Edge Function does not run for that request. + +### Frameworks and builds + +Launch builds from GitHub or Bitbucket, or from an uploaded file. Framework presets include Next.js, Nuxt, Gatsby, Remix, Astro, Eleventy, SvelteKit, Qwik City, Jekyll, Angular, Analog and static HTML, plus custom or other for anything else. Vue is client-side only on Launch; use Nuxt for SSR. Configure the build command, output directory and, for SSR, the server command per environment. Supported runtime Node versions are 20.x, 22.x and 24.x. Use npm, yarn (node-modules mode) or bun; pnpm and Yarn Plug'n'Play are not supported. Builds time out after 60 minutes. Larger build machines are available by plan. + +### Cloud Functions + +Use Cloud Functions for Node.js API routes next to the front end. Put files in `/functions` at the project root; file names become routes, and `[param]` creates dynamic segments. The handler is `export default function handler(request, response)`. Defaults: 1,024 MB memory and a 30-second timeout, which Support can raise per organization. The file system is read-only except `/tmp`. Expect cold starts after inactivity. Cloud Functions cannot run from a monorepo sub-app. + +### Edge Functions + +Use Edge Functions for per-request logic at the CDN: redirects with logic, header changes, geo or A/B routing, auth gates and IP filtering. The entry point is `functions/[proxy].edge.js`. Code must be WinterCG-compatible: no Node APIs such as `fs`, `path` or `node:crypto`, or the deployment fails. Forward to the origin with `context.fetch()`. Limits: 128 MB memory, 1 MiB minified bundle and a 400 ms startup budget for global scope. Visitor geo headers are available on the request. + +### launch.json + +Place `launch.json` at the repository root (not in a monorepo sub-app). It holds `redirects`, `rewrites` and `cache.cachePriming.urls`. Rules run top to bottom, redirects before rewrites, and can add request or response headers. Sources are relative paths; escape `?` as `\?`; hash fragments are not supported. Keep the file under about 200 KB and move complex logic into an Edge Function. See [references/launch-json.md](references/launch-json.md). + +### Caching and revalidation + +Launch purges the whole CDN cache when a new deployment goes live. Set `Cache-Control` headers to control caching. To refresh content without rebuilding, call `POST /projects/{project_uid}/environments/{environment_uid}/revalidate-cdn-cache` with an exact path, a path prefix (`isPrefix: true`), cache tags (set `Cache-Tag` response headers first) or hostnames. Revalidation has a daily quota that depends on the plan; tell users to check theirs. Cache priming warms up to 50 URLs after each deployment. Next.js App Router ISR and `revalidateTag()` are not supported; use the revalidation API instead. + +### Deploy triggers + +A deploy hook is a per-environment URL that starts a deployment on POST; treat the URL as a secret. To redeploy on CMS publish, point a CMS webhook at the deploy hook. Every publish then triggers a full build, so for frequent publishing prefer revalidating the cache from a webhook receiver. `autoDeployOnPush` controls deploys on Git pushes. Rollback to an earlier deployment is available in the UI and API. + +### Environment variables + +Variables apply at build time and runtime. Limits: 128 per environment, and a total size cap that is much smaller when Cloud Functions are enabled. Use framework prefixes (such as `NEXT_PUBLIC_`) only for values safe in the browser. + +### Domains and access control + +Use a CNAME for subdomains and an A record for apex domains. TLS is automatic, and custom certificates can be uploaded. Custom domains depend on the plan, and the environment needs a live deployment first. Password protection (HTTP Basic Auth) is set per environment and applies without a redeploy. There is no built-in IP allowlist; filter on the visitor IP header in an Edge Function. + +### Logs + +Log targets forward server logs (SSR, Cloud and Edge Functions) and CDN traffic logs over OpenTelemetry (OTLP) to Datadog, CloudWatch, Splunk or any OTLP endpoint. One target per log type per environment. Batches that fail to send are not retried. + +### Monorepos + +Scope the build command and output directory to the app, for example with Turborepo filters or Nx project builds. Gatsby, Cloud Functions and `launch.json` are not supported from a sub-app. + +### API and CLI + +The Launch API uses regional hosts (`launch-api.contentstack.com`, `eu-launch-api.contentstack.com`, `au-launch-api.contentstack.com`, `azure-na-launch-api.contentstack.com`, `azure-eu-launch-api.contentstack.com`, `gcp-na-launch-api.contentstack.com`, `gcp-eu-launch-api.contentstack.com`). Authenticate with an OAuth bearer token (recommended) or `authtoken` plus `organization_uid`. The API is rate limited per organization. The `csdx launch` CLI commands cover deployments, logs, environment variables and local Cloud Function testing. Full limits are in [references/limits.md](references/limits.md). + +## MCP tools + +When the Contentstack MCP server is connected with the `launch` group, these tools act on Launch: + +- Inspect: `get_all_projects`, `get_a_project`, `get_all_environments`, `get_an_environment`, `get_environments`, `get_all_deployments`, `get_deployment`, `get_latest_live_deployment`, `get_deployment_logs`, `get_server_logs`, `get_deploy_hooks`, `get_signed_download_url_deployment` (`launch`) +- Change: `create_a_project`, `update_a_project`, `create_an_environment`, `update_an_environment`, `create_deployment`, `revalidate_cdn_cache`, `create_deploy_hook`, `update_deploy_hook`, `trigger_deploy_hook`, `get_signed_upload_url_project`, `get_signed_upload_url_environment`, `get_signed_upload_url_deployment` (`launch`) +- Delete: `delete_a_project`, `delete_an_environment`, `delete_deploy_hook` (`launch`) + +Use the read tools to check a real project's build settings, deployment status and logs before diagnosing. `update_an_environment` replaces build settings, variables and password protection together, so read the environment first and send the full object. There are no tools for domains, log targets or rollback; use the Launch UI or API. + +## Output + +Be concise and practical. Lead with the recommended approach, then config or code. Call out unsupported features and limits up front. Use placeholders for tokens and hostnames. + +## Security + +### Defaults + +- Advisory by default. Use the MCP tools above only when the user asks for the change. +- Never expose authtokens, OAuth tokens, deploy hook URLs or environment secrets. +- Only use public framework prefixes for values that are safe in the browser. +- Use environment variables for credentials in examples. + +### Destructive Actions + +Before deleting projects, environments, deploy hooks or domains, deploying, rolling back, updating an environment or revalidating the cache, name the project, environment and effect (for example, a production deployment or a full cache purge) and wait for confirmation. Treat production environments as needing explicit confirmation every time. + +### Secrets + +Never print or echo secret values, deploy hook URLs or password-protection credentials. Use placeholders. + +### Environment Variables + +Use placeholder names such as `CONTENTSTACK_LAUNCH_OAUTH_TOKEN`. Keep secrets in Launch environment variables, never in the repo or `launch.json`. + +## References + +- [launch.json reference](references/launch-json.md): redirect, rewrite and cache-priming syntax with examples. +- [Launch limits](references/limits.md): build, function, routing, variable and delivery limits. diff --git a/skills/launch-platform/references/launch-json.md b/skills/launch-platform/references/launch-json.md new file mode 100644 index 0000000..575b9d9 --- /dev/null +++ b/skills/launch-platform/references/launch-json.md @@ -0,0 +1,82 @@ +# launch.json reference + +`launch.json` sits at the repository root. Launch evaluates it at the edge before Edge Functions and the cache. + +Order of evaluation: + +1. `redirects`, top to bottom +2. `rewrites`, top to bottom +3. Edge Function (`functions/[proxy].edge.js`), if no rule matched +4. CDN cache lookup +5. Origin + +## Redirects + +Fields: `source`, `destination`, `statusCode` and optional `response.headers`. There is no `permanent` flag; use the status code. `301` and `308` are permanent, `302` and `307` temporary (`307` and `308` keep the HTTP method). The default is `308`. + +```json +{ + "redirects": [ + { + "source": "/old-blog/:slug", + "destination": "/blog/:slug", + "statusCode": 301, + "response": { + "headers": { "x-redirected-by": "launch" } + } + } + ] +} +``` + +## Rewrites + +Fields: `source`, `destination`, optional `request.headers` (added before the request reaches the destination) and `response.headers`. The browser URL does not change. + +```json +{ + "rewrites": [ + { + "source": "/api/partner/:path(.*)", + "destination": "https://api.partner.example.com/:path", + "response": { + "headers": { "cache-control": "no-store" } + } + } + ] +} +``` + +Don't put secrets in `request.headers`: `launch.json` is committed to the repository. Inject secrets from an Edge Function that reads environment variables instead. + +## Patterns + +Sources use `path-to-regexp` syntax: + +- Named parameter: `/blog/:slug` → `/posts/:slug` +- Constrained parameter: `/category/:name(shoes|bags)` +- Catch-all: `/old/:path(.*)`. There is no bare `*` wildcard. +- Exclusions: `/:path((?!blog|docs).+)` + +Rules run top to bottom, so put catch-all rules last. + +## Cache priming + +```json +{ + "cache": { + "cachePriming": { + "urls": ["/", "/blog", "/pricing"] + } + } +} +``` + +Launch requests these paths through the CDN after a deployment goes live. Up to 50 literal relative paths; no wildcards. + +## Rules of thumb + +- Sources are relative paths only, with no hostname. +- Escape a literal `?` as `\?`. Hash fragments (`#`) are not supported. +- Keep the file small (under about 200 KB). Move complex or data-driven routing into an Edge Function. +- Not supported in a monorepo sub-app; the file must be at the repository root. diff --git a/skills/launch-platform/references/limits.md b/skills/launch-platform/references/limits.md new file mode 100644 index 0000000..bff4f98 --- /dev/null +++ b/skills/launch-platform/references/limits.md @@ -0,0 +1,31 @@ +# Launch limits + +Defaults can change and some depend on the plan. Check the current Launch documentation before relying on a number. + +| Area | Limit | +| --- | --- | +| Build duration | 60 minutes, then the build fails | +| Node.js runtime | 20.x, 22.x, 24.x | +| Package managers | npm, yarn (node-modules mode), bun. Not pnpm or Yarn Plug'n'Play | +| Cloud Function memory | 1,024 MB | +| Cloud Function / SSR timeout | 30 seconds by default; can be raised per organization through Support | +| Writable file system | `/tmp` only, ephemeral | +| Edge Function memory | 128 MB | +| Edge Function bundle | 1 MiB minified | +| Edge Function startup | 400 ms for global scope | +| Request / response body | 5 MB | +| Environment variables | 128 per environment; total size is capped, and the cap is lower with Cloud Functions enabled | +| `launch.json` | Keep under about 200 KB; source and destination URLs up to 512 characters | +| Cache priming | 50 URLs per deployment | +| Cache revalidation | Daily quota depends on the plan | +| Custom domains | Plan-gated; needs a live deployment | +| Launch API | Rate limited per organization | + +## Not supported + +- Docker containers +- pnpm and Yarn Plug'n'Play +- Vue.js SSR (use Nuxt) +- Next.js App Router ISR and `revalidateTag()` (use the revalidation API) +- Cloud Functions, Gatsby and `launch.json` in a monorepo sub-app +- A built-in IP allowlist (use an Edge Function) diff --git a/skills/launch-sync-environment-variables-from-env-example/SKILL.md b/skills/launch-sync-environment-variables-from-env-example/SKILL.md index e078bcb..52edda2 100644 --- a/skills/launch-sync-environment-variables-from-env-example/SKILL.md +++ b/skills/launch-sync-environment-variables-from-env-example/SKILL.md @@ -1,6 +1,6 @@ --- name: launch-sync-environment-variables-from-env-example -description: "Fetch environment variables from a Contentstack Launch project, compare them with a local `.env.example` file, and patch the target environment to add any missing keys. Use when a Launch environment must match the keys defined in a local `.env.example` file." +description: "Fetch environment variables from a Contentstack Launch project, compare them with a local `.env.example` file, and update the target environment to add any missing keys. Use when a Launch environment must match the keys defined in a local `.env.example` file." argument-hint: "[project_uid] [environment_uid] [env_example_path]" disable-model-invocation: true allowed-tools: Read Grep Glob Bash @@ -10,109 +10,97 @@ agent: general-purpose # Sync Launch environment variables from .env.example -## Description +Fetch a Contentstack Launch environment's variables, compare them with a local `.env.example` file, and add any missing keys without changing any other environment setting. -Fetch environment variables from a Contentstack Launch project, compare them with a local `.env.example` file, and patch the target environment to add any missing keys. +## When to use -## When to Use +- A Launch environment must match the keys defined in a local `.env.example` file. +- Auditing missing environment variables before a deployment. +- Adding missing variable names to a Launch environment without editing the config by hand. -Use when a Launch environment must match the keys defined in a local `.env.example` file. -Use to audit missing environment variables before a deployment. -Use to add missing variable names to a Launch environment without manually editing config. +Use another skill for: -## User Problem +- General Launch hosting, build settings and environment variable limits → `launch-platform` +- Triggering a deployment and watching it → `launch-trigger-and-monitor-launch-deployments` +- Setting up the Contentstack MCP server → `dx-mcp-server` -A Launch environment can drift from the variables expected by a frontend project. This skill checks the environment against `.env.example` and updates Launch when keys are missing. +## Before you answer -## Success Criteria +Collect the Launch project UID, environment UID, the path to `.env.example`, the region, and how the user authenticates (OAuth token or authtoken plus organization UID). Ask whether to run as a dry run; default to one. -All keys present in `.env.example` exist in the target Launch environment. -Missing keys are added through the Launch Projects/Environments API. -The script reports what was missing and what was updated. -No secrets are printed in logs. +## Instructions -## Expected Inputs +1. Read and parse the local `.env.example` file. +2. Fetch the target Launch environment and its current variables. +3. Compare keys from the file against keys in Launch. +4. Append only the missing keys to the environment's `environmentVariables` array. +5. Send the full environment back with `PUT /projects/{project_uid}/environments/{environment_uid}`. +6. Report the changes and stop without exposing values. -- Launch project UID -- Launch environment UID -- Path to local `.env.example` file -- Launch API credentials or authenticated context -- Optional: dry-run flag -## Expected Outputs +### Parse input -- List of keys found in `.env.example` -- List of keys already present in Launch -- List of missing keys that were patched -- Summary of API calls made -- Exit code indicating success or failure +Read the local .env.example file and extract variable names. Ignore comments, blank lines, and malformed entries. -## Example User Requests +### Fetch Launch state -- Generate a Node.js script that syncs Launch environment variables from `.env.example`. -- Compare my Launch environment variables with `.env.example` and add any missing keys. -- Write a script to patch missing Launch environment variables for project abc123 and environment dev456. +Call `GET /projects/{project_uid}/environments/{environment_uid}` and keep the whole `environment` object. Its `environmentVariables` array holds `{ "key", "value" }` pairs. -## Workflow Summary +Use the regional base URL: `launch-api.contentstack.com` (AWS NA), `eu-launch-api.contentstack.com`, `au-launch-api.contentstack.com`, `azure-na-launch-api.contentstack.com`, `azure-eu-launch-api.contentstack.com`, `gcp-na-launch-api.contentstack.com` or `gcp-eu-launch-api.contentstack.com`. -Read and parse the local `.env.example` file. -Fetch the target Launch environment and its current variables. -Compare keys from the file against keys in Launch. -Build a minimal patch payload containing only missing keys. -Call PATCH /projects/{project_uid}/environments/{environment_uid}. -Report the changes and stop without exposing values. +Authenticate with `Authorization: Bearer <OAuth access token>` (recommended; scope `launch.projects:write` or `launch:manage`), or with the `authtoken` and `organization_uid` headers. `x-cs-api-version` is optional and defaults to `1.0`. Read credentials from environment variables. -## Instructions +### Compare keys -### Parse input +Compare .env.example keys against Launch keys. Identify only missing keys unless the user explicitly asks to update existing values. -Read the local .env.example file and extract variable names. Ignore comments, blank lines, and malformed entries. +### Update environment -### Fetch Launch state +There is no PATCH endpoint. Use `PUT /projects/{project_uid}/environments/{environment_uid}` and treat it as a full replacement: -Use the Launch API to get the target project and environment, then read the current environment variables. +1. Start from the environment object you fetched. +2. Append the missing keys to `environmentVariables` with empty or placeholder values. Leave existing entries untouched. +3. Send the whole object back, keeping every other field (`name`, `gitBranch`, `frameworkPreset`, `buildCommand`, `outputDirectory`, `autoDeployOnPush`, `passwordProtection` and the rest) exactly as fetched. -### Compare keys +Never send a body with only `environmentVariables`: settings you leave out can be reset. If the GET response masks existing values, stop and tell the user, because sending masked values back would overwrite the real ones. An environment holds at most 128 variables. -Compare .env.example keys against Launch keys. Identify only missing keys unless the user explicitly asks to update existing values. +### Report results -### Patch environment +Return a concise summary of missing keys, added keys, and any errors. Never print secret values or tokens. -Call PATCH /projects/{project_uid}/environments/{environment_uid} with the minimal update needed to add missing keys. +### API behaviour -### Report results +Validate the project and environment UIDs before updating. Change only the `environmentVariables` array, and only by adding missing keys. If the API returns validation errors, surface them without retrying blindly. + +## MCP tools -Return a concise summary of missing keys, patched keys, and any errors. Never print secret values or tokens. +If the Contentstack MCP server is connected with the `launch` group, use its tools instead of writing a script or calling the API with curl: -## Output Format +- Inspect: `get_all_projects`, `get_all_environments`, `get_an_environment` (`launch`) +- Change: `update_an_environment` (`launch`), which takes `project_uid`, `environment_uid` and the full `environment_data` object -Use concise, machine-readable summaries. -Do not print secret values from Launch or local files. -Show missing keys and updated keys separately. -Include the project UID and environment UID in the summary. -If running in dry-run mode, clearly label that no changes were made. +The same rules apply: fetch with `get_an_environment`, append only missing keys, send the whole object back, and stop if values come back masked. Confirm before calling `update_an_environment`, as below. -## Tooling Notes +## Output -Use the Launch API and Projects/Environments endpoints. -Validate project and environment UIDs before patching. -Avoid logging deployment tokens or environment secrets. -Use the smallest possible PATCH payload. -If the API returns validation errors, surface them without retrying blindly. +- Use concise, machine-readable summaries, with the project UID and environment UID. +- Show the keys found in `.env.example`, the keys already in Launch, and the missing keys added, separately, plus a summary of API calls and the exit status. +- Do not print secret values from Launch or local files. +- In dry-run mode, clearly label that no changes were made. ## Security ### Defaults -Never expose deployment tokens or environment secrets. -Validate project and environment targets before patching. -Do not print variable values in logs or output. -Require explicit user intent before making any change. -Prefer dry-run behavior unless the user asks to apply changes. +- Never expose deployment tokens or environment secrets. +- Validate project and environment targets before updating. +- Do not print variable values in logs or output. +- Require explicit user intent before making any change, and prefer dry-run behavior unless the user asks to apply changes. +- Require confirmation for production environments, and never auto-deploy to production without review. ### Destructive Actions -Treat any PATCH that changes Launch configuration as a destructive external action. Confirm the target project and environment before applying changes. Do not broaden the update beyond missing keys unless the user explicitly requests it. +Treat any PUT that changes Launch configuration, through the API or `update_an_environment`, as a destructive external action. Name the target project and environment and the keys to add, and wait for confirmation before applying changes. Do not broaden the update beyond missing keys unless the user explicitly requests it. ### Secrets @@ -121,14 +109,3 @@ Never reveal secret values from .env.example or Launch. Log only key names and c ### Environment Variables Read environment variables only as needed for authentication and target selection. Never echo env var values. Support loading from local `.env.example` for comparison only, not for secret disclosure. - -## Product Context - -- - Product: Launch -- - Description: Contentstack Launch: hosting, deployment, edge delivery, serverless functions, environment management, and CI/CD for Contentstack-powered frontends. -- - Product safety rules: - Never expose deployment tokens or environment secrets. -- Validate deployment targets before triggering. -- Require confirmation for production deployments. -- Never auto-deploy to production without review. -- - Default tools: ["Launch API", "Projects", "Environments", "Deployments", "Serverless Functions", "Edge Delivery", "Custom Domains"] -- - Default connectors: ["Launch Proxy", "GitHub", "CI/CD Pipelines"] \ No newline at end of file diff --git a/skills/launch-trigger-and-monitor-launch-deployments/SKILL.md b/skills/launch-trigger-and-monitor-launch-deployments/SKILL.md index 781d940..520c881 100644 --- a/skills/launch-trigger-and-monitor-launch-deployments/SKILL.md +++ b/skills/launch-trigger-and-monitor-launch-deployments/SKILL.md @@ -10,95 +10,89 @@ agent: general-purpose # Trigger and Monitor Launch Deployments -## Description +Trigger a Launch deployment for a specific environment, poll its status until it reaches a terminal state, and on failure fetch the deployment log and diagnose the likely cause. -Trigger a Launch deployment for a specific environment, then poll its status until completion. If the deployment fails or is cancelled, fetch the deployment log and diagnose likely causes with next-step recommendations. +## When to use -## When to Use +- Automating a Launch deployment for a known project and environment, in CI/CD or operator workflows. +- Watching a deployment until it finishes, with a deterministic exit status. +- Diagnosing a failed or cancelled deployment from its log. -Use when you need to automate Launch deployments for a known project and environment, monitor progress, and surface failure diagnostics. This is appropriate for CI/CD or operator workflows that need a deterministic deploy status check and log-based troubleshooting. +Use another skill for: -## User Problem +- Build settings, frameworks, functions, caching and other Launch configuration → `launch-platform` +- Matching a Launch environment's variables to `.env.example` → `launch-sync-environment-variables-from-env-example` +- Redeploying on CMS publish with webhooks → `cms-webhooks` -Users need a reliable way to start a deployment in Launch, track it to completion, and quickly understand why it failed if it does. The skill should reduce manual polling and make deployment failures actionable. +## Before you answer -## Success Criteria +Collect the `project_uid` and `environment_uid`, any deployment payload or build reference (commit hash or upload UID), the region and auth method, and any context about the app, branch or build source. Get explicit confirmation of the project and environment before every deployment, production or not. -Deployment is triggered against the intended project and environment. -Status is polled at a fixed interval until a terminal state is reached. -The process exits non-zero on failed or cancelled deployments. -If the deployment fails, the deployment log is retrieved and summarized with likely causes and next steps. -Every deployment requires explicit confirmation of the project and environment before it is triggered; production targets are additionally validated. - -## Expected Inputs - -- project_uid -- environment_uid -- Optional deployment payload or build reference -- Explicit confirmation before every deployment (production or not) -- Any available context about the app, branch, or build source - -## Expected Outputs - -- Deployment UID and initial trigger response -- Current status updates during polling -- Terminal status and exit code -- Deployment log summary when failures occur -- Recommended next steps and probable root cause +## Instructions -## Example User Requests +1. Validate the project and environment identifiers. +2. Confirm the target is allowed, then get explicit approval for this deployment before triggering it, whatever the environment. Validate production targets again. +3. Trigger the deployment with the Launch API. +4. Poll deployment status every 10 seconds until it reaches a terminal state. +5. If status is FAILED, CANCELLED or SKIPPED, fetch the deployment logs and analyze them. +6. Return a concise summary with status, log findings, and recommended next steps. -- Trigger a deployment for project abc123 in environment prod456 and watch it until it finishes. -- Deploy the latest build to staging and tell me if it fails. -- If the deployment fails, check the logs and explain what went wrong. -- Poll a Launch deployment every 10 seconds and exit non-zero on failure. -## Workflow Summary +### API basics -Validate the project and environment identifiers. -Confirm the target is allowed, then get explicit approval for this deployment before triggering it, whatever the environment. -Trigger the deployment with the Launch API. -Poll deployment status every 10 seconds until it reaches a terminal state. -If status is failed or cancelled, fetch the deployment log and analyze it. -Return a concise summary with status, log findings, and recommended next steps. +Use the regional base URL: `launch-api.contentstack.com` (AWS NA), `eu-launch-api.contentstack.com`, `au-launch-api.contentstack.com`, `azure-na-launch-api.contentstack.com`, `azure-eu-launch-api.contentstack.com`, `gcp-na-launch-api.contentstack.com` or `gcp-eu-launch-api.contentstack.com`. -## Instructions +Authenticate with `Authorization: Bearer <OAuth access token>` (recommended; scope `launch.projects:write` or `launch:manage`), or with the `authtoken` and `organization_uid` headers. `x-cs-api-version` is optional and defaults to `1.0`. Read credentials from environment variables. ### Trigger Deployment -Call POST /projects/{project_uid}/environments/{environment_uid}/deployments with the required payload. Validate the target environment before sending the request. +Call `POST /projects/{project_uid}/environments/{environment_uid}/deployments`. For a Git-connected project, pass `{ "commitHash": "<sha>" }` to deploy a specific commit. For a file-upload project, pass `{ "uploadUid": "<uid>" }`, or an empty body to redeploy the most recent upload. Validate the target environment before sending the request. ### Poll Status -Check deployment status every 10 seconds. Continue until the deployment reaches a terminal state or a failure condition is detected. +Call `GET /projects/{project_uid}/environments/{environment_uid}/deployments/{deployment_uid}` every 10 seconds and read `deployment.status`. This endpoint returns metadata only, not logs. + +| Status | Meaning | +|---|---| +| `QUEUED`, `DEPLOYING` | In progress. Keep polling. | +| `DEPLOYED` | Built but not yet live. Keep polling. | +| `LIVE` | Success. The deployment is serving traffic. | +| `ARCHIVED` | Replaced by a newer deployment. | +| `SKIPPED` | Superseded in the queue by a newer deployment. | +| `FAILED` | Build error, or the 60-minute build timeout. | +| `CANCELLED` | Cancelled by a user or by quota. | ### Fail Fast -Exit with a non-zero code if status becomes failed or cancelled. Do not continue polling after a terminal failure state. +Exit 0 on `LIVE`. Exit non-zero on `FAILED`, `CANCELLED` or `SKIPPED`, and stop polling. ### Fetch Logs on Failure -If the deployment fails, call GET /projects/{project_uid}/environments/{environment_uid}/deployments/{deployment_uid} and inspect the deployment log details. +Call `GET /projects/{project_uid}/environments/{environment_uid}/deployments/{deployment_uid}/logs/deployment-logs` for the build log. Each entry has `message`, `timestamp` and `stage`. For runtime errors after a deployment went live, use `.../logs/server-logs`. ### Diagnose and Recommend Summarize the most likely cause from the log, note any missing prerequisites or configuration issues, and recommend the next corrective action. -## Output Format +### Rollback + +To return to an earlier deployment, `POST /projects/{project_uid}/environments/{environment_uid}/deployments/rollback` with `{ "deployment": "<deployment_uid>" }`. The target must have `isRollbackEligible: true`. Rollback is a production change: get explicit confirmation first. -Return a concise deployment summary first. -Include deployment UID, environment UID, and final status. -On failure, include a short log-based diagnosis and next steps. -Do not expose deployment tokens, secrets, or environment variables. -Use a non-zero exit code for failed or cancelled deployments. +## MCP tools -## Tooling Notes +If the Contentstack MCP server is connected with the `launch` group, use its tools instead of curl: -Use the Launch API for deployment trigger and status checks. -Use the deployment detail/log endpoint for failure analysis. -Validate the target environment before deploying. -Require explicit confirmation before every deployment, and an extra check for production. -Never auto-deploy to production without review. +- Inspect: `get_all_environments`, `get_an_environment`, `get_all_deployments`, `get_deployment` (status polling), `get_latest_live_deployment`, `get_deployment_logs` (build log), `get_server_logs` (runtime log) (`launch`) +- Change: `create_deployment` (`launch`), which deploys the latest commit of the environment's configured branch; `trigger_deploy_hook` (`launch`) for an existing deploy hook + +`create_deployment` takes only the environment UID, so to deploy a specific commit or upload, or to roll back, use the API calls above. The confirmation gate applies to MCP calls exactly as to API calls. + +## Output + +- Return a concise deployment summary first: deployment UID, environment UID, the trigger response and final status, with status updates while polling. +- On failure, include a short log-based diagnosis with the probable root cause and next steps. +- Do not expose deployment tokens, secrets, or environment variables. +- Use a non-zero exit code for FAILED, CANCELLED or SKIPPED deployments. ## Security @@ -106,12 +100,12 @@ Never auto-deploy to production without review. - Never expose deployment tokens or environment secrets. - Validate deployment targets before triggering. -- Require explicit confirmation before every deployment. +- Require explicit confirmation before every deployment, and an extra check for production. - Never auto-deploy to production without review. ### Destructive Actions -Treat deployment triggers as external side effects. Require explicit user confirmation before triggering any deployment. Requests to monitor or diagnose an existing deployment never trigger a new one. Do not retry failed deployments automatically unless the user requests it and the target is non-production. +Treat deployment triggers, through the API or MCP tools, as external side effects. Name the project and environment and require explicit user confirmation before triggering any deployment or rollback. Requests to monitor or diagnose an existing deployment never trigger a new one. Do not retry failed deployments automatically unless the user requests it and the target is non-production. ### Secrets @@ -121,17 +115,6 @@ Never print, log, or echo API tokens, deployment tokens, or environment secrets. Accept configuration via environment variables when needed, but never reveal their values. Validate required variables are present before attempting the deployment. -## Product Context - -- - Product: Launch -- - Description: Contentstack Launch: hosting, deployment, edge delivery, serverless functions, environment management, and CI/CD for Contentstack-powered frontends. -- - Product safety rules: - Never expose deployment tokens or environment secrets. -- Validate deployment targets before triggering. -- Require explicit confirmation before every deployment. -- Never auto-deploy to production without review. -- - Default tools: ["Launch API", "Projects", "Environments", "Deployments", "Serverless Functions", "Edge Delivery", "Custom Domains"] -- - Default connectors: ["Launch Proxy", "GitHub", "CI/CD Pipelines"] - ## Examples ### Trigger and monitor a deployment (few shot *) @@ -140,8 +123,8 @@ Accept configuration via environment variables when needed, but never reveal the **Assistant:** 1. Validate the project and environment. 2. Trigger the deployment via POST /projects/abc123/environments/staging456/deployments. -3. Poll status every 10 seconds. -4. If the deployment reaches failed or cancelled, fetch the deployment log and summarize the cause. +3. Poll GET /projects/abc123/environments/staging456/deployments/{deployment_uid} every 10 seconds. +4. Stop on LIVE (success). On FAILED, CANCELLED or SKIPPED, fetch .../logs/deployment-logs and summarize the cause. 5. Return the final status and exit non-zero on failure. *Demonstrates trigger → poll → fail-fast → diagnose behavior.* diff --git a/skills/lytics/SKILL.md b/skills/lytics/SKILL.md new file mode 100644 index 0000000..73caf65 --- /dev/null +++ b/skills/lytics/SKILL.md @@ -0,0 +1,111 @@ +--- +name: lytics +description: "Advise developers on Lytics, Contentstack's customer data platform: Lytics projects in Contentstack, installing the JavaScript tag (JStag), sending events and identities, schema fields and mappings (and legacy LQL), identity resolution, audiences and SegmentQL, content affinity and recommendations, the REST API and Segment API, consent and identity deletion, and connecting Lytics audiences to Personalize. Use when developers ask how to install or debug the Lytics tag, send custom events, map data onto profiles, build or automate audiences, read a profile, import Contentstack entries into Lytics, delete user data, or make Lytics audiences available in Personalize. For experiences, variants and the Personalize Edge SDK, use personalize." +allowed-tools: Read Grep Glob +--- + +# Lytics + +Advise on Lytics, the customer data platform inside Contentstack: collecting events with the JavaScript tag and APIs, resolving them into unified profiles, building audiences, content affinity, managing data through the API, consent and deletion, and feeding audiences into Personalize. + +## When to use + +- Lytics projects, installing or debugging the JavaScript tag (JStag), sending events and identities. +- Schema fields, mappings and LQL, and identity resolution. +- Audiences, SegmentQL and the Segment API, and profile lookups. +- Importing Contentstack entries, content collections and recommendations. +- Consent, identity deletion, and connecting Lytics audiences to Personalize. + +Use another skill for: + +- Experiences, variants, attributes set through the Edge SDK, and edge middleware → `personalize` +- Entry variants in the CMS and how they're modeled → `cms-variants-personalization` +- Hosting the site and its tag on Launch → `launch-platform` + +## Before you answer + +Find out how the site is set up (Launch, tag manager or manual) and how consent is handled, the data sources (web, server, warehouse, Contentstack entries), the identity keys (email, user ID, anonymous cookie), the audience goals, and the symptoms when debugging. + +## Instructions + +1. Confirm the project exists and the tag is installed and sending. Get the tag verified before anything else. +2. Identify data sources and identity keys. +3. Map events to profile fields. +4. Build audiences in the UI or with SegmentQL. +5. Activate: connect Personalize, exports, recommendations. +6. Cover consent and deletion, and flag irreversible deletes. + + +### Projects + +In Contentstack, Lytics is set up as a Lytics project. The Lytics app calls the same thing an account. Creating a project provisions the account, generates the JavaScript tag snippet and opens a default data stream. Projects replace the deprecated Data Activation Layer. From the project you install the tag, connect a CMS stack, Launch project or Personalize project, and invite collaborators. Roles are per project: organization membership alone does not grant access. + +### JavaScript tag + +On Launch, connecting the Launch project deploys the tag for you. Otherwise paste the snippet from **Install Lytics** on the project dashboard (also under **Data Pipeline → SDK** in Lytics) as the first element in `<head>`, or load it through a tag manager to fit your consent setup. Verify that `jstag` exists in the browser console and that the network tab shows requests to `lytics.io/c` (collection) and `lytics.io/api/personalize`. + +Core calls: `jstag.send(payload)` sends key-value data to the default stream (pass a stream name to target another), `jstag.identify(payload)` is a semantic alias for sending identity, and `jstag.page()` / `jstag.pageView()` records a page view with page analysis. In single-page apps, call `jstag.page()` on route changes. The tag tracks anonymous visitors with a `_uid` cookie. Client-side integrations (GA4, Google Ad Manager, Meta, Contentstack Personalize and others) are opt-in checkboxes in the account's JavaScript tag settings. + +### Identity + +Profiles merge when events share an identity field value. Send a known identifier such as `email` or a user ID together with the anonymous `_uid` (for example on login) so the anonymous history joins the known profile. Identity keys have ranks that decide which wins when they conflict. + +### Schema, mappings and LQL + +Fields and mappings define how stream events become profile fields. Manage them in **Schema Studio** in the UI, which replaced hand-written LQL for day-to-day work. LQL (`SELECT … FROM <stream> INTO user BY <identity field>`) is still available through the API for bulk or automated changes. A stream with no mapping is collected but never reaches profiles. Changes that alter identity fields or field types can trigger a full reprocess of history; adding a field doesn't. + +### Audiences + +An audience is a live filter over profiles. Membership updates as profiles change, and it has no age-based expiry. Build audiences in the audience builder or in SegmentQL, for example `FILTER AND (visits > 5) FROM user`. Conditions on map fields must name a key (`conversion_ts.purchase > "now-7d"`), or the API returns 400. Turn on **Enable for Personalization** for audiences the site or Personalize needs to see. **Expires After** deletes the audience definition, not the profiles, so it is not a data-retention tool. + +Automate audiences with the Segment API: `GET/POST /v2/segment`, `GET/PUT/DELETE /v2/segment/{id}`, `POST /v2/segment/{id}/reevaluate` after schema changes, and `/v2/segment/group` for groups. + +### Personalize connection + +Connect the Personalize project under **Settings → General → Connections** in the Lytics project. After that, Lytics audiences are available as audiences in Personalize for targeting and A/B tests, with no export job. In the browser, the tag writes audience membership to the `cs-lytics-audiences` cookie, and the Personalize Edge SDK sends it to the edge as a live attribute. The tag has to run before the edge request for the audience to apply, so first-visit targeting depends on Personalize preset attributes instead. + +### Contentstack entries and recommendations + +Authorize Contentstack in Lytics (**Account → Authorizations**) with the region, stack API key, delivery token and management token, then create an **Import Entries** job for the content types and taxonomies to sync. Each imported taxonomy becomes a context layer that scores profiles on that taxonomy's values. Content collections scope which documents are eligible for recommendations; the default collection includes every classified page, so create a narrower collection for production content. Lytics crawls live URLs and cannot classify localhost. + +### API + +Base URL `https://api.lytics.io`. Send the access token in the `Authorization` header (a query-string `access_token` also works but leaks into logs). Create tokens under **Account → Security → Access Tokens** with only the roles needed and an expiry; the token is shown once. Read a profile with `/api/entity/user/{field}/{value}`; the public personalization endpoint `/api/me/...` only returns fields on the allowlist in the account's API settings. + +### Consent and deletion + +Collect consent as events with a consistent event name, a `consented` value, and optional date, location and document context, mapped onto the profile so audiences can include or exclude by consent. Delete a profile with `DELETE /v2/identity/{table}/{field}/{value}`, or submit batches with `POST /v2/identity/delete` and track them with `GET /v2/identity/delete/status/{requestId}`. Deletion is permanent and does not remove data already exported to downstream tools; handle those separately. + +## MCP tools + +When the Contentstack MCP server is connected with the `lytics` group (authenticated with a Lytics access token), these tools act on the Lytics account: + +- Inspect: `get_accounts`, `get_tables`, `get_fields`, `lytics_get_all_audiences`, `lytics_get_a_single_audience`, `get_field_information`, `get_a_user_profile`, `get_content_topics` (`lytics`) +- Change: `classify_content` (stores the classified URL in Lytics), `enrich_content` (`lytics`) + +There are no tools for creating audiences, changing schema, sending events or deleting identities; use the Segment API, Schema Studio, the tag or the Identity API as described above. `get_a_user_profile` returns personal data, so only look up a profile the user names. + +## Output + +Lead with the step that unblocks the user. Show tag calls, payloads, SegmentQL and API requests as code with placeholders. Use short checklists for installation and debugging. + +## Security + +### Defaults + +- Advisory by default. Use the MCP tools above only when the user asks; otherwise provide the calls and let the user run them. +- Lytics access tokens are server-side only. The tag snippet and account ID are public by design. +- Give tokens the fewest roles and an expiry. +- Respect consent: don't recommend collecting or activating data the user hasn't consented to. + +### Destructive Actions + +Identity deletion is permanent. Deleting a token or an audience breaks anything that depends on it. Explain the impact, name the exact profile, token or audience, and require explicit confirmation before recommending or running these calls. + +### Secrets + +Never echo access tokens, Contentstack management tokens or authtokens. Use placeholders and environment variables. + +### Environment Variables + +Use variables such as `LYTICS_API_TOKEN` and `LYTICS_ACCOUNT_ID` for server-side calls. Never hardcode tokens in client code. diff --git a/skills/marketplace-apps/SKILL.md b/skills/marketplace-apps/SKILL.md new file mode 100644 index 0000000..2ad66cb --- /dev/null +++ b/skills/marketplace-apps/SKILL.md @@ -0,0 +1,135 @@ +--- +name: marketplace-apps +description: "Help teams choose, install, configure and use first-party Contentstack Marketplace apps. Cover the app catalog by category (DAM, video, commerce and PIM, search, translation, AI, analytics and experimentation, forms, editor and JSON RTE utilities, stack tools), stack apps vs organization apps, who can install and the Request Install flow, app configuration vs per-field Config Parameter, multiple configurations, UI locations, app webhooks and branch scope, updating, uninstalling, authorized apps, how app data is stored in entries and read on delivery, and when to build a custom app instead. Use when someone asks which Marketplace app fits a need, how to install or configure Bynder, Cloudinary, Shopify, commercetools, Algolia, Trados and similar apps, why an app field is empty or not syncing, or how to read app field data in a frontend. To build your own app, use developer-hub-app-architect; for the Assets DAM product, use asset-management." +allowed-tools: Read Grep Glob +--- + +# Contentstack Marketplace apps + +Help teams pick the right first-party Marketplace app, install and configure it safely, and use the data it stores in entries. + +## When to use + +- Finding an app for a need: DAM, video, commerce, search, translation, AI, analytics, forms or editor tools. +- Installing an app, or getting it installed without admin rights. +- Configuring an app, including several configurations and per-field overrides. +- Apps whose field is empty, whose sync doesn't run, or whose UI location is missing. +- Reading app field data on the frontend. +- Updating, uninstalling or revoking an app. + +Use another skill for: + +- Building a custom or private app, UI locations in code, App SDK → `developer-hub-app-architect` +- The Contentstack Assets DAM product → `asset-management` +- The stack asset library → `cms-assets` +- Fetching entries and references → `cms-entries` or `dx-delivery-sdk` +- Rendering JSON RTE content that RTE plugins produce → `cms-json-rte` +- Locales and fallback (translation apps write localized entries) → `cms-localization` + +## Before you answer + +Find out the need and the third-party system the team already uses, whether the app is needed in one stack or the whole organization, and the user's role (organization or stack owner or admin, or not). + +## Instructions + +1. Match the need to an app, or recommend a custom app if none fits. +2. Check the install permissions. +3. Install it, configure it, and add its UI locations to content types. +4. Show how to read its data on delivery. + +### Choose an app + +Apps documented in the Marketplace docs, by category (check the Marketplace for the current list and for plan availability): + +- **DAM:** Bynder, Cloudinary, Aprimo, Brandfolder, Frontify, Digizuite, Egnyte, Amazon S3. +- **Video:** Brightcove, JW Player, Mux, Vimeo, YouTube. +- **Commerce and PIM:** Shopify, commercetools, BigCommerce, Salesforce Commerce, SAP Commerce Cloud, Elastic Path, Akeneo, plus the Spartacus Contentstack Connector for SAP storefronts. +- **Search and merchandising:** Algolia, Constructor. +- **Translation:** Trados, Wordbee, XTM. +- **AI:** AI Assistant, AI Assistant with Brand Kit, MonkeyLearn. +- **Analytics and experimentation:** Google Analytics, Optimizely, Audience Insights (Lytics data), ClearWinner (A/B test cleanup), and the Audience and Variables plugins. +- **Forms:** Form.io, Marketo Forms. +- **Editor fields and JSON RTE plugins:** Ace Editor, JSON Editor, Color Picker, Star Ratings, Table, Fonts, Highlight, Info Panel, Word Count, Progress Bar, Interstack Reference, Image Preset Builder. +- **Stack tools and dashboards:** Bulk Operations, Calendar, Workflow Board, Content Type Visualizer, Developer Tools, Healthcheck. + +The Marketplace also offers starters and importable content models. Recommend a custom app (`developer-hub-app-architect`) when no app covers the system, the data shape must differ from what the app stores, or the integration needs logic the app's configuration can't express. Extensions are no longer actively maintained; build new integrations as apps. + +### Install + +- **Stack apps** apply to one stack. Stack owners and admins can install them, and so can organization owners and admins who are members of that stack. +- **Organization apps** apply to the whole organization (SCIM provisioning, for example). Only organization owners and admins can install them, and they ask you to authorize module access (**Authorize & Install**). +- Developers and content managers see **Request Install** instead. Admins review requests under **Marketplace > Manage > App Requests**. + +Install from **Marketplace > Discover > Explore > Apps**: open the app, click **Install App**, choose the stack, then fill in the configuration page and **Save**. Most third-party apps need credentials from the other system first (API keys, client ID and secret, store or index names), so collect those before installing. + +### Configure + +- **App configuration** is set once per installation, under **Manage > Installed Apps > [app] > Installed On > Configuration**. +- **Multiple configurations:** several apps (Bynder, Shopify and others) support more than one account or store. Add them with **+ New Configuration** and mark one **Set as Default**. A configuration migrated from an older version is named `legacy_config`; don't delete it, because entries that used it can lose access to their data. +- **Per-field override:** when you add the app's custom field to a content type, **Advanced > Config Parameter** takes a JSON object that overrides the app configuration for that field, for example choosing which store or configuration it uses. Some apps also accept per-locale overrides there. +- **What gets saved:** apps like Shopify and Bynder let you save **All Fields** from the source system, or only selected keys (**Custom Fields**). Saving fewer keys keeps entries small; saving all fields limits how many items an entry can hold. +- **UI locations:** the Installed Apps page lists the app's locations (custom field, sidebar, dashboard, full page, JSON RTE plugin and so on). Toggle optional ones off there; **Required** ones can't be disabled. A custom field or RTE plugin only appears in entries after you add it to the content type. +- **Webhooks:** apps that sync data (Algolia, translation apps) use app webhooks. When branches are enabled, set **Branch Scope** to all branches or specific ones on the app's **Webhook** tab, and check **Webhook Logs** when a sync doesn't run. + +App-specific behaviour worth knowing: + +- **Algolia** indexes on publish, only for the environments in its configuration, and maps each content type to an index. Without a mapping, entries of that content type aren't indexed. When the webhook fires from a branch, the branch UID is appended to the Algolia `objectID`. +- **Trados** can convert JSON RTE fields to HTML for translators, and can include referenced assets. It doesn't translate Select, Number, Date, File, URL, Link, Taxonomy, Boolean or Custom fields. + +### Use app data on delivery + +A custom field app stores its value in the entry as JSON (or another declared data type), and the CDA, the Delivery SDK and GraphQL return it as-is under the field UID. In GraphQL, query a JSON custom field by name with no sub-selection. The shape belongs to the app and its "save" settings, so fetch one published entry and inspect it before writing types. DAM and commerce apps usually store IDs, URLs and selected metadata, not the files or live prices. Fetch live data such as stock or price from the source system at request time. + +### Update, uninstall and revoke + +- Apps with an update show a red dot under **Manage > Installed Apps**; **Update** keeps the configuration and lets you review it. +- **Uninstall** (type the app name to confirm) permanently removes the app and all its associated data from that stack. Plan for the content types and entries that use its fields first. +- **Manage > Authorized Apps** lists apps holding OAuth access on users' behalf. Users can revoke their own grants; organization admins can revoke anyone's. + +## MCP tools + +When the Contentstack MCP server is connected, the `developerhub` group works through the Developer Hub API on apps your organization owns: + +- Inspect: `get_all_apps`, `get_an_app`, `get_app_installations` (`developerhub`) +- Change: `install_an_app`, `update_an_app_installation` (`developerhub`) +- Delete: `uninstall_an_app` (`developerhub`) + +`get_all_apps` lists your organization's apps, so third-party listings such as Bynder or Shopify may not appear there; install those from the Marketplace UI. There are no tools for app configuration screens, per-field Config Parameter, app requests or authorized apps. To check stored app data, read an entry with `get_single_entry` (`cma`) or `get_a_single_entry_cdn` (`cda`), and `get_a_single_content_type` (`cma`) shows which fields use the app. + +## Output + +Lead with the recommended app (or "build a custom app") and why, then install steps for the user's role, the configuration that matters, and a delivery snippet or sample field shape. + +## Security + +### Defaults + +- Advisory by default. Use the MCP tools above only when the user asks for the change. +- Prefer the least-privileged credentials the third-party system offers (read-only keys where the app only reads). +- Install on a development stack or branch first when the app writes to entries or external systems. + +### Destructive Actions + +Before uninstalling an app, deleting a configuration (especially `legacy_config`), or changing a configuration other fields depend on, name the stack, app and affected content types, explain that stored app data and dependent fields can stop working, and wait for confirmation. + +### Secrets + +App configurations hold third-party credentials, and `get_app_installations` can return them. Never print, echo or store API keys, client secrets or tokens; refer to them by name. + +### Environment Variables + +In frontend examples that read app data, use `CONTENTSTACK_API_KEY`, `CONTENTSTACK_DELIVERY_TOKEN` and `CONTENTSTACK_ENVIRONMENT`. Keep third-party secrets (for example a commerce admin key) server-side under their own variables. + +## References + +- https://www.contentstack.com/docs/marketplace +- https://www.contentstack.com/docs/marketplace/about-marketplace +- https://www.contentstack.com/docs/marketplace/installing-apps +- https://www.contentstack.com/docs/marketplace/installed-apps +- https://www.contentstack.com/docs/marketplace/updating-an-installed-app +- https://www.contentstack.com/docs/marketplace/uninstalling-an-app +- https://www.contentstack.com/docs/marketplace/authorized-apps +- https://www.contentstack.com/docs/marketplace/app-requests +- https://www.contentstack.com/docs/marketplace/difference-between-marketplace-apps-and-extensions +- https://www.contentstack.com/docs/marketplace/faqs +- Per-app guides follow the pattern https://www.contentstack.com/docs/marketplace/<app>, for example `bynder`, `shopify`, `algolia`, `trados`. diff --git a/skills/migration-companion/SKILL.md b/skills/migration-companion/SKILL.md index 1b52a69..2fa187e 100644 --- a/skills/migration-companion/SKILL.md +++ b/skills/migration-companion/SKILL.md @@ -14,7 +14,21 @@ consumes** (the create command produces a populated stack and a bundle with cred which feeds the code migration). Treat the artifact paths and counts that each command prints as state you must capture and carry forward. -## Operating principles +## When to use + +- Migrating, moving, switching, porting or re-platforming to Contentstack from another CMS. Contentful is supported today. +- Content models, content, assets, locales, application integrations or website code that need to move. + +Use another skill for: + +- Upgrading Contentstack SDK code from JavaScript to TypeScript → `dx-migrate-js-to-ts-sdk` +- Moving content between Contentstack stacks or branches, or `csdx cm:stacks:migration` scripts → `dx-cli` +- Designing the target content model from scratch → `cms-data-modeling-best-practices` +- Converting HTML RTE to JSON RTE, or rendering JSON RTE after the migration → `cms-json-rte` + +## Instructions + +### Operating principles Follow these throughout — they matter more than any single command: @@ -94,7 +108,7 @@ Follow these throughout — they matter more than any single command: the user must run themselves (for example `contentful login` or a manual plugin install), and a template these instructions tell you to render verbatim (such as the Step 5 welcome message). -## The migration at a glance +### The migration at a glance | # | Step | Command (core) | Produces | |---|------|----------------|----------| @@ -124,7 +138,7 @@ only the one you need, when you reach it. --- -## Step 1 — Prerequisites & inputs +### Step 1 — Prerequisites & inputs > **[PROGRESS]** Output the migration progress block: > Step 1 → `"in_progress"`, Steps 2–4 → `"pending"`. @@ -147,7 +161,7 @@ Summary of sub-steps (full commands, messages, and tables are in the reference f - **1.6 — Contentful Space ID.** Auto-select a single space; otherwise present a numbered menu and wait. Capture `SPACE_ID`. -### Eval — Verify Step 1 before proceeding +#### Eval — Verify Step 1 before proceeding ```bash # Use the Node the checker validated (NODE_BIN_DIR from the prereq JSON). @@ -184,7 +198,7 @@ Then proceed to Step 2. If any check fails, fix the issue (re-run the relevant s --- -## Step 2 — Install the migrate CLI plugin +### Step 2 — Install the migrate CLI plugin > **[PROGRESS]** Output the migration progress block: > Step 1 → `"completed"`, Step 2 → `"in_progress"`, Steps 3–4 → `"pending"`. @@ -197,7 +211,7 @@ Summary: run the bundled install/update block, which installs or updates `PLUGIN_NPM_UNAVAILABLE`). On a failure marker, ask the user to install manually and wait for them to continue, then run the Eval. -### Eval — Verify Step 2 before proceeding +#### Eval — Verify Step 2 before proceeding ```bash PATH="<NODE_BIN_DIR>:$PATH" csdx migrate --help @@ -211,7 +225,7 @@ If `migrate:create` is present, proceed to Step 3 (its **[PROGRESS]** block mark --- -## Step 3 — Content Migration +### Step 3 — Content Migration > **[PROGRESS]** Output the migration progress block: > Steps 1–2 → `"completed"`, Step 3 → `"in_progress"`, Step 4 → `"pending"`. @@ -236,7 +250,7 @@ Summary of sub-steps (full commands, expected output, and parsing script are in - **3.4 — Show the user a progress summary** (entity counts + final stack box). - **Token expiry mid-run:** re-run `csdx auth:login --oauth` and restart from 3.1. -### Eval — Verify Step 3 before proceeding +#### Eval — Verify Step 3 before proceeding Confirm the key artifacts exist: @@ -272,7 +286,7 @@ Entries 53 53 If all pass, proceed to Step 4 (its **[PROGRESS]** block marks Step 3 ✅ and Step 4 ⏳). If counts diverge or artifacts are missing, surface the discrepancy and point the user at `<LOG_DIR>` — do not proceed until resolved. -### Session variables carried into Step 4 +#### Session variables carried into Step 4 | Variable | Value | Purpose | | --------------- | -------------------------------------- | -------------------------------------------------------- | @@ -287,7 +301,7 @@ point the user at `<LOG_DIR>` — do not proceed until resolved. --- -## Step 4 — Code Migration +### Step 4 — Code Migration > **[PROGRESS]** Output the migration progress block: > Steps 1–3 → `"completed"`, Step 4 → `"in_progress"`. @@ -311,7 +325,7 @@ Summary of sub-steps (full detail in the reference file): file) → MIGRATE → LIVE PREVIEW → VERIFY with `bash "<SKILL_DIR>/scripts/run-all.sh" "<repoPath>"`. Hard-gate eval failures mean the migration is NOT done. -### 4.5 — Guardrails +#### 4.5 — Guardrails - Do NOT modify content modeling or move/import entries — out of scope for this step. - Convert every reference dereference to safe array access (`?.[0]`) and audit null safety @@ -319,7 +333,7 @@ Summary of sub-steps (full detail in the reference file): - When a UID or behavior is uncertain, leave a `// TODO(migration):` comment and list it — never guess silently. -### 4.6 — Wrap up +#### 4.6 — Wrap up > **[PROGRESS]** Output the migration progress block: > Steps 1–4 all → `"completed"`. Then immediately proceed to Step 5. @@ -342,13 +356,22 @@ Give the user a final summary of the whole migration journey: --- -## Step 5 — Welcome to Contentstack 🎉 +### Step 5 — Welcome to Contentstack 🎉 The migration is complete. Read [references/step-5-welcome-message.md](references/step-5-welcome-message.md) and display that message to the user — render it exactly as shown there, preserving the section headings, icons, and line breaks, and filling the file-location table with the real paths captured during this session. +## MCP tools + +The migration runs on the Contentstack CLI and its migrate plugin, as the steps above describe; it doesn't need MCP. When the Contentstack MCP server is connected, its read tools are an optional way to verify the result in the new stack (local server group in brackets): + +- Inspect: `get_all_content_types`, `get_a_single_content_type`, `get_all_entries`, `get_all_assets`, `get_all_languages` (`cma`) +- Check delivery: `get_all_entries_cdn` (`cda`) + +Don't use MCP write or delete tools to patch migrated content mid-run; rerun the relevant step instead. + ## Security ### Defaults diff --git a/skills/migration-companion/scripts/__pycache__/read_cf_token.cpython-314.pyc b/skills/migration-companion/scripts/__pycache__/read_cf_token.cpython-314.pyc deleted file mode 100644 index 402ad06..0000000 Binary files a/skills/migration-companion/scripts/__pycache__/read_cf_token.cpython-314.pyc and /dev/null differ diff --git a/skills/personalize/SKILL.md b/skills/personalize/SKILL.md new file mode 100644 index 0000000..4f5ac8c --- /dev/null +++ b/skills/personalize/SKILL.md @@ -0,0 +1,139 @@ +--- +name: personalize +description: "Advise developers on Contentstack Personalize: projects, segmented and A/B test experiences, audiences, preset and custom attributes, impression and conversion events, priority, the Personalize Edge SDK and Edge API, the Management API, and edge middleware patterns that resolve variants before rendering. Use when developers ask how to set up Personalize, integrate the Edge SDK in Next.js, Launch Edge Functions, Vercel, Netlify or Cloudflare, track impressions or conversions, pass user attributes, manage experiences through the API, read experience analytics, or debug wrong or missing variants. For modeling and fetching entry variants in the CMS (variant groups, x-cs-variant-uid, layering), use cms-variants-personalization. For Lytics data collection and audience building, use lytics." +allowed-tools: Read Grep Glob +--- + +# Personalize + +Advise on Contentstack Personalize, the edge personalization engine: projects, experiences, audiences, attributes, events, the Personalize Edge SDK and Edge API, the Management API, and the request flow that turns a visitor into variant aliases for the Content Delivery API, without leaking per-user content into shared caches. + +## When to use + +- Setting up or integrating Personalize, including the Edge SDK in Next.js, Launch Edge Functions, Vercel, Netlify or Cloudflare. +- Choosing between segmented and A/B test experiences, and defining audiences, attributes and priority. +- Tracking impressions and conversions, and reading experience analytics. +- Automating experiences through the Management API. +- Debugging a visitor who sees the wrong or missing variant. + +Use another skill for: + +- Linking variant groups to content types, creating entry variants, `x-cs-variant-uid` and variant layering → `cms-variants-personalization` +- The Lytics tag, schema and audience building → `lytics` +- Launch Edge Functions and hosting in general → `launch-platform` +- Delivery SDK queries outside the variant flow → `dx-delivery-sdk` + +## Before you answer + +Find out the framework and hosting (Next.js, Launch, Vercel, Netlify, Cloudflare, other), the rendering mode (SSR, SSG, CSR), the project region, the experience goal (segmented targeting or A/B test), the attributes and audiences involved, and the symptoms when debugging. + +## Instructions + +1. Confirm the stack, hosting, rendering mode and region. +2. Lay out the flow: edge decision → variant aliases → CDA fetch → impressions and conversions. +3. Recommend experience type, audiences, attributes and priority. +4. Provide SDK or API code for the user's runtime, using the real Edge SDK API and the correct regional hosts. +5. Cover caching, cookies, attribute registration and tracking before they cause bugs. +6. Debug in order: manifest, aliases, CDA request, entry variants, cache. + + +### Concepts + +A Personalize project connects to one CMS stack. Each experience in the project syncs to the stack as a variant group, and each of its variants maps to a CMS variant. Experiences come in two types: segmented experiences show a variant to visitors who match its audience, and A/B test experiences split traffic across variants (equal split, custom percentages, or Multi-Armed Bandit, which shifts traffic toward better performers). Within an experience only one variant is active per visitor: the first match in the variant order wins. Across experiences, variants that target different content layer together; when two experiences target the same content, the higher-priority experience wins. Experience tags (free-form strings such as `env:production`) group experiences and can limit which ones are evaluated at runtime. + +### Audiences and attributes + +Audiences are rules over attributes. Preset attributes are always available and cannot be edited: City, Country, Region, Date and Time, Device Type, Operating System, Query Parameter and Referrer. Custom attributes must be created in the project before they can be used; audiences can only use keys that exist in the project. Set custom attributes with the Edge SDK or Edge API, or through Google Tag Manager or a CDP integration. Attributes passed at `init` as `liveAttributes` apply to the current request only and are not stored on the profile; attributes set with `set()` are stored and may take up to a second to apply. + +Personalize audiences are evaluated at the edge on the first request; membership is not stored, so they have no analytics. Lytics audiences come from a connected Lytics project, carry unified customer data, and do support analytics. + +### Edge SDK + +Install `@contentstack/personalize-edge-sdk`. Call `Personalize.setEdgeApiUrl()` before `init` when the project is outside AWS NA. Regional Edge API hosts: `personalize-edge.contentstack.com` (AWS NA), `eu-personalize-edge`, `au-personalize-edge`, `azure-na-personalize-edge`, `azure-eu-personalize-edge`, `gcp-na-personalize-edge`, `gcp-eu-personalize-edge` (all `.contentstack.com`). + +`await Personalize.init(projectUid, options)` fetches the visitor's manifest and returns an SDK instance. Options: `request` (the incoming Web `Request`, used at the edge to read cookies and derive preset attributes), `userId`, `liveAttributes`, `tags` (up to 10, OR match), `customVariantQueryParam`, and `lyticsIdField` / `lyticsIdValue`. Instance methods: `getVariantAliases()`, `getVariantParam()`, `variantParamToVariantAliases()`, `getVariants()`, `getActiveVariant(experienceShortUid)`, `getExperiences()`, `set(attributes)`, `setUserId(userId, { preserveUserAttributes })`, `triggerImpression(experienceShortUid)`, `triggerImpressions({ experienceShortUids | aliases })`, `triggerEvent(eventKey)` and `addStateToResponse(response)`. The static helpers on `Personalize` other than `init`, `setEdgeApiUrl`, `variantParamToVariantAliases` and `getInitializationStatus` are deprecated; use the instance methods. + +### Request flow + +Resolve variants before rendering: + +1. In an edge function or middleware, `init` with the incoming `request`. +2. Read `getVariantParam()` (a compact string such as `a_0,b_1`) and rewrite the request URL with it under `Personalize.VARIANT_QUERY_PARAM`, so the visitor's URL doesn't change. +3. Call `addStateToResponse(response)` so the SDK's user and manifest cookies persist. The browser SDK then initializes from the cookie without another manifest call. +4. On the server, convert the param with `variantParamToVariantAliases()` and pass the aliases to the Delivery SDK (`.variants(aliases)`) or the CDA. With no variants, fetch the base entry. + +On Launch, use an Edge Function (`functions/[proxy].edge.js`) for this step, not Next.js middleware, because Next.js middleware runs at the origin on Launch. See [references/edge-integration.md](references/edge-integration.md) for a worked example. + +Browser-only (CSR) integrations can `init` without `request` and fetch variants client-side, at the cost of a content flash. + +### Events and analytics + +Impressions record that a visitor saw a variant and are counted once per visitor. Fire `triggerImpression` after the personalized content renders, once per experience, not on every re-render. Conversions are events created in the project with a key; fire them with `triggerEvent(eventKey)`. A/B test analytics compare variants by conversions; Personalize audiences have no analytics. The Management API exposes experience analytics as a summary and as a time series. + +### Identity + +The SDK generates an anonymous user ID at the edge. When a visitor logs in, call `setUserId(id, { preserveUserAttributes: true })` to carry their attributes over to the known ID. + +### Management API + +Use it to automate project configuration: attributes, audiences, events, experiences, experience versions, experience priority, analytics and geolocation lookups. Regional hosts: `personalize-api.contentstack.com` (AWS NA), `eu-personalize-api`, `au-personalize-api`, `azure-na-personalize-api`, `azure-eu-personalize-api`, `gcp-na-personalize-api`, `gcp-eu-personalize-api` (all `.contentstack.com`); the OpenAPI spec is at `/openapi` on the NA host. Authenticate with OAuth (`authorization` header, scopes `personalize:read` or `personalize:manage`, recommended) or an `authtoken`, plus `x-project-uid` for project resources. The limit is 10 requests per second per organization; back off on 429. The Edge API, by contrast, needs no authentication because it runs on public sites, and takes the project in `x-project-uid` and the visitor in `x-cs-personalize-user-uid`. + +### Caching + +A personalized response must never be served from a shared cache to another visitor. Render personalized routes per request (SSR), keep the variant param in the cache key when caching at the CDN, set `cache-control: no-store` on the browser response from the edge function, and prefer `must-revalidate` over `stale-while-revalidate` for personalized pages. Static (SSG) pages can only be personalized at the edge or in the browser. + +### Pitfalls + +- Edge middleware resolves variants but does not track. Impressions and conversions still need browser code. +- A/B bucketing relies on the SDK's user cookie. Forcing it to `SameSite=Strict` (for example through a consent tool) drops it on cross-site navigation, so returning visitors can land in a different variant. +- On runtimes that reuse isolates between requests, create a fresh SDK instance per request with `init`; never keep one at module scope. +- Lytics audience membership arrives in a cookie written by the Lytics tag, so it is missing on a visitor's very first request. + +### Debugging + +Work through the chain in order: + +1. **Manifest:** does `getExperiences()` show the experience with an active variant? If not, check that the experience is active, the audience rules match (including custom attributes registered in the project), and the region host is right. +2. **Aliases:** does the server receive the variant param and turn it into aliases? +3. **CDA:** does the delivery request carry the aliases? +4. **CMS:** does the entry have an entry variant for that variant, is the variant group linked to the content type, and is the variant published? (see `cms-variants-personalization`) +5. **Cache:** is a cached base response being served? + +## MCP tools + +When the Contentstack MCP server is connected with the `personalize` group (it needs the Personalize project ID), these tools act on the project: + +- Inspect: `get_all_experiences`, `get_single_experience`, `get_experience_versions`, `get_experiences_priority`, `get_all_audiences`, `get_all_attributes`, `get_all_events`, `get_analytics_summary`, `get_analytics_timeseries`, `get_countries`, `get_regions`, `get_cities` (`personalize`) +- Change: `create_experience`, `update_experience`, `update_experiences_priority`, `create_attribute`, `update_attribute`, `create_event`, `update_event` (`personalize`) +- Delete: `delete_experience`, `delete_experience_version` (drafts only), `delete_audience`, `delete_attribute`, `delete_event` (`personalize`) + +There is no tool for creating audiences; use the Personalize UI or the Management API. For the CMS side, `get_all_variants_of_a_content_type`, `get_all_variants_of_an_entry`, `create_an_entry_variant` and `publish_variants_of_an_entry` (`cma`) cover entry variants. When debugging, read the experience, its audiences and its versions with the inspect tools before changing anything. + +## Output + +Lead with the request flow or the failing step. Give runnable TypeScript or JavaScript for the user's runtime, with credentials and project UIDs from environment variables. Use short checklists for setup and debugging. + +## Security + +### Defaults + +- Advisory by default. Use the MCP tools above only when the user asks for the change. +- Never expose authtokens or OAuth tokens. The Management API is server-side only. +- The Edge API and the project UID are safe in browser code; Management API credentials are not. +- Keep personalized responses out of shared caches, and never cache them without the variant in the cache key. + +### Destructive Actions + +Before deleting or pausing experiences, audiences, attributes or events, or changing experience priority, name the project and the items affected, explain the impact, and wait for confirmation. Pausing an experience stops delivery of its variants; deleting one unlinks its variant group in the CMS but leaves the group and its entry variants in place. + +### Secrets + +Never echo authtokens, OAuth tokens or delivery tokens. Use placeholders and environment variables. + +### Environment Variables + +Use variables such as `CONTENTSTACK_PERSONALIZE_PROJECT_UID`, `CONTENTSTACK_PERSONALIZE_EDGE_API_URL`, `CONTENTSTACK_API_KEY` and `CONTENTSTACK_DELIVERY_TOKEN`. Keep authtokens and OAuth tokens server-side. + +## References + +- [Edge integration example](references/edge-integration.md): a worked edge-function integration that resolves variants before rendering. diff --git a/skills/personalize/references/edge-integration.md b/skills/personalize/references/edge-integration.md new file mode 100644 index 0000000..e430524 --- /dev/null +++ b/skills/personalize/references/edge-integration.md @@ -0,0 +1,104 @@ +# Edge integration example (Next.js on Launch) + +A minimal pattern based on the public "Set up a Next.js website with Personalize on Launch" guide, with its syntax errors fixed. Adapt the routing and data fetching to the user's app. + +## 1. Edge function: `functions/[proxy].edge.js` + +```js +import Personalize from '@contentstack/personalize-edge-sdk'; + +export default async function handler(request, context) { + const parsedUrl = new URL(request.url); + const { pathname } = parsedUrl; + + // Pass static assets straight through. + if (['_next', 'favicon.ico'].some((path) => pathname.includes(path))) { + return fetch(request); + } + + // Only needed outside AWS NA. + if (context.env.CONTENTSTACK_PERSONALIZE_EDGE_API_URL) { + Personalize.setEdgeApiUrl(context.env.CONTENTSTACK_PERSONALIZE_EDGE_API_URL); + } + + const personalizeSdk = await Personalize.init( + context.env.CONTENTSTACK_PERSONALIZE_PROJECT_UID, + { request } + ); + + // Rewrite the origin request with the variant param; the visitor's URL is unchanged. + const variantParam = personalizeSdk.getVariantParam(); + parsedUrl.searchParams.set(Personalize.VARIANT_QUERY_PARAM, variantParam); + + const response = await fetch(new Request(parsedUrl.toString(), request)); + const modifiedResponse = new Response(response.body, response); + + // Persist the user and manifest cookies, and keep the browser from caching this response. + personalizeSdk.addStateToResponse(modifiedResponse); + modifiedResponse.headers.set('cache-control', 'no-store'); + + return modifiedResponse; +} +``` + +On Vercel, Netlify or Cloudflare the same steps run in that platform's middleware or edge function: `init` with the request, rewrite with the variant param, `addStateToResponse`, return. + +## 2. Server: turn the param into aliases and fetch variants + +```ts +import Personalize from '@contentstack/personalize-edge-sdk'; +import { stack } from '@/lib/contentstack'; + +export default async function Page({ searchParams }: { searchParams: Promise<Record<string, string | undefined>> }) { + const params = await searchParams; + const variantParam = params[Personalize.VARIANT_QUERY_PARAM]; + + const entryCall = stack.contentType('home_page').entry(process.env.HOME_ENTRY_UID!); + const entry = variantParam + ? await entryCall + .variants(Personalize.variantParamToVariantAliases(decodeURIComponent(variantParam)).join(',')) + .fetch() + : await entryCall.fetch(); + + // render entry +} +``` + +The route must render per request (SSR). A statically generated page can't see the variant param. + +## 3. Browser: attributes, impressions, conversions + +Initialize once in a client provider. Because the edge function stored the manifest in a cookie, `init` in the browser does not refetch it. + +```tsx +'use client'; +import { createContext, useContext, useEffect, useState } from 'react'; +import Personalize from '@contentstack/personalize-edge-sdk'; + +type Sdk = Awaited<ReturnType<typeof Personalize.init>>; +const PersonalizeContext = createContext<Sdk | null>(null); +export const usePersonalize = () => useContext(PersonalizeContext); + +export function PersonalizeProvider({ children }: { children: React.ReactNode }) { + const [sdk, setSdk] = useState<Sdk | null>(null); + useEffect(() => { + Personalize.init(process.env.NEXT_PUBLIC_CONTENTSTACK_PERSONALIZE_PROJECT_UID!).then(setSdk); + }, []); + return <PersonalizeContext.Provider value={sdk}>{children}</PersonalizeContext.Provider>; +} +``` + +```tsx +const sdk = usePersonalize(); + +// Once, after the personalized block renders. +useEffect(() => { + if (sdk) void sdk.triggerImpression(EXPERIENCE_SHORT_UID); +}, [sdk]); + +// On a conversion. The event key must exist in the project. +const onClick = () => sdk?.triggerEvent('learnMoreClicked'); + +// Store a custom attribute. The attribute must exist in the project. +await sdk?.set({ age: 32 }); +``` diff --git a/skills/studio/SKILL.md b/skills/studio/SKILL.md index d85789e..82dc995 100644 --- a/skills/studio/SKILL.md +++ b/skills/studio/SKILL.md @@ -9,7 +9,22 @@ You are working on Contentstack Studio. The pack has one specialist skill per ta This SKILL.md is the only file loaded up front. Everything else is routed through `skills-index.md`. -## Resolving a skill file +## When to use + +- Anything that mentions Contentstack Studio or its concepts: the Studio SDK, canvas, compositions, Connected or Freeform templates, sections, section slots, repeaters, registering components, BYOC, Studio preview routes, or making a project Studio-ready. + +Use another skill for: + +- Live Preview or Visual Editor problems that don't involve Studio → `cms-live-preview-visual-editor-support-assistant` +- Delivery SDK code outside Studio → `dx-delivery-sdk` +- Content model design in general → `cms-data-modeling-best-practices` +- Rendering JSON RTE fields → `cms-json-rte` +- Hosting the site on Launch → `launch-platform` +- General MCP setup outside a Studio project → `dx-mcp-server` + +## Instructions + +### Resolving a skill file The specialist guides sit next to this file, in the same folder: a skill named `<name>` is `<name>.md` beside this SKILL.md, and the lookup table is `skills-index.md` in the same folder. Every cross-reference in this file and in `skills-index.md` is written as `<name>` or `<name>.md`. Resolve all of them this way. @@ -17,7 +32,7 @@ Fallback: if `<name>.md` isn't there, try `../<name>/SKILL.md` (a host that inst **If neither resolves, say so out loud**: "the Studio skill pack looks mis-installed, `<name>` didn't resolve", and do NOT quietly fall back to general knowledge. A silent fallback looks identical to the pack working, so the user never learns their install is broken. -## What to do +### What to do **Setup intent takes precedence over keyword matching.** “Enable Studio”, “make this project Studio ready”, “Studio setup”, and equivalent whole-app requests run `convert-project-to-studio` through verification. `install-studio` alone is for an explicit SDK-only request. Preserve the active setup objective across replies such as “yes”, “start”, and “OAuth done”; resume the pending step rather than routing that reply as a new task. Use the orchestrator's scoped completion checklist; authoring requirements below apply only when components or compositions are authored. @@ -31,36 +46,34 @@ Fallback: if `<name>.md` isn't there, try `../<name>/SKILL.md` (a host that inst 4. If nothing matches after both passes, answer from general Studio knowledge, but say so, and check whether a skill is missing that should exist. -## Standing rule: atomic and reusable, every time +### Standing rule: atomic and reusable, every time **This applies on every Studio turn, whatever the user asked for. It is not optional and it is not only a planning concern.** Before creating, registering, or authoring anything: -1. **Decompose first.** Never register or build a component that renders more than one piece of content. The test: does it render more than one heading / image / body / list? If yes, run [`decompose-jsx-to-atomics`](decompose-jsx-to-atomics.md) before anything else. One Section per page, or one component swallowing a whole page, is a defect, not a shortcut. -2. **Compose from atoms by default: do NOT reach for a pre-composed composite.** A palette usually contains both **atoms + layout primitives** (`*-atom-heading`, `*-stack`, `*-row`, `*-grid`, `*-section-shell`) and **composites** that already bundle a whole shape (`*-feature-card`, `*-hero`, `*-card-grid`). Dropping one composite is faster and looks identical on the canvas, and it is the wrong default: its internals are fixed in code, so the author cannot restructure it, the spacing inside it cannot be matched to a comp, and the design is no longer expressed as a composition. **Build the shape out of atoms unless the user explicitly asks to reuse an existing component.** If a composite would clearly save real work, say so and let them choose. Do not decide it silently. -3. **Reuse atoms, not shapes.** Reuse means the same `*-atom-heading` everywhere, not a ready-made card. Two things differing only in spacing are one Section plus an exposed prop, never two. -4. **Shared shells are Sections.** Header, footer, nav, menus, breadcrumbs render on every page and are the highest-value reusable pieces in any project. They are never "atomic, no binding". Their menus are **Repeaters over a field**, never hardcoded lists. Otherwise an author cannot add a menu item without a developer. -5. **One atomic prop maps to one CT field.** A single prop taking a whole object is how binding breaks: the binder recursively unwraps single-key objects for every type except `object`/`array`, so a coarse prop renders blank with no error. +<a id="section-rule"></a> -If the user's request would skip this ("just register the page component", "make the whole page one section"), say what it costs (nothing reusable, authors can't reorder, bindings break) and propose the decomposed shape instead. Do it their way only if they confirm after hearing that. +1. **The Section rule (defined here only; every other skill links to it): one Section per page region, always. Inside a Section, reuse a production component whole only when it is interactive or computes its values.** + - **Region**: a part of the page a person can point at and name (header, hero, form band, feature grid, card list, footer). A Template is a stack of these Sections, whether it is a new build or a migration, and whatever the reuse count. One Section for a whole page, or one component bound to the whole page body, is a defect. + - **Interactive**: local state, event handlers, forms, carousels, modals. Built-ins and atoms cannot express behavior, so the app's component stays whole inside its Section. + - **Computes its values**: it derives what it renders instead of printing fields (formatting, totals, sorting or filtering, a layout that depends on item count or position). Keep it whole too, or the logic has nowhere to live. + - **Everything else** (presentational markup that prints fields) is decomposed into the primitive library's atoms and layout primitives, one prop per CT field: run [`decompose-jsx-to-atomics`](decompose-jsx-to-atomics.md). The test for "everything else": does it render more than one heading / image / body / list and do nothing else? Then it is not registered whole. +2. **Plan the primitive library before any Section.** Every build has a library of atoms and layout primitives with menu-style design props and defaults ([`design-component-library`](design-component-library.md)), and Sections are composed from it. Sections built from generic boxes plus hard-coded CSS classes render, and an author cannot compose anything with them. [`plan-studio-architecture`](plan-studio-architecture.md) makes this a required step. +3. **Compose from atoms by default: do NOT reach for a pre-composed composite.** A palette usually contains both **atoms + layout primitives** (`*-atom-heading`, `*-stack`, `*-row`, `*-grid`, `*-section-shell`) and **composites** that already bundle a whole shape (`*-feature-card`, `*-hero`, `*-card-grid`). Dropping one composite is faster and looks identical on the canvas, and it is the wrong default: its internals are fixed in code, so the author cannot restructure it, the spacing inside it cannot be matched to a comp, and the design is no longer expressed as a composition. **Build the shape out of atoms unless the user explicitly asks to reuse an existing component.** If a composite would clearly save real work, say so and let them choose. Do not decide it silently. +4. **Reuse atoms, not shapes.** Reuse means the same `*-atom-heading` everywhere, not a ready-made card. Two things differing only in spacing are one Section plus an exposed prop, never two. +5. **Shared shells are Sections.** Header, footer, nav, menus, breadcrumbs render on every page and are the highest-value reusable pieces in any project. They are never "atomic, no binding". Their menus are **Repeaters over a field**, never hardcoded lists. Otherwise an author cannot add a menu item without a developer. +6. **One atomic prop maps to one CT field.** A single prop taking a whole object is how binding breaks: the binder recursively unwraps single-key objects for every type except `object`/`array`, so a coarse prop renders blank with no error. -## Standing rule: definition of done +If the user's request would skip this ("just register the page component", "make the whole page one section"), say what it costs (nothing reusable, authors can't reorder, bindings break) and propose the decomposed shape instead. Do it their way only if they confirm after hearing that. -**Also every Studio turn.** Eight deliverables decide whether what you built is usable by a marketer rather than only by a developer. Every one fails silently. The page still renders, nothing errors, and the gap surfaces only when a non-developer opens Studio: +### Standing rule: definition of done -1. `cslp: { appendTags: true }` in the SDK init, once per project. -2. `studioAttributes` on the root + `wrap: false` in the register entry + `$prop` twins, **every** registered component. -3. `thumbnailUrl`, **every** `registerComponent` call. -4. `ui_preview`, **every** Section composition. The editor writes this for itself by screenshotting the canvas on save. **A CMA-authored Section has it empty forever** unless you supply an asset uid explicitly. So "at write time" means have the asset ready and include it, not "the create call handles it". See [`author-composition-via-api`](author-composition-via-api.md) § Section thumbnails for the three ways to do it. Scripted authoring is exactly where this gets skipped, and the only signal is a blank tile in the palette. -5. Live Preview enabled on the stack + preview token, once per project. -6. The `/canvas` route mounted, once per project. -7. **Published** to the environment the project targets, every Section and Template, and again after every tree edit. Delivery otherwise serves the old version while the CMA and canvas both look correct. -8. **Composed of Sections**: every Template. Read the saved composition back and count `section-composition` nodes. Zero means the page is one component bound to the whole body. The standing rule above already forbids that shape, and a build still reached it, because a monolith renders correctly and fails no other check here. Counting is the only thing that catches it. +**Also every Studio turn.** Ten deliverables decide whether what you built is usable by a marketer rather than only by a developer. They are listed once, with owners and checks, in [`complete-the-build`](complete-the-build.md). Every one fails silently: the page still renders, nothing errors, and the gap surfaces only when a non-developer opens Studio. -Read [`complete-the-build`](complete-the-build.md) for the how and the verification block. The runnable node count is in [`build-connected-template`](build-connected-template.md) § Post-build structural check. Never report a build done with any of these outstanding. Say "created, thumbnail pending" and then finish it. State them in the plan up front too, so what the user approves is what they get. +Read that list before you plan, and again before you report done. State the ten in the plan up front, so what the user approves is what they get. Never report a build done with any of them outstanding: say "created, thumbnail pending" and then finish it. -## Standing rule: auth preflight +### Standing rule: auth preflight **Settle the credential before the first API call of the turn, not when one fails.** Every specialist repeats this. It lives here because it must hold even before a specialist is pulled. @@ -70,7 +83,7 @@ Read [`complete-the-build`](complete-the-build.md) for the how and the verificat 4. If nothing resolves, or a refresh returns `400 invalid_refresh_token`, hand the user `! CONTENTSTACK_REGION=<code> npx @contentstack/mcp --auth` and wait. It needs a TTY and a browser, you cannot run it for them. The region must be **in** the command. `DEFAULT_REGION` is `NA`, so omitting it silently authenticates against NA prod. 5. `403 error_code 316` is **not** an auth failure. It is a valid credential aimed at a stack outside its org. Re-authenticating fixes nothing. Check with `GET /v3/stacks` + `organization_uid`. -## Rules +### Rules - Always re-Read the skill file. Never answer from a remembered summary. The source of truth is the file on disk. - **A failed Read is a hard stop, not a shrug.** If a skill file won't resolve (see § Resolving a skill file), report it. Improvising over a broken install is worse than erroring: the user believes the pack ran when it didn't. @@ -78,17 +91,28 @@ Read [`complete-the-build`](complete-the-build.md) for the how and the verificat - If the user's request implies multiple skills (e.g. "install and set up preview routes"), pull each in the order they must run: skills-index.md's Trigger table encodes that ordering. - Prerequisite skills: many authoring skills assume `install-studio` + `configure-studio` are done. If the user's project state is unclear, run `analyze-project-fit` first. +## MCP tools + +Studio is MCP-first for the operations the server supports. [`install-contentstack-mcp`](install-contentstack-mcp.md) covers discovering, installing and verifying the server, and [`understand-contentstack-mcp`](understand-contentstack-mcp.md) covers what it does and doesn't do. Common tools (local server group in brackets): + +- Inspect: `get_all_content_types`, `get_a_single_content_type`, `get_all_entries`, `get_single_entry`, `get_all_environments`, `get_all_languages` (`cma`) +- Change: `create_a_content_type`, `update_content_type`, `create_an_entry`, `update_an_entry`, `publish_an_entry` (`cma`) +- Delete: `delete_an_entry`, `delete_content_type` (`cma`) + +The server doesn't cover every Studio operation: the Studio API project list and configuration (`/v1/projects`) and some token and Live Preview operations need OAuth HTTP or the Studio UI. Use only tools the host actually lists, and never claim an MCP call ran when none did. + ## Security ### Defaults +- Use MCP tools only for the Studio work the user asked for, and verify the target stack with a read call first. - Resolve credentials OAuth-first (see § Standing rule: auth preflight). Ask before reading or refreshing another tool's stored OAuth session. - Delivery and preview tokens may reach the browser; management tokens, OAuth tokens and session authtokens never do. - Never disable TLS certificate or hostname verification, and never ship permissive frame headers (`frame-ancestors *`, `Access-Control-Allow-Origin: *`) outside local development or the canvas route. ### Destructive Actions -Creating or changing stacks, content types, entries, compositions or Studio projects is a real mutation. State what will change and get confirmation first. Never delete or overwrite existing content, sections or templates without explicit approval. +Creating or changing stacks, content types, entries, compositions or Studio projects is a real mutation. Name the stack, branch and items, state what will change, and get confirmation first. This applies to MCP tool calls and OAuth HTTP calls alike. Never delete or overwrite existing content, sections or templates without explicit approval. ### Secrets diff --git a/skills/studio/adapt-collection-component.md b/skills/studio/adapt-collection-component.md index 0127c67..bbe5e6e 100644 --- a/skills/studio/adapt-collection-component.md +++ b/skills/studio/adapt-collection-component.md @@ -16,7 +16,7 @@ Use when the production wrapper's leaf takes an object shape that can't easily b Studio has two ways to render a CMS collection through a production wrapper. Pick before writing any adapter: -**Path A: native array prop** (`build-repeating-section` § array-prop alternative). Register the production wrapper with a `type: "array"` prop, bind that prop to a Reference field on the section's linked schema, populate `data_sources.resolvedReferences`. The SDK's data-binder reads the bound array and delivers full resolved entries. The component does its own `.map()`. No Repeater, no Condition Block. Simpler and native. +**Path A: native array prop** (`build-repeating-section` § array-prop alternative). Register the production wrapper with a `type: "array"` prop, bind that prop to a Reference field on the section's linked schema, populate `data_sources.resolvedReferences`. The SDK's data-binder reads the bound array and delivers the resolved entries, but on the live site only with the fields your bindings name (see [`register-component-prop-contracts`](register-component-prop-contracts.md) § Studio fetches only the fields your bindings name). The component does its own `.map()`. No Repeater, no Condition Block. Simpler and native. Use Path A when: the production wrapper already accepts an `array` prop of full entry-like objects. The list iterates a **single Reference field** (one CT, no per-item polymorphism). Template authors don't need to swap the child Section per template instance. You don't need Studio Preview Mode to show N rendered children on the canvas. diff --git a/skills/studio/analyze-project-fit.md b/skills/studio/analyze-project-fit.md index 24cdfb3..83a6a53 100644 --- a/skills/studio/analyze-project-fit.md +++ b/skills/studio/analyze-project-fit.md @@ -30,17 +30,17 @@ This skill is **read-only**. It never writes files, never installs packages, nev ### 1. Inspect the project (no writes) -Run these checks in order: +Run these checks in order. They use `find` and quoted patterns rather than bare globs: under zsh (the macOS default shell) a glob that matches nothing aborts the whole command with `no matches found`, before `2>/dev/null` can hide it. ```bash # 1a. package.json — root + workspace packages if monorepo cat {{projectRoot}}/package.json # 1b. Lockfile → package manager -ls {{projectRoot}}/pnpm-lock.yaml {{projectRoot}}/yarn.lock {{projectRoot}}/package-lock.json {{projectRoot}}/bun.lockb 2>/dev/null +find {{projectRoot}} -maxdepth 1 \( -name pnpm-lock.yaml -o -name yarn.lock -o -name package-lock.json -o -name 'bun.lock*' \) # 1c. Workspace / monorepo signals -ls {{projectRoot}}/turbo.json {{projectRoot}}/pnpm-workspace.yaml {{projectRoot}}/nx.json {{projectRoot}}/lerna.json 2>/dev/null +find {{projectRoot}} -maxdepth 1 \( -name turbo.json -o -name pnpm-workspace.yaml -o -name nx.json -o -name lerna.json \) # 1d. TypeScript presence ls {{projectRoot}}/tsconfig.json 2>/dev/null @@ -48,19 +48,22 @@ ls {{projectRoot}}/tsconfig.json 2>/dev/null # 1e. Single React check (the dual-React problem) cd {{projectRoot}} && npm ls react 2>&1 | head -20 -# 1f. Bundler configs -ls {{projectRoot}}/vite.config.* {{projectRoot}}/next.config.* {{projectRoot}}/webpack.config.* {{projectRoot}}/tsup.config.* 2>/dev/null +# 1f. Bundler configs, and the Next version (16+ defaults to Turbopack) +find {{projectRoot}} -maxdepth 1 \( -name 'vite.config.*' -o -name 'next.config.*' -o -name 'webpack.config.*' -o -name 'tsup.config.*' \) +cd {{projectRoot}} && npm ls next 2>/dev/null | grep -m1 'next@' # 1g. Local HTTPS readiness -which mkcert ; ls {{projectRoot}}/certs/*.pem 2>/dev/null +which mkcert ; find {{projectRoot}}/certs -maxdepth 1 -name '*.pem' 2>/dev/null # 1h. Deploy target -ls {{projectRoot}}/launch.json {{projectRoot}}/.cs-launch.json 2>/dev/null +find {{projectRoot}} -maxdepth 1 \( -name launch.json -o -name .cs-launch.json \) # 1i. App-side i18n (every locale needs its own env Base URL later) grep -E '"(next-intl|next-i18next|i18next|react-i18next|react-intl|@lingui/[a-z]+|next-international)"' {{projectRoot}}/package.json find {{projectRoot}}/app {{projectRoot}}/src/app -maxdepth 2 -type d \( -name '[[]locale[]]' -o -name '[[]lang[]]' \) 2>/dev/null -grep -n "i18n" {{projectRoot}}/next.config.* 2>/dev/null ; ls {{projectRoot}}/middleware.* {{projectRoot}}/src/middleware.* 2>/dev/null +find {{projectRoot}} -maxdepth 1 -name 'next.config.*' -exec grep -n "i18n" {} + +# Request middleware: middleware.* up to Next 15, proxy.* from Next 16 (same role, renamed) +find {{projectRoot}} {{projectRoot}}/src -maxdepth 1 \( -name 'middleware.*' -o -name 'proxy.*' \) 2>/dev/null ``` ### 2. Classify the project @@ -77,7 +80,8 @@ From the output above, extract: | **Monorepo** | Any of `turbo.json` / `pnpm-workspace.yaml` / `nx.json` / `lerna.json` | | **Existing Studio integration** | Any `@contentstack/studio-*` already in deps | | **Existing CS app** | Any `@contentstack/*` already in deps (not Studio), implies minimal-add | -| **Next + Turbopack** | Next deps + `next dev --turbo` or `next dev --turbopack` in scripts | +| **Next + Turbopack** | Next **16 or later** (Turbopack is the default for `next dev` and `next build` there, no flag needed; only an explicit `--webpack` opts out), or Next 15 and earlier with `--turbo` / `--turbopack` in scripts | +| **Request middleware** | `middleware.*` (Next 15 and earlier) or `proxy.*` (Next 16+), at the root or under `src/`. Either one can redirect the Studio iframe (locale prefix, auth): record it for the redirect probe | | **mkcert ready** | `which mkcert` returns a path | | **Data center / region** | See § 2b: Determine the data center below. **Never skip this.** | @@ -91,7 +95,9 @@ Resolve in this order, stopping at the first hit: ```bash # 1. Explicit host already in the app? -grep -rnE "https?://|host:|CONTENTSTACK_[A-Z_]+|CMA_HOST|STUDIO_API_HOST|CDA_HOST|PREVIEW_HOST|region" .env* src/ app/ lib/ 2>/dev/null | head +P="https?://|host:|CONTENTSTACK_[A-Z_]+|CMA_HOST|STUDIO_API_HOST|CDA_HOST|PREVIEW_HOST|region" +find . -maxdepth 1 -name '.env*' -exec grep -nE "$P" {} + # print host/region lines only; never paste token values back +grep -rnE "$P" src/ app/ lib/ 2>/dev/null | head # 2. Region shortcut in use? (standard regions only — NA/EU/AU/AZURE_NA/AZURE_EU/GCP_NA/GCP_EU) grep -rn "region:\s*['\"]" src/ app/ lib/ 2>/dev/null | head @@ -122,7 +128,7 @@ Pick exactly one based on the classification: | **A. Greenfield-friendly** | **Vite / CRA / Next (App or Pages Router)** + React 18 or 19 + single React + no existing CS deps | Run `install-studio` directly. No pre-work needed. A fresh Next 15 + React 19 app lands here — framework is not what gates Path A; a stale SDK pin (B), React 17 (C), existing CS deps (D) or duplicate React (G) are. On Next + Turbopack, also read Path E's flag before installing. | | **B. Stale SDK pin on React 19** | React 19.x detected AND `npm ls @contentstack/studio-react` (or the lockfile) shows 1.5.0 | **Block** `install-studio` until the pin moves. **Evidence:** `@contentstack/studio-react` 1.5.1+ peer-deps `react ^18.0.0 \|\| ^19.0.0`. 1.5.0 alone peer-deps `^18.0.0`, so the install fails with ERESOLVE on React 19. **Fix:** `<pm> add @contentstack/studio-react@latest` (run [`upgrade-studio-sdk`](upgrade-studio-sdk.md) so the sibling packages move together), then re-run `analyze-project-fit`. **Never downgrade React or Next to satisfy an old SDK pin.** A React 19 app with no prior Studio pin is Path A or D. | | **C. React 17 or older** | React < 18 detected | **Block** `install-studio`. The user must upgrade to React 18 first. Refer them to the React 18 upgrade guide. The SDK won't work on 17: this is a hard requirement, not a soft one. | -| **D. Existing CS app (minimal-add)** | `@contentstack/delivery-sdk` or `@contentstack/live-preview-utils` already present | Do NOT re-init Delivery SDK. Take the minimal-add branch in `install-studio` (Studio-only install, reuses existing Delivery SDK init). | +| **D. Existing CS app (minimal-add)** | `@contentstack/delivery-sdk` or `@contentstack/live-preview-utils` already present | Do NOT re-init Delivery SDK or Live Preview. With `delivery-sdk` present: take the minimal-add branch in `install-studio` (Studio-only install, reuses the existing stack). With `live-preview-utils` but **no** `delivery-sdk` (the app reads content with its own fetch client): take `install-studio` § Live Preview present, no Delivery SDK. It installs the Delivery SDK for Studio's own stack and leaves the app's Live Preview init as the only one. Studio cannot start without a Delivery SDK stack. | | **E. Next + Turbopack** | Detected | Proceed to `install-studio` with Next-specific paths. Flag: SDK ships a canvas-route built-ins auto-bootstrap, but if the user is pinned to an older SDK there's a known Turbopack tree-shake regression. `troubleshoot-canvas` has the safety-net snippet. | | **H. Custom / private data center** | Editor URL or config shows a custom host supplied by Contentstack | Not a blocker, but pin it before any state-changing skill. Set every host explicitly (no `region` shortcut); the MCP's OAuth login cannot reach a custom DC (its API calls can, with host overrides), and CMA calls use `curl`. Carry the hosts into `install-studio`, `verify-setup` and any provisioning skill. | | **F. Custom bundler** | Webpack/tsup/rollup with no `external: ['react']` detected | Walk the user through `dedupe` config OR (recommended) switch to serving a prod build for the Studio canvas. The dev bundler is the most common source of dual-React issues. | @@ -140,6 +146,8 @@ Print a single readable summary block: TypeScript: <yes | no> Monorepo: <yes (turbo/pnpm-workspace/etc.) | no> Existing CS SDKs: <none | delivery-sdk | live-preview-utils | studio-react> + Next / bundler: <n/a | Next 16 (Turbopack default) | Next 15 + --turbopack | webpack> + Middleware: <none | middleware.ts | proxy.ts> Local HTTPS: <mkcert ready | not installed> Deploy target: <launch | other host | none> i18n: <none | next-intl (always) en,fr | [locale] segment en,de | …> diff --git a/skills/studio/authenticate-cma.md b/skills/studio/authenticate-cma.md index fac08ca..9049d05 100644 --- a/skills/studio/authenticate-cma.md +++ b/skills/studio/authenticate-cma.md @@ -88,7 +88,7 @@ A `400 invalid_refresh_token` therefore means "something refreshed and didn't wr Two ways to survive it: -- **Re-mint before the run**: `csdx auth:login --oauth`, re-export the token. Fine for a short run. A coin-flip for a long one. +- **Re-mint before the run**: `csdx auth:login --oauth`, then re-export the token (§ [Export the `csdx` OAuth token for a script](#export-csdx-token)). Fine for a short run. A coin-flip for a long one. - **Refresh in-process**: hold the refresh token and `client_id`, refresh when stale, and retry once on a 401. Refresh tokens rotate, so after each refresh **write the new pair back** to the store it came from (atomically, keeping every other field). **A bare `curl` in a loop has neither.** If a snippet in this pack is going to run more than a few minutes, drive it from a script that refreshes. Don't paste a token into a shell loop and hope. @@ -166,7 +166,7 @@ GET /v3/content_types stack in a DIFFERENT org -> 403 error_code 316 ```bash # Read-only. Safe on any stack. Prints status codes only, never the token. -for ep in "content_types?limit=1" "stacks/settings" "delivery_tokens?limit=1"; do +for ep in "content_types?limit=1" "stacks/settings" "stacks/delivery_tokens?limit=1"; do code=$(curl -s -o /dev/null -w '%{http_code}' \ -H "authorization: Bearer $CS_OAUTH_ACCESS_TOKEN" -H "api_key: $CS_API_KEY" \ "https://api.contentstack.io/v3/$ep") @@ -194,6 +194,28 @@ A script closes that gap by preferring the env var, then falling back to **readi The MCP refreshes on its own schedule: `token_issued_at + expires_in` (3600s), refreshed within **5 minutes** of expiry. Match that margin in your script, so neither hands out a token the other would have rotated. +<a id="export-csdx-token"></a> + +### Export the `csdx` OAuth token for a script + +`csdx` keeps its session in an **encrypted** config, so `cat` shows nothing usable. Read it through the CLI's own `configHandler` from `@contentstack/cli-utilities` (checked against `@contentstack/cli-utilities` 2.0.1). **Ask the user first**, as for any stored session. The snippet prints no token, it only exports one: + +```bash +# Read-only. Exports CS_OAUTH_ACCESS_TOKEN and CS_ORG_UID into this shell. +eval "$(node -e ' +const u = require(require.resolve("@contentstack/cli-utilities", + { paths: [process.argv[1] + "/@contentstack/cli"] })); +const c = u.configHandler; +if (c.get("authorisationType") !== "OAUTH") { console.error("csdx is not logged in with --oauth"); process.exit(1); } +const age = (Date.now() - new Date(c.get("oauthDateTime"))) / 60000; +if (age > 55) { console.error("csdx token is " + Math.round(age) + " min old: run csdx auth:login --oauth"); process.exit(1); } +console.log("export CS_OAUTH_ACCESS_TOKEN=" + JSON.stringify(c.get("oauthAccessToken"))); +console.log("export CS_ORG_UID=" + JSON.stringify(c.get("oauthOrgUid") || "")); +' "$(npm root -g)")" +``` + +`npm root -g` locates the global `csdx` install. Run it in the shell the script runs in, then `CS_AUTH="authorization: Bearer $CS_OAUTH_ACCESS_TOKEN"` as usual. Don't call `authHandler.checkExpiryAndRefresh()` from a script to refresh: on a non-OAuth session it clears the CLI's whole login, and it prints to stdout. When the token is stale, re-run `csdx auth:login --oauth` (a fresh pair, stored by the CLI itself) and export again. + ## Why a session authtoken is the last rung, not a convenience A session `authtoken` is a **full user-session credential**: every org, every stack, every permission that user holds, valid until the session ends. It isn't a scoped grant. Pasting one into a shell, a script or a `.env` leaves copies in shell history, process lists and logs. diff --git a/skills/studio/author-composition-bindings.md b/skills/studio/author-composition-bindings.md index 204a310..87004b0 100644 --- a/skills/studio/author-composition-bindings.md +++ b/skills/studio/author-composition-bindings.md @@ -147,7 +147,7 @@ Also required on the Repeater: `metadata.mode: "preview"`, or the canvas shows o **Worked failure.** A carousel repeater bound with the reference shape (`{"case_study_cards": {"": {}}}`) over a modular-block field rendered **0** cards. The same tree with the plain path and `repeaterBindingFieldType: "modular_block"` rendered all **6**, with each Condition Block routing to its branch. Nothing in the entry data or the panel indicated which shape was wrong. The Layers panel read "Iterates Case Study Cards" in both cases. -**Scope the section at the content type root when the repeated field is top-level.** A `linked_schemas` entry of `{content_type_uid}` alone makes root fields addressable. Adding `selected_field` narrows the scope to that field, after which a path naming the same field no longer resolves. +**Scope the section at the content type root when the repeated field is top-level.** A `linked_schemas` entry of `{content_type_uid}` alone makes root fields addressable. Adding `selected_field` narrows the scope to that field, after which a path naming the same field no longer resolves. This is step 1 of the [canonical list recipe](author-composition-repeater-recipes.md#canonical-list-recipe). **Verify a `resolvedReferences` path against the API before assuming it is wrong.** The paths feed `include[]`, so they are directly testable: diff --git a/skills/studio/author-composition-encoding.md b/skills/studio/author-composition-encoding.md index 11cb0f9..cd26a8e 100644 --- a/skills/studio/author-composition-encoding.md +++ b/skills/studio/author-composition-encoding.md @@ -42,9 +42,9 @@ The SDK's runtime renderer walks the composition node tree and throws on certain Before calling `POST /v3/.../entries` (or `PUT` on update), walk the inflated `ui` tree and assert: -- [ ] **Every `slots[<slot_uid>]` value is a non-empty array**, no `{ "<uid>": [] }` entries. Either populate the slot with at least one node, OR remove the slot from the parent's `slots` object entirely. Two exceptions: a `section-slot` node keeps `slots: { "<its-own-uid>": [] }` until a template fills it, and the page-side `section-composition` node keeps an empty entry for each slot the template hasn't filled yet ([`author-composition-sections`](author-composition-sections.md) § `section-slot` node shape). Skip that shape in the check. +- [ ] **Every `slots[<slot_uid>]` value is a non-empty array**, no `{ "<uid>": [] }` entries. Either populate the slot with at least one node, OR remove the slot from the parent's `slots` object entirely. No exceptions: a `section-slot` node carries `"slots": {}`, and a template's `section-composition` node has no key for a slot it hasn't filled ([`author-composition-sections`](author-composition-sections.md) § `section-slot` node shape). The Compositions API enforces this with `422 Empty slots crash the renderer.` - [ ] **No node references a removed descendant**: every uid mentioned in a parent's `slots` array maps to an actual node in the tree. (Common when refactoring: removing a node but leaving its uid in a parent's slot array.) -- [ ] **Every Repeater has `metadata.mode: "preview"` AND a Condition Block child (for reference / modular block iteration).** Missing either silently renders zero iterations. +- [ ] **Every Repeater has `metadata.mode: "preview"`, an item Box as its only child, and (for reference / modular block iteration) a Condition Block inside that Box** ([canonical list recipe](author-composition-repeater-recipes.md#canonical-list-recipe)). A missing Condition Block silently renders zero iterations. - [ ] **Every Section Slot inside the iteration has its child Section's node in place**. See `build-repeating-section` § Step 6. Cross-check matches the renderer-side symptom row in [`troubleshoot-canvas`](troubleshoot-canvas.md) (the "Whole composition renders blank or partially blank" row). The runtime crash should never reach the user. That's a Studio bug, but emitting a clean tree avoids it deterministically. diff --git a/skills/studio/author-composition-entry.md b/skills/studio/author-composition-entry.md index e897939..a232ab7 100644 --- a/skills/studio/author-composition-entry.md +++ b/skills/studio/author-composition-entry.md @@ -21,13 +21,40 @@ A composition is a single entry on the compositions CT. The fields (see [`author | `place_composition_as` | string | `"page"` for a template composition (Connected or Freeform). `"section"` for a section composition. `"template"` also legal but not what Studio writes for the top-level template composition. The Compositions tab lists `"page"` entries under Templates | | `schema_version` | string | Composition schema version stamped by Studio (`"1.0.0"`, `"1"`, `"2"`, …), a **string**, not a number | | `ui` | string | The compressed composition tree, `zlib:<base64>` (see `troubleshoot-canvas` § Diagnostic tooling) | -| `data_sources` | JSON string | The stringified `resolvedReferences` map + pinned data-source list. Wire shape is `"[]"` when empty, not `[]` | +| `data_sources` | JSON string (raw CMA) | The `resolvedReferences` map + pinned data-source list. Shape per API: § [Wire shapes](#wire-shapes) | | `static_value` | group | Per-key literal store. Every `binding.type: "static_value"` on a node references a **key** here. The actual literal lives in `static_value.<subtype>[]` under that key. See [`author-composition-static-values`](author-composition-static-values.md) | | `url_metadata` | group | **Required on every template composition** (`place_composition_as: "page"`): `url_source` + `url_queries`. Never write `url` without it. See § `url` + `url_metadata`: a matched pair below. Sections carry neither `url` nor `url_metadata` | | `linked_schemas` | **group-multiple** | Sections only. Declares parent CT + `selectedField` the section scopes to. `[]` for templates / freeform | | `linked_sections` | reference (multiple) | Template's references to section-composition entries placed in its `ui` tree. `[]` for freeform / sections | | `ui_preview` | file (asset uid) | **Required on every section composition** (`place_composition_as: "section"`). The editor writes this for itself by screenshotting the canvas on save. **The CMA never populates it**, so an API-created Section stays blank in the Sections palette forever and nothing errors. Supply an asset uid. See [`author-composition-thumbnails`](author-composition-thumbnails.md) § Fix C for the calls. Templates don't need it | +<a id="wire-shapes"></a> + +### Wire shapes: raw CMA vs Compositions API + +This is the one definition of each shape. Every other file links here. The raw CMA is this skill's path. The managed Compositions API is [`use-compositions-api`](use-compositions-api.md). + +| Field | Raw CMA (`POST /v3/content_types/<ct>/entries`) | Compositions API (`POST /v1/projects/<p>/compositions`) | +|---|---|---| +| Body envelope | `{ "entry": { … } }` | The fields directly, no `entry` wrapper | +| `ui` | String `"zlib:<base64>"` you compress yourself | Uncompressed node **object**. A `zlib:` string fails validation | +| Where `ui` sits | Top-level entry field, sibling of `static_value`. **Never inside `static_value`** | Same | +| `data_sources` | JSON-encoded **string**: `"[]"`, `"[{\"uid\":\"template\",…}]"` | Plain **array**: `[]`, `[{ "uid": "template", … }]`. A string answers `400` | +| `resolvedReferences` key (template data source) | `"template"`. The SDK ignores the key's content, but write `"template"`: it is what Studio writes. Never `"root.<field>"` | Same | +| `resolvedReferences` key (`contentstack` pinned data source) | The pinned **entry uid**: the SDK looks it up by that key | Same | +| `static_value.number[].value` | Must match the compositions CT's `static_value.number.value` field type. The schema [`provision-studio-project`](provision-studio-project.md) ships makes it **text**, so write `"49"`. A CT whose field is `number` (a measured `422 … is not number`) needs `49`. Read the CT before the first write | Same: the service forwards to the CMA | +| `static_value.choice[].value` | List: `["h1"]` | Same | +| `static_value.object` / `.array` values | JSON-encoded string | Same | + +One data source, both forms: + +```jsonc +// raw CMA +"data_sources": "[{\"uid\":\"template\",\"data\":null,\"resolvedReferences\":{\"template\":[\"products\"]}}]" +// Compositions API +"data_sources": [{ "uid": "template", "data": null, "resolvedReferences": { "template": ["products"] } }] +``` + After write, publish the entry. Studio queries by `composable_uid` + published status. An unpublished entry won't appear in the project's Compositions list or load in the canvas. **A section write is not finished until `ui_preview` is set.** It fails the way publish state does (silently, with a correct-looking `200` and a page that renders), so it is invisible until an author opens the palette and finds unlabeled gray tiles. Set it in the same pass as the create, per [`author-composition-thumbnails`](author-composition-thumbnails.md): screenshot or generate the image, `POST /v3/assets`, then `PUT { entry: { ui_preview: <assetUid> } }`. Batching it to "later" is how a whole project ends up with none. Measured: 254 API-authored Sections in one project, zero thumbnails, while the UI-authored Sections beside them all had one. @@ -60,7 +87,7 @@ A published entry returns an array with one object per environment/locale. `[]` | Composition | `url` | `url_source` | Notes | |---|---|---|---| | **Connected** template, CT is a page type (`is_page: true` + CT `url_pattern`) | `/<route>/{{entry.title}}` | `content_type_url_pattern` | **The default for the API path.** The CT pattern is the derivation, so Studio re-derives correctly on every editor load. Pair with CT `url_prefix: "/<route>/"`. See [`build-connected-template` § Single-entry vs multi-entry URL pattern rule](build-connected-template.md#single-entry-vs-multi-entry-url-pattern-rule) | -| **Connected** template, CT **has** a `url_pattern` (entries get a real `url`) | `/<route>/{{entry.title}}` | `content_type_url_pattern` | Resolution keys on **`entry.url`**: the SDK finds the entry whose stored `url` equals the request path, so write the template `url` in the shape of a real entry's `url` (read one first; a CT `url_prefix` the stored `url` lacks misses with `PREVIEW_ENTRY_NOT_FOUND`). The composition `url` is NOT variable-substituted under this source (see below). Requires every entry to carry a populated `url` | +| **Connected** template, CT **has** a `url_pattern` (entries get a real `url`) | `/<route>/{{entry.title}}` | `content_type_url_pattern` | Resolution keys on **`entry.url`**: the SDK finds the entry whose stored `url` equals the request path, so write the template `url` in the shape of a real entry's `url` (read one first; a CT `url_prefix` the stored `url` lacks misses with `URL_MATCHED_NO_ENTRY`). The composition `url` is NOT variable-substituted under this source (see below). Requires every entry to carry a populated `url` | | **Connected** template, **singleton** CT with no `url_pattern` (homepage, pricing, a one-entry rewards page) | `/rewards` (literal) | `content_type_url_pattern`, **but only if you populate `url` on the entry by hand** | The source's real requirement is a non-empty `url` field on at least one entry, not a CT `url_pattern`. A singleton has no pattern to generate one, so set `url: "/rewards"` on the entry yourself. Skip that and the editor fails with `MISSING_CT_PATTERN_URL_FIELD`, shown as "No entry has a URL field populated yet", while SSR renders the page fine | | **Connected** template whose URL derives from a field **other than** `url` (`{{entry.slug}}`, `{{entry.author.slug}}`) | `/blog/{{entry.slug}}` | `user_specified_pattern` | This is the only source that extracts and matches on pattern variables. `content_type_url_pattern` would ignore the pattern and look for `url` instead | | **Freeform** template, identity URL | `/<compositions-CT-uid>/<composable_uid>` | `default_url_pattern` | The Studio-generated default. Use this unless the path was deliberately hand-chosen | diff --git a/skills/studio/author-composition-nested-repeaters.md b/skills/studio/author-composition-nested-repeaters.md index 0daa417..6391b98 100644 --- a/skills/studio/author-composition-nested-repeaters.md +++ b/skills/studio/author-composition-nested-repeaters.md @@ -70,8 +70,8 @@ Both Repeaters carry `props.children` pointing at their own slot. Without it the | Source-type combo | Extra prerequisites | |---|---| | group-multiple (e.g. `footer_columns`) containing group-multiple (`column.links`) | (nothing extra), inline values both ways | -| group-multiple containing multi-reference (e.g. column.related_entries) | Add `data_sources.resolvedReferences` for the nested reference path (e.g. `"root.footer_columns.0.related_entries": ["footer_columns.related_entries"]`). Add Condition Block as immediate child of `R-inner`. | -| multi-reference containing group-multiple (e.g. category.products[].images) | `resolvedReferences` for the outer reference (`root.products`). Condition Block as child of `R-outer`. Inner iteration is over the (inline) `images` group-multiple, no extra prerequisites for `R-inner`. | +| group-multiple containing multi-reference (e.g. column.related_entries) | Add `data_sources.resolvedReferences` for the nested reference path (e.g. `"template": ["footer_columns.related_entries"]`). Add Condition Block as immediate child of `R-inner`. | +| multi-reference containing group-multiple (e.g. category.products[].images) | `resolvedReferences` for the outer reference (`"template": ["products"]`). Condition Block as child of `R-outer`. Inner iteration is over the (inline) `images` group-multiple, no extra prerequisites for `R-inner`. | | multi-reference containing multi-reference | `resolvedReferences` entries for BOTH the outer reference AND each inner reference path, inner path is `<outer_ref>.<inner_ref>`, with **no** content-type uid between them (§ [Where a reference path must terminate](author-composition-repeater-recipes.md#reference-path-termination)). Condition Block in both Repeaters. | | **Modular Block with a reference inside a block** (e.g. `blocks[].feature_block.related_products`) | `resolvedReferences` path must include the **block uid** segment, `"blocks.feature_block.related_products"`, NOT `"blocks.related_products"`. Condition Block per block uid on the Repeater. See § Modular Block to reference: the block-uid segment below. | @@ -89,7 +89,8 @@ Omitting the block uid fails silently, for the reason given in § [Where a refer ```jsonc // Template composition — MB field `blocks`, block uid `feature_block`, -// reference field `related_products` inside that block. +// reference field `related_products` inside that block. Raw CMA form (a JSON string); +// the Compositions API takes the array: author-composition-entry § Wire shapes. "data_sources": "[{\"uid\":\"template\",\"data\":null,\"resolvedReferences\":{\"template\":[\"blocks.feature_block.related_products\"]}}]" ``` diff --git a/skills/studio/author-composition-pitfalls.md b/skills/studio/author-composition-pitfalls.md index cbd3644..1ef6c82 100644 --- a/skills/studio/author-composition-pitfalls.md +++ b/skills/studio/author-composition-pitfalls.md @@ -56,7 +56,7 @@ Three lines, catches the entire class. Run it in the same pass as the `static_va | Rebuilding a template's root slot from a fresh node list | Sibling nodes that sat beside the main one (a closing CTA band, a comparison table, a content list) are dropped. The page still renders, so nothing flags it | Rebuild from the newest version whose root slot holds no `section-composition` node, and carry every sibling across as its own Section | | Deriving a composition title by stripping a suffix from a scope name | Two scopes collapse to one title. Titles are unique per project, so the second create returns `409` and that Section is missing from the template | Keep the title 1:1 with the scope it is rooted at | | Creating a Section and placing it in the same pass without publishing | The Section resolves to nothing at delivery and the page renders `Component with type '<uid>' is not registered` | Publish every Section before the Template that references it. See [`complete-the-build`](complete-the-build.md) | -| Missing `data_sources.resolvedReferences` | Repeater renders 0 items, reference stubs never materialise | Add the resolution map entry: `{ uid:"template", resolvedReferences: { "root.<refField>": ["<refField>"] } }` | +| Missing `data_sources.resolvedReferences` | Repeater renders 0 items, reference stubs never materialise | Add the resolution map entry: `{ uid:"template", resolvedReferences: { "template": ["<refField>"] } }`, in the per-API form from [`author-composition-entry`](author-composition-entry.md#wire-shapes) § Wire shapes | | Authoring from the registration without reading the component source | The registration lists prop names and options but not how they are consumed. A prop that overrides another, or is compared by equality, discards correct values with no error | Open the component file for every component you author. See § Read the component source before authoring | | A list-wrapped `choice` hitting a strict equality check in component code | `['auto'] !== 'auto'` is true, so an override branch fires and the props it guards are dropped, `justify` silently stops working | Leave equality-compared props unwritten so the component's string default applies. Report the array-handling bug to the library owner | | Composing atomically without reading how the composite derives its values | A composite computes child values from its own scheme: `isDark ? 'medium' : 'light'`. Hand-authoring the same tree with the atom defaults gets colors wrong in a way that looks plausible | Treat the composite's derivation logic as the specification. See § Read the component source before authoring point 3 | @@ -66,7 +66,7 @@ Three lines, catches the entire class. Run it in the same pass as the `static_va | Assuming a card primitive's own `justify-between` will distribute your nodes | The padded inner element of a card component is often `justify: normal`, and it is inside the component where no prop reaches it. Children pack at the top however they are nested | Put one stack inside with an explicit height (card height minus the component's own padding) and set `justify` on that | | Setting `justify: between` on a container with no height | Distribution needs free space. A card whose height was removed to avoid clipping packs its children at the top, so CTAs sit at ragged heights instead of pinned to the bottom | Give the container the comp's height. Note that `height: 100%` may not chain through the SDK's per-node wrapper divs | | Setting `justify` on a row that has no width | `justify` distributes free space. A shrink-wrapped row has none, so the value applies and nothing moves. A parent stack with `align: start` shrink-wraps its children | Give the row a width and the parent `align: stretch`. Measure the rendered box, not the stored prop | -| A numeric value written into the `number` bucket as a string | The bucket is strictly typed: the CMA rejects the whole entry with `422 static_value.number.N.value is not number`. Easy to hit when defaults are parsed out of source, where `1` arrives as `"1"` | Coerce to a real int/float before writing. Only `choice` wraps in a list. `number` stays a bare number | +| A `number` bucket value whose JSON type doesn't match the compositions CT's `static_value.number.value` field | The CMA rejects the whole entry: `422 static_value.number.N.value is not number` on a CT that types the field `number`, `is not text` on one that types it text (the schema [`provision-studio-project`](provision-studio-project.md) ships) | Read the CT once and write the matching type: `49` for `number`, `"49"` for text. Only `choice` wraps in a list. See [`author-composition-entry`](author-composition-entry.md#wire-shapes) § Wire shapes | | Using a decorative shape primitive as an in-flow rule or divider | Its wrapper is `position:absolute; inset:0`, so it takes no space and pins to the section, not the card. It measures correct and renders in the wrong place: four stacked indicators read as one stray white line over the heading | Read the component first. Verify the rendered box sits inside its intended parent, not just that its size and color match | | Reporting a value as impossible without checking for a free-form primitive | A `Deviations` row that says impossible stops anyone looking again, so an expressible value stays unbuilt indefinitely | Search the registry for shape/box primitives with raw CSS `color` / `width` / `height` first | | `choice` value stored as a bare string instead of a list | **Renders correctly**: the canvas is right and every visual check passes, but the Settings panel matches it against the registered `options`, finds nothing, and shows `Select…`. The author re-picks a value that was already set | `"value": ["h1"]`. Only `choice` wraps. Boolean/text/number stay scalar. See § Node anatomy point 0a | @@ -77,7 +77,8 @@ Three lines, catches the entire class. Run it in the same pass as the `static_va | Design props left unwritten on an API-authored section | Section renders with the library's defaults, not the comp: a stack defaults to `column`, so side-by-side CTAs stack. No error | Author design props explicitly, skeleton or not. See § Skeleton first, bind later | | Card prop bindings use `template` type at `<refField>.0.<field>` (fixed `.0.` index) | EVERY iteration renders item 0, all cards identical (the worst kind: looks "correct" with a single-item list, fails as soon as the list grows) | Use `repeater` type with `repeaterUID`, path relative to the iteration item, no index | | Repeater node missing `metadata.mode: "preview"` | Canvas shows 1 placeholder iteration. Production still renders N | Set `metadata.mode: "preview"` on the Repeater node | -| Missing Condition Block child of Repeater (reference / modular-block iteration) | Iteration silently fails to resolve child bindings | Insert a Condition Block per allowed type (CT for refs, block UID for modular blocks) | +| Missing Condition Block under the Repeater (reference / modular-block iteration) | Iteration silently fails to resolve child bindings | Insert a Condition Block per allowed type (CT for refs, block UID for modular blocks) inside the item Box. See the [canonical list recipe](author-composition-repeater-recipes.md#canonical-list-recipe) | +| A Condition Block, Section or Section Slot as the Repeater's direct child | The list renders, but Visual Editor gets no item tags: no move, delete or add | Make an item Box the Repeater's only child. The validator's "items won't render" warning on that Box is expected | | Writing `url` on a template composition but omitting `url_metadata` ("minimal envelope") | **Nothing fails.** The CMA returns 201, the live route returns 200, delivery resolves off the `url` pattern alone, so the omission survives review. But Studio has no derivation to read, treats the composition as `legacy_url`, and the editor's Edit-URL panel shows the pattern as an opaque hand-typed string instead of "derived from the connected CT". The CT link is lost the first time an author edits the URL, and `legacy_url` matching (entry `url` field, leading slash, every entry) is stricter than what the pattern was written for | Always write the pair. For Connected on a page-type CT, use `url_source: "content_type_url_pattern"`. For a Freeform identity URL, use `default_url_pattern`. For a Freeform hand-chosen path, use `user_specified_pattern`. `url_queries` is a JSON string. See § [`url` + `url_metadata`: a matched pair](author-composition-entry.md#url-metadata-matched-pair) | | Compositions CT has no `url_metadata` group (or is missing the `url_source` / `url_queries` sub-fields) | CMA **silently drops** the object: 201 returned, entry reads back with no `url_metadata`, same `legacy_url` outcome as omitting it, with no error to trace | Verify the CT models `url_metadata` as a group with both text sub-fields (see `provision-studio-project`). Read the entry back after the first write and assert `url_metadata.url_source` is what you sent | | Setting `url_source: "user_specified_pattern"` on a **Connected** template via the API | Reverts: `url_queries` is only generated by the UI's Edit-URL then Save flow, and Studio re-derives from the CT pattern on editor load | Make the CT a page type (`is_page: true` + CT `url_pattern`), then use `content_type_url_pattern` with `{{entry.x}}` in the composition `url`. `user_specified_pattern` on the API path is for Freeform only | @@ -96,17 +97,17 @@ Three lines, catches the entire class. Run it in the same pass as the `static_va | Placing Condition Block's discriminator on the node as `condition: {...}` instead of `metadata: { condition: {...} }` | Discriminator ignored. Iteration item's type can't be narrowed. Bindings inside fail to resolve | Put `condition` inside `metadata`. Same pattern as Repeater's `metadata.mode`. | | `metadata.condition` without `conditionBinding` | Block renders nothing, no error. Design mode still shows it | Add it (`author-composition-repeater-recipes` step 4). Verify in preview. | | Adding `resolvedReferences` for a group-multiple iteration | No harm (the SDK ignores it for non-references), but creates noise + makes the composition look more complex than it is | Skip `resolvedReferences` for group-multiple, multi-file, and scalar-list iteration. Only references need it. | -| Adding a Condition Block for group-multiple iteration | The block has no discriminator to apply. Runtime behavior may be inconsistent | Skip the Condition Block for group-multiple. Place the iteration child directly in the Repeater's slot. | +| Adding a Condition Block for group-multiple iteration | The block has no discriminator to apply. Runtime behavior may be inconsistent | Skip the Condition Block for group-multiple. Place the iteration child directly in the item Box. | | Using `uid` instead of `queryUID` in a `contentstack_queries` binding value | Pinned-query result never resolves | Use `queryUID: "<pinned-query-uid>"`. | | Inner Repeater's `items` binding uses `type: "template"` with a nested-`.0.` path (e.g. `{ footer_columns: { 0: { links: {} } } }`) | Every outer iteration's inner-repeater reads column-0's links, all inner lists identical. The fixed-index (`.0.`) trap, one level up. | Inner Repeater's `items` MUST use `type: "repeater"` with `repeaterUID: "<outer-uid>"` and path `{ <field-on-outer-iteration>: {} }`. Then inner card bindings use the inner Repeater's UID. | | Compositions CT models `linked_schemas` as a reference field instead of group-multiple | CMA silently drops the `{content_type_uid, selected_field}` object on every `PUT` (`linked_schemas` reads back as `[]`), editor canvas shows blank repeaters on placed sections even though runtime works correctly | Model `linked_schemas` as a group-multiple on the compositions CT (see `provision-studio-project`). As a temporary runtime-only fix, set `sectionBindingOverride.selectedField` on each `section-composition` node, but the editor canvas won't render correctly until `linked_schemas` is also fixed. | -| A section iterates `selectedField` and its top-level Repeater binds `{ path: { <field>: {} } }` instead of scope-root `{ path: {} }` | Repeater renders nothing: `dataSources.template` IS the array (re-scoped via selectedField). `template.<field>` is undefined | Use scope-root `{ path: {} }` for the top-level Repeater inside a `selectedField`-scoped section. Leaf bindings inside still use `repeater` type with `repeaterUID`. | +| A section scoped to the list field (`selectedField: "<field>"`) | With a Repeater on `{ <field>: {} }` it renders nothing (`template` IS the array). With scope-root `{ path: {} }` it renders, but Visual Editor gets no list or item tags | Scope the section to the whole entry and bind the Repeater to `{ <field>: {} }`. See the [canonical list recipe](author-composition-repeater-recipes.md#canonical-list-recipe) | | Setting `selectedField` on a section that reads multiple page fields (Header reading brand + nav_links + signin_label. Hero reading the `hero` group) | Section loses access to the fields outside the scoped one, they resolve to undefined | Leave `selectedField` unset for multi-field sections. `template` becomes the whole page entry. Original `template.<field>` paths work verbatim. | | Authoring a section by API and expecting its own canvas to confirm scope | Section canvas has no data. One empty placeholder is normal | Verify via SSR cold-load: `await sdk.fetchCompositionData({ url })` then dump `spec.data.section_scoped_data[<instance-uid>]`. That's the runtime truth. | | API-placing a section in a template's `ui` without populating the template's `linked_sections` field | **"Template Did Not Load"** in the Studio editor, BUT the deployed site renders the template correctly via SSR: the editor needs the `?include[]=linked_sections` resolution to build `spec.sectionCompositions`. SSR walks the section-composition nodes directly so it works either way | When writing `section-composition` nodes into a template's `ui`, also populate the template entry's `linked_sections` field with `[{ uid: "<section-comp-entry-uid>", _content_type_uid: "<compositions-CT-uid>" }, …]`, one entry per distinct placed section (dedupe: a section placed multiple times needs only one reference). Publish the template entry. | | Passing the compressed `ui` inline via a shell variable in `curl -d "..."` | Blank canvas, no error, spec present, `hasSpec: true`, but every slot resolves empty because shell interpolation mutated `"children"` into `"c1ildren"` throughout the tree | Write payload to a temp file, POST with `-d @file`. Assert `raw.count('"children"') >= <expected>` before compressing. See § Never pass the compressed `ui` inline above. | | Using Python `urllib`/`requests` for CMA calls against a custom / private data center | `SSLCertVerificationError: unable to get local issuer certificate` when that DC's chain isn't in Python's default CA store. `curl` reads the system keychain and works | Use `curl` for the HTTP call. Keep Python for payload build + assertions only. If Python HTTP is unavoidable, point it at the trusted CA bundle for that host (`ssl.create_default_context(cafile=...)` or `REQUESTS_CA_BUNDLE`). Never disable certificate or hostname verification; if the chain can't be verified, stop and report it | -| `section-slot` node written without `slots: { "<own-uid>": [] }` (empty-array entry keyed by the node's own uid) | The template has no slot uid to fill under: canvas renders the wrapper with no slot placeholder. SSR resolves the slot as absent | Every `section-slot` node needs `slots: { "<own-uid>": [] }`, the one legitimate empty-array exception to the preflight rule. See § `section-slot` node shape | +| `section-slot` node written with `slots: { "<own-uid>": [] }` | The Compositions API rejects it: `422 Empty slots crash the renderer.` The raw CMA accepts it, and the renderer can then throw on the empty array | Write `"slots": {}`. The node's own uid is the fill key on the template side, it needs no entry here. See [`author-composition-sections`](author-composition-sections.md) § `section-slot` node shape | | Pattern B fill section's leaf bindings use `type: "repeater"` with the wrapper's `repeaterUID` | Bindings resolve to `undefined`: the wrapper's SDK scoping already unwraps the iteration item into the fill section's `template`. The wrapper's repeater scope isn't visible inside the fill section | Fill section's leaf bindings use `type: "template"` with paths relative to the reference target CT / block schema. Only the WRAPPER section uses repeater-scope bindings inside its own tree | | Pattern B fill written as `metadata.sectionSlotCompositions` on the placed wrapper | The SDK never reads that key, so each iteration's slot renders blank | Put the fill in the wrapper's `slots`: `slots.<section-slot-uid>` holds a `section-composition` node with `metadata.compositionUID` = the fill section's entry uid. Add the fill section entry to the template's `linked_sections` | | Exposed-prop declaration placed on the individual node's `metadata.sectionExposedProps` or on the composition entry, instead of `ui.metadata.sectionExposedProps` at the ui ROOT | Template's right panel doesn't render the field. Overrides never get read | Place `sectionExposedProps` on the section's `ui` ROOT node's `metadata` (the top-level `page` node). It's an array of `{nodeUid, propKey, uid, displayName, propType, bindingAtExposeTime}`, one entry per exposure. See § Recipe: Exposed section props | diff --git a/skills/studio/author-composition-practice.md b/skills/studio/author-composition-practice.md index ac35ce9..20907f7 100644 --- a/skills/studio/author-composition-practice.md +++ b/skills/studio/author-composition-practice.md @@ -50,24 +50,31 @@ For content props, prefer the comp's own copy as the `static_value` over relying [ { "key": "<uid>-items" } ] ``` -The wrapper holding the repeater carries `metadata.repeaterWrapper: true`, and its `direction` / `gap` / `wrap` props produce the row or grid the iterations flow into. Verified against a production section authored this way with `linked_schemas: []` and no data sources at all. +The wrapper holding the repeater carries `metadata.repeaterWrapper: true`, and its `direction` / `gap` / `wrap` props produce the row or grid the iterations flow into. The repeater's one child is an item Box ([canonical list recipe](author-composition-repeater-recipes.md#canonical-list-recipe)). Verified against a production section authored this way with `linked_schemas: []` and no data sources at all. **A list whose items have different shapes needs one Condition Block per shape, all of them, in the skeleton.** A carousel showing stat / image / quote cards is not three lists and not one card repeated. It is one Repeater with three branches. Build every branch. Stopping after the first leaves two thirds of the design unbuilt while the canvas looks plausible, and the omission is invisible once the sheet is written. ```jsonc -// repeater.slots.<contents> holds ONE condition-block per item type +// the item Box under the repeater holds ONE condition-block per item type +// (canonical list recipe: author-composition-repeater-recipes) { + "uid": "<cb-uid>", "type": "condition-block", + "attrs": {}, "metadata": { + "title": "Condition — <block_uid>", "slotNames": { "<slot-uid>": "Contents" }, "condition": { "type": "modular_block", "operator": "eq", "value": "<block_uid>", "conditionBinding": { "type": "repeater", "value": { "repeaterUID": "<repeater-uid>", "path": { "<block_uid>": {} } } }, - "dataBinding": { /* same shape */ } + "dataBinding": { "type": "repeater", + "value": { "repeaterUID": "<repeater-uid>", "path": { "<block_uid>": {} } } } } }, - "props": { "children": { "type": "slot", "slot": "<slot-uid>" } } + "props": { "children": { "type": "slot", "slot": "<slot-uid>" } }, + "slots": { "<slot-uid>": [ /* this branch's card */ ] }, + "styles": {} } ``` diff --git a/skills/studio/author-composition-repeater-recipes.md b/skills/studio/author-composition-repeater-recipes.md index 2180d28..20121d5 100644 --- a/skills/studio/author-composition-repeater-recipes.md +++ b/skills/studio/author-composition-repeater-recipes.md @@ -8,6 +8,117 @@ Worked recipes for repeating a card over a multi-reference field and over a GROU Part of the API-authoring set. Start at [`author-composition-via-api`](author-composition-via-api.md), which routes to this one. +<a id="canonical-list-recipe"></a> + +## The canonical list recipe {#canonical-list-recipe} + +Every list Section uses this one shape. Other files link here instead of restating it. + +1. **Scope the Section to the whole entry.** `linked_schemas: [{ "content_type_uid": "<page-ct>" }]`, no `selected_field`. A Section scoped to the list field renders, but Visual Editor finds no list or item tags ([`make-visual-editor-editable`](make-visual-editor-editable.md) § 2). +2. **A list Box holds the Repeater.** It carries `metadata.repeaterWrapper: true` (the list tag `<field>` lands on it) and the list layout: flex or grid with `gap` ([`build-repeating-section`](build-repeating-section.md) § The Repeater's wrapper element). +3. **The Repeater binds to the field**: `items` = `template` binding on `{ "<field>": {} }`, `metadata.mode: "preview"`, `metadata.repeaterBindingFieldType` per [`author-composition-bindings`](author-composition-bindings.md) § Binding a Repeater. +4. **An item Box is the Repeater's only child.** The item tag `<field>.<N>` lands on it. Without it, a Condition Block holding a Section or Section Slot drops every item tag. +5. **Condition Blocks go inside the item Box**, one per content type (references) or block uid (modular blocks), each with `operator` and `conditionBinding`. Group-multiple, multi-file and scalar lists skip this step: the card goes straight into the item Box. +6. **Card bindings** use `type: "repeater"` with the Repeater's `repeaterUID`, paths relative to the item. + +The section's full `ui` tree for a multi-reference `products` field, every node with all seven keys: + +```json +{ + "uid": "ROOT", + "type": "page", + "attrs": {}, + "metadata": { "title": "Product Grid", "sectionExposedProps": [] }, + "props": { "children": { "type": "slot", "slot": "ROOT-slot" } }, + "slots": { + "ROOT-slot": [ + { + "uid": "LIST", + "type": "box", + "attrs": {}, + "metadata": { "title": "Box — Product list", "repeaterWrapper": true }, + "props": { "children": { "type": "slot", "slot": "LIST-slot" } }, + "slots": { + "LIST-slot": [ + { + "uid": "R1", + "type": "repeater", + "attrs": {}, + "metadata": { "title": "Repeater — Products", "mode": "preview", "repeaterBindingFieldType": "reference" }, + "props": { + "items": { "binding": { "type": "template", "value": { "path": { "products": {} } } } }, + "children": { "type": "slot", "slot": "R1-slot" } + }, + "slots": { + "R1-slot": [ + { + "uid": "ITEM", + "type": "box", + "attrs": {}, + "metadata": { "title": "Box — Product item" }, + "props": { "children": { "type": "slot", "slot": "ITEM-slot" } }, + "slots": { + "ITEM-slot": [ + { + "uid": "CB1", + "type": "condition-block", + "attrs": {}, + "metadata": { + "title": "Condition — product", + "condition": { + "type": "reference", "operator": "eq", "value": "product", + "conditionBinding": { "type": "repeater", + "value": { "repeaterUID": "R1", "path": { "_content_type_uid": {} } } } + } + }, + "props": { "children": { "type": "slot", "slot": "CB1-slot" } }, + "slots": { + "CB1-slot": [ + { + "uid": "CARD1", + "type": "site-product-card", + "attrs": {}, + "metadata": { "title": "Product card" }, + "props": { + "title": { "binding": { "type": "repeater", "value": { "repeaterUID": "R1", "path": { "title": {} } } } }, + "price": { "binding": { "type": "repeater", "value": { "repeaterUID": "R1", "path": { "price": {} } } } } + }, + "slots": {}, + "styles": {} + } + ] + }, + "styles": {} + } + ] + }, + "styles": {} + } + ] + }, + "styles": {} + } + ] + }, + "styles": { + "default": { + "classes": [""], + "responsiveStyles": { "default": { "display": "grid", "gridTemplateColumns": "repeat(3, minmax(0, 1fr))", "gap": "24px" } } + } + } + } + ] + }, + "styles": {} +} +``` + +The composition also needs `data_sources` with `resolvedReferences` `{ "template": ["products"] }` (step 1 below). A modular-blocks list swaps the condition to `"type": "modular_block"` with the block uid ([step 4](#4-condition-block-inside-the-item-box)) and `repeaterBindingFieldType` to `"modular_block"`. + +**Expected validator warning.** The Compositions API validator warns on the item Box that the Repeater's items won't render. On this shape the warning is expected and wrong: leave the Box in place. It is a warning, not a `422`, and the items render (see [`use-compositions-api`](use-compositions-api.md) § Expected validator warnings). + +The recipes below explain each piece. + ## Recipe: Repeat a card over a multi-reference field The canonical pattern. Requires FIVE shapes in concert. Missing any one yields a silently-empty repeater or (worse) an all-items-identical render (the `template`-at-`.0.` trap below). @@ -30,13 +141,7 @@ The reference-resolution map. Tells the SDK "when bindings resolve through this ] ``` -Example: a section (Card Grid) that iterates `related_posts`: - -```json -"data_sources": "[{\"uid\":\"template\",\"data\":null,\"resolvedReferences\":{\"template\":[\"related_posts\"]}}]" -``` - -**Wire format quirk:** on the CMA `data_sources` is stored as a **JSON-encoded string** (`"[]"`, `"[{…}]"`), not the parsed array. Serialize before write. Parse after read. +Shown parsed. On the raw CMA `data_sources` is a **JSON-encoded string**, on the Compositions API a plain array: [`author-composition-entry`](author-composition-entry.md#wire-shapes) § Wire shapes. **Key semantics: depends on the data source, and getting it wrong fails silently:** @@ -96,58 +201,59 @@ The Repeater consumes its iteration source through its `items` prop: `props.children` is **required**: it wires the `R1-slot` slot to the Repeater's built-in `children` prop. Without it the Repeater iterates but renders nothing ([`author-composition-bindings`](author-composition-bindings.md) § Node anatomy, point 4). The `slots` key MUST match `props.children.slot`. -For a section whose `selectedField` is the reference itself (`build-section`'s scope-aware sections), the path collapses to scope-root `{ "path": {} }`, the section's `dataSources.template` is already the resolved array. +A Section scoped to the reference itself (`selectedField: "products"`) would need scope-root `{ "path": {} }`, because its `dataSources.template` is already the array. Don't author new Sections that way: it drops the Visual Editor list tags. Use the whole-entry scope of the [canonical list recipe](#canonical-list-recipe). ### 3. Repeater node's `metadata.mode: "preview"` On the same Repeater node, in `metadata` (parallel to `props`). Per `use-repeater`'s two-mode model: Design Mode (default) renders one placeholder. Preview Mode renders N real iterations. UI authors toggle this in Properties, under Configuration. API authors set the metadata directly. See the Repeater node shape in step 2 above for placement. -### 4. Condition Block as the immediate child of the Repeater (under `slots`) +### 4. Condition Block inside the item Box -Required by Studio's composition schema for reference iteration (single-CT AND multi-CT, `use-condition-block`). Narrows the iteration item to a specific content type before child bindings resolve. +Required for reference iteration (single-CT AND multi-CT, `use-condition-block`). Narrows the iteration item to a specific content type before child bindings resolve. -The Condition Block lives under a slot of the Repeater. Its discriminator is in `metadata.condition`: +The Condition Block sits in the slot of the item Box that is the Repeater's only child ([canonical list recipe](#canonical-list-recipe), step 4). Its discriminator is in `metadata.condition`. The Repeater's `slots`, from the item Box down: ```jsonc -{ - "type": "repeater", - "uid": "R1", - // …items binding / metadata.mode as in step 2… - "props": { - "items": { "binding": { /* …as step 2… */ } }, - "children": { "type": "slot", "slot": "<repeater-slot-uid>" } // ← wire the Repeater's slot - }, - "slots": { - "<repeater-slot-uid>": [ - { - "type": "condition-block", - "uid": "CB1", - "metadata": { - "condition": { - "type": "reference", "operator": "eq", "value": "product", - "conditionBinding": { "type": "repeater", - "value": { "repeaterUID": "R1", "path": { "_content_type_uid": {} } } } +"slots": { + "R1-slot": [ + { + "type": "box", "uid": "ITEM", "attrs": {}, "metadata": {}, "styles": {}, + "props": { "children": { "type": "slot", "slot": "ITEM-slot" } }, // ← the item Box + "slots": { + "ITEM-slot": [ + { + "type": "condition-block", + "uid": "CB1", "attrs": {}, "styles": {}, + "metadata": { + "condition": { + "type": "reference", "operator": "eq", "value": "product", + "conditionBinding": { "type": "repeater", + "value": { "repeaterUID": "R1", "path": { "_content_type_uid": {} } } } + } + }, + "props": { "children": { "type": "slot", "slot": "CB1-slot" } }, // ← Condition Block needs it too + "slots": { "CB1-slot": [ /* card subtree here — step 5 */ ] } } - }, - "props": { - "children": { "type": "slot", "slot": "<cb-slot-uid>" } // ← Condition Block needs it too - }, - "slots": { - "<cb-slot-uid>": [ - /* card subtree here — step 5 */ - ] - } + ] } - ] - } + } + ] } ``` Without `conditionBinding` the block silently renders nothing. Design mode hides this: verify in preview. -For modular-block iteration: `type: "modular_block"`, `value: "<block-uid>"`, `conditionBinding` and `dataBinding` on path `{ "<block-uid>": {} }`. +For modular-block iteration the condition reads the block uid, and carries a `dataBinding` of the same shape: + +```json +"condition": { + "type": "modular_block", "operator": "eq", "value": "<block-uid>", + "conditionBinding": { "type": "repeater", "value": { "repeaterUID": "R1", "path": { "<block-uid>": {} } } }, + "dataBinding": { "type": "repeater", "value": { "repeaterUID": "R1", "path": { "<block-uid>": {} } } } +} +``` -Both container nodes carry `props.children` pointing at their own slot ([`author-composition-bindings`](author-composition-bindings.md) § Node anatomy, point 4). The Condition Block also exposes a `children` slot prop. Skip its pointer and the card subtree never renders even though the Repeater iterates correctly. +All three container nodes (Repeater, item Box, Condition Block) carry `props.children` pointing at their own slot ([`author-composition-bindings`](author-composition-bindings.md) § Node anatomy, point 4). The Condition Block also exposes a `children` slot prop. Skip its pointer and the card subtree never renders even though the Repeater iterates correctly. ### 5. Card prop bindings: type `repeater`, with `repeaterUID` @@ -198,30 +304,19 @@ Composition entry ├── data_sources: [{ uid:"template", │ resolvedReferences:{ "template":["products"] } }] └── ui (zlib-inflated tree) - └── Section "ProductGrid" - └── Box (grid layout — register-component-render-contracts § Layout contract) - └── { type:"repeater", uid:"R1", - metadata:{ mode:"preview" }, - props:{ items:{ binding:{ type:"template", - value:{ path:{ products:{} } } } } }, - slots:{ "<R1-slot>":[ - { type:"condition-block", uid:"CB1", - metadata:{ condition:{ …step 4… } }, - slots:{ "<CB1-slot>":[ - { type:"site-product-card", uid:"CARD1", - props:{ - title:{ binding:{ type:"repeater", - value:{ repeaterUID:"R1", path:{ title:{} } } } }, - image:{ binding:{ type:"repeater", - value:{ repeaterUID:"R1", path:{ images:{ 0:{ url:{} } } } } } }, - price:{ binding:{ type:"repeater", - value:{ repeaterUID:"R1", path:{ price:{} } } } } - } } - ] } } - ] } } + └── Section "ProductGrid" (linked_schemas: [{ content_type_uid:"category" }], whole entry) + └── Box "list" (metadata.repeaterWrapper:true, grid + gap) + └── Repeater R1 (items: template path {products:{}}, metadata.mode:"preview") + └── Box "item" + └── Condition Block CB1 (type:"reference", operator:"eq", value:"product", + │ conditionBinding: repeater R1 path {_content_type_uid:{}}) + └── site-product-card CARD1 + title: repeater R1 path {title:{}} + image: repeater R1 path {images:{0:{url:{}}}} + price: repeater R1 path {price:{}} ``` -Five shapes: composition `resolvedReferences` + Repeater `props.items.binding` + Repeater `metadata.mode` + Condition Block in slot with `metadata.condition` + repeater-scope card bindings. Any one missing produces an empty render or an all-identical render. +The node-for-node JSON is in [the canonical list recipe](#canonical-list-recipe). Five shapes: composition `resolvedReferences` + Repeater `props.items.binding` + Repeater `metadata.mode` + Condition Block with `metadata.condition` (`conditionBinding` included) + repeater-scope card bindings. Any one missing produces an empty render or an all-identical render. ## Recipe: Repeat a card over a GROUP-MULTIPLE field @@ -234,7 +329,7 @@ Scenario: a `category` entry has a `features` field of type `group` + `multiple: | Shape | Multi-reference | **Group-multiple** | |---|---|---| | Composition `data_sources.resolvedReferences` | **Required**: reference stubs need resolving | **NOT needed**: values are inline on the parent entry | -| Condition Block as Repeater's immediate child | **Required**: references are polymorphic (any allowed CT). The block narrows | **NOT needed**: group-multiple has one shape, not polymorphic | +| Condition Block inside the item Box | **Required**: references are polymorphic (any allowed CT). The block narrows | **NOT needed**: group-multiple has one shape, not polymorphic | | Repeater `props.items.binding` | `type: "template"`, path at the multi-ref field | (same) `type: "template"`, path at the group-multiple field | | Repeater `metadata.mode: "preview"` | Required for canvas iteration | (same) Required | | Card prop bindings | `type: "repeater"`, `repeaterUID`, no index | (same) `type: "repeater"`, `repeaterUID`, no index | @@ -248,27 +343,17 @@ Composition entry ├── composable_uid: "<15-char>" (= CMA entry uid) ├── data_sources: [/* no resolvedReferences entry needed */] └── ui (zlib-inflated tree) - └── Section "FeatureList" - └── Box (flex / grid layout — register-component-render-contracts § Layout contract) - └── { type:"repeater", uid:"R2", - metadata:{ mode:"preview" }, - props:{ items:{ binding:{ type:"template", - value:{ path:{ features:{} } } } } }, - slots:{ "<R2-slot>":[ - /* card goes here directly — no Condition Block wrapper */ - { type:"site-feature-card", uid:"FC1", - props:{ - headline:{ binding:{ type:"repeater", - value:{ repeaterUID:"R2", path:{ headline:{} } } } }, - body:{ binding:{ type:"repeater", - value:{ repeaterUID:"R2", path:{ body:{} } } } }, - icon:{ binding:{ type:"repeater", - value:{ repeaterUID:"R2", path:{ icon:{ url:{} } } } } } - } } - ] } } + └── Section "FeatureList" (whole-entry scope) + └── Box "list" (metadata.repeaterWrapper:true, flex / grid + gap) + └── Repeater R2 (items: template path {features:{}}, metadata.mode:"preview") + └── Box "item" + └── site-feature-card FC1 (no Condition Block) + headline: repeater R2 path {headline:{}} + body: repeater R2 path {body:{}} + icon: repeater R2 path {icon:{url:{}}} ``` -The card sits **directly inside the Repeater's slot**, no Condition Block in between, because the iteration item has one shape (the group's sub-fields). Each card prop reads from the iteration item via `repeaterUID:"R2"` + the field path on the item. +The card sits **directly inside the item Box**, no Condition Block, because the iteration item has one shape (the group's sub-fields). Each card prop reads from the iteration item via `repeaterUID:"R2"` + the field path on the item. ### Decision rule: which recipe to use diff --git a/skills/studio/author-composition-section-slot-fill.md b/skills/studio/author-composition-section-slot-fill.md index 998e79f..961c11a 100644 --- a/skills/studio/author-composition-section-slot-fill.md +++ b/skills/studio/author-composition-section-slot-fill.md @@ -27,66 +27,98 @@ Sibling to the multi-reference recipe in [`author-composition-repeater-recipes`] ## Wrapper section: Repeater + CB + section-slot -```jsonc -// ui tree of the wrapper section +```json { - "type": "repeater", "uid": "R1", - "metadata": { "mode": "preview" }, + "uid": "R1", + "type": "repeater", + "attrs": {}, + "metadata": { "title": "Repeater — Authors", "mode": "preview" }, "props": { "items": { "binding": { "type": "template", "value": { "path": {} } } }, - "children": { "type": "slot", "slot": "<R1-slot>" } // required, see repeater-recipes step 2 + "children": { "type": "slot", "slot": "R1-slot" } }, - "slots": { "<R1-slot>": [ - { "type": "condition-block", "uid": "CB1", - "metadata": { "condition": { "type": "reference", "operator": "eq", "value": "author", - "conditionBinding": {…} } }, // required, see repeater-recipes step 4 - "props": { "children": { "type": "slot", "slot": "<CB1-slot>" } }, - "slots": { "<CB1-slot>": [ - { "type": "section-slot", "uid": "SS1", - "slots": { "SS1": [] } } - ] } } - ] } } + "slots": { + "R1-slot": [ + { + "uid": "CB1", + "type": "condition-block", + "attrs": {}, + "metadata": { + "title": "Condition — author", + "condition": { + "type": "reference", "operator": "eq", "value": "author", + "conditionBinding": { "type": "repeater", + "value": { "repeaterUID": "R1", "path": { "_content_type_uid": {} } } } + } + }, + "props": { "children": { "type": "slot", "slot": "CB1-slot" } }, + "slots": { + "CB1-slot": [ + { "uid": "SS1", "type": "section-slot", "attrs": {}, "metadata": { "title": "Section Slot — Author" }, + "props": {}, "slots": {}, "styles": {} } + ] + }, + "styles": {} + } + ] + }, + "styles": {} +} ``` +The wrapper section's `ui` tree, from its Repeater down. `props.children` on the Repeater and the Condition Block is required ([`author-composition-repeater-recipes`](author-composition-repeater-recipes.md) steps 2 and 4), and so is `conditionBinding`. The `section-slot` node is a leaf with `"slots": {}`, never `{ "SS1": [] }` ([`author-composition-sections`](author-composition-sections.md) § `section-slot` node shape). + +For Visual Editor item tags, a Box between the Repeater and the Condition Block is the general rule ([`make-visual-editor-editable`](make-visual-editor-editable.md) § 2). It has not been verified together with this pattern's `selected_field` scoping, so check the item tags after adding it. + Wrapper section entry also declares `linked_schemas: [{ content_type_uid: "<parent-CT>", selected_field: "<ref-or-mb-field>" }]` (group-multiple field, see [`author-composition-sections`](author-composition-sections.md) § The `linked_schemas`-as-reference trap). ## Fill section: Simple Section per branch One fill section per allowed CT (references) or block (MB). No root Repeater. Its top-level node is the leaf component. `linked_schemas.selected_field` names the reference target CT (or block schema path) so the SDK scopes each iteration's item into the section's `dataSources.template`. -```jsonc -// ui tree of the fill section (e.g. author-card) +```json { - "type": "author-card", "uid": "AC1", + "uid": "AC1", + "type": "author-card", + "attrs": {}, + "metadata": { "title": "Author card" }, "props": { "name": { "binding": { "type": "template", "value": { "path": { "name": {} } } } }, "photo": { "binding": { "type": "template", "value": { "path": { "photo": { "url": {} } } } } } - } + }, + "slots": {}, + "styles": {} } ``` +The fill section's leaf node (here an `author-card`), shown without the root `page` node that holds it. + Note the fill section's bindings use `type: "template"` (NOT `type: "repeater"`). The wrapper's SDK scoping already unwraps the iteration item into the fill section's `template` scope. ## Template: fill nodes in the placed wrapper's `slots` The template's `section-composition` node embedding the wrapper holds each fill as a real `section-composition` node in its `slots`, keyed by the section-slot uid. Studio writes this shape when an author fills a slot: -```jsonc +```json { - "type": "section-composition", "uid": "INST-1", + "type": "section-composition", + "attrs": {}, "metadata": { "compositionUID": "<wrapper section's entry uid, blt…>" }, + "props": {}, "slots": { "SS1": [ - { "type": "section-composition", "uid": "FILL-1", + { "uid": "FILL-1", "type": "section-composition", "attrs": {}, "metadata": { "compositionUID": "<author-fill section's entry uid, blt…>" }, - "props": {}, "slots": {}, "attrs": {}, "styles": {} } + "props": {}, "slots": {}, "styles": {} } ] - // one key per CB branch's section-slot uid, each holding its own fill - } + }, + "styles": {} } ``` +One key per CB branch's section-slot uid, each holding its own fill. A slot with no fill gets no key, not an empty array. + Also populate the template's `linked_sections` with references to BOTH the wrapper section entry AND every distinct fill section entry. ## Assembled: reference-iterating wrapper filled by an author fill @@ -95,7 +127,7 @@ Also populate the template's `linked_sections` with references to BOTH the wrapp Wrapper section linked_schemas: [{ ct: "blog_post", selected_field: "primary_author" }] ui: Repeater(items:{path:{}}, mode:"preview") └── ConditionBlock(condition:{type:"reference", value:"author", conditionBinding}) - └── section-slot uid:"SS1" slots:{"SS1":[]} + └── section-slot uid:"SS1" slots:{} Author fill section linked_schemas: [{ ct: "blog_post", selected_field: "primary_author" }] ui: author-card diff --git a/skills/studio/author-composition-sections.md b/skills/studio/author-composition-sections.md index 3692451..f2fc538 100644 --- a/skills/studio/author-composition-sections.md +++ b/skills/studio/author-composition-sections.md @@ -70,45 +70,21 @@ Two outcomes from this: So you can have a section that renders perfectly on the deployed site (runtime path uses the override) yet shows empty repeaters in the editor (editor path's `linked_schemas` is empty). Both metadata paths must point to the same `selectedField` for editor + runtime to agree. -### Recipe: section iterates one field on the page +### Recipe: a section that renders a list -When a section's `selectedField` is set to the iterated field (e.g. `selectedField: "products"` and the section's job is to render one card per product), the section's `dataSources.template` IS the products array. **A top-level Repeater inside the section must bind to scope-root, NOT to `{ path: { products: {} } }`**: +Use the [canonical list recipe](author-composition-repeater-recipes.md#canonical-list-recipe): whole-entry scope (no `selected_field`), a list Box, a Repeater bound to `{ "<field>": {} }`, an item Box, then the Condition Blocks. It is the only list shape that renders AND gives Visual Editor its list and item tags. -```jsonc -{ - "type": "repeater", - "uid": "R-section-products", - "metadata": { "mode": "preview" }, - "props": { - "items": { - "binding": { - "type": "template", - "value": { "path": {} } // ← scope-root, NOT { products: {} } - } - }, - "children": { "type": "slot", "slot": "<R-slot>" } // ← required, wires the slot below - }, - "slots": { - "<R-slot>": [ - // ConditionBlock + card here, per the multi-reference recipe; - // card bindings still use type:"repeater" with repeaterUID - ] - } -} -``` +**Inherited sections scoped to the list field.** A section with `selectedField: "products"` has the products array as its `dataSources.template`, so its top-level Repeater must bind to scope-root `{ "path": {} }` (`getBindingStringForCS` flattens it to `""`). A path of `{ products: {} }` there resolves to `template.products`, undefined. That section renders, but Visual Editor finds no list or item tags. When you touch it, re-scope it to the whole entry and bind the Repeater to `{ products: {} }`. -`getBindingStringForCS` flattens `{path: {}}` to `""`, so the binding resolves to `dataSources.template` directly. With `selectedField: "products"`, that IS the products array. A path of `{products: {}}` would resolve to `dataSources.template.products`, undefined when the section's template IS the array, not an object containing one. +Reference iterations still need `data_sources.resolvedReferences` on the composition: key `"template"`, value is the array of field paths on the parent entry (e.g. `"template": ["products"]`), in the per-API form from [`author-composition-entry`](author-composition-entry.md#wire-shapes) § Wire shapes. Set them on the parent (template) composition, not the section composition. -Reference iterations still need `data_sources.resolvedReferences` on the composition: key `"template"`, value is the array of field paths on the parent entry (e.g. `"template": ["products"]`). The runtime resolves these along the `selectedField` path. You set them on the parent (template) composition, not the section composition. - -### Decision rule: single-field repeater section vs whole-entry section +### Decision rule: list section vs whole-entry section | Section's job | `selectedField` | Repeater binding (if any) | When to use | |---|---|---|---| -| One repeater over one field of the page (a card grid, a feature list) | Set to that field (`"products"`) | Scope-root `{ path: {} }` | Most reusable sections: Editions list, Features grid, Card grid | +| A list over one field of the page (a card grid, a feature list) | **Leave unset**: whole entry | `template` path `{ "<field>": {} }` | Every list section. [Canonical list recipe](author-composition-repeater-recipes.md#canonical-list-recipe) | | A custom component reading several fields off the page entry (header reading brand + nav_links + signin_label. Hero reading the `hero` group) | **Leave unset**: section's `template` is the whole page entry | Original `template.<field>` paths work verbatim | Single-purpose sections with no iteration: Header, Hero, static 3D block | - -The choice is data-shape-driven: one field iterated calls for `selectedField` plus a scope-root. Many fields read calls for whole-entry scope and the original paths. +| One item of a top-level Group or single reference | Set to that field | None | A section that reads one nested object, not a list | ### Verifying a section's scope headlessly @@ -123,23 +99,26 @@ console.log(JSON.stringify(spec.data.section_scoped_data["<instance-uid>"], null `spec.data.section_scoped_data[<instance-uid>]` is the runtime resolution: `selectedField` + the actual scoped `template` array. If `template` shows the right array length and right entry sample, the section's scope is correct. If it's `undefined` or wrong-shaped, the bindings won't work. -## `section-slot` node shape: the one legitimate empty-slots node +## `section-slot` node shape: a leaf with no slot key -The built-in `section-slot` type declares a **placeholder** inside a section's `ui`. The template that embeds this section then fills the slot via a `section-composition` node (see next recipe: Pattern B). Non-obvious shape: +The built-in `section-slot` type declares a **placeholder** inside a section's `ui`. The template that embeds this section then fills the slot via a `section-composition` node (see next recipe: Pattern B). It is a leaf: write the full seven-key envelope with an **empty `slots` object**: -```jsonc +```json { - "type": "section-slot", "uid": "SS1", + "type": "section-slot", + "attrs": {}, + "metadata": { "title": "Section Slot — Card", "visible": true, "locked": false }, "props": {}, - "slots": { "SS1": [] } // ← key is the node's OWN uid; value is [] until filled + "slots": {}, + "styles": {} } ``` Two things that trip API authors: -1. **`slots.<own-uid>: []` is required**: the section-slot's own uid appears as the slot key, with an empty array. This is one of only two legitimate exceptions to the preflight rule "no empty slot arrays". The other is the page-side `section-composition` node, where Studio writes `slots.<section-slot-uid>: []` for each slot the template hasn't filled yet. Omitting the entry (no `slots` at all, or `slots: {}`) leaves the template unable to target the slot when composing. -2. **The slot is filled by the TEMPLATE, not the section.** In the section's authored tree the slot stays empty. The template's `section-composition` node holds the fill in its own `slots`, keyed by the section-slot uid ([Pattern B](author-composition-section-slot-fill.md)). +1. **No `slots.<own-uid>` entry.** `"slots": { "SS1": [] }` is rejected by the Compositions API with `422 Empty slots crash the renderer.` The raw CMA accepts it, so the same shape can reach the renderer that way. The node's uid is already the key the template fills under, and the SDK renders `section-slot` from the template's fill, never from the node's own `slots`. There is no exception to the preflight rule "no empty slot arrays" ([`author-composition-encoding`](author-composition-encoding.md) § Preflight). +2. **The slot is filled by the TEMPLATE, not the section.** In the section's authored tree the slot stays empty. The template's `section-composition` node holds the fill in its own `slots`, keyed by the section-slot uid ([Pattern B](author-composition-section-slot-fill.md)). A slot the template hasn't filled yet has no key there either. Section slots typically sit inside a Repeater + Condition Block on the wrapper section (Pattern B for reference / MB iteration) or directly under the section root (Pattern B for a simpler single-slot section). diff --git a/skills/studio/author-composition-static-values.md b/skills/studio/author-composition-static-values.md index f40f70f..5c04b4c 100644 --- a/skills/studio/author-composition-static-values.md +++ b/skills/studio/author-composition-static-values.md @@ -23,3 +23,4 @@ Consequences for API authoring: - **Widget-to-subtype pairing.** The node's `props.<name>.type` (`"string"` / `"number"` / `"href"` / `"imageurl"` / `"choice"` / …) determines which sub-bucket the SDK looks in. `string` is the exception: its literal never goes in `static_value.string`. A single-line string goes in **`text`**, a multiline string (textarea widget) in **`textarea`**, as in the template recipe's `headline` and `subhead`. Verify the bucket against the registered schema or a working composition before writing. Other types use the bucket of the same name. If you write the key to the wrong subtype array, the resolver misses and the prop renders as the schema default. - **`type` also decides whether the binder FLATTENS the value: never write `"any"` for structured data.** The binder runs a recursive single-key unwrap on every type except `object` and `array` (`retrieve-data.ts`: `if (type !== "object" && type !== "array" …) resolvedData = getFlattenedData(resolvedData)`), descending while an object has exactly one key (`$` and `_metadata` don't count). So a node prop typed `"any"` and bound to a single-field block (`image_grid → { image: [...] }`) receives the **inner array**, not the block: lookups return `undefined`, indices surface as `{0,1,2,3}`, and it renders blank with no error. Write `"object"` for a group / Global Field / block object and `"array"` for a list. Those two skip the unwrap. Full mechanism: [`register-component` § The `any` flatten trap](register-component.md). - **Convention Studio uses:** keys are `<node-uid>-<propName>` (e.g. `mHjnXsgKbBlj_PZ-headline`). Not required but stable and readable. +- **Leading and trailing spaces are trimmed.** A static value of `", "` renders as `","`, so a separator between two bound values collapses: `Hale,Co-founder`. Write a non-breaking space instead (`"\u00a0"` in the JSON payload, so `",\u00a0"`) where an edge space must survive, or better, give the component a `prefix` / `separator` prop and let it add the spacing in code. diff --git a/skills/studio/author-composition-templates.md b/skills/studio/author-composition-templates.md index 89c04fd..1be3675 100644 --- a/skills/studio/author-composition-templates.md +++ b/skills/studio/author-composition-templates.md @@ -190,13 +190,15 @@ The `ui` value on the wire is the tree above, JSON-stringified, `zlib.compress`e "datestring":[], "json_rte": [], "html_rte": [], - "object": [], - "ui": "zlib:eNqtlFtvgjAUgP9L…" - } + "object": [] + }, + "ui": "zlib:eNqtlFtvgjAUgP9L…" } } ``` +`ui` is a top-level entry field, a sibling of `static_value`, never inside it. The `number` value is a bare `49` because this stack's compositions CT types `static_value.number.value` as `number`. On a CT built from [`provision-studio-project`](provision-studio-project.md) it is text, so write `"49"` ([`author-composition-entry`](author-composition-entry.md#wire-shapes) § Wire shapes). + Studio-generated `_metadata.uid` on each `static_value.*` entry is added by the CMA on write. Do not populate it yourself. ### Python builder: produces the exact body above @@ -246,7 +248,7 @@ body = { {"key": "mHjnXsgKbBlj_PZ-ctaHref", "value": ""}, {"key": "Wo-7Ly0xHXWsOXQ-ctaHref", "value": "https://example.com"}, ], - "number": [{"key": "Wo-7Ly0xHXWsOXQ-price", "value": 49}], + "number": [{"key": "Wo-7Ly0xHXWsOXQ-price", "value": 49}], # "49" if the CT types it as text "choice": [{"key": "Wo-7Ly0xHXWsOXQ-badge", "value": []}], "array": [], "boolean": [], "any": [], "datestring": [], "json_rte": [], "html_rte": [], "object": [], diff --git a/skills/studio/author-composition-thumbnails.md b/skills/studio/author-composition-thumbnails.md index 1b84cfc..0a098a0 100644 --- a/skills/studio/author-composition-thumbnails.md +++ b/skills/studio/author-composition-thumbnails.md @@ -14,13 +14,13 @@ Part of the API-authoring set. Start at [`author-composition-via-api`](author-co **Symptom:** every API-authored Section shows a blank/placeholder tile in Studio's **Sections** accordion, while UI-authored ones show a picture of the section. Nothing errors. -**Why.** Studio's Sections accordion renders `section.ui_preview.url` (`ui_preview` is a `file` field holding an asset). When an author saves a Section **in the Studio UI**, the app screenshots the canvas node, converts the base64 to a blob, uploads it as an asset, and PUTs `{ entry: { ui_preview: <assetUid> } }` on the composition entry, fire-and-forget, master locale, then invalidates the sections query so the tile appears without a reload. **That whole chain lives in the editor.** A composition entry created by CMA has `ui_preview` empty forever. +**Why.** Studio's Sections accordion renders `section.ui_preview.url` (`ui_preview` is a `file` field holding an asset). When an author saves a Section **in the Studio UI**, or opens one that has no thumbnail, the app screenshots the canvas node, converts the base64 to a blob, uploads it as an asset, and PUTs `{ entry: { ui_preview: <assetUid> } }` on the composition entry, fire-and-forget, master locale, then invalidates the sections query so the tile appears without a reload. **That whole chain lives in the editor.** A Section created through the API or the CMA has `ui_preview` empty until someone opens or saves it in Studio. Same asymmetry family as `linked_sections`: the UI populates a field behind the scenes, so API authoring must do it explicitly. ### Fix A: zero-script backfill (best when the sections already exist) -Open each Section in Studio and hit **Save**, no edits needed. The screenshot/upload/PUT chain runs on save and backfills `ui_preview` with a real render of the section. Cheapest correct fix, and the thumbnail is an actual screenshot rather than a placeholder. Only worth scripting instead when the section count makes clicking impractical. +Open each Section in Studio, no edits needed. A Section with no thumbnail gets one once the canvas has rendered it, and **Save** takes one too. A Section whose props bind to entry data gets none on open until you pick a preview entry for it. The screenshot/upload/PUT chain backfills `ui_preview` with a real render of the section. Cheapest correct fix, and the thumbnail is an actual screenshot rather than a placeholder. Only worth scripting instead when the section count makes clicking impractical. ### Fix B: pick an image in the Edit Section modal (no canvas render, no script) diff --git a/skills/studio/author-composition-via-api.md b/skills/studio/author-composition-via-api.md index 7d6379e..ddfd60d 100644 --- a/skills/studio/author-composition-via-api.md +++ b/skills/studio/author-composition-via-api.md @@ -40,7 +40,7 @@ If any of these is missing, this skill is the wrong starting point. > **Auth: OAuth first.** Snippets here write `-H "$CS_AUTH"`, resolved once per run. **Assume there is no session `authtoken`**. The user authenticates with OAuth (MCP `--auth` / `csdx auth:login --oauth`) and never pastes one. Resolution order, scope headers (`organization_uid` vs `api_key`), the expiry/refresh rule and the 401/422 signature table: [`authenticate-cma`](authenticate-cma.md). -> **MCP-first execution.** Discover tools and verify the target with a read call per [`install-contentstack-mcp`](install-contentstack-mcp.md). When supported tools are callable, the entry write + publish run as MCP tool calls (`create_an_entry` / `update_an_entry` / `publish_an_entry`) with no pasted `authtoken`. The composition **shape** is unchanged: you still build the `ui` (`zlib:<base64>`), `data_sources`, `static_value`, and `composable_uid` (= entry uid) exactly as below. The MCP only replaces the raw CMA transport. **Shell-corruption caveat on the MCP path:** `create_an_entry` takes `entry_data` as an **inline JSON object: there is no file-path param**, so the temp-file / `curl -d @file` guard does NOT apply. Instead **build the entire `entry_data` object programmatically and hand it to the tool, never hand-transcribe the `ui` blob into the arguments**, and assert the `"children"` count in the tree before the call (see the shell-corruption pitfall). If hand-assembly is unavoidable, keep the raw `curl -d @file` path for the write. A missing readiness flag requires discovery; use HTTP only for unavailable operations or a recorded connection limitation. +> **MCP-first execution.** Discover tools and verify the target with a read call per [`install-contentstack-mcp`](install-contentstack-mcp.md). When supported tools are callable, the entry write + publish run as MCP tool calls (`create_an_entry` / `update_an_entry` / `publish_an_entry`) with no pasted `authtoken`. The composition **shape** is unchanged: you still build the `ui` (`zlib:<base64>`), `data_sources`, `static_value`, and `composable_uid` (= entry uid) exactly as below. The MCP only replaces the raw CMA transport. `entry_data` is wrapped like the HTTP body: `{ "entry": { … } }`. **If the compositions CT was created in this session, its entry create, update and publish go over HTTP until Claude Code restarts**: the MCP only accepts content types that existed when it started ([`install-contentstack-mcp`](install-contentstack-mcp.md#cts-created-after-start) Step 6b). **Shell-corruption caveat on the MCP path:** `create_an_entry` takes `entry_data` as an **inline JSON object: there is no file-path param**, so the temp-file / `curl -d @file` guard does NOT apply. Instead **build the entire `entry_data` object programmatically and hand it to the tool, never hand-transcribe the `ui` blob into the arguments**, and assert the `"children"` count in the tree before the call (see the shell-corruption pitfall). If hand-assembly is unavoidable, keep the raw `curl -d @file` path for the write. A missing readiness flag requires discovery; use HTTP only for unavailable operations or a recorded connection limitation. ## The API-authoring set: read the one you need @@ -77,14 +77,14 @@ This skill succeeds only when ALL are true. - [ ] **Every `place_composition_as: "page"` entry has BOTH `url` and `url_metadata`**, `url_source` matching how the pattern is derived (`content_type_url_pattern` for Connected, `default_url_pattern` / `user_specified_pattern` for Freeform), `url_queries` as a JSON string. **Read the entry back after the write** and confirm `url_metadata` is present: a compositions CT missing the group makes the CMA drop it silently, leaving the composition on `legacy_url`. - [ ] `data_sources.resolvedReferences` is set per reference field the composition iterates. - [ ] Every Repeater node has `metadata.mode: "preview"` (for canvas iteration). -- [ ] Every Repeater iterating a reference or modular block has a Condition Block as its immediate child. +- [ ] Every list follows the [canonical list recipe](author-composition-repeater-recipes.md#canonical-list-recipe): whole-entry scope, list Box with `repeaterWrapper: true`, Repeater bound to the field, an item Box as its only child, and (for references and modular blocks) the Condition Blocks inside that Box. - [ ] **Every Condition Block's `metadata.condition` has `operator` and `conditionBinding`**, and the composition was checked in **preview mode**. Without the binding the block renders nothing, but design mode still shows it. - [ ] Card / iteration child bindings use `type: "repeater"` with `repeaterUID`, NOT `type: "template"` with `.0.<field>` fixed-index paths. - [ ] **Every Section composition created in this run has `ui_preview` set, verified by re-reading each entry.** A section with a blank tile is an unfinished section. Do not report the work as done. Also true of sections created as a side effect (clones, rescopes, batch builds). - [ ] **Every Section composition has `ui_preview` set**, an uploaded, **published** asset uid, verified by re-reading the entry (`.entry.ui_preview.url` resolves). Blank tiles in Studio's Sections accordion otherwise. Fastest fix is often opening each section in Studio and pressing Save. See § [Section thumbnails](author-composition-thumbnails.md#section-thumbnails-ui-preview) - [ ] **Envelope complete**: every node in the written tree has all seven keys (`uid`, `type`, `props`, `metadata`, `slots`, `attrs`, `styles`), verified by re-reading the entry. A missing `slots` renders fine and white-screens the Data tab later. -- [ ] **Preflight passed**: every `slots[<uid>]` is a non-empty array (exception: `section-slot` nodes carry `slots: { "<own-uid>": [] }` by design). No orphan descendant references in the tree. -- [ ] **[Pattern B](author-composition-section-slot-fill.md) (if used)**: wrapper section has `section-slot` node with `slots:{"<own-uid>":[]}`. Fill section's leaf bindings use `type:"template"` (NOT `repeater`). Template's `section-composition` node has `slots.<slot-uid>: [<fill section-composition node>]`. Template's `linked_sections` includes BOTH wrapper and fill entries. +- [ ] **Preflight passed**: every `slots[<uid>]` is a non-empty array, no exceptions (`section-slot` nodes carry `"slots": {}`). No orphan descendant references in the tree. +- [ ] **[Pattern B](author-composition-section-slot-fill.md) (if used)**: wrapper section has a `section-slot` node with `"slots": {}`. Fill section's leaf bindings use `type:"template"` (NOT `repeater`). Template's `section-composition` node has `slots.<slot-uid>: [<fill section-composition node>]`. Template's `linked_sections` includes BOTH wrapper and fill entries. - [ ] **Every `section-composition` node's `metadata.compositionUID` is the placed section's entry uid** (`blt…`), never its `composable_uid`. A `composable_uid` there fails in the canvas with `Component with type '<it>' is not registered`. - [ ] **A section whose `selected_field` is a multiple group / global field / file / link** has `cardinality` from the user's answer (asked, never assumed): `"collection"` with a root Repeater, or `"single"` without one. Reference and modular-block sections carry no `cardinality` ([`understand-linked-schemas`](understand-linked-schemas.md)). - [ ] **Exposed section props (if used)**: `sectionExposedProps` array on section's `ui` ROOT `metadata`. Template's `section-composition.props` map keyed by each exposure's `uid`. Per-instance override values use distinct static_value keys. diff --git a/skills/studio/build-repeating-section.md b/skills/studio/build-repeating-section.md index 21917d3..d134028 100644 --- a/skills/studio/build-repeating-section.md +++ b/skills/studio/build-repeating-section.md @@ -33,7 +33,7 @@ Before reaching for the parent+child+Repeater pattern, check whether a **single The pattern: -1. **Register a component** with an `array`-typed prop (e.g. `cards: { type: "array" }`). +1. **Register a component** with an `array`-typed prop (e.g. `cards: { type: "array", items: { type: "any" } }`; `items` is required on studio-react 2.x). 2. **Bind that prop to a Reference field** on the section's linked schema. 3. **Populate `data_sources.resolvedReferences`** in the composition. This tells the SDK to include the referenced entries via CDA `?include[]=<ref_field>`, so the bound array arrives at the component as **full resolved entry objects**, not `{uid}` stubs. 4. **The component does its own `.map()`** to render each item, no Studio Repeater, no Condition Block, no iteration scope inside Studio. @@ -44,7 +44,7 @@ registerComponent({ type: "doc-card-list", component: CardList, props: { - cards: { type: "array" }, // <-- bound to a multi-Reference field on the section's linked schema + cards: { type: "array", items: { type: "any" } }, // <-- bound to a multi-Reference field on the section's linked schema }, }); @@ -95,7 +95,7 @@ Gate: open the child standalone. It must adapt at any canvas width. If it render In Studio, open the Sections tab, then click + New Section. Name it `parentSectionName`. -Link the parent to `parentLinkedSchema` (the CT / Global Field that holds `iterationSourceField`). Use the same Connect-A-Schema flow `build-section` describes. +Link the parent to `parentLinkedSchema` (the CT / Global Field that holds `iterationSourceField`). Use the same Connect-A-Schema flow `build-section` describes. **Link the whole schema, don't select `iterationSourceField` as the section's field.** A section scoped to the list field renders, but Visual Editor gets no list or item tags. This step and Steps 3 to 6 are the [canonical list recipe](author-composition-repeater-recipes.md#canonical-list-recipe) built in the UI. ### Step 3: Drop a layout container Box first @@ -108,7 +108,15 @@ Link the parent to `parentLinkedSchema` (the CT / Global Field that holds `itera | `vertical-list` | `display: flex; flex-direction: column; gap: 16px;` | | `horizontal-scroll` | `display: flex; gap: 16px; overflow-x: auto; scroll-snap-type: x mandatory;` with `flex: 0 0 280px` on the cell | -The Box defines the cell size. The Slot will sit inside the Repeater inside this Box, so each iteration fills one grid track / flex item / list row. +The Box defines the cell size. The Slot will sit inside the Repeater inside this Box, so each iteration fills one grid track / flex item / list row. Always space items with `gap` on this Box, never with sibling selectors: see § The Repeater's wrapper element below. + +#### The Repeater's wrapper element + +The Repeater wraps each item in its own `<div data-composable-studio-wrapper="repeater" style="display: contents">`. A list Box rendered as `<ul>` with `<li>` items becomes `ul > div > li`: + +- **Sibling- and child-based CSS stops matching.** `space-y-*` / `space-x-*`, `divide-*`, `> * + *` margins, `li:first-child`, `:nth-child()` and `ul > li` all see the wrapper divs, not the items. Margins on the wrapper do nothing, because a `display: contents` element has no box. +- **Flex and grid still work.** `display: contents` lifts each item into the list Box's layout, so `display: flex` / `grid` with `gap` spaces the items exactly as authored. Use that for every list container. +- **`ul > div` is invalid list markup.** Screen readers may stop announcing the list and its count. If list semantics matter, render the list Box as a plain `div` and give it `role="list"` with `role="listitem"` on each item's element, or keep the `ul`/`li` inside a registered component that maps the array itself (§ When to skip the Repeater entirely). ### Step 4: Drop a Repeater inside the Box @@ -118,18 +126,17 @@ Bind the Repeater's **Bind items** property to `iterationSourceField` via the Da For multi-reference / Modular Block iteration, see Step 5 (Condition Block). For `group` / `array` / `pinned-query`, skip to Step 6. -### Step 5: Wrap in a Condition Block (when required) +### Step 5: Drop an item Box, then a Condition Block (when required) -If `iterationItemKind` is `reference` (single-CT or multi-CT) or `modular-block`, the Repeater's immediate child must be a **Condition Block** (Studio schema requirement); for Visual Editor's item controls, those may sit inside one Box that is the Repeater's only child ([`make-visual-editor-editable`](make-visual-editor-editable.md) § 2). Two ways: +Drop a **Box** inside the Repeater as its only child, whatever `iterationItemKind` is. Visual Editor's per-item tag (move, delete, add) lands on that Box. Without it, a Condition Block holding a Section or Section Slot drops every item tag ([`make-visual-editor-editable`](make-visual-editor-editable.md) § 2). -- **Preferred**: drop a Section Slot inside the Repeater (Step 6), then when you later drop the child Section into the Slot at template time, Studio surfaces an inline hint that this iteration source needs a Condition Block. Accept the wrap and Studio inserts it for you. -- **Manual**: drop a Condition Block inside the Repeater first. Then drop the Section Slot inside the Condition Block. +If `iterationItemKind` is `reference` (single-CT or multi-CT) or `modular-block`, drop a **Condition Block** inside the item Box, one per content type or block. Studio's inline "needs a Condition Block" prompt (Step 8) inserts one directly under the Repeater, without the Box, so add it manually here instead. -For `group` / `array` / `pinned-query`, no Condition Block is needed. +For `group` / `array` / `pinned-query`, no Condition Block is needed: the Section Slot goes straight into the item Box. -### Step 6: Drop exactly ONE Section Slot inside the Repeater (or its Condition Block) +### Step 6: Drop exactly ONE Section Slot inside the Condition Block (or the item Box) -Drag a **Section Slot** from the palette into the Repeater (or the Condition Block from Step 5). One Slot, not multiple. +Drag a **Section Slot** from the palette into the Condition Block from Step 5 (or the item Box, when there is no Condition Block). One Slot per branch, not multiple. Set the Slot's **Drop placeholder label** to something intent-revealing: `Drop ${childSectionName} here` (e.g. `Drop ProductCard here`). The label is the only contract template authors have with this Section. @@ -140,11 +147,12 @@ Set the Slot's **Drop placeholder label** to something intent-revealing: `Drop $ Save. In Layers, the tree should read: ``` -Section "<parentSectionName>" -└── Box (layoutShape container) +Section "<parentSectionName>" (linked to the whole schema) +└── Box (layoutShape container, gap) └── Repeater (bound to <iterationSourceField>) - └── (Condition Block, if reference/modular-block) - └── Section Slot (label: Drop <childSectionName> here) + └── Box (item) + └── (Condition Block, if reference/modular-block) + └── Section Slot (label: Drop <childSectionName> here) ``` In the parent Section's own canvas (with no template open), the Repeater shows one placeholder iteration containing the empty Slot. This is normal. Fills happen at template / parent-of-parent time. @@ -158,7 +166,7 @@ What happens: - Section-level auto-binding kicks in: the child's linked schema (`childLinkedSchema`) is matched to the Repeater's iteration scope. The child Section's bindings resolve against each iterated item. - Zero manual binding clicks. The child Section is pre-bound by virtue of being a Section. -If the iteration source is a single-CT / multi-CT Reference (or Modular Block) and you didn't add a Condition Block manually in Step 5, Studio prompts you to wrap. Accept. +If the iteration source is a single-CT / multi-CT Reference (or Modular Block) and you didn't add a Condition Block in Step 5, Studio prompts you to wrap. Accepting puts the Condition Block directly under the Repeater, so move it into the item Box afterwards. ### Step 9: Toggle Preview Mode to verify @@ -190,7 +198,9 @@ This skill succeeds only when ALL are true: - [ ] **BOTH Sections (child and parent) have a `ui_preview`**, a blank tile in the Sections accordion otherwise. UI-created sections get one from Studio on Save. API-created ones need one attached explicitly. See [`author-composition-via-api` § Section thumbnails](author-composition-thumbnails.md#section-thumbnails-ui-preview). - [ ] The parent Section's Box has explicit layout CSS for `layoutShape` (grid tracks, flex with basis, etc.) defining each cell's size. - [ ] The Repeater is bound to `iterationSourceField` via the Data Picker (confirmed by inspecting the Bind items breadcrumb). -- [ ] Exactly ONE Section Slot sits inside the Repeater (or its Condition Block). +- [ ] Exactly ONE Section Slot sits inside each Condition Block (or the item Box). +- [ ] The parent Section is linked to the whole schema, not scoped to `iterationSourceField`. +- [ ] The list Box spaces items with flex/grid `gap`, not sibling selectors (`space-y-*`, `> * + *`). - [ ] When the parent Section is dropped on a template and the child Section is dropped into the Slot, **no manual prop binding** is needed. The child binds automatically at Section level. - [ ] Preview Mode on the Repeater renders N iterations of the child Section, each iteration's bindings showing the corresponding item's data. - [ ] No iteration renders full-bleed (parent Box defines the cell size correctly). @@ -202,7 +212,8 @@ This skill succeeds only when ALL are true: | Dropping a raw registered card into the Slot | Build it as a Section first. Drop the Section. See `understand-section-slots`. | | Skipping the Box (Step 3), Repeater bare on Section root | Wrap Repeater in a sized Box. See `use-section-slot` § Layout container. | | Multiple Section Slots inside the Repeater | Exactly ONE Slot per Repeater. Put multi-region content inside the child. | -| Skipping Condition Block for Reference / Modular Block iteration | Accept Studio's inline wrap prompt, or insert Condition Block manually (Step 5). | +| Skipping Condition Block for Reference / Modular Block iteration | Insert it inside the item Box (Step 5). Studio's inline wrap prompt puts it under the Repeater instead: move it. | +| `space-y-*`, `divide-*` or `li + li` spacing on the list | The Repeater's per-item `display: contents` wrapper breaks sibling selectors. Use flex/grid `gap` (§ The Repeater's wrapper element). | | Binding from parent's linked-schema root inside the Repeater | Inside Repeater, picker shows "Repeater Data". Bind from that root. See `use-repeater` § The scope rule. | | Child depends on layout ancestor (`.fs-grid > .fs-card`) | Keep child layout-agnostic. See `register-component` § Layout contract. | | Empty Slot in Design Mode mistaken for binding error | Toggle Preview Mode. See `use-repeater` step 7. | diff --git a/skills/studio/build-section-component-choice.md b/skills/studio/build-section-component-choice.md index fa8b305..3f6dd7c 100644 --- a/skills/studio/build-section-component-choice.md +++ b/skills/studio/build-section-component-choice.md @@ -12,15 +12,15 @@ Part of the Section-authoring set. Start at [`build-section`](build-section.md). Studio palettes ship two kinds of nodes you can drop into a Section. **All editing happens in the right panel. Neither kind supports inline-typing in the canvas.** The real tradeoff is shape and styling: -| | **Basic field components** (palette: Basic): `Heading`, `Paragraph`, `Text`, `Image`, `Link`, `RichText`, etc. | **Custom registered components** (palette: Registered Components): your `<Hero>`, `<ProductCard>`, etc. | +| | **Basic field components** (palette: Basic): `header`, `text`, `image`, `link`, `rich-text`, etc. (SDK type names; see [`plan-studio-architecture`](plan-studio-architecture.md#built-in-names)) | **Custom registered components** (palette: Registered Components): your `<Hero>`, `<ProductCard>`, etc. | |---|---|---| -| Shape | Single value (a string for `Heading` / `Paragraph` / `Text`, a URL for `Image`, a JSON tree for `RichText`). The author sets it via the right panel: either as a **static value** typed inline, or as a **binding** to a CT field. | N props as declared in `registerComponent` schema. Each prop set via the right panel: static value or binding to a CT field. | +| Shape | Single value (a string for `header` / `text`, a URL for `Image`, a JSON tree for `rich-text`). The author sets it via the right panel: either as a **static value** typed inline, or as a **binding** to a CT field. | N props as declared in `registerComponent` schema. Each prop set via the right panel: static value or binding to a CT field. | | Visual styling | Plain HTML by default: `<h1>`, `<p>`, `<img>`. Styling comes from node `styles.default.responsiveStyles.default` set via the Design panel, OR app-side CSS targeting the rendered DOM. RTE embeds use the default serializer unless you register a custom one (see `register-json-rte`). | Whatever your React + CSS produces. Pixel-perfect. Uses your design system. | | Layout authoring | Composable via Basic `Box` + node `responsiveStyles` (see pitfall row below). | Owned by the React component code. | | Authoring UX | Quick: drop, type a value or bind a field in the right panel, done. Good for editorial copy that lives ONLY in the composition (not modelled in the CMS). | Higher-fidelity: drop a fully-styled block, bind props to CT fields. Right-panel surface is exactly the prop schema you registered. | | Best for | Headlines, body copy, captions, CTA labels, RTE blocks, wherever literal copy can live in the composition itself OR map 1:1 to a CT field with no surrounding shell. | Marquee / styled / interactive sections: hero, carousel, 3D viewer, anything pixel-sensitive or with multi-field internal structure. | -**These are two render paths.** The local standalone page (custom `Hero`) and the Studio composition (Basic `Heading` + `Paragraph`) will NOT look identical by default. Treat the gap as design work, not a bug. +**These are two render paths.** The local standalone page (custom `Hero`) and the Studio composition (Basic `header` + `text`) will NOT look identical by default. Treat the gap as design work, not a bug. **Recommended pattern: hybrid.** Use both inside one Section: @@ -36,7 +36,7 @@ Decision rule: ### Migrating an existing component: register it whole, or decompose it? -When a Section is being built from JSX that already exists, there's a prior question the rule above doesn't answer: does the whole `<Hero>` become **one registered component**, or does it get **decomposed into built-ins** (`Box` + `Image` + `Heading` + `Paragraph` + `Link`) that reproduce the same markup? +When a Section is being built from JSX that already exists, there's a prior question the rule above doesn't answer: does the whole `<Hero>` become **one registered component**, or does it get **decomposed into built-ins** (`box` + `image` + `header` + `text` + `link`) that reproduce the same markup? A Section made of one registered component is a **black box to authors**: they can re-bind its props and nothing else. No restyling, no reordering its internals, no removing the subtitle. That's often the right trade, but it must be a stated choice, not a default that happens because registering was easier. diff --git a/skills/studio/build-section.md b/skills/studio/build-section.md index 3f6671c..8be59fd 100644 --- a/skills/studio/build-section.md +++ b/skills/studio/build-section.md @@ -81,7 +81,7 @@ The section's own canvas therefore always shows **one empty placeholder** (no pa | Section shape | Rule | Where its bindings resolve against | |---|---|---| -| **One Repeater over one field** (Card Grid over `related_posts`, feature list over `features[]`) | Set `selectedField=<that field>` + use scope-root repeater binding `{ path: {} }` inside | `template` becomes the field's array value directly | +| **One Repeater over one field** (Card Grid over `related_posts`, feature list over `features[]`) | Keep whole-entry scope and bind the Repeater to `{ <field>: {} }`, per the [canonical list recipe](author-composition-repeater-recipes.md#canonical-list-recipe) | `template` stays the entry; the Repeater iterates the field | | **Multiple fields on the page entry** (Header reading `brand` + `nav_links` + `signin_label`, Hero reading a `hero` group) | **Leave `selectedField` unset**: the section needs the whole entry | `template` is the full page entry. Keep original `template.<field>` binding paths | Setting `selectedField` on a multi-field section loses access to everything outside the scoped path. Bindings to those fields resolve to `undefined`. diff --git a/skills/studio/byoc-end-to-end.md b/skills/studio/byoc-end-to-end.md index 8bcffd7..78b435e 100644 --- a/skills/studio/byoc-end-to-end.md +++ b/skills/studio/byoc-end-to-end.md @@ -56,14 +56,14 @@ Gate: open Studio, open any composition (even an empty one). Canvas iframe loads ### Phase 1: Register components (lazy by default) -- **`register-component`**: for each component the user wants Studio to surface in the palette. Default to the LAZY shape: `component: () => import("./Foo").then(m => ({ default: m.Foo }))`. Eager only when the component is tiny and on a hot path where Suspense fallback is unacceptable. +- **`register-component`**: for each component the user wants Studio to surface in the palette. Default to the LAZY shape: `registerLazyComponent(config, () => import("./Foo").then((m) => m.Foo))`, the loader resolving to the component itself. Every `object` prop needs `properties`, every `array` prop needs `items`. After changing any registration, restart the dev server before checking the canvas. Eager only when the component is tiny and on a hot path where Suspense fallback is unacceptable. - **`register-breakpoints`** (optional): if the site needs custom responsive breakpoints registered for Studio's responsive editor. - **`register-json-rte`** (optional): only if the site uses JSON-RTE-bound props. - **`import-design-tokens`**: register the customer's design system. Do this BEFORE building any Section. Tokens here drive visual fidelity of every later canvas preview. Detour: `component: Hero` where `Hero` is `function Hero() { ... }` (no `props` arg) produces "Invalid hook call". Back to `register-component`, read the arity rule. The lazy shape avoids the trap. -Gate: open the canvas. Switch the palette to **Registered Components**. Every component the user expects to see is present, with a non-blank preview (defaults populated). +Gate: open the canvas. Switch the palette to **Registered Components**. Every component the user expects to see is present, with a non-blank preview (defaults populated). Without the Studio UI, run the dev-only registry check from `register-component` step 7 on `/canvas`: every expected `type` is listed. ### Phase 2: Build the first Section @@ -122,7 +122,7 @@ Phase 0 includes project bootstrap. Recommended: Next.js App Router (catch-all r | Studio canvas blank, no console error | env Base URL empty, or Canvas URL set to a full URL instead of a path | `setup-section-preview` + `understand-canvas-url` | | Components don't show up in the palette | Registration file isn't imported at app startup | `register-component`: `import "./register-components"` from the app entry | | Sections panel is empty even after Phase 2 finished | Wrong project selected in Studio top-bar | switch project, OR re-run `provision-studio-project` if it was provisioned against a different stack | -| "Internal components missing: page" on the visitor route | SDK predates the release that auto-registers built-ins on the visitor render path | `upgrade-studio-sdk`: bump to the patch that ships the visitor-path auto-register | +| `COMPONENT_NOT_REGISTERED` naming `page` on the visitor route | The visitor bundle never imports `@contentstack/studio-react` | `troubleshoot-ssr-rendering`: add that import above the fetch | | Template renders in Studio but not on the live route | The visitor route isn't mounting `<StudioComponent />`, OR is mounting `<StudioCanvas />` by mistake | `understand-canvas-vs-component` + `setup-template-preview-routes` | | "registered as lazy but hasn't been loaded yet" | `fetchSpec` / `fetchCompositionData` wasn't awaited before `<StudioComponent />` mounted | `configure-csr-vs-ssr`: the SSR / RSC patterns show the right await placement | | `useData()` warning in dev console only | Benign. Basic component hit the SSR pass before `DataCtxProvider` attached. Disappears post-hydration. | `troubleshoot-ssr-rendering`: confirm it's a warning, not an error | diff --git a/skills/studio/complete-the-build.md b/skills/studio/complete-the-build.md index 56382a1..f6c3513 100644 --- a/skills/studio/complete-the-build.md +++ b/skills/studio/complete-the-build.md @@ -33,7 +33,7 @@ Each is owned by exactly one skill. This file is the checklist and the gate: rea | 9 | Live Preview `mode: "builder"` (with `editInVisualBuilderButton: { enable: false }`) | once per project | [`install-studio`](install-studio.md) | Visual Editor opens on "Initializing the editor…" and nothing is clickable. Nothing errors | | 10 | A **new** entry is editable on the page: `fetchTemplateEntry` completes it, lists carry `<field>` and `<field>.<N>` tags | **every** Template | [`make-visual-editor-editable`](make-visual-editor-editable.md) | A new entry shows "Your text here", its empty fields and image can't be clicked, and list items can't be added, moved or deleted | -**Why this matters.** Every failure above is silent and asymmetric: the developer's own loop (SSR renders, unit tests, a page in the browser) passes in all six cases, because none of them affect rendering. They only affect the editor, which is the audience the entire exercise exists for. That asymmetry is why these get skipped, and why they need a gate rather than good intentions. +**Why this matters.** Every failure above is silent and asymmetric: the developer's own loop (SSR renders, unit tests, a page in the browser) passes in all ten cases, because none of them affect rendering. They only affect the editor, which is the audience the entire exercise exists for. That asymmetry is why these get skipped, and why they need a gate rather than good intentions. ## The two thumbnail systems are not the same thing @@ -48,7 +48,7 @@ They are constantly conflated, and fixing one does nothing for the other. Between the two, **nothing in the Components panel is ever a blank tile.** That is the acceptance bar, not "most tiles have pictures". -### Unpublished is the hardest of the runtime seven to see +### Unpublished is the hardest of the ten to see at runtime (Deliverable 8 is invisible in a different way: it never misrenders at all. Only a node count finds it.) @@ -84,6 +84,25 @@ grep -rn 'mode: "builder"\|fetchTemplateEntry' <init-file> curl -s "<new entry URL>" | grep -o 'data-cslp="[^"]*"' | sort -u ``` +**What the author sees in the Components panel (#3), checked without opening Studio.** The project's registered-components list in Studio stays empty for a code-registered library: registration happens in the browser, at runtime. To see the palette headlessly, read the registry on the `/canvas` page in development. `getComponentRegistryInstance` is exported by `@contentstack/studio-registry` (a dependency of `@contentstack/studio-react`, not re-exported by it in 2.0.1), so add it as a direct dev dependency at the same version before importing it, and keep the check out of production builds: + +```tsx +// Dev-only, in the /canvas page (or a component it renders), after the registration module has run. +import { useEffect } from "react"; +import { getComponentRegistryInstance } from "@contentstack/studio-registry"; + +export function PaletteCheck() { + useEffect(() => { + if (process.env.NODE_ENV !== "development") return; + const mine = getComponentRegistryInstance().getComponents({ registry: "user-defined" }); + console.table(mine.map((c) => ({ type: c.type, name: c.displayName, thumbnail: Boolean(c.thumbnailUrl) }))); + }, []); + return null; +} +``` + +Load `/canvas` in a browser (or Playwright) and read the table from the console: every registration should be listed, and `thumbnail` should be `true` on every row. A type missing from the table never registered on the client, whatever the source says. + ```bash # 4 — sections missing ui_preview, over the CMA (never the CDA — it hides unpublished sections) # @@ -128,12 +147,14 @@ print('ui_preview field:', 'present' if 'ui_preview' in f else 'MISSING - writes ## Where this runs in the build -Not a final sweep. Items 1, 5 and 6 are **preflight**. They gate everything after them. Items 2, 3 and 4 are **per-piece**, done as each piece is created, never batched to the end: +Not a final sweep. Items 1, 5, 6 and 9 are **preflight**. They gate everything after them. Items 2, 3 and 4 are **per-piece**, done as each piece is created, never batched to the end: ``` preflight : 1 appendTags · 5 Live Preview + token · 6 /canvas route · 9 builder mode per component : 2 CSLP tags · 3 thumbnailUrl ← at registration time, same commit per section : 4 ui_preview ← immediately after the composition write +per template : 8 Sections counted · 10 new entry editable +every write : 7 publish the Section or Template ← after every tree edit too handover : re-run the verification block; report counts, not adjectives ``` @@ -154,7 +175,7 @@ Print them under a heading like **"What you'll be able to do yourself"**: the ap ## Acceptance -- [ ] All six rows verified by the commands above (or the DOM check for #2), and the **counts printed**, never "thumbnails added" without the number. +- [ ] All ten rows verified by the commands above (the DOM check for #2, the palette check for #3), and the **counts printed**, never "thumbnails added" without the number. - [ ] `registerComponent` count **equals** `thumbnailUrl` count in every registration file. - [ ] Zero Section compositions with an empty `ui_preview`, audited over the **CMA**, including sections created as a side effect (clones, rescopes, batch builds). The audit filtered on **`place_composition_as`**, paged past `limit=100`, and printed how many entries it actually inspected: `0 sections` is a broken audit, not a clean one. - [ ] A rendered atom's root shows `data-cslp` in the DOM **and** its bound fields show their own. @@ -166,7 +187,7 @@ Print them under a heading like **"What you'll be able to do yourself"**: the ap | Pitfall | Why it bites | Fix | |---|---|---| | Editing a composition's tree and not republishing | The edit creates an unpublished version, delivery keeps serving the old one. The CMA, the canvas and the resolved `url` all look right, so the investigation goes to the template instead of the sections | Publish after every write **and** every edit, assert `publish_details` is non-empty | -| Treating these as polish to do later | All six are invisible to the developer loop, so "later" never arrives: the user discovers them when they open Studio and can't work | Preflight items gate the build, per-piece items ship in the same step that creates the piece | +| Treating these as polish to do later | All ten are invisible to the developer loop, so "later" never arrives: the user discovers them when they open Studio and can't work | Preflight items gate the build, per-piece items ship in the same step that creates the piece | | Filtering compositions on a `type` field | There is no such field: the discriminator is `place_composition_as`. The audit matches nothing and reports success having inspected **zero** sections. Measured on a live project holding 94 | Filter on `place_composition_as == "section"` and print the inspected count, so an empty match is visible | | Trusting a single page of results | `limit` is capped at 100 server-side, so a project with 141 compositions silently audits the first 100 | Page with `skip` until `count` is reached | | Auditing `ui_preview` over the delivery API | The published list is short: one project read 86 compositions on the CDA vs **122** on the CMA, hiding 36 blank sections | Audit over the CMA | @@ -177,7 +198,7 @@ Print them under a heading like **"What you'll be able to do yourself"**: the ap ## See also -- [`plan-studio-architecture`](plan-studio-architecture.md): prints these six in the plan up front, so the user approves a build that includes them +- [`plan-studio-architecture`](plan-studio-architecture.md): prints these ten in the plan up front, so the user approves a build that includes them - [`byoc-end-to-end`](byoc-end-to-end.md): the execution macro, this checklist is its Phase 6 gate - [`authenticate-cma`](authenticate-cma.md): how the audits above authenticate, OAuth-first, no pasted session token - [`verify-setup`](verify-setup.md): proves the installation works, this proves the build is usable diff --git a/skills/studio/configure-csr-vs-ssr.md b/skills/studio/configure-csr-vs-ssr.md index 0811f3f..bb5b6f5 100644 --- a/skills/studio/configure-csr-vs-ssr.md +++ b/skills/studio/configure-csr-vs-ssr.md @@ -22,7 +22,7 @@ Pick your recipe from the framework recipes chapter and copy it verbatim: - **Next.js App Router: RSC entry**: CI-tested on Next 14 + Next 16. Smallest possible client bundle. - **Next.js App Router: client-component SSR**: main-entry path, simplest wiring, full SEO. -Same three-call contract everywhere (`fetchCompositionData`, then `<StudioComponent />`, then `getSSRStyleTags` or `<StudioServerStyles>`), plus `getCompositionMetadata` for SEO. Sections below cover the render-strategy decision itself. The mechanics live in the recipes above. +Same contract everywhere: `fetchCompositionData`, `<StudioComponent />` (`<StudioServerComponent />` on the RSC entry), then the server styles (`getSSRStyles` in React, `getSSRStyleTags` outside React, or `<StudioServerStyles />` on the RSC entry), plus `getCompositionMetadata` for SEO. Sections below cover the render-strategy decision itself. The mechanics live in the recipes above. ## Context @@ -44,18 +44,18 @@ const specOptions = await sdk.fetchCompositionData({ url, searchQuery }, { local ## The render-pipeline rule (read this first) -What determines **true SSR** (the composition DOM in the server HTML) is whether `<StudioComponent>` renders through a **`renderToString` pipeline** (Pages Router / Vite custom server / Remix: server React dispatcher present, built-ins execute server-side) OR through **React Server Components** (App Router: built-ins are `"use client"` and the SDK renderer detects them as `Symbol.for("react.client.reference")` and so emits `data-cs-defer-builtin` placeholders, deferring real render to client hydration). The data-fetch function (`fetchCompositionData` vs `useCompositionData`) is orthogonal and does **not** by itself produce SSR. +What determines **true SSR** (the composition DOM in the server HTML) is whether `<StudioComponent>` renders through a **`renderToString` pipeline** (Pages Router / Vite custom server / Remix) or, on App Router, through a **`"use client"` wrapper**, which Next also renders to HTML on the server. The RSC entry (`@contentstack/studio-react/rsc`) is the other App Router path: its `<StudioServerComponent />` renders in a Server Component. `@contentstack/studio-react` ships no `"use client"` directive and its renderer calls hooks, so a Server Component cannot render it directly. The data-fetch function (`fetchCompositionData` vs `useCompositionData`) is orthogonal and does **not** by itself produce SSR. -**Net:** every mainstream host, including Next App Router (main entry and RSC entry), Pages Router, Remix, Astro, Gatsby, and plain Node, renders the composition into the server HTML via the SDK's three-call contract. See [`install-studio`](install-studio.md) for the per-host wiring. The historical `data-cs-defer-builtin` placeholder path was removed in a later SDK release. +**Net:** every mainstream host, including Next App Router (main entry and RSC entry), Pages Router, Remix, Astro, Gatsby, and plain Node, renders the composition into the server HTML via the SDK's contract. See [`install-studio`](install-studio.md) for the per-host wiring. The main entry's `<StudioComponent />` can't render straight from a Server Component (its renderer calls hooks): put it behind a `"use client"` wrapper, or render `<StudioServerComponent />` from `@contentstack/studio-react/rsc`. ## Decision table | Situation | Path | True SSR? (composition DOM in server HTML) | | --- | --- | --- | | Vite / React SPA, no SEO | **CSR**: `useCompositionData` | No (client only) | -| **Next.js Pages Router** | **SSR**: `sdk.fetchCompositionData` inside `getServerSideProps` | **Yes**: `renderToString` | -| **Next.js App Router (main entry)** | `sdk.fetchCompositionData` in a Server Component, then a client wrapper renders `<StudioComponent />` | **Yes**: Next server-renders the client component's initial output | -| **Next.js App Router (RSC entry)** | `sdk.fetchCompositionData` in a Server Component, then `<StudioServerStyles />` plus `<StudioComponent />` from `@contentstack/studio-react/rsc` | **Yes**: ~142B client bundle for structural content. Custom components register as client islands automatically | +| **Next.js Pages Router** | **SSR**: `sdk.fetchCompositionData` and `getSSRStyles` inside `getServerSideProps` | **Yes**: `renderToString` | +| **Next.js App Router (main entry)** | `sdk.fetchCompositionData` and `getSSRStyles` in a Server Component, then a client wrapper renders `<StudioComponent />` | **Yes**: Next server-renders the client component's initial output | +| **Next.js App Router (RSC entry)** | `sdk.fetchCompositionData` in a Server Component, then `<StudioServerStyles />` plus `<StudioServerComponent />` from `@contentstack/studio-react/rsc` | **Yes**: ~142B client bundle for structural content. Custom components register as client islands automatically | | Vite custom-server SSR | `renderToString` + `hydrateRoot` + `sdk.fetchCompositionData` | **Yes** | | Remix | `sdk.fetchCompositionData` inside the `loader` | **Yes**: `renderToString` | | Static / rarely changing | **SSG/ISR**: `getStaticProps` + `revalidate` | **Yes** (at build/revalidate. Via `renderToString`) | @@ -71,7 +71,7 @@ On a miss, `sdk.fetchCompositionData` **throws**. It does not resolve with `hasS What it throws is a `ComposableStudioError`. **Do not branch on a list of names.** Call `isCompositionNotFound(err)`, exported for exactly this decision: ```ts -import { isCompositionNotFound } from "@contentstack/studio-client"; +import { isCompositionNotFound } from "@contentstack/studio-react"; ``` It answers *did we establish that nothing is here?* and is the only thing that should decide a 404. A list of names has to be kept in step with the SDK, and the cost of drift is one-way: mistake a setup failure for a missing page and a wrong api key or a delivery outage returns **404 on pages that exist**, de-indexing a working site. @@ -79,20 +79,35 @@ It answers *did we establish that nothing is here?* and is the only thing that s Also on the error: `err.code` (the precise case, for logs and support) and `err.errorClass`, of which only `NOT_FOUND` is a 404. `err.causes` carries `{ cause, fix }` pairs for that failure, most likely first: log it in dev, it is the fastest diagnosis available. ```ts -// lib/resolve-composition.ts -import { isCompositionNotFound } from "@contentstack/studio-client"; - -export async function resolveComposition(sdk, args, opts = {}) { +// lib/resolve-composition.ts — server-only +import { isCompositionNotFound, type studioSdk } from "@contentstack/studio-react"; +// The Delivery stack your Studio sdk was initialized with (`stackSdk`). On the +// minimal-add branch that module is lib/studio.ts. +import { stack } from "@/lib/contentstack"; + +type StudioSdk = ReturnType<typeof studioSdk.init>; +type FetchArgs = Parameters<StudioSdk["fetchCompositionData"]>; + +export async function resolveComposition(sdk: StudioSdk, args: FetchArgs[0], opts: FetchArgs[1] = {}) { + // The stack is shared by every request this server handles. The SDK SETS a + // request's preview hash on it but never clears it, so without this line the + // next visitor is served the last editor's drafts. Clearing first is always + // safe: when this request carries a hash, the SDK sets it again itself. + stack.livePreviewQuery({ live_preview: "" }); try { const specOptions = await sdk.fetchCompositionData(args, opts); - return { specOptions, notFound: false }; // resolved ⇒ renderable + return { specOptions, notFound: false as const }; // resolved ⇒ renderable } catch (err) { - if (isCompositionNotFound(err)) return { specOptions: null, notFound: true }; + if (isCompositionNotFound(err)) return { specOptions: null, notFound: true as const }; throw err; // real failure — must stay a 500 } } ``` +**Every call on one request must forward the same `searchQuery`.** A layout or an embedded composition that passes `""` while the page passes the preview hash clears the hash the page's fetch just set, since both run concurrently on the same stack, and part of the page renders published content inside the editor ([`troubleshoot-canvas`](troubleshoot-canvas.md) § Live Preview state resets mid-request). Thread the page's query string down to them. + +The `as const` flags let TypeScript narrow `specOptions` to non-null after `if (miss) notFound()`. [`install-studio`](install-studio.md) emits this file next to the SDK init, so every route that uses it gets the hash reset without thinking about it (§ A server with a preview token can serve drafts to visitors). + **Do not blanket-catch.** A bare `try { … } catch { notFound() }` turns every CDA outage, expired delivery token, and network blip into a silent 404. The site looks empty instead of broken, and monitoring sees nothing. Ask the predicate. Rethrow the rest. **`content_type_url_pattern` moves the failure, it doesn't remove it.** With `url_metadata.url_source: "content_type_url_pattern"`, the composition's pattern matches a whole family of URLs (`/blog/{{entry.title}}` matches any `/blog/*`), so a bad slug sails past **pattern match** and dies at **entry lookup** — a different code, `URL_MATCHED_NO_ENTRY` rather than `NO_COMPOSITION_FOR_URL`. Both are `NOT_FOUND`, so the predicate covers both without you listing either; this is the case that used to catch people out when they hand-rolled the list. See [`troubleshoot-composition-resolution`](troubleshoot-composition-resolution.md) § Runtime error taxonomy. @@ -105,14 +120,7 @@ Every fetch snippet below is written against `resolveComposition`. If you copy a The Delivery stack is shared by every request on a server process. `fetchCompositionData` sets a request's preview hash on it and never clears it when the next request has none (by design), so later visitor requests go to the preview host with the last hash. Measured: one request with `?live_preview=<hash>`, then the next plain request went to `rest-preview.contentstack.com`. Mechanism: [`troubleshoot-canvas`](troubleshoot-canvas.md) § shared stack instance hazard. -A deployment with **no** preview token is safe ([`deploy-studio-site`](deploy-studio-site.md)). On a preview deployment, clear the hash when a request has none: - -```ts -// `stack` is the Delivery stack your Studio sdk was initialized with -// (contentstack.stack({...}) in lib/studio.server.ts); `searchQuery` is the raw request query string. -const q = new URLSearchParams(searchQuery); -if (!q.get("hash") && !q.get("live_preview")) stack.livePreviewQuery({ live_preview: "" }); -``` +A deployment with **no** preview token is safe ([`deploy-studio-site`](deploy-studio-site.md)). Everywhere else, [`resolveComposition`](#resolve-composition-helper) clears the hash before every fetch: `stack.livePreviewQuery({ live_preview: "" })`, after which the SDK re-applies the request's own `hash` / `live_preview` param when there is one (it only ever sets a hash that is present). A route that calls `sdk.fetchCompositionData` or `sdk.fetchComposition` directly needs that same line in front of the call. Measured: the plain request then stayed on the CDN. Requests in flight at the same time still share the stack, so on a preview server under concurrent load, create the stack and the Studio sdk **per request** instead of sharing one module-level instance: @@ -132,6 +140,20 @@ export function createStudioSdk() { } ``` +With a per-request stack nothing carries over between requests, so the reset in `resolveComposition` has nothing to clear on it. Keep the helper for its 404 classification. + +<a id="static-generation-and-caching"></a> + +## Static generation and caching on a Studio route (Next.js) + +Three things change when a route starts rendering Studio compositions. None of them errors; they show up as a slower site or stale pages. + +- **Reading the request query makes the render dynamic.** Studio needs the query string (editor mode, preview hash, locale, variant) on server-rendered routes, and awaiting `searchParams` in an App Router page (or using `getServerSideProps` in Pages Router) opts that render out of static prerendering. A greenfield catch-all is dynamic, and that is the price of in-place editing. On an app whose catch-all also serves its own pages, read `searchParams` only inside the Studio branch ([`setup-template-preview-routes`](setup-template-preview-routes.md) § Existing catch-all), so the app's own pages keep rendering the way they did. +- **Studio's fetches bypass Next's data cache.** The Delivery SDK uses axios, not `fetch`, so `fetch`-level `revalidate` and cache tags never apply to composition or entry reads. Cache at the route level (`export const revalidate = <seconds>` on a route that doesn't read `searchParams`, or the host's CDN). Don't expect a tag-based `revalidateTag` to touch Studio content. +- **The app's publish webhook doesn't cover compositions.** Compositions are entries of the compositions content type, and an author publishing a page publishes those entries, often without touching any entry the existing webhook listens for. If the app revalidates on publish, add the compositions content type to the webhook and revalidate the composition's `url` (or the whole catch-all) when one is published. A composition with a URL pattern (`/blog/{{entry.url}}`) also changes every page its template serves, so revalidate that path prefix. + +Preview and editor requests must never be served from a cache: they carry `hash` / `live_preview` / `cs-composable-studio` params, and a cached copy shows the wrong draft or no editor at all. Keep any CDN rule keyed on the full query string, or bypass the cache when one of those params is present. + ## Task 1. **Pick the path** from the decision table using `framework`. Run only the matching block below. Skip the others. @@ -139,8 +161,7 @@ export function createStudioSdk() { 2. **CSR (Vite / SPA, also the universal fallback on any framework, including App Router).** ```tsx "use client"; - import { useCompositionData, StudioComponent } from "@contentstack/studio-react"; - import { isCompositionNotFound } from "@contentstack/studio-client"; + import { useCompositionData, StudioComponent, isCompositionNotFound } from "@contentstack/studio-react"; // Loading, ErrorState and NotFound are your app's own components. import { Loading, ErrorState, NotFound } from "@/components/states"; @@ -164,9 +185,10 @@ export function createStudioSdk() { **Live Preview wiring is separate.** If this route needs to reflect author edits in real time, add the `onEntryChange` bridge from `install-live-preview` (step 7). CSR pages call the hook's `refetchData` inside the loop-safe callback — it re-fetches just the bound data, which is all a live edit changes. -3. **App Router (main entry): SSR through a client wrapper.** A Server Component fetches, a `"use client"` wrapper renders, and Next puts that output in the initial HTML (verified with `curl` in the App Router recipe). Initialize the sdk in a server-only module (no `"use client"`). Register components in a `"use client"` module the wrapper imports. Rendering `<StudioComponent />` directly in a Server Component makes built-ins `react.client.reference` proxies: the SDK emits `data-cs-defer-builtin` placeholders, filled only on hydration. +3. **App Router: SSR through a client wrapper.** A Server Component fetches and computes the styles, a `"use client"` wrapper renders, and Next puts that output in the initial HTML (verified with `curl` in the App Router recipe). Initialize the sdk in a server-only module (no `"use client"`). Register components in a `"use client"` module the wrapper imports. Rendering `<StudioComponent />` directly in a Server Component fails with a Next.js client-component error: the renderer calls hooks. ```tsx // app/[[...slug]]/page.tsx — NO "use client" + import { getSSRStyles } from "@contentstack/studio-react"; import { sdk } from "@/lib/studio.server"; import { resolveComposition } from "@/lib/resolve-composition"; import { ClientRender } from "./ClientRender"; @@ -188,7 +210,13 @@ export function createStudioSdk() { // 404s every freeform page however it is published. `miss` already carries // the only not-found answer there is. if (miss) notFound(); - return <ClientRender specOptions={specOptions} />; + // getSSRStyles here, never in ClientRender: hydration mismatch on the <style>. + return ( + <> + <style dangerouslySetInnerHTML={{ __html: getSSRStyles(specOptions.spec) }} /> + <ClientRender specOptions={specOptions} /> + </> + ); } export async function generateMetadata({ params, searchParams }) { @@ -207,7 +235,7 @@ export function createStudioSdk() { : {}; } ``` - Client wrapper (just renders, no Live Preview wiring here): + Client wrapper (renders only; no styles or Live Preview here): ```tsx "use client"; import { StudioComponent } from "@contentstack/studio-react"; @@ -228,23 +256,33 @@ export function createStudioSdk() { 4. **SSR: Next.js Pages Router.** Also full SSR: the composition DOM lands in the server HTML via `renderToString`. Register all components (built-ins + custom) at module scope in `pages/_app.tsx` with **static imports** so the same React instance runs server + client. ```ts - // pages/blog/[slug].tsx + // pages/[[...slug]].tsx import { sdk } from "@/lib/studio.server"; import { resolveComposition } from "@/lib/resolve-composition"; + import { getSSRStyles } from "@contentstack/studio-react"; - export async function getServerSideProps(context) { - const searchQuery = context.req.url.split("?")[1] ?? ""; - const url = `/blog/${context.params.slug}`; - const { specOptions, notFound } = await resolveComposition(sdk, { url, searchQuery }); + export async function getServerSideProps({ resolvedUrl }) { + // NOT req.url: on client-side navigation it is /_next/data/<build>/....json. + const { pathname, search } = new URL(resolvedUrl, "http://localhost"); + const { specOptions, notFound } = await resolveComposition(sdk, { + url: pathname, + searchQuery: search.replace(/^\?/, ""), + }); if (notFound) return { notFound: true }; - return { props: { specOptions } }; + return { + props: { + // holds `undefined` values, which next dev refuses to serialize + specOptions: JSON.parse(JSON.stringify(specOptions)), + css: getSSRStyles(specOptions.spec), + }, + }; } ``` - Page component renders `<StudioComponent specOptions={specOptions} />`. `specOptions` is JSON-serializable. + Page renders `<style dangerouslySetInnerHTML={{ __html: css }} />` then `<StudioComponent specOptions={specOptions} />`. The JSON round trip drops `undefined` values (e.g. an unset image), matching what production sends anyway. **Live Preview wiring is separate.** If this route needs real-time author updates, mount the `LivePreviewBridge` from `install-live-preview` (step 7) in `pages/_app.tsx` inside a client boundary. Pages Router routes call `router.replace(router.asPath, undefined, { scroll: false })` in the loop-safe callback. Do NOT put `[router]` in the effect deps. The router identity changes on every replace, which would produce an infinite refetch loop. - Verify true SSR with a **prod build** (`next build && next start`), not `next dev`. Dev is per-boot racy (recompile + module-init order + StrictMode double-invoke). Prod is deterministic. Acceptance: `curl <url>` returns 200 with the composition DOM (incl. Repeater iteration items) in `<body>` after stripping `<script>` tags, **no `data-cs-defer-builtin`** attributes. + Verify true SSR with `next dev` or a prod build (`next build && next start`): both render the same server HTML (checked across five requests and three cold `next dev` boots). Acceptance: `curl <url>` returns 200 with the composition DOM (incl. Repeater iteration items) in `<body>` after stripping `<script>` tags. 5. **SSR (Remix).** Put sdk init in `app/lib/studio.server.ts` so secrets tree-shake out of the browser bundle. ```ts @@ -264,7 +302,7 @@ export function createStudioSdk() { ``` Component: `const { specOptions } = useLoaderData(); return <StudioComponent specOptions={specOptions} />;` -6. **SSG / ISR.** Pass `searchQuery: ""` (no Studio iframe at build time), set `revalidate: 60` for ISR, and wire a Contentstack publish webhook to `res.revalidate(path)` for instant invalidation. Live Preview won't update SSG output until revalidation. If that matters, move the route to SSR. +6. **SSG / ISR.** Pass `searchQuery: ""` (no Studio iframe at build time), set `revalidate: 60` for ISR, and wire a Contentstack publish webhook to `res.revalidate(path)` for instant invalidation. The webhook must fire on the compositions content type too ([§ Static generation and caching](#static-generation-and-caching)). Live Preview won't update SSG output until revalidation. If that matters, move the route to SSR. 7. **Hydration discipline.** Pass the **same** `specOptions` the server fetched straight to the client wrapper. Do **not** re-fetch on mount with `useCompositionData` after an SSR fetch. That produces a hydration mismatch. Pin `locale` and `variantAlias` to identical values on both sides. @@ -304,6 +342,8 @@ return ( ); ``` +**Compute it on the server only**: the Server Component (App Router) or `getServerSideProps` (Pages Router). In a `"use client"` component the browser computes different CSS, a hydration mismatch. + It takes one spec or an array of them, so a page assembled from several compositions emits one stylesheet. Outside React (Express, a custom server) `getSSRStyleTags(spec)` returns the same CSS already wrapped in a `<style>` tag, to concatenate into an HTML template after `renderToString`. CSR routes need neither: there is no server pass to be unstyled. @@ -317,7 +357,7 @@ Succeeds only when ALL of the following are true: - [ ] Server-side paths forward `searchQuery` from the incoming request to `fetchCompositionData` - [ ] **Every `fetchCompositionData` call is wrapped**, via `resolveComposition` or an equivalent try/catch that classifies known not-found ids as 404 and **rethrows everything else**. No bare `await sdk.fetchCompositionData(...)` on a route, and no blanket `catch { notFound() }` - [ ] **Verified with a real miss:** request a URL with no composition (e.g. `/no-such-page`) and a valid-prefix-but-bad-slug URL under a connected template (e.g. `/blog/zzz-not-an-entry`). **Both return 404, not 500.** The second one is the `content_type_url_pattern` case and fails at a different stage. Testing only the first proves nothing about it -- [ ] `<StudioComponent />` is rendered inside a `"use client"` boundary on App Router / RSC paths +- [ ] On App Router, the main entry's `<StudioComponent />` is rendered inside a `"use client"` boundary (the RSC entry's `<StudioServerComponent />` renders in a Server Component instead) - [ ] If Live Preview is in scope for this route, the `LivePreviewBridge` from `install-live-preview` (step 7) is mounted in the app shell. Not needed for pure public SSR routes without Live Preview. - [ ] No `useCompositionData` call exists on a route that already fetched server-side (no double-fetch) - [ ] App Router routes export `generateMetadata` populated from `specOptions.seo` @@ -332,27 +372,32 @@ If acceptance fails, report exactly which step broke and stop. | Pitfall | Symptom | Fix | | --- | --- | --- | | `useCompositionData` in a Server Component | "Hooks can only be called inside a function component" | Switch to `sdk.fetchCompositionData` + client wrapper | -| `<StudioComponent />` directly in a Server Component | Next.js client-component error | Wrap in a `"use client"` child taking `specOptions` as prop | +| `<StudioComponent />` directly in a Server Component | Next.js client-component error | Wrap in a `"use client"` child taking `specOptions` as prop, or use `<StudioServerComponent />` from `@contentstack/studio-react/rsc` | +| RSC entry route: components registered through the main entry or a `"use client"` module | Those components don't render in `<StudioServerComponent />` output: the RSC renderer keeps its own registry | Register them with `registerComponents` from `@contentstack/studio-react/rsc`, in a server module (no `"use client"`) | | `searchQuery` not forwarded | Iframe shows wrong locale/variant. Edit mode doesn't engage | Forward `request.url.search` / `new URLSearchParams(searchParams).toString()` | | Re-fetched on the client after SSR | Hydration mismatch warning | Pass server `specOptions` straight to the client wrapper | | Different `locale` server vs client | Mismatch warning, content flash | Pin both sides via the second arg of `fetchCompositionData` | | Live Preview never updates the running page | Missing `onEntryChange` bridge | Wire the `LivePreviewBridge` from `install-live-preview` (step 7). Not a CSR-vs-SSR issue, same fix regardless of framework. | | **Infinite `_next/data/<id>/<route>.json` (Pages) or RSC payload refetch: Network tab fires hundreds of times per edit** | `onEntryChange` wired in a `[router]`-dep effect | Move the wiring to the loop-safe pattern in `install-live-preview` (step 7): subscribe ONCE with empty deps, hold router in a `useRef`, skip the register-time fire, unsubscribe in cleanup. | -| **App Router route renders fine in the browser but `view-source` shows no composition DOM**: bound content only in `self.__next_f` flight payload | `<StudioComponent />` is rendered directly in a Server Component: the RSC pass sees every built-in as `react.client.reference`, emits `data-cs-defer-builtin` placeholders, and defers the real render to hydration. **Not true SSR.** | Render it through a `"use client"` wrapper (step 3), or move the route to **Pages Router** (or Vite custom-server / Remix). | | **`transpilePackages: ['@contentstack/studio-*']` on App Router** | 500 with `Cannot read properties of null (reading 'useContext')` at `useData → RepeaterPreview` during the RSC server prerender | Don't transpile the SDK on App Router. It defeats the SDK's safe client-deferral (the basics resolve to real components, run on the server, and read a null React dispatcher). On Pages Router, transpiling is fine. | | **Coexisting `app/` dir while SSR-ing a `pages/` route** | The Pages-Router SDK React resolves to the app-page runtime, which gives a null dispatcher and a crash | Make the project Pages-Router-only for the Studio SSR route (remove `app/` from that workspace). | | **Judging SSR from `next dev`**: Repeater items intermittently render field defaults ("Your text here") | Per-boot dev race (recompile + module-init order + React StrictMode double-invoke) | Verify and ship from a **prod build**: `next build && next start`. Prod is deterministic. Dev is racy. | -| **`dynamic(() => import('@contentstack/studio-react'))` for the canvas, prod build fails** | `Package path "." is not exported from package` | `studio-react`'s `exports` map only exposes `.` under the `import` condition. Dynamic-import a **local wrapper** that statically imports `StudioCanvas`. | +| **`dynamic(() => import('@contentstack/studio-react'))` for the canvas, prod build fails** | `Package path "." is not exported from package` | Older `studio-react` releases exposed `.` only under the `import` condition (2.0.1 adds `default`). On 2.x the dynamic import isn't needed at all: `StudioCanvas` is a client component that renders `null` until mount ([`setup-section-preview`](setup-section-preview.md) step 2). On an older SDK, dynamic-import a **local wrapper** that statically imports `StudioCanvas`. | | **Bare `await sdk.fetchCompositionData(...)` with an `if (!hasSpec) notFound()` guard below it** | Every unmatched URL returns **500, not 404**. The fetch rejects, so the guard line never executes. Error boundary shows a generic failure. Nothing names the missing composition | Wrap with [`resolveComposition`](#resolve-composition-helper). The guard was written for a resolve-with-falsy contract the SDK doesn't have | | Blanket `try { … } catch { notFound() }` around the fetch | Every CDA outage, expired delivery token, and network blip silently becomes a 404. The site reads as "empty" instead of "broken" and monitoring stays green | Ask `isCompositionNotFound(err)`: true is a 404. Rethrow the rest so real failures stay 500s | | Testing 404 handling only with a totally unmatched URL | Passes, while every bad slug under a `content_type_url_pattern` template still 500s if you hand-rolled a list of names. That path matches the pattern and fails later, at entry lookup, with a different code | Test both shapes. Use the predicate rather than a list, and both are covered | | **Server-rendered route paints unstyled, then snaps into place on hydration** | Design-panel styles are generated at render time, not in your stylesheet. Markup alone carries none of them | Emit `getSSRStyles(specOptions.spec)` with the markup. See § Studio-authored styles on a server-rendered route | +| `getSSRStyles` in the App Router `"use client"` wrapper | Hydration mismatch on the `<style>` | Call it in the Server Component `page.tsx` | +| Pages Router reads `context.req.url` | 404 on client-side navigation: `req.url` is `/_next/data/<build>/....json` | Use `context.resolvedUrl` | +| Pages Router returns raw `specOptions` | `next dev`: "Error serializing ... `undefined` cannot be serialized as JSON" | Return `JSON.parse(JSON.stringify(specOptions))` | | **Every bound image 400s through `next/image`** (`"hostname … is not configured under images"`) | Assets come from a Contentstack CDN host the optimizer doesn't trust by default. Nothing in the composition is wrong | Add the asset hosts to `images.remotePatterns` in `next.config.mjs`: `images.contentstack.io` plus your region's `*-images.contentstack.com`. Region-dependent, so read the URL the binding actually returned rather than guessing | -| SSG never updates after publish | Stale content | Configure ISR `revalidate` or wire a publish webhook to `res.revalidate(path)` | +| SSG never updates after publish | Stale content | Configure ISR `revalidate` or wire a publish webhook to `res.revalidate(path)`, including publishes of the compositions content type ([§ Static generation and caching](#static-generation-and-caching)) | | Management token in client module | Token shipped to browser | Move sdk init into `*.server.ts` / non-`"use client"` module | ## See also - `install-studio`: installs the three SDKs and wires `studioSdk.init` +- [`csr-and-ssr-requirements`](csr-and-ssr-requirements.md): what each mode needs in Live Preview, Visual Editor and the canvas, and converting between them +- [`handle-404-pages`](handle-404-pages.md): the 404 branch on every framework - `setup-section-preview`: adds the `<StudioCanvas />` route - `troubleshoot-canvas`: when the iframe still doesn't render after both skills are run diff --git a/skills/studio/configure-slot-defaults.md b/skills/studio/configure-slot-defaults.md index 231e34d..5fb3095 100644 --- a/skills/studio/configure-slot-defaults.md +++ b/skills/studio/configure-slot-defaults.md @@ -67,7 +67,7 @@ Design mode keeps the drop zone on purpose: design is for building, preview is f - Drop a real component or section into the slot, and it replaces the default. - Delete that content, and the default reappears. -8. **Verify the whitelist** (only if set). Drag a section that is not on the list into the slot. Studio rejects it with "This section isn't allowed in this slot. Choose one of the slot's allowed sections, or drop a different component." and nothing is placed. The in-canvas **Select section** button and the right-panel picker offer only the allowed sections. A plain component still drops in: the gate is sections-only. +8. **Verify the whitelist** (only if set). Drag a section that is not on the list into the slot. Studio rejects it with "This section isn't allowed in this slot. Choose one of the slot's allowed sections, or drop a different component." and nothing is placed. The slot's **+** Insert handle opens **Insert component**, which offers only the allowed sections. A plain component still drops in: the gate is sections-only. ## Nested defaults diff --git a/skills/studio/configure-studio.md b/skills/studio/configure-studio.md index 6899fb4..93a36c8 100644 --- a/skills/studio/configure-studio.md +++ b/skills/studio/configure-studio.md @@ -29,8 +29,8 @@ Studio installs a content type into your stack the first time a project is provi To find the exact UID for THIS project: - **Studio web app:** open the project, go to Settings, then Configuration, and read the "Composition Content Type" field (it shows the UID). -- **Contentstack web app:** open Content Models and look for a content type with the description "Studio compositions" (or similar). The UID column is what you want. -- **API:** `GET /v3/content_types?query={"_metadata.studio_managed":true}` returns the compositions content type. Its `uid` is the value. +- **Contentstack web app:** Content Models shows the content type, but a name or description match is a guess when the stack hosts several projects. Confirm the uid against the project. +- **API:** the project's `contentTypeUid` in the `GET /v1/projects` listing (Step 0). That field is the source of truth. Don't search content types for a marker: one created by API carries no Studio flag in `_metadata`, so a `_metadata.studio_managed` query finds nothing. Pass this UID to `studioSdk.init({ contentTypeUid: "<uid>" })` in your app's `lib/contentstack.ts` (the `install-studio` skill writes it from the `studioContentTypeUid` input). **This is the SDK-init `contentTypeUid`, NOT `templateContentTypeUid`, which identifies a composition's CONNECTED content type at fetch time.** diff --git a/skills/studio/convert-project-to-studio.md b/skills/studio/convert-project-to-studio.md index a5878e0..f5dd36d 100644 --- a/skills/studio/convert-project-to-studio.md +++ b/skills/studio/convert-project-to-studio.md @@ -93,9 +93,10 @@ Studio treats locale as a context variable, not as part of a composition URL. Co 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 +# Next config i18n key, locale middleware (Next 16 renames middleware.* to proxy.*), +# React Router locale param. `find`, not bare globs: zsh aborts a line whose glob matches nothing. +find . -maxdepth 1 -name 'next.config.*' -exec grep -n "i18n" {} + 2>/dev/null +find . src -maxdepth 1 \( -name 'middleware.*' -o -name 'proxy.*' \) -exec grep -ln "locale\|createMiddleware" {} + 2>/dev/null grep -rn 'path="/:\(lang\|locale\)' src 2>/dev/null | head -3 ``` @@ -143,7 +144,7 @@ Run [`setup-template-preview-routes`](setup-template-preview-routes.md) in **wil |---|---| | Route mode (step 3) | **A. Wildcard.** Code routes win by precedence, so nothing the app owns changes | | Opt-outs | None needed. Code routes already win — routes are ranked by specificity, so `/api/*` beats the catch-all whatever the order | -| `[...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 | +| `[...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. **If the app already has a catch-all** (CMS pages, a legacy router): add none. Chain Studio into the existing one: app page first, then Studio composition, then the app's 404 ([`setup-template-preview-routes` § Existing catch-all](setup-template-preview-routes.md#existing-catch-all)) | | `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 | | Locale | From Step 3f | @@ -174,7 +175,7 @@ Print, filled in: ``` Minimal conversion done. Existing routes: untouched (<n> code routes win by precedence) - Fallback route: <path> mounts <StudioComponent /> for unclaimed URLs + Fallback route: <path> mounts <StudioComponent /> for unclaimed URLs (<new file | chained into the app's existing catch-all>) 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> @@ -206,7 +207,7 @@ This skill succeeds only when ALL of the following are true. - [ ] 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. +- [ ] No existing route file was modified or deleted. The only route files added are the canvas route and the catch-all. **One exception:** when the app already had a catch-all, that file gained the Studio and 404 steps after its own branch, which stayed unchanged, and the report says so. - [ ] 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. @@ -217,6 +218,7 @@ This skill succeeds only when ALL of the following are true. |---|---|---| | 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 pages | Ask the three-option gate verbatim. Minimal is the default | | Wiring the catch-all before `/canvas` exists | The catch-all claims `/canvas` and every Section renders as a composition miss or the 404 page | Step 5 before Step 6, always | +| Adding a second catch-all next to the app's own `[...slug]` | Next.js refuses two catch-alls at one level; replacing it drops every page it served | Chain Studio into the existing one (§ Existing catch-all in `setup-template-preview-routes`) | | Optional `[[...slug]]` on an app with a root page | Both 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 greenfield | A fixed path list misses `src/app` and locale segments, so a catch-all lands in an app full of pages | Use 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 visitors | The site never 404s. Correct for the editor iframe, wrong for everyone else | Gate on `isStudioEditorMode`: empty div in the editor, the project's 404 page for visitors | diff --git a/skills/studio/csr-and-ssr-requirements.md b/skills/studio/csr-and-ssr-requirements.md new file mode 100644 index 0000000..d72c171 --- /dev/null +++ b/skills/studio/csr-and-ssr-requirements.md @@ -0,0 +1,359 @@ +--- +name: csr-and-ssr-requirements +--- + +## When to use + +What a Studio page needs to work as a CSR page or an SSR page, everywhere it runs: the visitor site, the Live Preview pane, Visual Editor and Studio's canvas. Also covers converting a page from one mode to the other. Phrases: "what does a CSR page need", "what does an SSR page need", "CSR vs SSR with Live Preview", "Visual Editor with SSR", "convert to SSR", "switch to CSR", "Live Preview doesn't update", "Visual Editor doesn't show my edits", "edit button shows in Visual Editor", "Edit button missing on the preview site", "Visual Editor shows Live Preview behaviour". + +Do NOT use for: framework-specific route wiring for Pages Router, Remix and SSG ([`configure-csr-vs-ssr`](configure-csr-vs-ssr.md)), or 404 handling ([`handle-404-pages`](handle-404-pages.md)). + +# What a Studio page needs: CSR and SSR + +## At a glance + +| Need | CSR | SSR | +| --- | --- | --- | +| Fetch | `useCompositionData({ url })` in the browser | `sdk.fetchCompositionData({ url, searchQuery })` on the server, after clearing the shared stack's preview hash ([`resolveComposition`](configure-csr-vs-ssr.md#resolve-composition-helper) does both) | +| Query string | The hook reads `window.location` itself | Pass the full request query string as `searchQuery` | +| Unknown URL (`isCompositionNotFound`) | Render the 404 page, status 200 + `noindex` | Real 404 ([`handle-404-pages`](handle-404-pages.md)) | +| Studio-authored styles | Nothing to add | `getSSRStyles(specOptions.spec)` on the server | +| SEO metadata | `specOptions.seo` in the browser. Crawlers that don't run JavaScript miss it | `generateMetadata` or `getCompositionMetadata` on the server | +| Live Preview `ssr` | `false` | `true` | +| Live Preview `stackSdk` | `stack.config` | Leave it out | +| How the preview hash arrives | In the URL inside Visual Editor; through Live Preview's handshake in the Live Preview pane, written onto the stack via `stackSdk` | In the URL, as `live_preview`. Reaches the server only if `searchQuery` is forwarded | +| Showing an author's edit | `onEntryChange` → `refetchData()` (connected template) or `refetchSpec()` (freeform) | Nothing to wire. The editor reloads the page | +| Preview hash on a shared server | Not applicable | Clear it before every fetch | +| Hydration | Not applicable | Never fetch again in the browser | + +## In each editor + +| | Live Preview pane | Visual Editor | +| --- | --- | --- | +| **CSR** | The page loads published content, then `onEntryChange` fires and `refetchData()` loads the draft entries. Each edit refetches the data. Edit button shows | The preview hash is already in the URL, so the first load is the draft. Each edit refetches the data. Edit button hidden | +| **SSR** | The hash is in the URL, so the server's first fetch is the draft. Each edit reloads the page. Edit button shows | Same. Edit button hidden | + +Studio's canvas needs no Live Preview setup. `<StudioComponent />` handles it in both modes, as long as SSR pages forward `searchQuery`. + +## Five settings that all have to be right + +Visual Editor and Live Preview work on a Studio page only when all five agree. Each one fails silently. + +| Setting | Where it lives | Right value | When it's wrong | +| --- | --- | --- | --- | +| Live Preview on for the stack | Stack Settings › Visual Experience › General ([`enable-visual-experience`](enable-visual-experience.md)) | On | No Live Preview or Visual Editor at all | +| The Delivery stack's `live_preview` | The stack passed to `studioSdk.init` | `enable: true`, the preview token, your region's preview host | Studio shows published content in the editors | +| Edit tags | `studioSdk.init({ cslp: { appendTags: true } })` | `true` (the default is `false`) | Nothing is clickable, and no Edit button appears | +| `mode` | Live Preview `init` | `"builder"` | Visual Editor can't edit the page and shows Live Preview's Edit button | +| `ssr` | Live Preview `init`, overridden by `live_preview.ssr` on the stack passed as `stackSdk` | `true` for server-fetched routes, `false` for browser-fetched ones | Edits don't show up | + +They're separate switches: turning on Live Preview for the stack doesn't set `mode`, and `mode` doesn't set `ssr`. + +**A site that mixes both fetches still has one `init`, so one `ssr` value.** Pick the value for the site's main Studio route. A route using the other fetch has to cope with it: under `ssr: true`, a browser-fetched route updates only when the editor reloads the page. + +## Preview mode and builder mode + +| `mode` | What runs | Inside Visual Editor | Needs | +| --- | --- | --- | --- | +| `"preview"` (default) | Live Preview: content updates, and the hover Edit button opens the entry | Shows Live Preview's Edit button; the editor can stay on "Initializing the editor…" | `stackSdk` for CSR pages | +| `"builder"` | Visual Editor's editing layer: click a field on the page to edit it | Works | `stackDetails.apiKey` and `stackDetails.environment`, or `init` throws (the environment falls back to `stackSdk`'s; the API key never does) | + +**Visual Editor decides which one runs, not `mode` alone.** In builder mode, Live Preview asks the page around it which editor it's in. Visual Editor answers `builder`; the Live Preview pane answers `preview`. So the same page shows Live Preview behaviour in the pane and Visual Editor behaviour in Visual Editor, by design. With no answer (the page isn't in an editor, or the handshake fails), no editing layer starts. + +A Studio page fetched by URL also tells Visual Editor it's a Studio page, so Visual Editor can offer to open it in Studio. Editing on the page works either way. + +## Needed in both modes + +**One Live Preview module, browser only, loaded only with a preview token.** Two lines differ by mode: + +```ts +// lib/live-preview.ts +import { useEffect } from "react"; +import type { IStackSdk } from "@contentstack/live-preview-utils"; +import { stack } from "./stack"; + +// Vite names shown. On Next.js use NEXT_PUBLIC_… via process.env. +const previewToken = import.meta.env.VITE_CONTENTSTACK_PREVIEW_TOKEN; +const apiKey = import.meta.env.VITE_CONTENTSTACK_API_KEY; +const environment = import.meta.env.VITE_CONTENTSTACK_ENVIRONMENT; +const appHost = import.meta.env.VITE_CONTENTSTACK_APP_HOST ?? "app.contentstack.com"; // your region's app host + +// Imported here, not at the top: Live Preview draws its Edit button as soon as it's imported. +const livePreview = + previewToken && typeof window !== "undefined" + ? import("@contentstack/live-preview-utils") + .then(({ default: lp }) => { + lp.init({ + enable: true, + mode: "builder", // both modes + ssr: false, // CSR: false · SSR: true + stackSdk: stack.config as IStackSdk, // CSR only · SSR: remove this line + stackDetails: { apiKey, environment }, // both required in builder mode, or init throws + clientUrlParams: { host: appHost }, + editButton: { enable: true }, + editInVisualBuilderButton: { enable: false }, // else a "Start Editing" link on every page + }); + return lp; + }) + // init throws on a bad config; without this the rejection is easy to miss and the page + // silently runs without Live Preview or Visual Editor + .catch((err) => { + console.warn("[live-preview] init failed", err); + return null; + }) + : null; + +// CSR pages: refetch the data after each author edit. +export function useLivePreviewEdits(refetch: () => void) { + useEffect(() => { + let uid: string | undefined; + livePreview?.then((lp) => { if (lp) uid = lp.onEntryChange(refetch); }); + return () => void livePreview?.then((lp) => lp && uid && lp.unsubscribeOnEntryChange(uid)); + }, [refetch]); +} +``` + +Region and non-production hosts: [`install-live-preview`](install-live-preview.md). + +- **`mode: "builder"`.** Without it, Live Preview never completes Visual Editor's handshake. Visual Editor can't edit the page, and the Edit button stays visible inside it. +- **`stackDetails: { apiKey, environment }`.** In builder mode `init` throws "api key is required" or "environment is required" without them. The environment falls back to `stackSdk`'s when it isn't in `stackDetails`, so a CSR init gets it from `stack.config`; an SSR init has no `stackSdk` and must pass it. The API key never falls back: always pass it. A throw stops Live Preview completely, so `mode: "builder"` is in the code and Visual Editor still can't edit the page. +- **The dynamic import.** A static `import` of Live Preview draws the Edit button for every visitor, even when `init` never runs. The `PURGE_PREVIEW_SDK` build flag from the Live Preview docs doesn't remove it on Vite. +- **Exactly one `init`.** A second call doesn't restart Live Preview, but it can overwrite the first one's settings. Search first: `grep -rn "ContentstackLivePreview.init\|lp.init(" src app lib`. +- **`ssr` that matches how the page renders.** It tells the Live Preview pane and Visual Editor whether to reload the page or send it the change. Live Preview reads `live_preview.ssr` on the stack passed as `stackSdk` before `init({ ssr })`, so don't set it on the stack, or keep both the same. Always pass `ssr`: left out, it defaults to `false` when `stackSdk` is passed and `true` when it isn't. +- **Restart the dev server after changing `init`.** Live Preview keeps the instance from its first `init`: a second `init`, which is what hot reload runs, takes the new settings but returns the old instance, and Visual Editor's part is only created by the first one. So `mode: "builder"` added while the dev server runs never sends Visual Editor's handshake until a restart. `NEXT_PUBLIC_` / `VITE_` values are also read at server start. +- **A canvas route** with `<StudioCanvas />`, client-only ([`setup-section-preview`](setup-section-preview.md)). + +### Where the Edit button shows + +| Where the page is open | Edit button | What controls it | +| --- | --- | --- | +| Production site (no preview token) | Hidden | Live Preview isn't imported | +| Preview site in its own browser tab | Shown on hover | `editButton: { enable: true }` with no `exclude` | +| Live Preview pane | Shown on hover | Same | +| Visual Editor | Hidden | `mode: "builder"`: Visual Editor answers the handshake as the builder, and Live Preview removes the button | + +`exclude: ["outsideLivePreviewPortal"]` hides the button on the preview site in its own tab. Leave `exclude` out. + +### Edit button settings + +| Setting | What it does | Default | +| --- | --- | --- | +| `enable` (the whole `init`) | `false` turns Live Preview off; with `cleanCslpOnProduction`, it also strips the edit tags | `true` | +| `editButton.enable` | The hover Edit button | `true` | +| `editButton.exclude` | `"outsideLivePreviewPortal"` hides it outside the editors; `"insideLivePreviewPortal"` hides it inside | `[]` | +| `editButton.includeByQueryParameter` | Lets `?cslp-buttons=false` hide the button on a page, and `?cslp-buttons=true` force it on, overriding `exclude` | `true` | +| `editButton.position` | Where the button sits: `top`, `top-left`, `top-right`, `top-center`, `left`, `right`, `bottom`, `bottom-left`, `bottom-right`, `bottom-center`. One element can override it with `data-cslp-button-position` | `"top"` | +| `editInVisualBuilderButton.enable` / `.position` | The floating "Start Editing" link that opens the page in Visual Editor | on, `"bottom-right"` | +| `cleanCslpOnProduction` | Removes `data-cslp` edit tags when `init` runs with `enable: false` | `true` | + +Gate the import on the preview token rather than relying on `enable: false`: importing Live Preview is enough to draw the Edit button. + +## What a CSR page needs + +1. `useCompositionData({ url })`, with no `searchQuery`. +2. `if (error)`: `NotFound` when `isCompositionNotFound(error)`, the error state otherwise ([`handle-404-pages`](handle-404-pages.md)). +3. The Live Preview module with `ssr: false` and `stackSdk: stack.config`, imported once when the app starts. +4. `useLivePreviewEdits(refetch)`, where `refetch` is `refetchData` on a connected template and `refetchSpec` on a freeform composition (§ Which refetch on studio-react 2.0.1). + +```tsx +// src/routes/StudioPage.tsx +import { useLocation } from "react-router-dom"; +import { useCallback } from "react"; +import { useCompositionData, StudioComponent, isCompositionNotFound } from "@contentstack/studio-react"; +import { useLivePreviewEdits } from "../lib/live-preview"; +import NotFound from "../pages/NotFound"; +import ErrorState from "../pages/ErrorState"; + +export function StudioPage() { + const { pathname } = useLocation(); + const { specOptions, isLoading, error, refetchData, refetchSpec } = useCompositionData({ url: pathname }); + // 2.0.1: refetchData only refreshes a connected template's entry; it does nothing + // on a freeform composition or before the first load succeeds. + const connected = Boolean(specOptions?.connectedContentTypeUid); + const refetch = useCallback(() => (connected ? refetchData() : refetchSpec()), [connected, refetchData, refetchSpec]); + useLivePreviewEdits(refetch); + + if (error) return isCompositionNotFound(error) ? <NotFound /> : <ErrorState />; + if (isLoading || !specOptions) return <div>Loading…</div>; + + return <StudioComponent specOptions={specOptions} />; +} +``` + +The hook fetches again when the URL changes, so a catch-all route keeps working across client-side navigation: `<Route path="*" element={<StudioPage />} />`. + +### Which refetch on studio-react 2.0.1 + +**Use `refetchData` for author edits on a connected template.** An edit changes an entry, not the composition, so `refetchData` fetches only the data and keeps the composition on screen: one request for a linked composition where `refetchSpec` makes four. Use `refetchSpec` when the composition itself changed. + +**On 2.0.1, `refetchData` does nothing unless a connected composition has loaded.** It refetches only when the loaded composition has a connected content type. On a freeform composition, or when the first load failed, the call is silently ignored, so an edit never appears. That's why the snippet falls back to `refetchSpec` when `connectedContentTypeUid` is empty. Edits made while a fetch is running are not queued on 2.0.1: each one starts its own refetch. Recheck this section when you upgrade: a later SDK release is expected to make `refetchData` handle all three cases. + +**The Live Preview pane shows the published composition.** The first load comes from the CDN, before Live Preview passes its preview hash, and `refetchData` refreshes only the entries. Layout changes made in Studio appear in the pane once the composition is published. On 2.0.1 an unpublished composition doesn't open in the pane: its first load fails, so the page shows `NotFound` and the edit callback has nothing to refetch. Publish the composition, or edit it in Studio's canvas. + +## What an SSR page needs + +1. `sdk.fetchCompositionData({ url, searchQuery })` on the server, with the full request query string. +2. `try` / `catch`: a real 404 when `isCompositionNotFound(err)`, rethrow anything else so it stays a 500 ([`handle-404-pages`](handle-404-pages.md)). +3. `getSSRStyles(specOptions.spec)` and metadata, both on the server. +4. `<StudioComponent />` in a `"use client"` wrapper (App Router). +5. The Live Preview module with `ssr: true` and no `stackSdk`, imported once from a client component in the root layout. +6. On a deployment with a preview token, clear the shared stack's preview hash before every fetch: `stack.livePreviewQuery({ live_preview: "" })`. The SDK sets a request's hash but never clears it, so without this the next visitor gets the last editor's drafts. When the request carries a hash, the SDK sets it again itself ([`configure-csr-vs-ssr`](configure-csr-vs-ssr.md) § A server with a preview token can serve drafts to visitors). + +Next.js App Router: + +```tsx +// app/[[...slug]]/page.tsx — Server Component, no "use client" +import { notFound } from "next/navigation"; +import { getSSRStyles, isCompositionNotFound, type StudioComponentSpecOptions } from "@contentstack/studio-react"; +import { sdk, stack } from "@/lib/studio.server"; +import { StudioRender } from "./StudioRender"; + +type Props = { + params: Promise<{ slug?: string[] }>; + searchParams: Promise<Record<string, string | string[] | undefined>>; +}; + +export default async function Page({ params, searchParams }: Props) { + const { slug } = await params; + const url = "/" + (slug ?? []).join("/"); + const searchQuery = new URLSearchParams( + Object.entries(await searchParams).flatMap(([k, v]) => (Array.isArray(v) ? v.map((x) => [k, x]) : v === undefined ? [] : [[k, v]])), + ).toString(); + + let specOptions: StudioComponentSpecOptions; + try { + stack.livePreviewQuery({ live_preview: "" }); // shared stack: drop the last request's preview hash + specOptions = await sdk.fetchCompositionData({ url, searchQuery }); + } catch (err) { + if (!isCompositionNotFound(err)) throw err; // a real failure stays a 500 + notFound(); + } + + return ( + <> + <style dangerouslySetInnerHTML={{ __html: getSSRStyles(specOptions.spec) }} /> + <StudioRender specOptions={specOptions} /> + </> + ); +} +``` + +```tsx +// app/[[...slug]]/StudioRender.tsx +"use client"; +import { StudioComponent, type StudioComponentSpecOptions } from "@contentstack/studio-react"; + +export function StudioRender({ specOptions }: { specOptions: StudioComponentSpecOptions }) { + return <StudioComponent specOptions={specOptions} />; +} +``` + +```tsx +// app/LivePreviewInit.tsx — render once in app/layout.tsx +"use client"; +import "@/lib/live-preview"; + +export function LivePreviewInit() { + return null; +} +``` + +**No `onEntryChange` on SSR pages.** With `ssr: true`, Live Preview never calls it. The Live Preview pane and Visual Editor reload the page after each edit, the new preview hash arrives in the URL, and `fetchCompositionData` fetches the draft. + +Pages Router, Remix and SSG: [`configure-csr-vs-ssr`](configure-csr-vs-ssr.md). + +## Converting between CSR and SSR + +Converting means swapping one "What … needs" list for the other. + +**CSR to SSR:** + +1. Move the fetch to the server, and pass `searchQuery`. +2. Remove `useCompositionData` and `useLivePreviewEdits` from the route. +3. Add `getSSRStyles` and server metadata. +4. Change the 404 branch to a real 404. +5. In the Live Preview module, set `ssr: true` and remove `stackSdk`. Import it from the root layout. +6. Keep the delivery token in a server-only module. +7. On a preview deployment, clear the shared stack's preview hash before every server fetch. + +**SSR to CSR:** + +1. Fetch in the browser with `useCompositionData({ url })`. Remove the server fetch. +2. Remove `getSSRStyles` and server metadata. +3. Change the 404 branch to `NotFound` + `noindex`. +4. In the Live Preview module, set `ssr: false` and add `stackSdk: stack.config`. +5. Add `useLivePreviewEdits(refetch)` (§ Which refetch on studio-react 2.0.1). +6. Keep CSR routes under their own path prefix. Don't switch a whole SEO site to CSR. + +## Acceptance + +Succeeds only when ALL of the following are true: + +- [ ] Production build with no preview token: the page renders and no Live Preview code loads (Network tab). SSR: `curl -s <url> | grep '<known text>'` finds the content. +- [ ] Preview site in its own tab: the Edit button shows on hover. +- [ ] Live Preview pane: after load the page shows the draft, an edit appears without a manual reload, and the Edit button shows. Reload the pane three times; every load reaches the draft. +- [ ] Visual Editor: the page is editable, an edit appears, and **no Edit button** shows. +- [ ] Studio's canvas: a template opens and renders. +- [ ] Unknown URL: shows the 404 page (SSR: status 404). +- [ ] Exactly one Live Preview `init`, with `mode: "builder"`, `stackDetails: { apiKey, environment }` and the `ssr` value that matches the route. +- [ ] Framed with `?builder=true`, the page sends Visual Editor's `init` handshake with `isSSR` matching `ssr` (§ Troubleshooting). +- [ ] SSR: no `useCompositionData` on a route that fetched on the server, and no hydration warning in the console. + +If acceptance fails, report which check broke and stop. + +## Troubleshooting: Visual Editor shows Live Preview behaviour + +**First, without signing in:** on any page of the site, frame the same page with `?builder=true` and listen for Visual Editor's handshake: + +```js +// in the console of a page on the site (e.g. http://localhost:3000/) +window.addEventListener("message", (e) => { + if (e.data?.channel === "visual-builder") console.log(e.data.type, e.data.payload); +}); +const f = document.createElement("iframe"); +f.src = location.pathname + "?builder=true"; +document.body.append(f); +``` + +Builder mode logs `init` with `{ isSSR: … }` (matching your `ssr`) within a few seconds. Only `live-preview` messages, or nothing, means the page isn't in builder mode, its `init` threw, or the dev server still runs an older setup. + +Then open the page in Visual Editor. In the console of the **site's frame** (pick it in the DevTools context menu): + +1. `location.search` contains `builder=true`. If not, the page isn't open in Visual Editor; it's probably the Live Preview pane, where Live Preview behaviour is correct. +2. `document.getElementById("cslp-tooltip")` returns `null`. If it returns an element, the builder handshake didn't happen: check `mode: "builder"`, then the next steps. +3. No "You have already initialized the Live Preview SDK" warning. If it's there, a second `init` ran (another module, or hot reload) and may have changed `mode` or `ssr`, while the first one's instance stays: restart the dev server. +4. No "Live preview SDK: api key is required" or "environment is required" error: builder mode needs both in `stackDetails`. +5. Live Preview loaded at all. If the preview token was added after the dev server started, the import never ran. Restart the dev server, then hard-reload the editor. + +## Common pitfalls + +| Pitfall | Why it bites | Fix | +| --- | --- | --- | +| Live Preview off for the stack | Every piece works locally and nothing reaches the editors | Turn it on ([`enable-visual-experience`](enable-visual-experience.md)) | +| `cslp.appendTags` not set | The default is `false`: no edit tags, so nothing to click and no Edit button | `studioSdk.init({ …, cslp: { appendTags: true } })` | +| `init` without `mode: "builder"` | Visual Editor's handshake never completes: the page isn't editable and the Edit button shows inside Visual Editor | Add `mode: "builder"` | +| `stackDetails.apiKey` or `.environment` missing in builder mode | `init` throws: the API key never comes from `stackSdk`, and without `stackSdk` (SSR) neither does the environment | Always pass `stackDetails: { apiKey, environment }` | +| `init` inside `.then()` with no `.catch` | A throw becomes an unhandled rejection: the page renders, Live Preview never starts, and Visual Editor can't edit it although the code says `mode: "builder"` | Add `.catch` and log the error | +| A second `init` call elsewhere | It can overwrite the first one's settings, so `mode` or `ssr` silently changes | Keep one `init`. Search for others | +| `editButton.exclude: ["outsideLivePreviewPortal"]` | Hides the Edit button on the preview site in its own tab | Remove `exclude` | +| Static `import` of Live Preview | It draws the Edit button on import, so production visitors see it. `PURGE_PREVIEW_SDK` doesn't help on Vite | Import it dynamically, only with a preview token | +| `clientUrlParams.host` hard-coded to the US host | Edit and Start Editing open the wrong region's app | Use your region's Contentstack app host | +| `ssr` doesn't match how the page renders | The editors push changes to a page that needs a reload, or reload a page that needed a callback. Edits don't appear | Set `ssr` to the route's render mode | +| `live_preview.ssr` set on the Delivery stack | It takes precedence over `init({ ssr })` | Remove it, or keep both the same | +| CSR page calls `refetchSpec` on every edit of a connected template | It reloads the composition and its sections each time: four requests per edit instead of one | Call `refetchData` | +| CSR freeform page calls only `refetchData` (2.0.1) | `refetchData` ignores a composition with no connected content type, so edits never appear | Fall back to `refetchSpec` when `connectedContentTypeUid` is empty | +| SSR fetch without clearing the preview hash | The SDK sets a request's hash on the shared stack and never clears it, so later visitors get drafts | `stack.livePreviewQuery({ live_preview: "" })` before each fetch, or use `resolveComposition` | +| SSR `catch { notFound() }` with no check | A bad token or an outage becomes a 404 on every page | 404 only when `isCompositionNotFound(err)`; rethrow the rest | +| CSR `init` without `stackSdk` | Live Preview can't write the preview hash onto the stack, so refetches return published content | Pass `stackSdk: stack.config` | +| SSR route with an `onEntryChange` → `router.refresh()` bridge | With `ssr: true`, Live Preview never calls `onEntryChange` | Delete it. The editor reloads the page | +| SSR `searchQuery` not forwarded | The preview hash and Studio's editor context never reach the server | Pass the full request query string | +| Fetching again in the browser after an SSR fetch | Hydration mismatch | Pass the server's `specOptions` to the client wrapper | +| `init` changed but nothing changed in the editor | Live Preview keeps its first instance: a hot-reloaded `init` never creates Visual Editor's part | Restart the dev server and hard-reload the editor | + +## See also + +- [`configure-csr-vs-ssr`](configure-csr-vs-ssr.md): route wiring for Pages Router, Remix and SSG +- [`handle-404-pages`](handle-404-pages.md): unknown URLs in both modes +- [`install-live-preview`](install-live-preview.md): Delivery SDK preview settings, regions and hosts +- [`enable-visual-experience`](enable-visual-experience.md): turning on Live Preview for the stack +- [`deploy-studio-site`](deploy-studio-site.md): production builds without Live Preview diff --git a/skills/studio/decompose-design.md b/skills/studio/decompose-design.md index 5d4a7cd..8478388 100644 --- a/skills/studio/decompose-design.md +++ b/skills/studio/decompose-design.md @@ -28,10 +28,11 @@ Ship this decomposition BEFORE registering anything. Registrations are expensive This skill runs **all four questions** of the [decision framework](design-component-library.md) once per identified section on the design. The framework is an open validator. It doesn't assume any specific pre-registered library: -- **Q1. Classify: Atom, Layout, or Compound?** For each irreducible fragment of the design, ask what shape it is. A single content unit is an Atom. A slot-carrying arrangement is a Layout. Both content AND arrangement is a Compound. Refuse and decompose further, running Q1 recursively on each sub-fragment until every leaf is an atom or a layout. Sections themselves are compositions of primitives, not primitives. A marketing section design almost always starts as a compound that decomposes to Section + Stack + atoms. -- **Q2. Does the library already have it?** Check the target project's existing registry **and** the Studio project itself. [`match-existing-pattern`](match-existing-pattern.md) § Step 1b returns the palette ranked by usage plus every Section grouped by the schema it binds. If a matching primitive exists, mark the row as reuse. If a Section already covers the target schema, the row is reuse or extend, not a build. Otherwise mark as new registration. Aggressively dedup across sections on the same design. -- **Q3. Which props?** For each proposed prop on a new primitive: varies per-instance AND functional/content intent (not visual polish)? Refuse props that fail either half. -- **Q4. Which are Exposed?** For each schema prop: author-understandable AND DS-portable? If both, mark as Exposed Prop candidate. Never expose atom props. +The four questions are defined once in [`design-component-library`](design-component-library.md) § The four-question decision framework. What this skill adds: + +- **Q1** — run it on each irreducible fragment, then recursively on every sub-fragment, until every leaf is an atom or a layout. Sections are compositions of primitives, not primitives: a marketing section design almost always starts as a compound that decomposes to Section + Stack + atoms. +- **Q2** — check the target project's registry **and** the Studio project itself. [`match-existing-pattern`](match-existing-pattern.md) § Step 1b returns the palette ranked by usage plus every Section grouped by the schema it binds. A matching primitive makes the row reuse; a Section already covering the target schema makes it reuse or extend, not a build. Aggressively dedup across sections on the same design. +- **Q4** — a prop that passes becomes an Exposed Prop candidate on the Section. Every "Components" row in the emitted sheet ends with the Q1 to Q4 verdict for that component. When a customer designer or developer reads the sheet, they can see WHY each primitive was proposed. No black-box decisions. diff --git a/skills/studio/decompose-jsx-to-atomics.md b/skills/studio/decompose-jsx-to-atomics.md index f4eda4c..0261d64 100644 --- a/skills/studio/decompose-jsx-to-atomics.md +++ b/skills/studio/decompose-jsx-to-atomics.md @@ -10,7 +10,7 @@ Use when the customer HAS code and a component is too coarse to reuse: a page-le > **Mandatory 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 (**ask the user before reading or refreshing it**: refreshing rotates the refresh token; see that skill for where it lives). **Don't ask for a session `authtoken` up front**: it is the last rung, considered only after a freshly refreshed OAuth token has been observed to fail, and only after explaining the risk. 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. -> **Reuse preflight: mandatory when the project already has compositions.** Before naming a single new atomic, component or Section, run [`match-existing-pattern`](match-existing-pattern.md) § Step 1b: it groups existing Sections by the schema they bind and ranks the component palette by real usage. Decomposition is where reuse has to bite. Once this step names `Heading` instead of the project's `alpha-atom-heading`, the duplicate survives into the plan and into the build. Mark every row reuse, extend, or new, and never new without having looked. +> **Reuse preflight: mandatory when the project already has compositions.** Before naming a single new atomic, component or Section, run [`match-existing-pattern`](match-existing-pattern.md) § Step 1b: it groups existing Sections by the schema they bind and ranks the component palette by real usage. Decomposition is where reuse has to bite. Once this step names the built-in `header` instead of the project's `alpha-atom-heading`, the duplicate survives into the plan and into the build. Mark every row reuse, extend, or new, and never new without having looked. # Decompose existing JSX into atomics and Sections @@ -27,34 +27,36 @@ Use when the customer HAS code and a component is too coarse to reuse: a page-le An existing React component is **not** a reason to skip decomposition. It's the input to it: **a component becomes a Section by being broken up**, and its parts become the reusable pieces. -Taxonomy (Layer-1 atomic / Layer-2 container / Layer-3 Section) is defined once in [`decompose-design`](decompose-design.md) and applied to visual input by [`decompose-design`](decompose-design.md). This skill applies the same taxonomy to **code**. Don't re-derive the layers here. Read them there. +Taxonomy (Layer-1 atomic / Layer-2 container / Layer-3 Section) is defined once in [`decompose-design`](decompose-design.md), which applies it to visual input. This skill applies the same taxonomy to **code**. What gets decomposed at all is decided by the [Section rule](SKILL.md#section-rule): a component that is interactive or computes its values stays whole inside its Section, everything else is broken up here. Don't re-derive the layers here. Read them there. ## Task 1. **Read the render tree, not the prop list.** Open `componentPath` and walk what it returns. Props tell you what the current caller passes. The render tree tells you what the component actually contains. That's where the reusable pieces are hiding. Note for each element: tag/component, what content it renders, and where that content comes from (a prop, a literal, a `.map()`, a fetch). -2. **Classify every leaf as a Layer-1 atomic, one piece of content each.** +2. **Classify every leaf as a Layer-1 atomic, one piece of content each.** Map it to the project's library atom first ([`design-component-library`](design-component-library.md)). The built-in column is the fallback when the library has none, written as the SDK `type` (palette name in brackets); the full list is in [`plan-studio-architecture`](plan-studio-architecture.md#built-in-names) § Step 2b. - | In the JSX | Atomic | Prop type | - |---|---|---| - | `<h1>`, `<h2>`, heading, title, label | `Heading` / `Text` | `string` | - | `<p>` of **plain** copy, no formatting possible | `Text` / `Paragraph` | `string` | - | `<img>`, background image | `Image` | `imageurl` | - | `<a>`, button with a destination | `Link` / `Button` | `href` (+ `string` label) | - | **Formatted copy**: `dangerouslySetInnerHTML`, a markdown renderer, `ReactMarkdown`, `<div>` holding `<strong>`/`<ul>`/`<a>` from data, anything an author can style | **`RichText`** | `json_rte` (JSON RTE) or `any` (HTML RTE / Markdown) | - | Copy that **truncates**, "read more", accordion body, `<details>`, show/hide | **`CollapsibleText`** | as above | - | Formatted copy that can **embed a card or media** mid-flow | `RichText` **+ `registerRTERenderer`** with `embeddedEntry` / `embeddedAsset` | as above | - | A date, price, count | `Text` | `datestring` / `number` | - | An interactive control (form field, toggle, carousel arrow) | **stays inside a registered component**: built-ins can't express behavior | - | + | In the JSX | Library atom | Built-in fallback | Prop type | + |---|---|---|---| + | `<h1>`, `<h2>`, heading, title | `heading` | `header` (Header): `<h1>`…`<h6>` | `string` | + | `<p>` of **plain** copy, label, no formatting possible | `body-string` | `text` (Paragraph): `<p>` | `string` | + | `<img>`, background image | `media` | `image` (Image): `<img>` | `imageurl` | + | `<a>`, button with a destination | `cta-button` | `link` (Link) / `button` (Button) | `href` (+ `string` label) | + | **Formatted copy**: `dangerouslySetInnerHTML`, a markdown renderer, `ReactMarkdown`, `<div>` holding `<strong>`/`<ul>`/`<a>` from data, anything an author can style | `body-json-rte` / `body-html-rte` | **`rich-text`** (Rich Text): `<div>` | `json_rte` (JSON RTE) or `any` (HTML RTE / Markdown) | + | Copy that **truncates**, "read more", accordion body, `<details>`, show/hide | - | **`collapsible-text`** (Collapsible Text) | as above | + | Formatted copy that can **embed a card or media** mid-flow | rich-text atom | `rich-text` **+ `registerRTERenderer`** with `embeddedEntry` / `embeddedAsset` | as above | + | A date, price, count | `body-string` | `text` (Paragraph) | `datestring` / `number` | + | An interactive control (form field, toggle, carousel arrow) | **stays inside a registered component**: atoms and built-ins can't express behavior | - | - | + + **Never use `plain-text` (palette name "Text") for a leaf.** It renders the bare string with no element, so it cannot take a class or carry an edit tag: authors can't style it or click it. And never write `Heading`, `Paragraph` or `RichText` as a `type`: no component is registered under those names. **On an existing codebase, flag the upgrade: don't perform it.** Code that renders prose through a plain `<p>` is often just what was quickest to write. Two cases, very different costs: - - The CT field is **already rich text** and the code renders it plainly, so switching to `RichText` is **free**: no schema change, and it fixes markup currently showing as literal characters. + - The CT field is **already rich text** and the code renders it plainly, so switching to `rich-text` (or the library's rich-text atom) is **free**: no schema change, and it fixes markup currently showing as literal characters. - The field is **plain text** holding prose, so suggest `json_rte`, state that it is a schema change on live entries ([`migrate-ct-schema`](migrate-ct-schema.md)), and proceed with plain text if the user declines. Say it as a recommendation with the cost attached, then respect the answer. Never migrate a field as a side effect of decomposing a component. - **Deciding plain vs rich:** ask whether an author could ever want **bold, a link, a list, or an embed** in that copy. If yes it is rich text. `Text`/`Paragraph` render `String(text)` and would show the markup as literal `<p>` characters. Body copy, descriptions and long-form prose are rich text far more often than they look. Headings and labels almost never are. Mapping: [`register-json-rte` § Which component for which field](register-json-rte.md#rte-field-to-component). + **Deciding plain vs rich:** ask whether an author could ever want **bold, a link, a list, or an embed** in that copy. If yes it is rich text. `header` and `text` render the string as-is and would show the markup as literal `<p>` characters. Body copy, descriptions and long-form prose are rich text far more often than they look. Headings and labels almost never are. Mapping: [`register-json-rte` § Which component for which field](register-json-rte.md#rte-field-to-component). **A hardcoded literal is still an atomic.** `<h1>Join our rewards</h1>` becomes a `string` atomic bound to a CT field. That's precisely how the author gains the ability to edit it. Leaving literals in code is the single biggest source of "why can't I change this text". @@ -74,7 +76,7 @@ Taxonomy (Layer-1 atomic / Layer-2 container / Layer-3 Section) is defined once **A partially-handled block list is a bug worth surfacing.** Code like `if (block.hasOwnProperty("marquee")) return <Marquee …/>` renders **nothing** for every other block type, silently. Enumerate the CT's block types and give each a Condition Block, or state explicitly that the others are unsupported. -5. **Draw Layer-3 Section boundaries: one per recognizable page part.** A Section is a hero, a form band, a feature grid, a CTA strip. **One Section for a whole page is a defect, not a shortcut.** If your boundary list has one entry and the page has visibly distinct regions, go back to step 1. +5. **Draw Layer-3 Section boundaries: one per page region**, per the [Section rule](SKILL.md#section-rule). A Section is a hero, a form band, a feature grid, a CTA strip. If your boundary list has one entry and the page has visibly distinct regions, go back to step 1. Boundary test, in order: - Could a person point at it and name it? If yes, it is its own Section. @@ -87,10 +89,10 @@ Taxonomy (Layer-1 atomic / Layer-2 container / Layer-3 Section) is defined once ``` Hero band (Section, scoped to entry.hero group) - Image image → entry.hero.image (imageurl) - Heading headline → entry.hero.headline (string) - Text body → entry.hero.body (string) - Link cta → entry.hero.cta.href (href) + media image → entry.hero.image.url (imageurl) + heading headline → entry.hero.headline (string) + body-string body → entry.hero.body (string) + cta-button cta → entry.hero.cta.href (href) ``` **A single prop taking the whole object is the failure mode**, not a shortcut. `content: { type: "any" }` fed the `hero` group renders blank. The binder recursively unwraps single-key objects for every type except `object`/`array`. Bind field-by-field. Use `object`/`array` only when the component genuinely consumes a whole group or list. @@ -105,13 +107,13 @@ Taxonomy (Layer-1 atomic / Layer-2 container / Layer-3 Section) is defined once Sections (build in this order): 1. <Hero band> scope: entry.hero reusable on: any page with a hero - atomics: Image, Heading, Text, Link + atomics: media, heading, body-string, cta-button 2. <Offers list> scope: entry.offers Repeater + child Section per offer - atomics (child): Image, Heading, Text + atomics (child): media, heading, body-string 3. <Signup form> stays one registered component — interactive Reused, not rebuilt: - <Heading / Text / Image already registered — no work> + <heading / body-string / media already registered — no work> New CT fields needed: <entry.hero.headline (string), entry.hero.image (file), …> @@ -211,7 +213,7 @@ The full contract, the `wrap` alternative and the `cslp: { appendTags: true }` p | Pitfall | Why it bites | Fix | | --- | --- | --- | -| Registering the page component whole because "it's used on one route" | Reuse count is a prioritization signal, not a qualification gate. A one-off page component is the **strongest** decomposition candidate. It's where the monolith is | Decompose first, then register the parts. See `discover-sections` § scoring | +| Registering the page component whole because "it's used on one route" | Reuse count is a prioritization signal, not a qualification gate. A one-off page component is the **strongest** decomposition candidate. It's where the monolith is | Decompose first, then register the parts ([Section rule](SKILL.md#section-rule)). See `discover-sections` § scoring | | Reading props instead of the render tree | Props describe today's caller. The reusable pieces are in what the component renders | Walk the JSX return value | | Leaving hardcoded copy in code | Authors then can't change it and file it as a bug. "Why can't I edit this heading" is almost always a literal that was never made an atomic | Every literal string/image becomes an atomic bound to a CT field | | `.map()` kept as a hardcoded list | Content can't grow, adding a fourth offer needs a developer | Repeater + child Section (+ Condition Block for blocks/references) | @@ -223,9 +225,8 @@ The full contract, the `wrap` alternative and the `cslp: { appendTags: true }` p ## See also -- [`decompose-design`](decompose-design.md): the Layer-1/2/3 taxonomy this applies -- [`decompose-design`](decompose-design.md): the Layer-1/2/3 taxonomy this applies -- [`decompose-design`](decompose-design.md): same taxonomy, visual input instead of code +- [`decompose-design`](decompose-design.md): the Layer-1/2/3 taxonomy this applies, on visual input instead of code +- [`design-component-library`](design-component-library.md): the primitive library the atomics map to - [`discover-sections`](discover-sections.md): which components recur across routes. Run this after decomposition - [`design-section-from-jsx`](design-section-from-jsx.md): derive a Section's linked schema once boundaries are drawn - [`register-component`](register-component.md): register each atomic and container. Prop types and the `any` trap diff --git a/skills/studio/decompose-site.md b/skills/studio/decompose-site.md index ba44f88..006a7a1 100644 --- a/skills/studio/decompose-site.md +++ b/skills/studio/decompose-site.md @@ -44,10 +44,12 @@ This skill fixes all four by decomposing site-wide, not page-wide. It emits **on Every fragment on every page is walked through the [four-question decision framework](design-component-library.md). The framework is an open validator (works on any input, no assumed registry). The multi-page context adds a deduplication dimension the single-template `decompose-design` doesn't have: -- **Q1. Classify: Atom, Layout, or Compound?** Applied per distinct shape across ALL templates. A `Card` shape identified on 6 pages is classified once as a layout. Compounds (marketing sections, page hero blocks) always decompose recursively until every leaf is an atom or a layout. -- **Q2. Does the shared registry already have it?** Two layers: (a) does the customer's existing project registry cover it? (b) has it already been proposed for the shared registry by an EARLIER template in this pass? Aggressively dedup: 6 pages proposing "Card" produce one Card entry, not six. -- **Q3. Which props?** For each proposed prop on a proposed new primitive: does the value vary across the SITE (not just across one template) AND represent functional/content intent? Site-wide invariants (e.g. Section spacing is always `comfortable` across all 30 templates) are hardcoded, not exposed as a prop. -- **Q4. Which are Exposed?** For each schema prop: author-understandable + DS-portable? Additionally: does any template on the site actually need to override this prop? If zero templates need override, keep static globally. Site-wide "exposed for one template only" is a valid reason. +The four questions are defined once in [`design-component-library`](design-component-library.md) § The four-question decision framework. Site-wide, each gains a deduplication dimension the single-template `decompose-design` doesn't have: + +- **Q1** — applied per distinct shape across ALL templates. A `Card` shape identified on 6 pages is classified once as a layout. +- **Q2** — two layers: (a) does the customer's existing project registry cover it? (b) has an EARLIER template in this pass already proposed it for the shared registry? Six pages proposing "Card" produce one Card entry, not six. +- **Q3** — the value must vary across the SITE, not just across one template. Site-wide invariants (Section spacing always `comfortable` across all 30 templates) are hardcoded, not exposed. +- **Q4** — additionally: does any template on the site actually need to override this prop? If none do, keep it static globally. "Exposed for one template only" is a valid reason. **The shared component registry every skill invocation produces has one Q1 to Q4 verdict PER PROPOSED PRIMITIVE**, not per template use. The build sheets reference the registry by name. Each template's build order just says "drop Section S3 (registered per registry)" and stops. diff --git a/skills/studio/deploy-studio-site.md b/skills/studio/deploy-studio-site.md index e9e50a1..d21bc66 100644 --- a/skills/studio/deploy-studio-site.md +++ b/skills/studio/deploy-studio-site.md @@ -117,7 +117,7 @@ This skill succeeds only when ALL of the following are true. If any fails, do no | Deployed but visitors see old content | Launch CDN cache, or a statically built page | `force-dynamic` / `revalidate = 0` with `Cache-Control`, purge with `revalidate_cdn_cache`; static pages need a deploy hook on publish. `revalidatePath`/`revalidateTag` do nothing on Launch | | Visitors get a Contentstack login screen | Contentstack Authentication is on by default per Launch environment | Turn it off on the production environment | | Page renders blank post-deploy | Connected entry not published to target env | Publish the entry to `<prodEnvironment>` | -| Visitors see draft content after someone previewed | A server with a preview token keeps the last request's preview hash on its shared stack | No preview token on production. On a preview deployment, clear the hash per request: [`configure-csr-vs-ssr`](configure-csr-vs-ssr.md) | +| Visitors see draft content after someone previewed | A server with a preview token keeps the last request's preview hash on its shared stack | No preview token on production. On a preview deployment, fetch only through [`resolveComposition`](configure-csr-vs-ssr.md#resolve-composition-helper), which clears the hash before each fetch, and use a per-request stack under concurrent load | | Visitors see Live Preview edit buttons / overlays | `enable: true` unconditional in prod build, or `live-preview-utils` imported statically (it draws the button on import) | Import it only when the preview token exists (step 5) | | Preview environment doesn't update live | `onEntryChange` not wired to refetch, or preview token mismatch | Run `verify-setup`. Verify token matches the one in Contentstack | | `region` / `host` mismatch error | Non-US stack with US defaults | Set Delivery SDK `region` and Live Preview `host` to the region | diff --git a/skills/studio/design-component-library.md b/skills/studio/design-component-library.md index 739bf03..fee1bcc 100644 --- a/skills/studio/design-component-library.md +++ b/skills/studio/design-component-library.md @@ -20,10 +20,10 @@ The trap is registering either (a) one giant Hero with 20 props, or (b) ten diff This skill walks the four-question framework below. **The framework is an open validator**. It works on any input (React component, Figma frame, verbal description) regardless of what's already registered: -- **Q1. Classify: Atom, Layout, or Compound?** Look at the input in isolation. A component that carries a single content unit is an Atom. One that arranges children via slots is a Layout. One that does both content AND arrangement is a Compound (refuse and decompose recursively). This is a validator question. It tells you what shape the component actually is. +- **Q1. Classify: Atom, Layout, or Compound?** Look at the input in isolation. A component that carries a single content unit is an Atom. One that arranges children via slots is a Layout. One that does both content AND arrangement is a Compound (refuse and decompose recursively). - **Q2. Does the library already have it?** Only after Q1 confirms atom or layout, check reuse. On a fresh project the answer is trivially "no" for the first primitives. In an existing library, check the registry. With legacy monoliths, check the atom/layout registry, not the monolith list. -- **Q3. Which props?** For each proposed prop, does it vary per-instance AND represent functional/content intent (not visual polish)? -- **Q4. Which are Exposed?** For each schema prop, is it author-understandable AND DS-portable? +- **Q3. Which props?** For each proposed prop, does it vary per-instance AND represent functional/content intent (not visual polish)? Refuse props that fail either half. +- **Q4. Which are Exposed?** For each schema prop, is it author-understandable AND DS-portable? Never expose atom props. **Halt if any answer is ambiguous.** Never guess. Ask the user to disambiguate before proceeding. @@ -50,7 +50,7 @@ Walk the user through a five-step decomposition of their design system into a pr Ask (only if not stated): - Which UI library are you on? (Tailwind + shadcn/ui, Chakra, MUI, custom Radix, custom tokens) -- Where are your semantic tokens defined? (CSS variables, Tailwind config, theme file) +- Where are your semantic tokens defined? (CSS variables, Tailwind v3 config, a Tailwind v4 `@theme` block, theme file) - Does the DS already expose semantic variant names (primary / secondary / default) or raw color tokens? Do NOT invent tokens. Every semantic option in a primitive's schema must map to an existing DS token. If a token doesn't exist for a proposed option, flag it and offer to defer that primitive until the DS adds the token. @@ -155,6 +155,57 @@ One content prop, three design props, closed options on each. A `string` design **Give every prop a `defaultValue`.** It is what lets a composition omit design props entirely and still render. The authored section then carries only its content bindings, and the primitive supplies the rest. +### Utility-CSS projects (Tailwind): write the menu in source + +On a Tailwind (or any on-demand utility CSS) project, the `choice` menu is also what keeps the styling alive. Tailwind generates only the classes it finds in source files, so a class an author types in Studio (the Design panel's class field, a free `string` prop) is never generated and renders unstyled, with no error. Map each option to a **complete class string written out in the component**, never a class built from the value (`` `gap-${gap}` `` is invisible to Tailwind's scanner): + +```tsx +const GAP = { tight: "gap-2", normal: "gap-4", loose: "gap-8" } as const; // full class strings, in source +const ALIGN = { start: "items-start", center: "items-center", end: "items-end" } as const; + +export function Stack({ gap = "normal", align = "start", className = "", children, studioAttributes }: StackProps) { + return ( + <div className={`flex flex-col ${GAP[gap]} ${ALIGN[align]} ${className}`} {...studioAttributes}> + {children} + </div> + ); +} +``` + +```ts +registerComponent({ + type: "acme-stack", + component: Stack, + wrap: false, + props: { + gap: { type: "choice", options: ["tight", "normal", "loose"], defaultValue: "normal" }, + align: { type: "choice", options: ["start", "center", "end"], defaultValue: "start" }, + children: { type: "slot" }, + }, +}); +``` + +The option names come from the DS (`tight` / `normal` / `loose`), the classes from the `@theme` scale ([`import-design-tokens`](import-design-tokens.md) reads the same `@theme` block). Leave the Design panel's class field out of `styleSections` for these components. + +### Design props (`design: true`): per-breakpoint values without a class list + +When a design value must change per breakpoint (padding that tightens on mobile), mark the prop `design: true` instead. The SDK generates the CSS itself, so Tailwind's scan doesn't matter: + +```ts +paddingBlock: { + type: "choice", + design: true, // key must be a camelCase CSS property + options: [ + { label: "None", value: "0" }, + { label: "Compact", value: "var(--spacing-8)" }, + { label: "Roomy", value: "var(--spacing-16)" }, + ], + defaultValue: "var(--spacing-8)", +}, +``` + +Rules from `@contentstack/studio-registry` 2.0.1: only `choice` (options as `{ label, value: <CSS value> }`) and `string` props take the flag. The prop key must be a camelCase CSS property (`paddingBlock`, `gap`, `maxWidth`) and not a reserved key or a style-group name. Authors set the value per breakpoint in the Settings tab. The component receives a **generated class name** under that prop, not the CSS value, and must put it on an element (`className={paddingBlock}`). A `var(--…)` value only works if the variable exists in the page's CSS (see the Tailwind v4 note in `import-design-tokens`). Reach for `design: true` for responsive spacing and sizing. Keep plain `choice` menus for semantic options (`variant`, `emphasis`, `tag`). + Confirm the DS has: - Padding tokens for `spacing: compact/comfortable/spacious` at both Section and Card levels. - Background theme tokens for `background: none/surface/muted/brand/inverse`. @@ -260,6 +311,7 @@ If a proposed library would produce sections averaging 2 to 3 nodes, or would bi | Exposing `size` on Heading | Layout drift: every H1 picks its own size | `level` drives size via DS tokens. `size` never appears | | `color: hex` on any atom | Color drift across the library | `emphasis: default/muted/inverse` maps to DS tokens | | Padding in pixels on Section | Spacing scale fragments | `spacing: compact/comfortable/spacious` maps to Tailwind padding tokens | +| Building Tailwind classes from a prop value (`` `gap-${gap}` ``) or letting authors type classes | Tailwind never sees the class in source, so it is not generated and the element renders unstyled, silently | A lookup of full class strings in the component (§ Utility-CSS projects), or a `design: true` prop | | Registering `Hero` as an atom | Monolithic Section: new variant needs a code change | Register the 4 atoms + 4 layouts. Compose Heros on canvas | | A `Rating` atom before you need it | Library bloat, unused primitives | Grow additively. Add only when a section actually needs it | | Semantic option without a DS token | Silent visual failure at render | Add the token before enabling the option in the schema | diff --git a/skills/studio/discover-sections-from-ct.md b/skills/studio/discover-sections-from-ct.md index b2325cd..0bd4a9e 100644 --- a/skills/studio/discover-sections-from-ct.md +++ b/skills/studio/discover-sections-from-ct.md @@ -37,25 +37,25 @@ This skill is **discovery, not authoring**. It doesn't write any composition. It 4. **Propose a component structure per Section.** Best-effort inference from field names + types: - `title` / `headline` / `heading` maps to a Heading component with `text` bound to the field. - `image` / `cover_image` / `hero_image` maps to an Image component. - - **Rich text: decide by `data_type`/`field_metadata`, never by field name.** These carry formatting, so `Text`/`Heading` would render markup as literal characters: + - **Rich text: decide by `data_type`/`field_metadata`, never by field name.** These carry formatting, so `text`/`header` would render markup as literal characters: | Schema shape | Component | Prop type | |---|---|---| - | `data_type: "json_rte"` | **`RichText`** | `json_rte` | - | `data_type: "text"` + `field_metadata.allow_rich_text` (HTML RTE) | **`RichText`** | `any` | - | `data_type: "text"` + `field_metadata.markdown` | **`RichText`** | `any` | - | `data_type: "text"` + `multiline`, no rich-text flags | `Text` | `plaintext` | + | `data_type: "json_rte"` | **`rich-text`** | `json_rte` | + | `data_type: "text"` + `field_metadata.allow_rich_text` (HTML RTE) | **`rich-text`** | `any` | + | `data_type: "text"` + `field_metadata.markdown` | **`rich-text`** | `any` | + | `data_type: "text"` + `multiline`, no rich-text flags | `text` | `plaintext` | - Choose **`CollapsibleText`** instead wherever the design truncates the copy ("read more", accordion). If any such field permits **embedded entries or assets**, record it. The project needs one `registerRTERenderer` call, and unregistered embeds render as bare delivery-API spans. See [`register-json-rte`](register-json-rte.md#rte-field-to-component). + Choose **`collapsible-text`** instead wherever the design truncates the copy ("read more", accordion). If any such field permits **embedded entries or assets**, record it. The project needs one `registerRTERenderer` call, and unregistered embeds render as bare delivery-API spans. See [`register-json-rte`](register-json-rte.md#rte-field-to-component). - `body` / `content` / `description` that is plain long text maps to a Text component, **and record it as an upgrade candidate** (below). **Rich-text upgrade candidates: always surface these, never apply them silently.** An existing model often holds prose in a plain text field simply because that was the quickest thing to pick when it was created. Report them and let the user decide: | What you found | Suggest | Cost to accept | |---|---|---| - | Field is **already** `json_rte` / rich text, but the current code renders it through a plain text component | Switch the component to `RichText` | **None**: no schema change, no migration. Free correctness win | + | Field is **already** `json_rte` / rich text, but the current code renders it through a plain text component | Switch the component to `rich-text` | **None**: no schema change, no migration. Free correctness win | | Plain **multiline `text`** field holding body copy, description or prose | Consider `json_rte` so authors can bold, link, list and embed | A schema change on a live CT. See [`migrate-ct-schema`](migrate-ct-schema.md). Existing entry values carry over as plain paragraphs | - | Long prose the site truncates behind "read more" / an accordion | `CollapsibleText` over the same field | None if the field is already rich text | + | Long prose the site truncates behind "read more" / an accordion | `collapsible-text` over the same field | None if the field is already rich text | Phrase it as a recommendation with the cost attached ("`summary` is plain text today. Making it `json_rte` would let authors add links, but it's a schema change on N live entries. Want it?") and proceed with the existing type if they decline. Never migrate a field as a side effect of a discovery run. - `cta_label` + `cta_url` (or `link`) maps to a Button component. diff --git a/skills/studio/embed-composition.md b/skills/studio/embed-composition.md index c4a2ed1..e0524e4 100644 --- a/skills/studio/embed-composition.md +++ b/skills/studio/embed-composition.md @@ -106,15 +106,21 @@ import { sdk } from "@/lib/contentstack"; // or @/lib/studio for additive int import { resolveComposition } from "@/lib/resolve-composition"; // configure-csr-vs-ssr import { StudioEmbedClient } from "./StudioEmbedClient"; -export async function StudioEmbed({ compositionUid }: { compositionUid: string }) { +export async function StudioEmbed({ compositionUid, searchQuery = "" }: { + compositionUid: string; + searchQuery?: string; // the page's request query string, so a preview hash applies here too +}) { // fetchCompositionData THROWS when the composition is missing — it does not // resolve empty. Unwrapped, a deleted or misspelled embed takes down the whole // host page with a 500. An embed must degrade to nothing, never to an outage. // NOTE: a compositionUid lookup throws COMPOSITION_UID_NOT_FOUND — NOT the - // ..._BY_URL id. resolveComposition covers all three not-found ids. + // ..._BY_URL id. isCompositionNotFound (inside resolveComposition) covers every not-found case. const { specOptions, notFound } = await resolveComposition(sdk, { compositionUid, - searchQuery: "", // required on server (no window.location.search) + // Required on the server (no window.location.search). Pass the page's own + // query: resolveComposition clears the preview hash on the shared stack, so + // an empty string here would undo the hash the page's fetch just set. + searchQuery, }); if (notFound) return null; // real failures still throw — see resolveComposition return <StudioEmbedClient specOptions={specOptions} />; @@ -134,7 +140,7 @@ Detect the framework (Next App Router vs Vite/CRA) before picking. Same rule as ### Step 3: Insert the embed into the target page -Read the target page file. Find the `insertionAnchor`. Insert the `<StudioEmbed compositionUid="{bandName}" />` line at `insertionPosition` (before/after the anchor). +Read the target page file. Find the `insertionAnchor`. Insert the `<StudioEmbed compositionUid="{bandName}" />` line at `insertionPosition` (before/after the anchor). With the SSR variant, also pass `searchQuery`: the page's request query string (`new URLSearchParams(await searchParams).toString()` in an App Router page), so the embed previews the same draft as the page. Also add the import at the top of the file: - Relative path inferred from where the wrapper landed in step 2 (e.g. `import { StudioEmbed } from "@/components/StudioEmbed"`). diff --git a/skills/studio/enable-visual-experience.md b/skills/studio/enable-visual-experience.md index 6447cff..07c4ee3 100644 --- a/skills/studio/enable-visual-experience.md +++ b/skills/studio/enable-visual-experience.md @@ -6,12 +6,14 @@ name: enable-visual-experience Enable Live Preview + Visual Experience at the stack level: prerequisite before any Studio / Live Preview / Visual Editor work. Without it, app installs silently fail. -Use as the FIRST step before `install-studio`, `install-live-preview`, `configure-studio`, or any Studio setup skill. Phrases: "enable Live Preview", "set up Visual Experience", "canvas is blank", "preview events not arriving". Also when `verify-setup` reports Layer 2 failing or `troubleshoot-canvas` finds canvas blank with no preview events. Not doable from CLI: click-through in the Contentstack web app, gates downstream skills. +Use as the FIRST step before `install-studio`, `install-live-preview`, `configure-studio`, or any Studio setup skill. Phrases: "enable Live Preview", "set up Visual Experience", "canvas is blank", "preview events not arriving". Also when `verify-setup` reports Layer 2 failing or `troubleshoot-canvas` finds canvas blank with no preview events. Done by API (`POST /v3/stacks/settings` plus one token call) with an OAuth credential. Only the optional Custom Preview URL needs the Contentstack web app. Gates downstream skills. > **Mandatory 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 (**ask the user before reading or refreshing it**: refreshing rotates the refresh token; see that skill for where it lives). **Don't ask for a session `authtoken` up front**: it is the last rung, considered only after a freshly refreshed OAuth token has been observed to fail, and only after explaining the risk. 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. # Enable Visual Experience + Live Preview at the stack level +**Order of work:** the [API path](#api-path) first, every time. It enables Live Preview, sets the default preview environment and mints the preview token, with no clicks. Then decide on the Custom Preview URL (§ [When the Custom Preview URL matters](#custom-preview-url)). The UI walkthrough is the fallback for when no credential works. + ## Why this skill exists Studio, Live Preview, and Visual Editor share **one stack-level checkbox**: `Stack → Settings → Visual Experience → General → Live Preview → Enable Live Preview`. When unchecked, every downstream piece works locally and then **silently fails**: @@ -94,6 +96,20 @@ So confirm an API-driven enable in this order: The top-nav link is the cheapest human-visible signal: it appears when Live Preview is on, whether it was switched on by API or by checkbox, and disappears when it is off. +<a id="custom-preview-url"></a> + +## When the Custom Preview URL matters + +The Custom Preview URL (Settings, Visual Experience, **Preview URL** sub-tab) tells Contentstack how to build a page URL **from an entry**: alias plus path patterns such as `/blog/{{entry.url}}`. Who reads it decides whether you need it: + +| Consumer | Uses the Custom Preview URL? | +|---|---| +| **Studio canvas iframe** (`/canvas`) | No. Its address is the targeted environment's per-locale URL plus the project's Canvas URL ([`setup-section-preview`](setup-section-preview.md) § 0, [`configure-studio`](configure-studio.md)) | +| **Live Preview / Visual Editor opened from an entry** in the CMS | Yes, when set. It turns the entry into the URL the preview panel loads | +| **Your app's own routes** | No. The app routes by itself | + +So set it when editors will open entries in Live Preview or Visual Editor **and** the page for an entry is not simply the environment URL plus the entry's `url` field. A Studio template served at a prefixed route (`/blog/<slug>`) is the usual case. A setup that only authors in the Studio canvas can skip it and add it later. It has no API path that has been verified, so it is the one step that needs a human (Step 3). + ## Fallback: the UI walkthrough Use this when the API path 401s with a **freshly refreshed** token (an expired OAuth token 401s identically, refresh before concluding anything), or no credential resolves at all. It is also what a human follows to verify what the API did. @@ -106,11 +122,11 @@ Use this when the API path 401s with a **freshly refreshed** token (an expired O | --- | --- | --- | | 1. Enable Live Preview | Yes, `POST /v3/stacks/settings` | **SKIP the walkthrough.** Confirm by read-back: `live_preview.enabled === true`. Do not ask for a "done" | | 2. Default Preview Environment | Yes, the same call with `default-env: <env-uid>` | **SKIP.** Confirm `live_preview["default-env"]` is non-empty | -| 3. Custom Preview URL | No, not API-settable (untested) | **Walk the human through it**, the only step that still needs one | +| 3. Custom Preview URL | No, not API-settable (untested) | **Only if it matters** (§ [When the Custom Preview URL matters](#custom-preview-url)): walk the human through it. Otherwise record it as skipped | | 4. Preview Token | Yes, `POST …/delivery_tokens?create_with_preview_token=true` | **SKIP.** Confirm `token.preview_token` came back | | 5: Summary | - | Always print it, stating which steps were done by API and which by hand | -So on the API path the human is asked for **one** thing (Step 3), not four. Announce what you did rather than asking them to verify it: "Live Preview is on, default preview environment is `development`, preview token minted, all by API. One thing left for you: the Custom Preview URL." +So on the API path the human is asked for **at most one** thing (Step 3), not four. Announce what you did rather than asking them to verify it: "Live Preview is on, default preview environment is `development`, preview token minted, all by API. One thing left for you: the Custom Preview URL." If no credential resolved at all, or a write failed with a **freshly refreshed** token, fall through to the full walkthrough below. **Then** (and only then) ask the user to explicitly type "done" after each step, and do NOT batch the checks. @@ -150,6 +166,8 @@ Tell the user verbatim: ### Step 3: Set the Custom Preview URL on the Preview URL sub-tab +**Only when it matters** for this project: § [When the Custom Preview URL matters](#custom-preview-url). If not, skip it and say so in the summary. + Tell the user verbatim: > 1. On the **Visual Experience** page in **Settings**, click the **Preview URL** sub-tab. If it's locked with "Custom Preview URL Unavailable: You don't have permission to use a Custom Preview URL because Live Preview is disabled in the General settings," go back and re-do Step 1. @@ -196,7 +214,7 @@ Tell the user verbatim: Live Preview: ENABLED (Settings → Visual Experience → General) Default Preview env: <env name from Step 2> Custom Preview URL: - Base URL alias: <alias from Step 3> + Base URL alias: <alias from Step 3, or "skipped: Studio canvas only"> Base URL pattern: <pattern from Step 3> URL Path default: /{{entry.url}} (plus prefixed per-CT rows) Preview Token: generated on <delivery token name from Step 4> @@ -212,16 +230,15 @@ Next: ## Inputs needed from the user 1. `stackApiKey` (required). -2. `canvasAppUrl` (required): used in Step 3's Base URL pattern. +2. `canvasAppUrl`: used in Step 3's Base URL pattern, only when Step 3 applies. 3. `previewEnvironmentName` (default `preview`): selected in Step 2, scopes the Delivery Token in Step 4. ## Acceptance -- [ ] **CSLP tags reach the DOM**: the SDK config sets `cslp: { appendTags: true }`, and a rendered component's root shows `data-cslp` with each bound field tagged too. Live Preview can be fully enabled and still not be editable if components drop these, see [`register-component` § The `studioAttributes` contract](register-component.md). -- [ ] User confirmed Step 1 (Enable Live Preview checked + Save clicked + top-nav `Visual Experience` link appeared) -- [ ] User confirmed Step 2 (Default Preview Environment selected, NOT production) -- [ ] User confirmed Step 3 (Enable Custom Preview URL ON, Base URL alias + pattern set, URL Path set, Save clicked) -- [ ] User confirmed Step 4 (Preview Token generated, value copied) +- [ ] Step 1: `GET /v3/stacks/settings` reads `live_preview.enabled === true` (or, on the UI fallback, the user confirmed the checkbox, Save and the top-nav `Visual Experience` link). +- [ ] Step 2: `live_preview["default-env"]` is a non-production environment uid. +- [ ] Step 3: Custom Preview URL set (Enable Custom Preview URL ON, Base URL alias + pattern, URL Path, Save), **or** recorded as skipped because nothing opens entries in Live Preview / Visual Editor yet. +- [ ] Step 4: the delivery token's `preview_token` is non-empty (API) or the user copied it into `.env.local` (UI). - [ ] Final summary printed with all captured values ## Common pitfalls diff --git a/skills/studio/expose-section-props.md b/skills/studio/expose-section-props.md index 2e55cbb..0b67f7e 100644 --- a/skills/studio/expose-section-props.md +++ b/skills/studio/expose-section-props.md @@ -62,7 +62,7 @@ When the user is unsure whether they want Expose Props, use this decision table: In this order. If unclear, ask before proceeding. 1. `sectionName`: the section to edit -2. `propsToExpose`: concrete list of `component → prop → friendly label` triples. Push back if the user says "expose everything". That defeats the purpose of a section. +2. `propsToExpose`: concrete list of `component → prop → friendly label` triples. Take it from the approved plan: every Section in the [`plan-studio-architecture`](plan-studio-architecture.md) build sheet carries an `author can change` line, decided at plan time with the primitive library's design props (Step 2c). If the plan has none for this Section, ask rather than invent one. Push back if the user says "expose everything". That defeats the purpose of a section. ## Acceptance diff --git a/skills/studio/gate-third-party-scripts.md b/skills/studio/gate-third-party-scripts.md index 8be4380..c1c483f 100644 --- a/skills/studio/gate-third-party-scripts.md +++ b/skills/studio/gate-third-party-scripts.md @@ -30,6 +30,7 @@ Classify each hit with the recipe's rule: analytics / flags / banners / chat / p | JS init in app code (banner, chat, flags) | `if (!isStudioCanvas()) { … }` | | Analytics / pixels in app code | `if (!isStudioEditorMode()) { … }` (preview sessions aren't visitors either) | | GTM snippet in `<head>` (runs before SDK) | Inline `window.dataLayer = window.dataLayer || []; window.dataLayer.push({ cs_studio_canvas: new URLSearchParams(location.search).has("cs-composable-studio") })` BEFORE the GTM script, then tell the user to add the per-tag trigger condition `{{cs_studio_canvas}} equals false` in the GTM console. The SDK cannot do the GTM-console half | +| Analytics loader tag in `<head>` (`<script async src=".../gtag/js?id=…">`, a pixel's base script) | Leave the tag. Loading the library sends nothing; the `gtag("config" \| "event", …)` and `fbq(…)` calls do, so gate those calls in app code with `isStudioEditorMode()` (row 2). Don't add a `location.search` check to the tag: only the GTM flag above may read the URL | | Declaratively injected widgets you can't reach in code | CSS: `html.cs-studio-canvas <widget-selector> { display: none !important; }` | | Server-emitted script tags (SSR) | `isStudioCanvas(<request search params>)` (no-arg form is always `false` on the server) | diff --git a/skills/studio/handle-404-pages.md b/skills/studio/handle-404-pages.md new file mode 100644 index 0000000..57ad504 --- /dev/null +++ b/skills/studio/handle-404-pages.md @@ -0,0 +1,307 @@ +--- +name: handle-404-pages +--- + +## When to use + +Show the site's 404 page for any URL whose composition can't be loaded, on CSR and SSR routes. Phrases: "404 page", "not found page", "page not found", "unknown URL shows 500", "unknown URL shows a blank page", "unknown URL shows an error", "bad slug", "my 404 page stopped working after adding Studio", "Studio-authored 404". Covers wildcard (catch-all) and dedicated routes on Next.js (App and Pages Router), Remix and Vite / React Router. + +Do NOT use for: choosing CSR or SSR ([`configure-csr-vs-ssr`](configure-csr-vs-ssr.md)), or wiring the catch-all route itself ([`setup-template-preview-routes`](setup-template-preview-routes.md)). + +# Handle 404 pages on a Studio site + +## The rule + +**If there's no composition for the URL, render the 404 page. Any other failure is an error, not a 404.** + +- `useCompositionData` (CSR) returns the failure in `error`. +- `sdk.fetchCompositionData` (SSR) throws it. +- `isCompositionNotFound(error)` (from `@contentstack/studio-react`) tells "no composition for this URL" apart from everything else: the preview host down, a bad token, a network failure. + +Branch on that and nothing else. Never check `specOptions.spec`, `hasSpec` or `hasTemplate`: a successful fetch always returns a spec, and `hasTemplate` is `false` for every freeform composition. + +**Why not 404 on every error:** an outage would show visitors "Page not found" with status 404, and search engines drop pages that answer 404. A real failure has to stay a 500 (SSR) or an error state (CSR), so it's noticed and retried. + +## Why this matters + +A Studio catch-all route matches every URL, including ones with no composition behind them. Without this branch, unknown URLs render a blank page, an error screen or a 500, and the site's own 404 page never shows. Under a connected template, a mistyped slug (`/blog/no-such-post`) matches the URL pattern and fails later at entry lookup, so it needs the same branch. + +## Task + +1. **Find the site's existing 404 page and reuse it.** + + ```bash + find app src/app pages src -name 'not-found.*' -o -name '404.*' -o -name 'NotFound.*' 2>/dev/null + ``` + + Create one only when nothing exists. Next.js App Router: `app/not-found.tsx`. Pages Router: `pages/404.tsx`. React Router: a `NotFound` component. + +2. **Add the branch to every route that fetches a composition.** Use the CSR or SSR block below that matches the route. + +3. **Retire the old catch-all 404 route** (React Router only). See § React Router: one `path="*"`. + +4. **Run the acceptance checks.** + +## CSR: `useCompositionData` + +Vite / React Router: + +```tsx +// src/routes/StudioPage.tsx +import { useLocation } from "react-router-dom"; +import { useCompositionData, StudioComponent, isCompositionNotFound } from "@contentstack/studio-react"; +import NotFound from "../pages/NotFound"; // the site's existing 404 page +import ErrorState from "../pages/ErrorState"; // the site's error page + +export function StudioPage() { + const { pathname } = useLocation(); + const { specOptions, isLoading, error } = useCompositionData({ url: pathname }); + + if (error) return isCompositionNotFound(error) ? <NotFound /> : <ErrorState />; + if (isLoading || !specOptions) return <div>Loading…</div>; + + return <StudioComponent specOptions={specOptions} />; +} +``` + +Next.js client page: the same component with `"use client"` at the top and `usePathname()` from `next/navigation` instead of `useLocation()`. + +The hook reads the query string from `window.location` itself. Don't pass `searchQuery`. + +**A CSR page can't send a 404 status.** The server has already answered 200 with the app shell before the browser fetches the composition. Tell search engines not to index the page instead: + +```tsx +// pages/NotFound.tsx +export default function NotFound() { + return ( + <main> + <meta name="robots" content="noindex" /> + <h1>Page not found</h1> + <a href="/">Back to home</a> + </main> + ); +} +``` + +React 19 hoists the `<meta>` into `<head>`. On React 18, set it with your head manager (`react-helmet-async` or similar). If a real 404 status matters for SEO, make the route SSR. + +## SSR: `sdk.fetchCompositionData` + +Wrap the fetch in `try` / `catch`. In `catch`, send the 404 when `isCompositionNotFound(err)`, and rethrow anything else so it stays a 500. Pass the full request query string as `searchQuery`: Studio's editor context (locale, variant, edit mode) and its preview hash reach the server only that way. + +**Clear the preview hash before each fetch.** The stack in `lib/studio.server` is shared by every request. The SDK sets a request's preview hash on it but never clears it, so on a deployment with a preview token the next visitor is served the last editor's drafts. `stack.livePreviewQuery({ live_preview: "" })` before the fetch prevents that; when the request carries a hash, the SDK sets it again itself. The [`resolveComposition`](configure-csr-vs-ssr.md#resolve-composition-helper) helper does this and the `isCompositionNotFound` check in one call, if you'd rather not repeat them per route. + +### Next.js App Router + +```tsx +// app/[[...slug]]/page.tsx — Server Component, no "use client" +import { cache } from "react"; +import { notFound } from "next/navigation"; +import { getSSRStyles, isCompositionNotFound, type StudioComponentSpecOptions } from "@contentstack/studio-react"; +import { sdk, stack } from "@/lib/studio.server"; +import { StudioRender } from "./StudioRender"; + +type Props = { + params: Promise<{ slug?: string[] }>; + searchParams: Promise<Record<string, string | string[] | undefined>>; +}; + +// one fetch per request, shared by the page and generateMetadata +const getComposition = cache((url: string, searchQuery: string) => { + stack.livePreviewQuery({ live_preview: "" }); // shared stack: drop the last request's preview hash + return sdk.fetchCompositionData({ url, searchQuery }); +}); + +async function toQuery({ params, searchParams }: Props) { + const { slug } = await params; + const url = "/" + (slug ?? []).join("/"); + const searchQuery = new URLSearchParams( + Object.entries(await searchParams).flatMap(([k, v]) => (Array.isArray(v) ? v.map((x) => [k, x]) : v === undefined ? [] : [[k, v]])), + ).toString(); + return { url, searchQuery }; +} + +export default async function Page(props: Props) { + const { url, searchQuery } = await toQuery(props); + + let specOptions: StudioComponentSpecOptions; + try { + specOptions = await getComposition(url, searchQuery); + } catch (err) { + if (!isCompositionNotFound(err)) throw err; // a real failure stays a 500 + notFound(); // renders app/not-found.tsx with status 404 + } + + return ( + <> + <style dangerouslySetInnerHTML={{ __html: getSSRStyles(specOptions.spec) }} /> + <StudioRender specOptions={specOptions} /> + </> + ); +} + +export async function generateMetadata(props: Props) { + const { url, searchQuery } = await toQuery(props); + + try { + const { seo } = await getComposition(url, searchQuery); + return seo ? { title: seo.pageTitle, description: seo.pageDescription } : {}; + } catch { + return {}; // the page itself sends the 404 or the 500 + } +} +``` + +```tsx +// app/[[...slug]]/StudioRender.tsx +"use client"; +import { StudioComponent, type StudioComponentSpecOptions } from "@contentstack/studio-react"; + +export function StudioRender({ specOptions }: { specOptions: StudioComponentSpecOptions }) { + return <StudioComponent specOptions={specOptions} />; +} +``` + +Call `notFound()` in `catch`, never inside `try`. It works by throwing, so inside `try` your own `catch` swallows it. `cache()` makes the page and `generateMetadata` share one fetch per request instead of two. + +### Next.js Pages Router + +```tsx +// pages/[[...slug]].tsx +import type { GetServerSidePropsContext } from "next"; +import { getSSRStyles, isCompositionNotFound, StudioComponent, type StudioComponentSpecOptions } from "@contentstack/studio-react"; +import { sdk, stack } from "@/lib/studio.server"; + +export async function getServerSideProps({ resolvedUrl }: GetServerSidePropsContext) { + // resolvedUrl, not req.url: on client-side navigation req.url is /_next/data/....json + const { pathname, search } = new URL(resolvedUrl, "http://localhost"); + + let specOptions: StudioComponentSpecOptions; + try { + stack.livePreviewQuery({ live_preview: "" }); // shared stack: drop the last request's preview hash + specOptions = await sdk.fetchCompositionData({ url: pathname, searchQuery: search.slice(1) }); + } catch (err) { + if (!isCompositionNotFound(err)) throw err; // a real failure stays a 500 + return { notFound: true as const }; // renders pages/404.tsx with status 404 + } + + return { + props: { + specOptions: JSON.parse(JSON.stringify(specOptions)), // drops undefined values Next can't serialize + css: getSSRStyles(specOptions.spec), + }, + }; +} + +export default function Page({ specOptions, css }: { specOptions: StudioComponentSpecOptions; css: string }) { + return ( + <> + <style dangerouslySetInnerHTML={{ __html: css }} /> + <StudioComponent specOptions={specOptions} /> + </> + ); +} +``` + +### Remix + +```tsx +// app/routes/$.tsx +import { json, type LoaderFunctionArgs } from "@remix-run/node"; +import { useLoaderData } from "@remix-run/react"; +import { getSSRStyles, isCompositionNotFound, StudioComponent, type StudioComponentSpecOptions } from "@contentstack/studio-react"; +import { sdk, stack } from "~/lib/studio.server"; + +export async function loader({ request }: LoaderFunctionArgs) { + const { pathname, search } = new URL(request.url); + + let specOptions: StudioComponentSpecOptions; + try { + stack.livePreviewQuery({ live_preview: "" }); // shared stack: drop the last request's preview hash + specOptions = await sdk.fetchCompositionData({ url: pathname, searchQuery: search.slice(1) }); + } catch (err) { + if (!isCompositionNotFound(err)) throw err; // a real failure stays a 500 + throw new Response("Not Found", { status: 404 }); // rendered by the route's ErrorBoundary + } + + return json({ specOptions, css: getSSRStyles(specOptions.spec) }); +} + +export default function Page() { + const { specOptions, css } = useLoaderData<typeof loader>(); + return ( + <> + <style dangerouslySetInnerHTML={{ __html: css }} /> + <StudioComponent specOptions={specOptions} /> + </> + ); +} +``` + +## React Router: one `path="*"` + +The Studio catch-all and the old 404 route are both `path="*"`. The first match wins, so the old `<NotFound />` route can never render. Delete it. The catch-all's `error` branch now shows `NotFound`: + +```tsx +<Routes> + <Route path="/canvas" element={<CanvasRoute />} /> + <Route path="*" element={<StudioPage />} /> + {/* deleted: <Route path="*" element={<NotFound />} /> */} +</Routes> +``` + +## Dedicated routes + +A dedicated route (`/blog/:slug` rather than a catch-all) still fetches, and a bad slug still fails. Add the same branch to that route's component. URLs that no route matches keep falling through to the framework's own 404. + +## Inside Studio's canvas + +An unpublished template still opens in the canvas. Studio loads the canvas with its preview `hash` in the URL, and with the query string passed through (`searchQuery` on SSR; the hook reads it itself on CSR) the fetch returns the unpublished template, so the 404 branch never fires for it. Checked on a Next.js App Router site: the same unpublished template's URL with Studio's hash renders on the server; without the hash, the fetch fails. So the 404 branch needs no editor exception, as long as the query string reaches the fetch. + +- **CSR:** the hook reports the failure to Studio, so the canvas shows the cause and the page shows `NotFound`. +- **SSR:** a 404 stops the page before it renders, so nothing on that page starts the browser SDK. Call the browser `studioSdk.init()` from the root layout, as [`install-studio`](install-studio.md) wires it, so Studio still connects. With init only inside the page component, Studio reports "Studio Can't Connect to Your Site", which points at the wrong problem. + +## Optional: a 404 page authors can edit + +Build a freeform composition at `/404` ([`build-freeform-template`](build-freeform-template.md)). In the 404 branch, fetch `{ url: "/404" }` and render it. Keep the hard-coded page as the fallback when that fetch also fails. On SSR, still return status 404. + +## Acceptance + +Succeeds only when ALL of the following are true: + +- [ ] `/definitely-not-a-page` shows the site's 404 page with its header and footer. +- [ ] A bad slug under a connected template (`/blog/zzz-not-an-entry`) shows the same page. +- [ ] SSR: both URLs return status 404. Check with `curl -s -o /dev/null -w '%{http_code}' <url>`. +- [ ] CSR: the 404 page carries `<meta name="robots" content="noindex">`. +- [ ] A real page still renders, and its `generateMetadata` (App Router) returns its title. +- [ ] A real failure isn't a 404: with the delivery token changed to a wrong one, a page returns 500 (SSR) or the error state (CSR), not the 404 page. +- [ ] A new, unpublished template opens in Studio's canvas without showing the 404 page. +- [ ] React Router: only one `path="*"` route exists. +- [ ] No route checks `specOptions.spec`, `hasSpec` or `hasTemplate` to decide a 404, and every 404 branch checks `isCompositionNotFound`. + +If acceptance fails, report which check broke and stop. + +## Common pitfalls + +| Pitfall | Why it bites | Fix | +| --- | --- | --- | +| `await sdk.fetchCompositionData()` with no `try` | The fetch throws on a miss, so the framework's error page shows a 500 | Wrap it and send the 404 from `catch` | +| Every error sent to the 404 page | An outage (preview host down, bad token) shows "Page not found" with status 404, and search engines drop the pages | 404 only when `isCompositionNotFound(err)`; rethrow anything else | +| `if (!specOptions?.spec)`, `!hasSpec` or `!hasTemplate` as the 404 test | A successful fetch always has a spec, so the check never fires. `hasTemplate` is `false` on every freeform composition, so that one 404s working pages | Branch on `error` (CSR) or `catch` (SSR) only | +| `if (error) return <ErrorState />` on the CSR hook | Every unknown URL looks like a crash | `isCompositionNotFound(error) ? <NotFound /> : <ErrorState />` | +| `notFound()` inside `try` | It throws, and your own `catch` swallows it | Call it in `catch` | +| `generateMetadata` without `try` / `catch` | A miss throws there first, and the page errors instead of 404ing | Return `{}` from `catch` | +| Old `<Route path="*" element={<NotFound />} />` kept | The Studio catch-all matches first, so it never renders | Delete it. The catch-all's `error` branch renders `NotFound` | +| A new 404 page when the site already has one | Two designs, and the old one silently stops showing | Reuse the existing page | +| SSR `searchQuery` not forwarded | Studio's editor context and preview hash never reach the server, so the canvas renders the wrong locale or variant, and an unpublished template 404s in the canvas | Pass the full request query string | +| `generateMetadata` calling `fetchCompositionData` itself | Two fetches per request | Wrap the fetch in React's `cache()` and call that from both | +| SSR fetch without clearing the preview hash | The SDK sets a request's preview hash on the shared stack and never clears it, so later visitors get the last editor's drafts | `stack.livePreviewQuery({ live_preview: "" })` before each fetch, or use `resolveComposition` | +| Testing with one unknown URL only | A bad slug under a connected template fails at a later step | Test both URLs in Acceptance | +| SSR browser init only inside the page component | On a 404 the page never mounts, and Studio shows "Can't Connect" | Init from the root layout | + +## See also + +- [`setup-template-preview-routes`](setup-template-preview-routes.md): the catch-all route and site chrome +- [`configure-csr-vs-ssr`](configure-csr-vs-ssr.md): choosing and wiring CSR or SSR +- [`csr-and-ssr-requirements`](csr-and-ssr-requirements.md): what a CSR or SSR page needs, including Live Preview and Visual Editor +- [`troubleshoot-composition-resolution`](troubleshoot-composition-resolution.md): why a URL that should resolve doesn't diff --git a/skills/studio/import-design-tokens.md b/skills/studio/import-design-tokens.md index 2803674..bbf31af 100644 --- a/skills/studio/import-design-tokens.md +++ b/skills/studio/import-design-tokens.md @@ -4,9 +4,9 @@ name: import-design-tokens ## When to use -Read a project's design tokens from Tailwind, CSS variables, or JSON and register them with Studio so the Design Panel offers them for spacing, colors, typography, radius, shadow. +Read a project's design tokens from Tailwind (a v3 `tailwind.config` or a v4 CSS `@theme` block), CSS variables, or JSON and register them with Studio so the Design Panel offers them for spacing, colors, typography, radius, shadow. -Use when the user wants Studio's Design Panel to show THEIR tokens instead of Studio's defaults, e.g. "use my Tailwind colors in Studio", "register my CSS variables", "I have a tokens.json from Style Dictionary". Run AFTER `register-component`. Do NOT use to register breakpoints (use `register-breakpoints`). Do NOT use to register fonts as static assets, tokens only. +Use when the user wants Studio's Design Panel to show THEIR tokens instead of Studio's defaults, e.g. "use my Tailwind colors in Studio", "Tailwind v4 @theme", "register my CSS variables", "I have a tokens.json from Style Dictionary". Run AFTER `register-component`. Do NOT use to register breakpoints (use `register-breakpoints`). Do NOT use to register fonts as static assets, tokens only. > **Mandatory 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 (**ask the user before reading or refreshing it**: refreshing rotates the refresh token; see that skill for where it lives). **Don't ask for a session `authtoken` up front**: it is the last rung, considered only after a freshly refreshed OAuth token has been observed to fail, and only after explaining the risk. 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. @@ -56,6 +56,23 @@ If the user reports "every field is disabled" or "there's no Design tab," walk t ``` Pull `theme.colors`, `theme.spacing`, `theme.fontFamily`, `theme.fontSize`, `theme.borderRadius`, `theme.boxShadow`, `theme.maxWidth`. Tailwind uses nested objects for color shades (`{ primary: { 500: '#…' } }`). Flatten to a token name like `primary-500`. + **`tokenSource=tailwind` on Tailwind v4 (no `tailwind.config`, a CSS file with `@import "tailwindcss"` and an `@theme { … }` block).** The `@theme` block is the token source. Parse its `--*` declarations like CSS variables, and let the namespace pick the section: + + | `@theme` variable | Goes in | Token name | + |---|---|---| + | `--color-brand-500` | `colorTokens` | `brand-500` | + | `--spacing: 0.25rem` (one base unit) | `spaceTokens` | v4 derives the scale (`p-4` = 4 × base). Emit the steps the design uses as literals: `{ "2": "0.5rem", "4": "1rem", "8": "2rem" }`. Explicit `--spacing-*` entries map one to one | + | `--text-xl`, `--text-xl--line-height` | `typography.fontSize`, `typography.lineHeight` | `xl` | + | `--font-weight-semibold` | `typography.fontWeight` (number) | `semibold` | + | `--font-sans`, `--font-display` | `typography.style[name].fontFamily` | `sans`, `display` | + | `--tracking-*`, `--leading-*` | `typography.letterSpacing`, `typography.lineHeight` | suffix | + | `--radius-md` | `border.radius` | `md` | + | `--shadow-md` | `shadow.style` (decompose, one layer) | `md` | + | `--container-3xl` | `size.maxWidth` | `3xl` | + | `--breakpoint-*` | not here: [`register-breakpoints`](register-breakpoints.md) | | + + Skip `--*: initial` resets and `--animate-*` / `--ease-*`. Resolve every value to a literal (and any `var(--…)` it references): by default Tailwind v4 writes a theme variable into the built CSS only when a utility uses it, so a `var(--color-brand-500)` token can point at nothing in the canvas. + **`tokenSource=css`**. Parse `:root { --primary: …; }` rules from the CSS file. Map each `--*` var to a token name under the right section (user must annotate which `--*` vars are which scope, ask if unclear). **`tokenSource=json`**. Parse Style Dictionary / Theo / DTCG JSON. For DTCG `$value` references, resolve them to literals before registration (Studio doesn't follow `$value` chains). @@ -117,7 +134,7 @@ If the user reports "every field is disabled" or "there's no Design tab," walk t ## Inputs needed from the user -1. `tokenSource`: required. Auto-detect a default if there's an obvious match (if `tailwind.config.{js,ts}` exists, suggest `tailwind`, and if `tokens.json` exists, suggest `json`). +1. `tokenSource`: required. Auto-detect a default if there's an obvious match (if `tailwind.config.{js,ts}` exists, or a CSS file holds `@import "tailwindcss"` plus an `@theme` block, suggest `tailwind`, and if `tokens.json` exists, suggest `json`). 2. `tokenSourcePath`: required for tailwind/css/json modes, skip for manual. 3. `scopes`: default to all five. @@ -147,7 +164,9 @@ If the user has tokens spread across multiple sources (e.g. Tailwind colors + a | Design tab appears for built-ins but not your component | Your `registerComponent` has no `styles` group | Add a `styles` group to the component (`register-component`) | | Registering a `var(--…)` color not defined in the canvas-app stylesheet | Studio shows the token name but the canvas renders unstyled | Resolve the `var()` to a literal at import time, or load the same stylesheet in the canvas app | | Importing Tailwind's full theme (defaults + extend) | Duplicate keys. First registration wins silently | Read `theme.extend`, or use `resolveConfig` and the merged result | -| Forgetting to re-run after editing the source file | Tokens are read once at startup | Add a comment in the registration file: "Re-run import-design-tokens after editing tailwind.config" | +| Forgetting to re-run after editing the source file | Tokens are read once at startup | Add a comment in the registration file: "Re-run import-design-tokens after editing tailwind.config / the @theme block" | +| Tailwind v4 project: reading only `tailwind.config` | v4 has no config file by default. The tokens live in a CSS `@theme` block, so a config-only import finds nothing | Parse `@theme` (Task step 1, Tailwind v4) | +| Utility-CSS project (Tailwind): authors type class names in Studio | Tailwind generates only classes it finds in source files. A class typed in Studio is never generated and renders unstyled, with no error | Give components closed design-prop menus whose class strings are written in source, or SDK `design: true` props ([`design-component-library`](design-component-library.md) § Utility-CSS projects) | ## See also diff --git a/skills/studio/install-contentstack-mcp.md b/skills/studio/install-contentstack-mcp.md index 48e5495..6132378 100644 --- a/skills/studio/install-contentstack-mcp.md +++ b/skills/studio/install-contentstack-mcp.md @@ -21,6 +21,8 @@ Run discovery on whole-project setup and API-provisioning flows, including when 3. If no tools are callable, inspect the current host's supported MCP configuration mechanism before registering the server. Resolve the final stack and region first. Use the host's documented restart/reconnect behavior; do not invent commands or edit another IDE's configuration. 4. After login or restart, repeat discovery and the read probe. Record supported operations and any remaining HTTP fallback reasons, without credentials. +The same gate applies to the **hosted Contentstack connector** on claude.ai: if its tools are callable here and the read probe passes on the target stack, it satisfies the gate for those operations. What it is and what is still unconfirmed about it: [`understand-contentstack-mcp`](understand-contentstack-mcp.md) § The hosted connector on claude.ai. + The `claude mcp` commands below are Claude Code examples only. In other hosts, use their own configuration interface. CLI login commands shown with leading `!` use Claude's shell shortcut; omit `!` in an ordinary terminal. ## MCP-first credential boundary @@ -172,12 +174,20 @@ Then hand them the Step 3 command again. A `refresh_token` is stored alongside t ### Step 4: Register the MCP server +**Resolve the stack first, then register once.** The stack key is part of the registration, and changing it later means remove, re-add and restart. Run the Step 5 loop before this step whenever you can: it needs only the OAuth login from Step 3, not the MCP server. Register keyless only when the stack really is undecided. + **claude-code CLI:** ```bash -claude mcp add contentstack -e GROUPS=cma,cma-extended -e CONTENTSTACK_REGION=<resolved-region> -- npx -y @contentstack/mcp +claude mcp add contentstack --scope local \ + -e GROUPS=cma,cma-extended -e CONTENTSTACK_REGION=<resolved-region> -e CONTENTSTACK_API_KEY=<blt...> \ + -- npx -y @contentstack/mcp ``` -`<resolved-region>` is the Step 0 region (`NA` for prod NA, etc.). **Pin it explicitly, see the callout below.** Omitting it lets a stray ambient value crash the server. +`<resolved-region>` is the Step 0 region (`NA` for prod NA, etc.). **Pin it explicitly, see the callout below.** Omitting it lets a stray ambient value crash the server. Drop `-e CONTENTSTACK_API_KEY` only for a keyless registration. + +**Scope decides where the tools exist.** `--scope local` (the default) records the server for the folder you ran the command in. Its tools load only in a `claude` session started from that same folder. Run `claude mcp add` from the project root, and restart from there too. `--scope user` makes it available in every folder (with one stack key for all of them). `--scope project` writes `.mcp.json` into the repo for the team; the stack key is not a secret, and each person still runs their own `--auth`. + +**Then restart Claude Code fully**: exit the session and launch `claude` again from the same folder. MCP tool schemas load at session start, so the current session never sees the new server. **Claude Desktop / Cursor / VS Code (JSON config):** ```json @@ -195,7 +205,7 @@ Config locations: Claude Desktop (macOS) `~/Library/Application Support/Claude/c > `GROUPS=cma,cma-extended` enables the write tools the provisioning skills need. Add `cda` only if a skill needs published-content reads. Do NOT set a `CONTENTSTACK_MANAGEMENT_TOKEN` when using OAuth. The stored session is the credential. -> **Pin `CONTENTSTACK_REGION` in the add config: the MCP inherits your shell environment.** The stdio server inherits the ambient env. If you're in a repo whose `.env` (or your shell) sets `CONTENTSTACK_REGION` to a non-standard value (a custom data-center name), the server reads THAT instead of your OAuth region and **throws `Unknown region "<x>"` at startup**, so `claude mcp list` shows `✘ Failed to connect — Connection closed` even though your OAuth session is valid and you never passed a bad region. This is the single most common "the MCP won't connect" cause on a machine that also runs an app against a custom / private data center. `-e CONTENTSTACK_REGION=<resolved-region>` (CLI) or the `env` block (JSON) overrides the stray value. Diagnose via the connection log: `~/Library/Caches/claude-cli-nodejs/<project-path>/mcp-logs-contentstack/`. The newest `.jsonl` prints the exact `Server stderr: Error: Unknown region …`. Pin `CONTENTSTACK_API_KEY` the same way for the same reason once the target stack is resolved (Step 5). +> **Pin `CONTENTSTACK_REGION` in the add config: the MCP inherits your shell environment.** The stdio server inherits the ambient env. The most common source is **the app's own `CONTENTSTACK_REGION`**: Contentstack apps and starters commonly name their own region setting exactly that, in the repo's `.env` / `.env.local` or your shell, and the server picks it up when launched from that folder. A standard value such as `us` or `eu` normalizes and works by luck. A custom data-center name, or any value outside the region list, does not: the server reads THAT instead of your OAuth region and **throws `Unknown region "<x>"` at startup**, so `claude mcp list` shows `✘ Failed to connect — Connection closed` even though your OAuth session is valid and you never passed a bad region. This is the single most common "the MCP won't connect" cause on a machine that also runs an app against a custom / private data center. `-e CONTENTSTACK_REGION=<resolved-region>` (CLI) or the `env` block (JSON) overrides the stray value. Diagnose via the connection log: `~/Library/Caches/claude-cli-nodejs/<project-path>/mcp-logs-contentstack/`. The newest `.jsonl` prints the exact `Server stderr: Error: Unknown region …`. Pin `CONTENTSTACK_API_KEY` the same way for the same reason once the target stack is resolved (Step 5): an app's own `CONTENTSTACK_API_KEY` can point the server at a different stack. ### Step 5: Choose the target stack (a loop of new-or-existing, then permission, then recover) @@ -211,11 +221,11 @@ The MCP operates on ONE stack (`CONTENTSTACK_API_KEY`), which **MUST belong to t - **Choose existing stack**: the user pastes `api_key`, then **go to "Set the key" (loop exit).** - **Choose different org**: **re-run Step 3, the browser re-opens.** Before logging in, the user switches their **active org** (Contentstack top-left switcher) to one where they can create (there's no in-flow org-picker, the login captures the active org). New org captured, so **loop back to Q1** (re-ask new-vs-existing for the new org). Repeat until exit. -**Set the key** (loop exit): the server was registered keyless in Step 4, and **`claude mcp add` errors if the name already exists**, so **remove, then re-add** with the key (CLI), or edit the `env` block directly (JSON config), then restart and go to Step 6: +**Set the key** (loop exit): if you ran this loop before Step 4, go back and register once with the key. Only if the server was already registered keyless: **`claude mcp add` errors if the name already exists**, so **remove, then re-add** with the key (CLI, same `--scope` and same folder as before), or edit the `env` block directly (JSON config), then restart and go to Step 6: ```bash -claude mcp remove contentstack -claude mcp add contentstack -e GROUPS=cma,cma-extended -e CONTENTSTACK_REGION=<resolved-region> -e CONTENTSTACK_API_KEY=<blt...> -- npx -y @contentstack/mcp +claude mcp remove contentstack --scope local +claude mcp add contentstack --scope local -e GROUPS=cma,cma-extended -e CONTENTSTACK_REGION=<resolved-region> -e CONTENTSTACK_API_KEY=<blt...> -- npx -y @contentstack/mcp ``` For multi-stack work in one session, repeat this remove-then-re-add when switching stacks. @@ -230,6 +240,25 @@ After a **session restart** (so the MCP's tools load into the agent's toolset), Note `claude mcp list` showing `✔ Connected` only means the server process started. It does NOT confirm CMA calls work. The tool call is the real test. +<a id="cts-created-after-start"></a> + +### Step 6b: Content types created after start-up need HTTP (or a restart) + +The server reads the stack's content types **once, at start-up**, and writes them into its tool schemas as the allowed values (an `enum`) of every `content_type_uid` argument (`@contentstack/mcp` 0.9.0, tool enrichment; environments and locales get the same treatment). Its update, read and publish tools then accept only those content types: a content type created in this session (by its own `create_a_content_type`, by `curl`, or in the UI) is rejected until Claude Code restarts. A new environment is likewise missing from the `environment` enums. + +So, after creating a content type in a running session: + +| Step on the new content type | Path until the next restart | +|---|---| +| Create it | MCP `create_a_content_type` works | +| Follow-up schema change (the compositions CT's `linked_sections` PUT, [`provision-studio-project`](provision-studio-project.md) step 8) | HTTP `PUT /v3/content_types/<uid>` with `$CS_AUTH` | +| Verify it (`GET /v3/content_types/<uid>`) | HTTP | +| Create, update or publish its entries (composition entries, seed entries) | HTTP | + +Either plan it: create every new content type first, then restart Claude Code once (same folder) and run the rest through the MCP. Or keep everything that touches the new type on HTTP for the rest of the session. Record which, so a later step doesn't retry the MCP tool. + +The source also has an off switch, `CONTENTSTACK_MCP_ENRICH_TOOLS=false` in the registration's `env`, which skips the enums. It has not been tried end to end with the Studio flow: if you use it, verify an update on a freshly created content type before relying on it. + ### Step 7: Write the session flag Record an in-conversation note downstream skills read. Get the org by reading `organization_uid` from the stored session (`~/Library/Application Support/ContentstackMCP/oauth-config.json`): **ask the user before reading it**, extract only that field, and never print the token. **Use the exact flag names the provisioning skills + the Playwright hook already consume** (`CS_RECENT_STACK_API_KEY`, `CS_ACTIVE_ORG`). A differently-named key is never read: @@ -250,6 +279,7 @@ Print: Runs as tool calls now (no curl, no pasted authtoken): • provision-studio-stack — global fields, CTs, entries, asset dedupe + publish • provision-studio-project — compositions CT create + environment create + (until a restart, the CT's PUT/verify/entries go over HTTP: Step 6b) • author-composition-via-api — entry create/update + publish STILL raw HTTP (not in MCP scope) — same OAuth credential, sent as `Authorization: Bearer`: @@ -274,7 +304,9 @@ STILL raw HTTP (not in MCP scope) — same OAuth credential, sent as `Authorizat - [ ] If accepted: `claude mcp list` shows `contentstack`. `--auth` completed (browser OAuth, region selected, org auto-captured). - [ ] Stack key resolved (existing pasted, or new stack created) AND it belongs to the OAuth'd org. - [ ] Smoke test (`get_all_content_types` / `get_all_environments`) returns real data, NOT just `✔ Connected`. On the `-32603 …'data'` crash, re-auth for a fresh token and retry. +- [ ] Registered once, with the stack key, from the project folder, and Claude Code restarted from that same folder. - [ ] Session note written: `CONTENTSTACK_MCP_READY=true`, region, org, stack API key. +- [ ] Any content type created this session is handled over HTTP (or after a restart) for update, read and publish (Step 6b). - [ ] User told which provisioning steps run as tools vs stay raw HTTP. ## Common pitfalls @@ -298,6 +330,9 @@ STILL raw HTTP (not in MCP scope) — same OAuth credential, sent as `Authorizat | `GROUPS` too narrow (`cma` only) | Some skills need CMA-extended (workflows/versions) | Register with `GROUPS=cma,cma-extended` | | `--auth` dies instantly: `Error: User force closed the prompt with 0 null` | Run in a shell with **no TTY**. The arrow-key menus can't read input | Hand the user `! CONTENTSTACK_REGION=<resolved-region> npx @contentstack/mcp --auth` to run in their own terminal (Step 3) | | MCP installed but not restarted | Tools don't appear. Skills silently fall back to curl | Restart the `claude` session / IDE after `claude mcp add`. MCP tool schemas load at session start | +| Restarted `claude` from a different folder | `--scope local` (the default) binds the server to the folder `claude mcp add` ran in: the tools are missing, `claude mcp list` there shows nothing | Restart from the same folder, or re-add with `--scope user` | +| Updating, reading or publishing a content type created after the server started | The tools only accept content types that existed at start-up, so the call is rejected | HTTP for that content type until a restart (Step 6b) | +| Registering keyless, then remove + re-add with the key | An extra restart, and a window where tools run against no stack | Resolve the stack (Step 5) before Step 4 and add once with `CONTENTSTACK_API_KEY` | | Assuming CMA works because `claude mcp list` shows `✔ Connected` | "Connected" only means the server process started, not that CMA calls succeed | Confirm with an actual read tool (Step 6), not the connection status | | Session flag lost to context compaction | Downstream skill re-prompts for authtoken mid-flow | Re-run the Step 6 smoke test when a provisioning skill starts rather than trusting the flag indefinitely | diff --git a/skills/studio/install-studio.md b/skills/studio/install-studio.md index 2dec727..a567e2f 100644 --- a/skills/studio/install-studio.md +++ b/skills/studio/install-studio.md @@ -84,11 +84,50 @@ Before doing anything destructive, check whether the user already has `@contents - Search for `Contentstack.stack({` or `contentstack.stack({`. The existing stack factory is what to reuse. - Search for `ContentstackLivePreview.init(`. If present, do NOT call `init()` again. Doing so double-initializes the channel. - Add `@contentstack/studio-react` only. -- Create a SEPARATE module (e.g. `lib/studio.ts`) that imports the existing `stack` and calls `studioSdk.init({ stackSdk: stack, contentTypeUid: "<studioContentTypeUid>" })`, using the uid resolved in § 0c. Export `sdk` from there. This branch needs § 0c exactly as much as the greenfield one does. +- Create a SEPARATE module (e.g. `lib/studio.ts`) that imports the existing `stack` and calls `studioSdk.init({ stackSdk: stack, contentTypeUid: "<studioContentTypeUid>", cslp: { appendTags: true } })`, using the uid resolved in § 0c. Export `sdk`, and export the stack it passed as `stack`: [`resolveComposition`](configure-csr-vs-ssr.md#resolve-composition-helper) imports it (step 6b). This branch needs § 0c exactly as much as the greenfield one does. - Do not modify the existing `lib/contentstack.ts` (or equivalent module). The full emit would clobber a custom stack factory. If NEITHER package is installed, proceed with the greenfield emit below. +<a id="live-preview-without-delivery-sdk"></a> + +### Live Preview present, no Delivery SDK + +Common: the app reads content with its own `fetch` client and only uses `@contentstack/live-preview-utils` for the edit channel. Studio cannot run without a Delivery SDK stack (`studioSdk.init` needs `stackSdk`), so this branch adds one, for Studio alone: + +- `<pm> add @contentstack/delivery-sdk @contentstack/studio-react`. Leave `@contentstack/live-preview-utils` at the version the app pins. +- Create `lib/studio.ts` with its own stack, built from the same credentials the app's fetch client already reads. Do not route the app's own reads through it. +- Keep the app's existing `ContentstackLivePreview.init(...)` exactly as it is, and **do not call `init()` again**. One init per page serves both the app's edit tags and Studio. +- Take region and hosts from the Regional host map above, as in the greenfield file. + +```ts +// lib/studio.ts — the app already starts Live Preview; it had no Delivery SDK +import Contentstack from "@contentstack/delivery-sdk"; +import { studioSdk } from "@contentstack/studio-react"; + +const previewToken = process.env.NEXT_PUBLIC_CONTENTSTACK_PREVIEW_TOKEN; + +// Studio's own stack. Same credentials the app's fetch client uses; region and +// host exactly as in the greenfield file below (CDN_HOSTS / PREVIEW_HOSTS). +export const stack = Contentstack.stack({ + apiKey: process.env.NEXT_PUBLIC_CONTENTSTACK_API_KEY!, + deliveryToken: process.env.NEXT_PUBLIC_CONTENTSTACK_DELIVERY_TOKEN!, + environment: process.env.NEXT_PUBLIC_CONTENTSTACK_ENVIRONMENT!, + ...(previewToken ? { + live_preview: { enable: true, preview_token: previewToken, host: "<preview-host>" }, + } : {}), +}); + +// No ContentstackLivePreview.init() here: the app's existing call stays the only one. +export const sdk = studioSdk.init({ + stackSdk: stack, + contentTypeUid: process.env.NEXT_PUBLIC_CONTENTSTACK_STUDIO_CONTENT_TYPE ?? "<studioContentTypeUid>", + cslp: { appendTags: true }, +}); +``` + +Use the app's existing env variable names rather than inventing new ones, and the framework's env syntax (step 6 table). Then continue with steps 6b to 10, skipping the greenfield file in step 6. + > **Hazard: reusing the existing stack instance means Studio shares its Live Preview state with everything else that touches it.** `stack.livePreviewQuery(...)` doesn't scope to one call: it mutates a single shared object (`_client.stackConfig.live_preview` in `@contentstack/delivery-sdk`, the equivalent on the legacy `contentstack` v3 SDK) that lives on the stack instance itself, not on the request. If the app's OWN code calls `.livePreviewQuery(...)` on that SAME stack anywhere else — even with a legitimate, unrelated reason, e.g. a parent layout's own data fetch doing `stack.livePreviewQuery(live_preview ?? {})` with no override — it resets that shared state to empty. Next.js (and most SSR frameworks) run a page's layout(s) and the page itself concurrently on one request, so that reset can land in between two of Studio's OWN internal composition-resolution queries for the SAME lookup, flipping some of them back to the Delivery API mid-resolution with no code change and no error. Symptom: Live Preview intermittently doesn't apply — see [`troubleshoot-canvas`](troubleshoot-canvas.md) § Live Preview state resets mid-request for the full diagnosis. **If the app makes its own `.livePreviewQuery(` calls elsewhere on the same stack, give Studio's `stackSdk` its own independent stack instance instead of reusing the app's.** Reuse the app's Delivery Token/credentials to construct it, just not the same object. ## Task @@ -122,8 +161,9 @@ grep -rEn "Contentstack\.stack\(|contentstack\.stack\(" --include='*.ts' --inclu # Existing Live Preview init (NEVER call init() twice) grep -rEn "ContentstackLivePreview\.init\(" --include='*.ts' --include='*.tsx' --include='*.js' --include='*.mjs' . 2>/dev/null | head -5 -# Existing Contentstack-config module (common names) -ls src/lib/contentstack.* lib/contentstack.* src/contentstack.* contentstack.config.* 2>/dev/null +# Existing Contentstack-config module (common names). `find`, not a bare glob: +# zsh aborts the whole line with "no matches found" when a glob matches nothing. +find . -maxdepth 3 -path ./node_modules -prune -o \( -name 'contentstack.*' -o -name 'contentstack.config.*' \) -print 2>/dev/null # package.json — Delivery SDK + Live Preview already installed? grep -E "@contentstack/delivery-sdk|@contentstack/live-preview-utils" package.json | head -3 @@ -134,7 +174,8 @@ grep -E "@contentstack/delivery-sdk|@contentstack/live-preview-utils" package.js | Signal | Branch to take | |---|---| | Both `@contentstack/delivery-sdk` AND `@contentstack/live-preview-utils` already in `package.json` | **Minimal-add**: `npm install @contentstack/studio-react` only, create separate `lib/studio.ts` (see § Existing Contentstack app above) | -| Only one of the two installed | Ask user, likely partial install in progress. Do NOT auto-install the other | +| `@contentstack/live-preview-utils` installed, **no** `@contentstack/delivery-sdk` (the app fetches with its own client) | **Live Preview without Delivery SDK**: install `@contentstack/delivery-sdk` + `@contentstack/studio-react`, give Studio its own stack in `lib/studio.ts`, keep the app's Live Preview init and don't start it a second time ([§ Live Preview present, no Delivery SDK](#live-preview-without-delivery-sdk)) | +| `@contentstack/delivery-sdk` installed, **no** `@contentstack/live-preview-utils` | **Minimal-add** on the existing stack for Studio, then add Live Preview to that stack with [`install-live-preview`](install-live-preview.md). Ask first if the stack factory looks half-built (a partial install in progress) | | **Existing `lib/contentstack.ts`** (or similar) with a stack factory | **Minimal-add**: even if package.json looks "clean," the project may have a custom stack module. Don't overwrite. | | **Existing `ContentstackLivePreview.init(...)` call** anywhere in the codebase | **Minimal-add**: adding a second `init()` double-initializes the channel and breaks Live Preview. | | NEITHER package installed AND no existing stack module | **Greenfield**: proceed with the full emit below | @@ -420,6 +461,37 @@ curl -s "https://<studio-api-host>/v1/projects" \ `export const sdk = studioSdk.init(...)`: the return value is the live SDK that downstream skills call (`sdk.fetchCompositionData(...)`). The `studioSdk` namespace export is un-initialized. +6b. **Create `lib/resolve-composition.ts` next to the init file, on every branch.** Every server route fetches through it. It does two things a bare `sdk.fetchCompositionData` call doesn't: + + - **Clears the last request's preview hash off the shared stack.** The stack above lives for the whole server process. The SDK sets a request's `hash` on it but never clears it, so after one Studio preview every later visitor is served that session's drafts until restart. Reproduced on a live app. + - **Turns a miss into a 404 and leaves real failures as 500s**, via `isCompositionNotFound`. + + ```ts + // lib/resolve-composition.ts — server-only + import { isCompositionNotFound, type studioSdk } from "@contentstack/studio-react"; + // The Delivery stack your Studio sdk was initialized with (`stackSdk`). On the + // minimal-add branch that module is lib/studio.ts. + import { stack } from "@/lib/contentstack"; + + type StudioSdk = ReturnType<typeof studioSdk.init>; + type FetchArgs = Parameters<StudioSdk["fetchCompositionData"]>; + + export async function resolveComposition(sdk: StudioSdk, args: FetchArgs[0], opts: FetchArgs[1] = {}) { + // Clearing first is always safe: when this request carries a hash, the SDK + // sets it again itself. + stack.livePreviewQuery({ live_preview: "" }); + try { + const specOptions = await sdk.fetchCompositionData(args, opts); + return { specOptions, notFound: false as const }; + } catch (err) { + if (isCompositionNotFound(err)) return { specOptions: null, notFound: true as const }; + throw err; // real failure — must stay a 500 + } + } + ``` + + This is the canonical helper from [`configure-csr-vs-ssr` § resolveComposition](configure-csr-vs-ssr.md#resolve-composition-helper), which explains both halves. Vite SPAs fetch in the browser with `useCompositionData` and don't need it until they add a server render. + 7. **Create `.env.local`** (gitignored) with the collected values: ``` <PREFIX>_CONTENTSTACK_API_KEY=<stackApiKey> @@ -549,6 +621,7 @@ This skill succeeds only when ALL of the following are true. If any fails, do no - [ ] Delivery credentials passed the delivery read; preview credentials passed `verify-setup` Layer 6 on the preview host. These are separate checks. - [ ] The target environment has a non-empty Base URL for **every locale the app serves** (asserted from the `environments` pre-flight — the default locale alone is not enough on a localized app), or the user was told which locales are missing and stopped - [ ] `src/lib/contentstack.ts` (or framework equivalent) exists with the three SDK inits +- [ ] On a server-rendering framework, `lib/resolve-composition.ts` exists (step 6b), imports the same `stack` the init passed as `stackSdk`, and clears the preview hash before each fetch - [ ] **`cslp: { appendTags: true }` is set in `studioSdk.init`**. Every Studio project is a Visual Editor project. With this off or absent, `studioAttributes` and every `$`-twin arrive empty across the whole app and no component-level fix helps. See [`complete-the-build`](complete-the-build.md) item 1. - [ ] Live Preview init is wrapped in **try/catch** and the `live_preview` block on the stack is **conditional on a non-empty preview token** - [ ] **A Studio project was resolved from an actual `GET /v1/projects` listing, not assumed.** With 2+ projects the user picked; with exactly 1 it was named back to them. Its `connectedStackApiKey` equals `<stackApiKey>` @@ -585,7 +658,7 @@ Same fetch, same hash, sometimes hits `rest-preview.contentstack.com` and someti Not a broken install, three separate gates control the Design tab: -1. **Design tab missing entirely** (right panel is Settings only) means **Enable Freeform Feature** is off on the Studio project. Toggle it in Studio's Settings, under Configuration, or PUT `settings.configuration.isFreeformEnabled: true` via the Studio API. +1. **Design tab missing entirely** (right panel is Settings only) means **Enable Freeform Feature** is off on the Studio project. Toggle it in Studio's Settings, under Configuration, or set `settings.isFreeformEnabled: true` with `PUT /v1/projects/<uid>` (a sibling of `settings.configuration`, not inside it; full body in [`provision-studio-project`](provision-studio-project.md) step 10). Read the project back and confirm the flag before reopening Studio. 2. **Design tab present but every control looks disabled / read-only** means `registerDesignTokens(tokens)` was called **without** the options arg. `allowedValuesLevel` defaults to `"dynamic"` which permits data-binding only, not token selection. **Fix:** always pass `{ allowedValuesLevel: "tokens" }` as the second arg. See [`import-design-tokens`](import-design-tokens.md) for the full token payload shape. 3. **Design tab appears but is empty for one specific component** means that component's `registerComponent` schema didn't declare a `styles` block. See [`register-component`](register-component.md) § Design panel: component must declare `styles`. diff --git a/skills/studio/make-visual-editor-editable.md b/skills/studio/make-visual-editor-editable.md index 911f991..8c48397 100644 --- a/skills/studio/make-visual-editor-editable.md +++ b/skills/studio/make-visual-editor-editable.md @@ -28,6 +28,8 @@ Visual Editor's list controls read two tags: the **list tag** `<…>.<field>` on | The Repeater's child is a Condition Block holding a Section, or a Section Slot | The item tag never reaches an element: a Section or Section Slot renders no element of its own, so every item tag is dropped | Make a **Box** the Repeater's only child and put the Condition Blocks inside it: Repeater → Box → Condition Blocks → Sections | | A Section scoped to the list field (`selected_field: "blocks"`) with its Repeater bound to the Section root | The list renders, but the tags are looked up on the bare array, which has none: no list tag and no item tags | Scope the Section to the whole entry and bind the Repeater to `<field>` | +All three fixes together are the [canonical list recipe](author-composition-repeater-recipes.md#canonical-list-recipe): whole-entry scope, Repeater bound to the field, a Box as its only child, then the Condition Blocks. By API, the Compositions API validator warns on that Box that the items won't render. The warning is expected on this shape: keep the Box. + **Worked example (measured):** a page-builder template built in Studio's UI (Repeater over a modular-blocks field → Condition Block → Section per block type) rendered the list tag `page_sections` and the fields inside each block (`page_sections.0.cta_block.label`), but **no** `page_sections.0` / `.1` / `.2`. With a Box between the Repeater and the Condition Blocks, all three item tags appeared and nothing else changed. Check, on the rendered HTML of an entry with at least one item: diff --git a/skills/studio/match-existing-pattern.md b/skills/studio/match-existing-pattern.md index 2aea1ec..b0244f9 100644 --- a/skills/studio/match-existing-pattern.md +++ b/skills/studio/match-existing-pattern.md @@ -145,7 +145,10 @@ Matching the shape is not enough. The bindings have to match too. Read them off ### Step 6: Record the choice, then enforce it Re-asking on every build is noise, and it lets two answers coexist in one project. Write the decision to -`.studio/pattern.json` in the consuming repo the first time it is made, and read it before Step 1: +`.studio/pattern.json` in the consuming repo as soon as the pattern is settled, and read it before Step 1. Settled +means one pattern found (`"chosen_by": "scan"`) or the user picked one (`"chosen_by": "user"`). Write it even when +nothing is being built yet ("what pattern do we use?") and even when only one Template exists: the record is what +makes the next build follow it without a fresh scan. ```json { diff --git a/skills/studio/migrate-page-to-studio.md b/skills/studio/migrate-page-to-studio.md index 42923d2..64df6fc 100644 --- a/skills/studio/migrate-page-to-studio.md +++ b/skills/studio/migrate-page-to-studio.md @@ -38,11 +38,13 @@ The route-swap itself is a four-step transform: 3. **Scaffold**: create a Studio Connected template + section structure with the bindings pre-populated. 4. **Swap**: replace the route's JSX with `<StudioComponent specOptions={…} />`. Preserve the original as a commented block above the new render so the user can diff. -> **Design-fidelity contract: the migrated render MUST be visually identical to the original page.** Migration re-homes the SAME UI into Studio. It never redraws it. This is not aspirational. It is the definition of done, and the user should never have to point out that the design drifted. For every block: -> - **1:1 reuse is the DEFAULT. Decomposition is opt-in.** Each component the original route renders maps to the SAME registered component in Studio, bound to the whole object/array, so the layout is identical by construction, right on the first render, with no correction round-trip. Do NOT split a block into a Repeater + per-item components or child Sections **unless the user explicitly asked to make items author-editable.** Decomposition re-lays-out the block (a uniform sequence instead of the original's bespoke arrangement) and is the #1 cause of first-render drift, the exact failure that makes a user re-prompt "the design doesn't match." When in doubt, reuse whole. -> - **Reuse the EXACT production component**: register it and bind the whole object/array (see [`register-component`](register-component.md)). Never hand-author a lookalike, a "close enough" reimplementation, or a built-in stand-in for a component the app already ships. Registering the real component is the only way brand styling + layout survive. -> - **Never substitute a uniform Repeater for a bespoke/asymmetric layout** (a bento mosaic, a 1-big-plus-small grid, a carousel). A Repeater renders items uniformly in sequence. Different design. Bind the whole array/object to the real component and let its own markup + CSS run (see [`register-component-prop-contracts`](register-component-prop-contracts.md) § Array & object props: binding rules). -> - **Slots and per-field bindings reproduce the original's structure and order**, not an approximation. +> **Structure: the [Section rule](SKILL.md#section-rule) applies to migrations too.** One Section per page region, on the first pass. Inside a Section, the production component is reused whole only when it is interactive or computes its values. Everything else is rebuilt from the project's primitive library ([`plan-studio-architecture`](plan-studio-architecture.md) Step 2c). Don't restate or relax that rule here. +> +> **Design-fidelity contract: the migrated render MUST be visually identical to the original page.** Migration re-homes the SAME UI into Studio. It never redraws it. This is the definition of done, and the user should never have to point out that the design drifted: +> - **A component kept whole is the EXACT production component**, registered as-is (see [`register-component`](register-component.md)). Never a lookalike or a "close enough" reimplementation. +> - **A decomposed region gets its fidelity from the library**, not from approximation: the library's design props carry the original classes and tokens, with defaults that reproduce today's render. +> - **A list whose layout depends on item position or count** (a bento mosaic, 1-big-plus-small) is a component that computes its layout: keep it whole and bind the array. A uniform list is a Repeater inside a layout primitive. +> - **Per-field bindings reproduce the original's structure and order**, not an approximation. > > "It renders" is not the bar. "It renders identically to the current site" is. @@ -56,6 +58,7 @@ The skill is conservative. It stops on anything it can't represent cleanly. Bett - Direct field reads: `headline={entry.title}` becomes the binding `headline ← template.title` - Nested object reads: `cover={entry.featured_image.url}` becomes the binding `cover ← template.featured_image.url` - **Data fetching.** The entry lookup at the top of the route (`stack.entry(...)`, `useCompositionData(...)`, `getStaticProps`, etc.). Confirm it's a single-entry fetch by uid/slug. Flag anything else. + - **Route-level SEO and build exports.** `generateMetadata` / `metadata`, JSON-LD `<script>` tags, `generateStaticParams`, `revalidate`. They are not blockers: they stay in the route through the swap (step 5). List them so none is dropped. - **Blockers.** Anything that isn't "render an entry's fields through components": - `useState`, `useEffect`, `useReducer` inside the route - Conditional component switches based on runtime state (`{flag ? <A /> : <B />}`) @@ -81,6 +84,9 @@ The skill is conservative. It stops on anything it can't represent cleanly. Bett Body.markdown ← template.body RelatedPosts.ids ← template.related + KEPT IN THE ROUTE: + generateMetadata, JSON-LD (BlogPosting), generateStaticParams + BLOCKERS: none ``` @@ -91,11 +97,12 @@ The skill is conservative. It stops on anything it can't represent cleanly. Bett 4. **Scaffold the Studio composition.** With every component now registered, create the Connected template via Studio's web UI flow OR by chaining `build-connected-template`: - Connected content type: `contentTypeUid` - URL pattern: `urlPattern` - - Template root: one section (or a sequence of sections) matching the route's render order - - **For each component in the inventory:** drop the registered component onto the section canvas and apply the binding that was detected in step 1 (so the user does not re-author bindings) + - Template root: **one Section per page region**, in the route's render order ([Section rule](SKILL.md#section-rule)). Never one Section for the whole route + - **For each region:** build its Section from the library primitives (or the production component the rule keeps whole) and apply the bindings detected in step 1, so the user does not re-author them + - **Bind one field per prop.** See § Studio fetches only the fields your bindings name, below, before binding any whole entry or reference to an `object` prop - Save the template -5. **Generate the swap.** Rewrite `routeFilePath`: +5. **Generate the swap: server-rendered, with the route's SEO kept.** Rewrite `routeFilePath` so the server fetches the composition through the canonical [`resolveComposition`](configure-csr-vs-ssr.md#resolve-composition-helper) helper (it lives in `configure-csr-vs-ssr`, don't write a second one) and renders through the `"use client"` wrapper from [`configure-csr-vs-ssr`](configure-csr-vs-ssr.md) step 3. Next.js App Router shown. Other frameworks: same contract, that skill's recipe for the framework. ```tsx // Before — preserved as a reference comment so you can diff: @@ -110,23 +117,46 @@ The skill is conservative. It stops on anything it can't represent cleanly. Bett // ); // } - "use client"; - import { use } from "react"; - import { StudioComponent, useCompositionData } from "@contentstack/studio-react"; - - export default function BlogPost({ params }) { - const { slug } = use(params); // params is a Promise on Next 15+ - const { specOptions, isLoading, error } = useCompositionData({ - url: `/blog/${slug}`, // or the pathname for this route - templateContentTypeUid: "blog_post", // narrows resolution to this CT + // app/blog/[slug]/page.tsx — a Server Component: NO "use client" + import { notFound } from "next/navigation"; + import { getSSRStyles } from "@contentstack/studio-react"; + import { sdk } from "@/lib/studio.server"; // server-only Studio init + import { resolveComposition } from "@/lib/resolve-composition"; // configure-csr-vs-ssr + import { ClientRender } from "@/components/studio/ClientRender"; // "use client" wrapper, configure-csr-vs-ssr step 3 + import { getPost, blogPostingJsonLd } from "@/lib/blog"; // the app's own helpers, unchanged + + // KEPT from the original route, unchanged: they read the entry, not the composition. + export async function generateStaticParams() { /* original body */ } + export async function generateMetadata({ params }) { /* original body */ } + + export default async function BlogPost({ params, searchParams }) { + const { slug } = await params; // a Promise on Next 15+ + const searchQuery = new URLSearchParams((await searchParams) as Record<string, string>).toString(); + const { specOptions, notFound: miss } = await resolveComposition(sdk, { + url: `/blog/${slug}`, + templateContentTypeUid: "blog_post", // narrows resolution to this CT + searchQuery, }); - if (isLoading) return null; - if (error) throw error; - if (!specOptions) return null; // hook may return null before resolving - return <StudioComponent specOptions={specOptions} />; + if (miss) notFound(); // no composition or no entry at this URL: the app's 404, never a 500 + const post = await getPost(slug); // the app's own fetch, for JSON-LD only + return ( + <> + <script type="application/ld+json" + dangerouslySetInnerHTML={{ __html: JSON.stringify(blogPostingJsonLd(post)) }} /> + <style dangerouslySetInnerHTML={{ __html: getSSRStyles(specOptions.spec) }} /> + <ClientRender specOptions={specOptions} /> + </> + ); } ``` + Rules for the swap: + + - **A missing page stays a 404.** `fetchCompositionData` throws on a miss. `resolveComposition` turns only the not-found case (`isCompositionNotFound`, exported by `@contentstack/studio-react`) into `notFound()` and rethrows every real failure. Never `if (error) throw error` on a client hook: every bad URL becomes a 500. + - **SEO survives.** Keep `generateMetadata`, the JSON-LD and `generateStaticParams` exactly as the original route had them. If the composition's own SEO fields should drive the head instead, build it from the fetched `specOptions` with `getCompositionMetadata(specOptions, { baseUrl })` (exported by `@contentstack/studio-react` and `@contentstack/studio-react/rsc`; it returns `title`, `description`, `ogImage`, `canonical` and `tags`). `renderMetadataToHtml` renders the same object to a string for non-React hosts. Pick one source per tag, never both. + - **Reading `searchParams` makes this route dynamic.** That is what lets Live Preview and the editor pass their hash. What it does to static generation and caching, and how to keep the app's strategy: [`configure-csr-vs-ssr`](configure-csr-vs-ssr.md). + - **Client-only fallback.** On a framework with no server render, use the CSR recipe in `configure-csr-vs-ssr` step 2, which classifies `error` with `isCompositionNotFound` and renders the app's 404 for a miss. + Print the diff to chat for review before writing the file. 6. **Verify: visual parity against the original, not just "it renders."** BEFORE the swap, capture the current render (Playwright MCP screenshot if available, plus a note of each section's order, key text, and images. Otherwise have the user capture it). AFTER the swap, capture the Studio render the same way and **compare the two**. The migration succeeds only when they match: same sections in the same order, same images, same text, same layout. Then invoke `verify-setup` (Layer 4 canvas). If the renders differ, that is a binding / component / layout defect, not a pass. Common causes: a prop typed `any` collapsing structured data ([`register-component`](register-component.md) § The `any` flatten trap), an unwired container `props.children` ([`author-composition-bindings`](author-composition-bindings.md) § Node anatomy), or a Repeater swapped in for a bespoke layout. Fix it and re-compare before declaring success. **Do NOT hand the user a "done" they have to visually reject.** If a layer fails outright, restore the original JSX from the preserved comment and report which layer broke. @@ -158,7 +188,10 @@ This skill succeeds only when ALL of the following are true. - [ ] Every prop assignment in the original JSX was either represented as a Studio binding OR explicitly listed as a blocker. - [ ] If any blocker was reported, the skill stopped and did NOT proceed to scaffold. - [ ] If the skill proceeded - a new Connected template exists in the Studio project with bindings populated. The user did NOT re-author bindings in the canvas. -- [ ] The route file is rewritten - new render is `<StudioComponent specOptions={…} />`. Original JSX is preserved as a commented block above so the user can diff. +- [ ] The route file is rewritten - new render is `<StudioComponent specOptions={…} />` (through the client wrapper), fetched server-side via `resolveComposition`. Original JSX is preserved as a commented block above so the user can diff. +- [ ] **A bad URL returns the app's 404, not a 500**, checked with `curl -s -o /dev/null -w '%{http_code}'` on a slug that doesn't exist. +- [ ] **The route's metadata, JSON-LD and static params survived the swap**: every item under KEPT IN THE ROUTE in the report is still exported or rendered. +- [ ] **One Section per page region**, per the [Section rule](SKILL.md#section-rule), and every whole-entry `object` prop either replaced by per-field bindings or backed by an explicit field request (§ Studio fetches only the fields your bindings name), verified on the live route. - [ ] `verify-setup` ran and Layer 4 (Studio canvas) renders the converted template successfully. - [ ] **Visual parity confirmed**: the Studio render was compared against the pre-migration render and matches (same sections, order, images, text, layout), not merely "renders." Any drift was traced to a binding / component / layout defect and fixed BEFORE declaring success. - [ ] A next-step checklist of further route candidates was printed. @@ -175,8 +208,10 @@ This skill succeeds only when ALL of the following are true. | Scaffolding the template without first running `register-component` for unregistered ones | The template's bindings reference unregistered types and render as "Component Loading Error" | Always chain `register-component` first. Never assume registration | | Discarding the original JSX without preserving it as a comment | Rollback requires git history. User can't easily diff visually | Always keep the original as a commented block in the rewritten file | | Failing to invoke `verify-setup` after the swap | The route may render blank in prod and you wouldn't know until a visitor hits it | Verify before declaring success | -| Declaring success on "it renders" without comparing to the ORIGINAL design | Design drift ships (a crude lookalike, a bespoke layout flattened to a uniform Repeater, a section rendering blank because a prop collapsed) and the USER catches it, re-prompting "the design doesn't match" | Run the before/after visual-parity gate (step 6). Capture the original render first, compare after, fix any drift before declaring done. Reuse the real production component. Never hand-roll or decompose a bespoke layout | -| Rebuilding a block as a hand-authored lookalike instead of registering the real component | Loses the exact styling/layout. The user sees a "close but wrong" render and has to correct it | Register the app's actual component and bind the whole object/array. The design comes for free. See the Design-fidelity contract above | +| Declaring success on "it renders" without comparing to the ORIGINAL design | Design drift ships (a crude lookalike, a position-dependent layout flattened to a uniform Repeater, a section rendering blank because a prop collapsed or received only some fields) and the USER catches it, re-prompting "the design doesn't match" | Run the before/after visual-parity gate (step 6). Capture the original render first, compare after, fix any drift before declaring done | +| Rebuilding an interactive or computing component as a hand-authored lookalike | Loses its behavior and exact layout. The user sees a "close but wrong" render and has to correct it | Register the app's actual component whole, as the [Section rule](SKILL.md#section-rule) says for these two cases | +| Registering a presentational component whole "for fidelity" | One block an author can't restructure, and a coarse prop that only receives the fields bound to it (§ Studio fetches only the fields your bindings name) | Rebuild it from the library primitives with per-field bindings. The library's design props carry the original classes | +| Client-rendered swap with `if (error) throw error` | Every bad URL becomes a 500, and the route loses its metadata, JSON-LD and static params | Server-rendered swap through `resolveComposition` (step 5), SEO exports kept | | Inferring `templateEntryUid` wrong (e.g. passing `params.slug` when the entry is looked up by a different field) | The route fetches the wrong entry or 404s | Re-read the original entry-fetch code carefully. Use the same lookup key Studio's spec resolver expects | ## Where does call-site display logic go? @@ -193,13 +228,22 @@ Hand-coded pages routinely do data massage at the call site (before passing to t Rule of thumb: **if the call site is doing anything the Data Picker can't express in a bare path, the migration must decide where that logic lives: adapter / static / branch / product-gap.** Silently dropping it is not an option. -## Recommended migration shape: top-down +## Studio fetches only the fields your bindings name + +This bites migrations more than any other build, because the original route passed whole entries to its components. On the live site the SDK projects the Delivery API query (`only[]`) down to the fields the composition's bindings name. Inside the editor it fetches everything. So a whole referenced entry bound to one `object` prop renders fully while you author and arrives as little more than its `uid` on the published page, with no error. Measured on a product card: no title, price or images. + +- **Bind one field per prop**: the default, and what the [Section rule](SKILL.md#section-rule) already asks for on presentational components. +- **A component kept whole that genuinely needs the whole entry** (interactive, or computing from many fields): request the extra fields explicitly. The options in 2.0.1, `extendQuery.only` (deprecated, still honoured) or fetching the data yourself through the `data` prop, and their caveats are defined once in [`register-component-prop-contracts`](register-component-prop-contracts.md) § Studio fetches only the fields your bindings name. Pass `extendQuery` as the third argument of `resolveComposition`, which forwards it to `fetchCompositionData`. + +Verify on the published route in a plain browser tab, not in the editor: the editor fetches everything and hides the gap. + +## Migration shape: region by region -If the route you're migrating is rendered by a **compound component** (an outer wrapper that iterates and dispatches to inner components), start by authoring ONE Section that wraps the whole compound. Studio renders the existing page inside its canvas in ~5 minutes with no source-code changes. Marketing gains a real win (change bound content, publish without a deploy) at that first checkpoint. +Every pass produces one Section per page region ([Section rule](SKILL.md#section-rule)). A route rendered by a **compound component** (an outer wrapper that iterates and dispatches to inner components) is split at its regions first, never wrapped in one Section. For a modular-blocks body, [`decompose-blocks-page`](decompose-blocks-page.md) gives one Section per block type, and leaves the render byte-identical. -Only decompose further when a specific inner level needs to become author-editable: reorder body blocks, swap a variant, add a new block type. At each drill-down step: author the next Section, preview it in Studio's canvas, verify the inner sub-tree still renders, then decide whether to go deeper. Every step is opt-in. Every step is reversible. +Work one region at a time: author its Section, preview it on `/canvas`, compare it against the original, then move to the next. Inside a region, the component the rule keeps whole stays whole. Everything else is built from the primitive library. -For each Section you author along the way, use [`build-section`](build-section.md) or [`build-repeating-section`](build-repeating-section.md). +For each Section you author along the way, use [`build-section`](build-section.md) or [`build-repeating-section`](build-repeating-section.md). ## See also diff --git a/skills/studio/plan-studio-architecture.md b/skills/studio/plan-studio-architecture.md index c213851..e53375f 100644 --- a/skills/studio/plan-studio-architecture.md +++ b/skills/studio/plan-studio-architecture.md @@ -20,10 +20,12 @@ Given requirements, classify Sections, Connected templates, the content model, a Every component in the emitted plan carries a Q1 to Q4 verdict from the [decision framework](design-component-library.md). The plan produces the sequence: "here's what to register, in what order, with what shape". The framework's four questions decide the shape. -- **Q1. Classify: Atom, Layout, or Compound?** For each component in the plan's inventory, classify what shape it is. Atom means a single content unit. Layout means a slot-carrying arrangement. Compound means refuse. Decompose into atoms + layouts, add each fragment to the inventory, and reclassify. The plan's component list has zero compounds. -- **Q2. Does the registry already have it?** Check the existing project registry AND earlier steps of THIS plan's build order. If a primitive matching the classified shape exists, the plan marks it as reused. If not, it adds a new-registration step to the build order before any Section that depends on it. -- **Q3. Which props?** Each primitive's proposed schema is emitted with a per-prop check against the varies-per-instance-AND-functional-intent rule. Refuse polish props at plan time, cheaper than discovering them in code review. -- **Q4. Which are Exposed?** Each schema prop's exposure status is emitted at plan time. This lets the design review sign off on the exposure surface BEFORE registration ships. +The four questions are defined once in [`design-component-library`](design-component-library.md) § The four-question decision framework. What the plan does with each verdict: + +- **Q1** — a compound is decomposed, each fragment added to the inventory and reclassified, so the plan's component list has zero compounds. +- **Q2** — checks the existing project registry AND earlier steps of THIS plan's build order, then adds a new-registration step before any Section that depends on it. +- **Q3** — each primitive's schema is emitted with a per-prop check, so polish props are refused at plan time rather than in code review. +- **Q4** — each prop's exposure status is emitted at plan time, so design review signs off on the exposure surface BEFORE registration ships. **The plan halts** if any component's Q1 to Q4 verdict is ambiguous. Ask the user to disambiguate. Add the resolution to the plan. Only then proceed to the build order. @@ -111,7 +113,7 @@ whenever the listing above returns any composition at all. Routing "decompose the coarse ones" isn't coverage: coarse is a judgment call, and the component that quietly holds four atomics is exactly the one that gets waved through. **Walk the whole component inventory and record a verdict for each**, using one test: -> **Does it render more than one heading / image / body / list?** If yes it is **coarse**: read its render tree via [`decompose-jsx-to-atomics`](decompose-jsx-to-atomics.md). If no, it is genuinely atomic: register it as-is. +> Apply the [Section rule](SKILL.md#section-rule) (defined once in the `studio` router). **Does it render more than one heading / image / body / list?** If no, it is genuinely atomic: it maps to a library atom (Step 2c) or registers as-is. If yes, it is **coarse**: it is **kept whole** inside its Section only if it is interactive or computes its values, and otherwise its render tree is read via [`decompose-jsx-to-atomics`](decompose-jsx-to-atomics.md). ```bash # Every component file, with a rough leaf count to triage by @@ -128,7 +130,8 @@ Print the verdict table in the plan. **An unclassified component is an incomplet |---|---|---|---| | `<RewardsPage>` | 9 | coarse | decompose into 3 Sections | | `<Marquee>` | 1 | atomic | register as-is | -| `<Header>` | 6 | coarse **+ shared** | decompose into one shared Section (see 0b) | +| `<RewardsSignupForm>` | 5 | coarse, **interactive** | keep whole inside its own Section | +| `<Header>` | 6 | coarse **+ shared** | decompose into one shared Section (see 0c) | #### 0c. Shared shells first: header, footer, nav, menus @@ -200,7 +203,7 @@ Three conditions make a Section **mandatory**, but the default already is a Sect 1. **Reuse across Templates.** A bound compound component appearing on more than one Template MUST become a Section. Otherwise every Template repeats the drop-and-bind work and any binding tweak is applied N times. **Reuse count 1 does not exempt a piece from being a Section**. It only lowers its build priority. 2. **Schema iteration with bindings.** Any Modular Block field, multi-Reference field, group with `multiple: true`, or otherwise-iterated collection whose items need bound rendering MUST become a **List Section**, not authored inline on the Template. The iteration wiring belongs at the Section layer where it's reusable and testable. Putting it at Template level fragments it across every Template that needs the same iteration. - **A List Section has TWO build paths: pick by layout fidelity, don't default to Repeater.** If the collection has a **bespoke or asymmetric layout** (a mosaic, a feature strip, a carousel), or you are **migrating and must preserve an existing design**, build it by binding the whole array to the real production component (`type: "array"`, see [`register-component-prop-contracts`](register-component-prop-contracts.md) § Array & object props), so the component's own layout renders pixel-identically on the first pass. Use a **Repeater + Condition Block** only when the list is a **uniform** grid/row, OR items are **polymorphic** and need per-item authoring. A Repeater renders items uniformly in sequence. Choosing it for a bespoke layout is the #1 cause of first-render design drift (the "why is it a plain sequence?" re-prompt). + **A List Section has TWO build paths, and the [Section rule](SKILL.md#section-rule) picks between them.** If the production list component is **interactive** (a carousel, a filterable list) or **computes its layout from the items** (a mosaic whose tile sizes depend on position or count), bind the whole array to that component (`type: "array"`, see [`register-component-prop-contracts`](register-component-prop-contracts.md) § Array & object props), so its own layout renders identically. Every other list is a **Repeater** (+ **Condition Block** for polymorphic items) over the library's primitives. A Repeater renders items in sequence inside the layout primitive you give it (a grid or stack), so a uniform grid stays a grid. 3. **Any bound region an author should be able to move, swap, or remove.** If it's a recognizable page part (hero, form band, feature grid, CTA) it's a Section, reuse count irrelevant. Build the Section first (`build-section` for Simple, `build-repeating-section` for List), THEN drop it on the Template. Direct-drop of registered components on a Template is reserved for genuine one-offs that are not a page part (a stray spacer, a single decorative element) and should be a rare line in the plan, called out as an exception with a reason. @@ -227,28 +230,29 @@ Setting `selectedField` on a multi-field Section loses access to everything outs **The failure mode this prevents:** everything becomes a registered component, because registering is the easiest thing to do. The plan then looks complete while the author ends up with a page they can only re-bind (no reordering, no swapping, no restyling, no reuse) and never discovers that Studio had a primitive for what they wanted. Registering is one tool out of many, and it is the least author-editable of them. -**Migrating an existing design? Fidelity outranks editability on the first pass.** Pick the primitive that reproduces the current render exactly, usually reuse the real production component whole (`object` / `array` binding), because its own markup + CSS already are the design. Introduce built-in decomposition and Repeaters as a SECOND, opt-in phase, once the user has confirmed the render matches and explicitly asks for per-field / per-item authoring. Decomposing on the first pass is what forces the user to re-prompt "the design doesn't match". Get parity first, add editability second. +**Migrating an existing design follows the same [Section rule](SKILL.md#section-rule).** One Section per page region, on the first pass too. Inside each Section, the production component stays whole only when it is interactive or computes its values. Presentational markup is rebuilt from the primitive library (Step 2c), whose design props carry the existing classes and tokens, so parity comes from the library rather than from keeping the component whole. Verify parity with [`verify-visual-parity`](verify-visual-parity.md) before reporting done. -**This governs a Section's internals. It never governs the page.** Keeping a carousel or a card grid whole is fidelity. Binding one component to the page's entire body is a monolith. The two are easy to confuse when the body happens to be a single modular-blocks field. Fidelity-first means this Section keeps the production component inside it. It is not permission for the Template to hold one node. A Template still gets one Section per schema scope on the first pass ([`decompose-blocks-page`](decompose-blocks-page.md)), each of which may keep its production component whole. Parity and decomposition are not in tension at that level. The measured proof is that splitting a blocks page into one Section per block type leaves the rendered output byte-identical. +**The rule governs the page as well as each Section's internals.** Keeping a carousel whole inside its Section is allowed. Binding one component to the page's entire body is a monolith. The two are easy to confuse when the body happens to be a single modular-blocks field. A Template gets one Section per schema scope ([`decompose-blocks-page`](decompose-blocks-page.md)). The measured proof that this costs no fidelity: splitting a blocks page into one Section per block type leaves the rendered output byte-identical. Work through each piece from Steps 1 to 2 and pick from **evidence**, not convenience: | Evidence in the code / design / content model | Primitive | Skill | |---|---|---| -| Plain copy (headline, body, caption, CTA label) mapping 1:1 to a field | **Built-in field component** (`Heading` / `Paragraph` / `Text` / `Image` / `Link` / `RichText`) | `build-section-component-choice` § Basic field components vs custom | -| Interactive: local state, handlers, cookies, modals, carousel logic | **Registered component** (mandatory, built-ins can't express behavior) | `register-component` | -| Pixel-sensitive: bespoke CSS, animation, layering | **Registered component** | `register-component` | -| Static presentational layout (image + heading + body + link) | **Decompose into built-ins**: authors get full editing. This is the case most often wrongly registered | `build-section-component-choice` § Register it whole, or decompose it | +| Plain copy (headline, body, caption, CTA label) mapping 1:1 to a field | **The library's content atom** (`heading`, `body-string`, `cta-button`, `media`). A **built-in** only when the library has no matching atom: `header`, `text`, `image`, `link` (names below) | `design-component-library`, `build-section-component-choice` | +| Interactive: local state, handlers, cookies, modals, carousel logic, script-driven animation | **Registered production component, kept whole** (built-ins and atoms can't express behavior) | `register-component` | +| Computes its values: formatters, totals, sorting, a layout derived from item count or position | **Registered production component, kept whole** | `register-component` | +| Bespoke styling with no behavior (custom CSS, layering) | **Library primitives with design props** that carry the classes: not a whole-component registration | `design-component-library` | +| Static presentational layout (image + heading + body + link) | **Decompose into library atoms + layout primitives**: authors get full editing. This is the case most often wrongly registered | `decompose-jsx-to-atomics` | | A styled shell wrapping editable content | **Registered component with a `slot` prop** + built-ins inside | `register-component`, `use-section-slot` | | A recognizable page part (hero, form band, feature grid, CTA) | **Section**: regardless of reuse count | `build-section` | -| Multi-valued field with a **uniform** grid/row, OR polymorphic items needing per-item authoring | **List Section via Repeater** (+ **Condition Block** per block or CT for refs/MBs) | `build-repeating-section`, `use-repeater`, `use-condition-block` | -| Multi-valued field with a **bespoke layout**, or **migrating an existing design** | **List Section via array-prop reuse**: bind the whole array to the real component. Its own layout renders identically, first pass | `register-component-prop-contracts` § Array & object props, `build-repeating-section` § array-prop alternative | +| Multi-valued field rendered by presentational markup (uniform grid/row, polymorphic items needing per-item authoring) | **List Section via Repeater** (+ **Condition Block** per block or CT for refs/MBs) | `build-repeating-section`, `use-repeater`, `use-condition-block` | +| Multi-valued field rendered by an **interactive** component (carousel) or one that **computes its layout** from the items (mosaic) | **List Section via array-prop reuse**: bind the whole array to the real component | `register-component-prop-contracts` § Array & object props, `build-repeating-section` § array-prop alternative | | A region a template author must fill differently per instance | **Section Slot** (filled by a Section, not a raw component) | `use-section-slot` | | One prop an author should override per template instance | **Exposed section prop**: cheaper than forking the Section | `expose-section-props` | | Same field set across several CTs | **Global Field** (+ a Slot on the Section that renders it) | `plan-studio-architecture` § Step 4 | -| **Any rich text**: JSON RTE, HTML RTE or Markdown | **`RichText` component** (never `Heading`/`Paragraph`, which show markup as literal characters) | `register-json-rte` § Which component for which field | -| Rich text that **truncates behind "read more" / an accordion** | **`CollapsibleText`**: same content model, plus expand/collapse | `register-json-rte` | -| Rich text that **embeds entries or assets** | `RichText` **+ `registerRTERenderer` with `embeddedEntry` / `embeddedAsset` handlers**. Unregistered, an embedded entry renders as a bare delivery-API span (title only) | `register-json-rte` § Embedded entries and embedded assets | +| **Any rich text**: JSON RTE, HTML RTE or Markdown | **`rich-text`** built-in (palette name "Rich Text") or the library's rich-text atom, never `header`/`text`, which show markup as literal characters | `register-json-rte` § Which component for which field | +| Rich text that **truncates behind "read more" / an accordion** | **`collapsible-text`**: same content model, plus expand/collapse | `register-json-rte` | +| Rich text that **embeds entries or assets** | `rich-text` **+ `registerRTERenderer` with `embeddedEntry` / `embeddedAsset` handlers**. Unregistered, an embedded entry renders as a bare delivery-API span (title only) | `register-json-rte` § Embedded entries and embedded assets | | One-off page owning its own copy, no CT worth creating | **Freeform template** + **Pinned Entries** | `build-freeform-template`, `pin-entry-to-freeform` | | "Show the latest / top N / filtered X" on a Freeform page | **Pinned Query** as a Repeater source | `pin-query-to-freeform` | | Data outside Contentstack (pricing, geo, inventory) | **`data` prop on `<StudioComponent>`**: not a fake CT | `wire-external-data` | @@ -259,10 +263,33 @@ Work through each piece from Steps 1 to 2 and pick from **evidence**, not conven | Brand spacing / color / type scale the author should reuse | **Design tokens** registered with Studio | `import-design-tokens` | | Author needs to see tablet/mobile | **Breakpoints** registered at boot | `register-breakpoints` | +<a id="built-in-names"></a> + +**Built-in components use the SDK's type names.** These are the `type` values in a composition's `ui` tree and in `BUILT_IN_COMPONENTS` / the basic-component registry of `@contentstack/studio-react` 2.0.1. The palette shows a different display name, so write both in the plan: + +| `type` | Palette name | Renders | +|---|---|---| +| `header` | Header | `<h1>`…`<h6>`, level chosen by a prop | +| `text` | Paragraph | `<p>` with plain text | +| `plain-text` | Text | the bare string, **no element**: it cannot take a class or carry an edit tag. Avoid it for anything an author styles or clicks | +| `rich-text` | Rich Text | `<div>` holding the rendered rich text | +| `collapsible-text` | Collapsible Text | rich text with expand/collapse | +| `image` | Image | `<img>` (`imageurl` + alt text) | +| `link` | Link | `<a>` (`href` + label + new-tab flag) | +| `link-container`, `button`, `video`, `embed` | Link Container, Button, Video, Embed | as named | +| `box` | Box | `<div>` with a children slot | +| `hstack` | Columns | `<div>` laying out N columns | +| `vstack` | Rows | `<div>` laying out N rows | +| `repeater`, `condition-block`, `section-slot` | Repeater, Condition Block, Section Slot | structural, no markup of their own to style | + +`section` is deprecated (hidden from the palette, still registered so old compositions render). Never write `Heading`, `Paragraph` or `RichText` as a `type`: no component is registered under those names. + **Bias check before printing the plan.** Count the primitives you selected: - **Every interactive component's behavior hardcoded, no `action` props?** Authors can place it but not wire it. If the page has buttons that should do something the author picks, that's a function + `action` prop. -- **One Section covering a whole page?** A defect, not a shortcut. Nothing to reorder, nothing reusable, and the author never meets slots or repeaters. Run [`decompose-jsx-to-atomics`](decompose-jsx-to-atomics.md) and re-plan. A single-Section page needs a stated reason, not silence. +- **One Section covering a whole page?** A defect under the [Section rule](SKILL.md#section-rule). Nothing to reorder, nothing reusable, and the author never meets slots or repeaters. Run [`decompose-jsx-to-atomics`](decompose-jsx-to-atomics.md) and re-plan. A single-Section page needs a stated reason (the page genuinely has one region), not silence. +- **A production component kept whole that is neither interactive nor computing its values?** Decompose it into library primitives. +- **Sections composed of `box` + hard-coded CSS classes instead of library primitives?** The library step (Step 2c) was skipped. Authors can't pick a spacing or a variant from a menu, so nothing is composable. - **Any Section whose binding is one coarse prop taking a whole object?** Decomposition was skipped. Atomic props bind one CT field each. A single prop swallowing a group renders blank when typed `any` (the binder recursively unwraps single-key objects for every type except `object`/`array`). - **Only registered components?** Almost certainly wrong. Re-walk the table. Plain copy belongs in built-ins, lists belong in a List Section, per-instance variation belongs in a Slot or an exposed prop. - **No Section Slots anywhere?** Legitimate for a small, fully-fixed page, but state that you checked and why none is needed. Silence reads as "not considered". @@ -271,6 +298,18 @@ Work through each piece from Steps 1 to 2 and pick from **evidence**, not conven **Name the features you ruled out.** The plan prints a "Studio features considered" line listing what you rejected and why (e.g. "Pinned Query: not needed, the list is a Modular Block on the page entry"). This is how the user learns the primitive exists at all. A plan that silently omits half of Studio teaches them Studio is just component registration. +### Step 2c: Plan the primitive library. Required on every build. + +**The failure this prevents:** going straight from existing components to Sections. Measured on a migration that skipped it: Sections made of generic `box` nodes plus hard-coded CSS classes, 6 one-off helper components and 1 exposed prop. It rendered, and the user rejected it as "not composable": an author could not pick a spacing, a variant or a column count, only retype text. + +Run [`design-component-library`](design-component-library.md) and put its output in the plan, whatever the starting point (fresh build, existing codebase, migration). On a project that already has a library, list it and mark the gaps. The library is: + +- **Atoms**: one content unit each (`heading`, `body-string`, `body-json-rte`, `cta-button`, `media`, …), content props bound to CT fields. +- **Layout primitives**: arrange children through slots (`section-shell`, `stack`, `split-row`, `grid`, …). +- **Design props as menus, with defaults.** Every visual choice an author may make is a `choice` prop whose options map to the project's existing classes or tokens, with a `defaultValue` that reproduces today's design (`gap: tight | normal | loose`, default `normal`). Not a free-text class field and not a CT field. A `choice` or `string` prop marked `design: true` is the SDK's per-breakpoint CSS variant of the same idea (see `design-component-library`). + +Every Section in Step 5 is composed from these primitives, plus any production component the [Section rule](SKILL.md#section-rule) keeps whole. **For each Section, name the props a template author can change** (its exposed props, see [`expose-section-props`](expose-section-props.md)), so the approver sees the editing surface before it is built. + ### Step 3: Classify each page as a Connected template For every page (not a Section), pick the CT backing: @@ -314,9 +353,9 @@ Content model: | The copy is… | Field type | Renders through | |---|---|---| -| A heading, label, name, short value | `text` (single-line) | `Heading` / `Text` | -| Body copy, description, prose an author may want to **bold, link, list, or embed into** | **`json_rte`** (or multiline + rich-text / markdown to match the team's editing convention) | **`RichText`** | -| Long prose the design **truncates**: "read more", accordion, "show details" | same rich-text type | **`CollapsibleText`** | +| A heading, label, name, short value | `text` (single-line) | library `heading` atom, or built-in `header` / `text` | +| Body copy, description, prose an author may want to **bold, link, list, or embed into** | **`json_rte`** (or multiline + rich-text / markdown to match the team's editing convention) | library rich-text atom, or built-in **`rich-text`** | +| Long prose the design **truncates**: "read more", accordion, "show details" | same rich-text type | **`collapsible-text`** | **Propose the rich-text type by default for body copy**, and say so in the plan: "`body` is `json_rte` so authors can format and embed. If it will only ever be a flat paragraph, tell me and I'll make it plain text." Choosing plain text silently is the costlier mistake. An author who later needs a link in a sentence needs a schema migration, whereas an over-provisioned rich-text field is merely unused. @@ -371,7 +410,7 @@ Templates: Connected / homepage CT (single) → Hero + FeaturedCategories Connected /category/{{slug}} category CT → Hero + ProductGrid Connected /product/{{slug}} product CT → ProductDetailHero + ProductSpecsTable + RelatedProducts - Connected /blog/{{slug}} blog_post CT → BlogHeader + RichText + RelatedPosts + Connected /blog/{{slug}} blog_post CT → BlogHeader + BlogBody + RelatedPosts ``` ### Step 7: Build order @@ -386,7 +425,10 @@ Build order: setup-section-preview for the COMPULSORY /canvas route; skip any step Step 0's inventory shows is already in place) 1. Create / update CTs and Global Fields (in the Contentstack web app) - 2. Register atomic React components (lazy by default; layout-agnostic — see register-component) + 2. Register the primitive library from Step 2c (atoms + layout primitives, menu-style design props + with defaults — design-component-library -> register-component; + lazy by default, layout-agnostic), then any production + component the Section rule keeps whole every registration ships thumbnailUrl, every component spreads studioAttributes + its $-twins — same step, never a follow-up 3. Import design tokens (before any Section is authored — see import-design-tokens) @@ -399,7 +441,7 @@ Build order: Run `byoc-end-to-end` to walk this in execution mode. ``` -The inline notes on steps 0, 2, 4 and 7 are the six deliverables from [`complete-the-build`](complete-the-build.md): CSLP tags, component thumbnails, section thumbnails, Live Preview, the `/canvas` route. **Print them in the build order, don't leave them implied.** Each is invisible to a developer loop (the page renders fine without all six) and each breaks the editor for the non-developer this plan exists to serve, so a plan that omits them gets approved as complete and delivered incomplete. +The inline notes on steps 0, 2, 4 and 7 carry the ten deliverables listed in [`complete-the-build`](complete-the-build.md). **Print them in the build order, don't leave them implied.** Each is invisible to a developer loop (the page renders fine without them) and each breaks the editor for the non-developer this plan exists to serve, so a plan that omits them gets approved as complete and delivered incomplete. ### Step 8: Sanity checks before handoff @@ -411,7 +453,8 @@ Before printing the final plan, flag any of these at the top: - Global Field shared by ≥2 CTs without its own Section. - Connected page without an identified backing CT. - Child Section whose linked schema doesn't match its parent Repeater's iterated CT. -- Any of the six in [`complete-the-build`](complete-the-build.md) missing from the plan, or an `[exists]` component carrying no `thumbnailUrl` / no `studioAttributes` (it's `[modify]`). +- No primitive library (Step 2c) in the plan, or a Section composed of `box` + hard-coded classes. +- Any of the ten deliverables in [`complete-the-build`](complete-the-build.md) missing from the plan, or an `[exists]` component carrying no `thumbnailUrl` / no `studioAttributes` (it's `[modify]`). ### Step 9. Print the plan in TWO layers: plain language first, build sheet second @@ -457,7 +500,7 @@ How you'll edit it: • Click any text or image on a preview of the real page and change it right there. • Every building block and reusable block shows a picture of itself, so you can tell them apart when adding one — no blank tiles to guess at. - <these two lines are ALWAYS included and always true — they are the six + <these two lines are ALWAYS included and always true — they are the deliverables in complete-the-build, which the build order commits to> Size of the job: @@ -492,7 +535,7 @@ Already in place / being reused: - **"You can edit" and "Fixed for now" are the point.** That's the line a non-technical approver actually cares about, and it's what makes the A-vs-B choice in Step 10 meaningful rather than abstract. - **No counts without meaning.** "2 reusable blocks" is fine. "4 `linked_schemas`" is not. - **Say what's reused.** If Step 0 found existing pieces, name them here. Otherwise the reader assumes everything is new and the size looks wrong. -- **"How you'll edit it" is not optional and is not a promise you can drop.** It is the plan's commitment to the six deliverables in [`complete-the-build`](complete-the-build.md). Say it in the words in that skill's plain-language table, never as `data-cslp`, `thumbnailUrl`, `ui_preview` or "Live Preview". +- **"How you'll edit it" is not optional and is not a promise you can drop.** It is the plan's commitment to the ten deliverables in [`complete-the-build`](complete-the-build.md). Say it in the words in that skill's plain-language table, never as `data-cslp`, `thumbnailUrl`, `ui_preview` or "Live Preview". - **Keep it short.** A wall of plain text is no more approvable than a wall of jargon. If Layer 1 runs past roughly a screen, the plan is probably too big to approve in one go. Split the work. #### Layer 2: Build sheet (technical. The build steps read this, not the user) @@ -511,22 +554,32 @@ Sanity flags (reconsider before proceeding): Content model: <CT and GF listing from Step 4> +Primitive library (Step 2c — required): + <one line per primitive — name [exists|create] atom|layout + content props -> CT field type + design props -> menu options (default)> + <heading [create] atom content: text design: level h1..h4 (h2), align (left)> + <stack [create] layout slot: children design: gap tight|normal|loose (normal)> + Sections (build in this order): <for EACH Section from Step 5, print EVERY line below — never just the name. `atomics` is what build-section and register-component consume: one atomic, one prop, one CT field. A Section with no atomics line was not decomposed.> - <Section name> [exists|modify|create] shared: <yes — rendered from layout | no> linked schema : <CT / Global Field / Group / Modular Block / Block / Reference> (+ selectedField set|unset) - components : <registered: Hero, ProductCard | built-in: Heading, Paragraph, Image> + components : <library: heading, media, stack | kept whole (interactive/computes): SignupForm + | built-in (SDK type): header, text, image> atomics : <one line per atomic — atomic -> prop -> CT field (type)> - <Image -> image -> entry.hero.image (imageurl)> - <Heading -> headline -> entry.hero.headline (string)> + <media -> image -> entry.hero.image.url (imageurl)> + <heading -> headline -> entry.hero.headline (string)> rich text : <every RTE field on this Section — field (json_rte|html rte|markdown) - -> RichText|CollapsibleText -> embeds: entries <CT list>|assets|none - | none. NEVER map rich text to Heading/Paragraph.> + -> rich-text|collapsible-text (or the library's rich-text atom) + -> embeds: entries <CT list>|assets|none + | none. NEVER map rich text to header/text.> repeaters : <field iterated -> child Section -> Condition Blocks needed | none> section slots : <slot name -> what a template author drops there | none, and why> bindings : <any binding NOT covered by the atomics lines above | none> + author can change: <exposed props, each with its label — "Hero headline", "Card columns" | none, and why> Templates: <Template inventory from Step 6 — for EACH: kind (Connected|Freeform), connected CT, @@ -538,17 +591,17 @@ Registered components needed: with no thumbnail or no studioAttributes is [modify], not [exists]> Built-in (field) components used: - <Heading / Paragraph / Text / Image / Link / RichText -> which Section, bound to which field> + <SDK type names (header / text / image / link / rich-text — see Step 2b) -> which Section, bound to which field> <if this list is empty, say why — a plan where every visible element is a custom registered component is usually over-registered; plain copy belongs in built-ins> Suggested upgrades (recommendations — the build proceeds without them if declined): <existing rich-text field rendered through a plain text component - -> switch to RichText — no schema change, fixes markup showing as literal text> + -> switch to rich-text — no schema change, fixes markup showing as literal text> <plain multiline text field holding prose -> consider json_rte so authors can format and embed — schema change on N live entries via migrate-ct-schema> - <long prose the design truncates -> CollapsibleText> + <long prose the design truncates -> collapsible-text> <or "None — the existing model already types its rich text correctly"> RTE renderer (omit ONLY if the model has no rich-text field anywhere): @@ -563,7 +616,7 @@ Studio features considered: exposed props, Global Fields, pinned entry/query, external data, variants, tokens> ruled out: <primitive -> one-line reason it isn't needed here> -Editing experience (mandatory — the six from complete-the-build; never listed as optional): +Editing experience (mandatory — the ten from complete-the-build; never listed as optional): preflight : Live Preview + preview token <on | to enable> · cslp.appendTags <on | to enable> · /canvas route <exists | to add> per component: thumbnailUrl + studioAttributes/$-twins on every registration above @@ -657,7 +710,8 @@ The last question is Connected vs Freeform. Ask it that way, never by those name - [ ] **Every Section row names its Section Slots and its built-in (field) components, not just registered components.** A plan listing only `registerComponent` work is incomplete. Say explicitly where plain copy uses built-ins and where a template author needs a swap point. - [ ] **Each page has an explicit Connected-vs-Freeform decision** with the connected CT (or the reason there isn't one), never left implicit. - [ ] **The plan was printed in full and explicitly approved before any build skill ran.** Every Section line carries linked schema + components + slots + bindings. Every Template carries kind + URL + its Section list. No piece was created on an unapproved or half-filled plan. -- [ ] **The plan commits to the six deliverables in [`complete-the-build`](complete-the-build.md)**: Layer 1 carries the "How you'll edit it" lines, Layer 2 carries the Editing experience block, and the build order names them on steps 0, 2, 4 and 7. A plan that covers only Sections, bindings and templates describes something that renders but that a marketer cannot use. +- [ ] **The plan lists the primitive library (Step 2c)**, with menu-style design props and defaults, and every Section names the props a template author can change. +- [ ] **The plan commits to the ten deliverables in [`complete-the-build`](complete-the-build.md)**: Layer 1 carries the "How you'll edit it" lines, Layer 2 carries the Editing experience block, and the build order names them on steps 0, 2, 4 and 7. A plan that covers only Sections, bindings and templates describes something that renders but that a marketer cannot use. - [ ] **The A-vs-B architecture choice was put to the user** (decomposed Sections vs a single template with components dropped in) and their answer recorded. If B was chosen, its cost is written into the plan. The skill succeeds when: @@ -685,10 +739,10 @@ The skill succeeds when: ## See also -- [`design-component-library`](design-component-library.md): foundational pattern doc. Before this skill's build order sequences the work, that doc decides what to register: atomic + layout primitives instead of monolithic Sections. Read alongside [`decompose-design`](decompose-design.md) when the plan will produce a fresh component library. +- [`design-component-library`](design-component-library.md): run in Step 2c on every build. It decides what to register: atomic + layout primitives instead of monolithic Sections. Read alongside [`decompose-design`](decompose-design.md) when the plan will produce a fresh component library. - `start-here-zero-knowledge`: the conceptual prerequisite (run BEFORE this skill if the user is new) - `understand-templates`, `understand-sections`, `understand-section-slots`, `understand-linked-schemas`, `understand-auto-binding`: the concept ladder this skill assumes is internalised - `byoc-end-to-end`: the procedural macro that walks the plan in execution mode - `complete-the-build`: the ten always-mandatory deliverables this plan commits to and the handover gate that verifies them -- `enable-visual-experience`, `install-live-preview`: the preflight half of those six +- `enable-visual-experience`, `install-live-preview`: the preflight items among those ten - `build-section`, `build-connected-template`, `build-repeating-section`, `use-section-slot`: the implementation skills the build order references diff --git a/skills/studio/provision-studio-project.md b/skills/studio/provision-studio-project.md index 1771d2b..25ce8c4 100644 --- a/skills/studio/provision-studio-project.md +++ b/skills/studio/provision-studio-project.md @@ -35,7 +35,7 @@ Regional hosts: see Step 10 below (Studio API hosts mirror the CMA region prefix 1. **Confirm prerequisites.** You have a resolved CMA credential, **OAuth Bearer by default**, session `authtoken` only as the last rung ([`authenticate-cma`](authenticate-cma.md)), AND `api_key` for the target stack. Both Studio API and CMA expect these two headers together. Know the region (regional Studio API hosts are listed in Step 10). NA defaults: CMA `api.contentstack.io`, Studio API `composable-studio-api.contentstack.com`. **Custom / private data center (hosts supplied by Contentstack): set every host explicitly.** There is no `region` shortcut. See [`install-studio`](install-studio.md) § Custom / private data center. Establish which data center you're on first via [`analyze-project-fit`](analyze-project-fit.md) § 2b: Determine the data center. Getting this wrong here provisions a CT, tokens and a Studio project **in the wrong data center**, with no error. The calls succeed against the default region. - > **MCP-first execution.** Discover tools and verify the target with a read call per [`install-contentstack-mcp`](install-contentstack-mcp.md). When supported tools are callable, the **CMA** steps here run as MCP tool calls with no pasted `authtoken`: step 8 CT-create (POST then PUT) becomes `create_a_content_type` plus `update_content_type`, step 4 environment-create becomes `create_an_environment`, and verify becomes `get_a_single_content_type`. The MCP does **not** cover the Studio API or tokens: steps **5 (enable Live Preview), 6 (delivery+preview token), 10 (`/v1/projects` register)**, and stack-create stay raw HTTP, authenticated with the same OAuth credential (`-H "$CS_AUTH"`), not a pasted session token. **Step 10 (`/v1/projects`) has no MCP tool, but use the OAuth token there (`Authorization: Bearer`). A session authtoken is the last rung only, per step 10's callout.** Org comes from the login. Read the stack `api_key` from the MCP registration ([`understand-contentstack-mcp`](understand-contentstack-mcp.md)) and confirm before asking. A missing readiness flag requires discovery; use HTTP only for unavailable operations or a recorded connection limitation. + > **MCP-first execution.** Discover tools and verify the target with a read call per [`install-contentstack-mcp`](install-contentstack-mcp.md). When supported tools are callable, the **CMA** steps here run as MCP tool calls with no pasted `authtoken`: step 8's create (the POST) becomes `create_a_content_type`, and step 4 environment-create becomes `create_an_environment`. **Everything after the create that names the new CT goes over HTTP in the same session**: step 8's PUT, step 9's verify, and the step 11 composition entries. The MCP fixes the list of content types (and environments) its tools accept when it starts, so it rejects the CT it just created until Claude Code restarts ([`install-contentstack-mcp`](install-contentstack-mcp.md#cts-created-after-start) Step 6b). The MCP does **not** cover the Studio API or tokens: steps **5 (enable Live Preview), 6 (delivery+preview token), 10 (`/v1/projects` register)**, and stack-create stay raw HTTP, authenticated with the same OAuth credential (`-H "$CS_AUTH"`), not a pasted session token. **Step 10 (`/v1/projects`) has no MCP tool, but use the OAuth token there (`Authorization: Bearer`). A session authtoken is the last rung only, per step 10's callout.** Org comes from the login. Read the stack `api_key` from the MCP registration ([`understand-contentstack-mcp`](understand-contentstack-mcp.md)) and confirm before asking. A missing readiness flag requires discovery; use HTTP only for unavailable operations or a recorded connection limitation. 2. **FIRST: does a Studio project already exist? List, then ASK. Never create blind.** @@ -55,7 +55,7 @@ Regional hosts: see Step 10 below (Studio API hosts mirror the CMA region prefix | **A project's `connectedStackApiKey` matches the target stack** | **Almost certainly the one they want.** Say so and default to reusing it: "Project `<name>` (`<uid>`) is already connected to this stack with `contentTypeUid: <ct>`. Reuse it, or create a second project against the same stack?" A second project on one stack is legal but rarely intended. It splits compositions across two projects | | **Projects exist, none on this stack** | Present the numbered list below and **ask**: reuse one (repointing it, see the warning at the end) or create new. Don't assume new just because the stack differs | | **No projects at all** | Proceed to create, and say that's what you're doing | - | **`GET` fails** (401/422) | **Stop.** An auth failure is not evidence that no project exists. Resolve auth per step 10's ladder, then re-check | + | **`GET` fails** (401) | **Stop.** An auth failure is not evidence that no project exists. Resolve auth per step 10's ladder, then re-check | ### Present the list as a numbered choice: never ask for a project id @@ -131,54 +131,123 @@ Regional hosts: see Step 10 below (Studio API hosts mirror the CMA region prefix 8. **Create the compositions CT: two calls, not one.** CMA validates reference targets at creation time, so **`linked_sections`** (a self-reference) must be added on a follow-up PUT. In the initial POST it returns `error_code 115: linked_sections.reference_to: content type does not exist`. The CT can't reference itself before it exists. ``` - POST /v3/content_types (body = every field in the table below EXCEPT linked_sections) - PUT /v3/content_types/<uid> (body = every field including linked_sections) + POST /v3/content_types (body = the JSON below WITHOUT the linked_sections field) + PUT /v3/content_types/<uid> (body = the JSON below, complete) ``` - Every field is **required by the editor or runtime**. Do not drop, rename, or change cardinality. - -### Compositions CT: required fields + Every field is **required by the editor or runtime**. Do not drop, rename, or change cardinality. Replace `<compositions_ct_uid>` (twice: the CT `uid` and `linked_sections.reference_to`) with the step 7 uid, and send the rest unchanged: + +### Compositions CT: the schema + +```json +{ + "content_type": { + "title": "Compositions", + "uid": "<compositions_ct_uid>", + "options": {"is_page": false, "singleton": false, "title": "title", "sub_title": []}, + "schema": [ + {"display_name": "Title", "uid": "title", "data_type": "text", "mandatory": true, "unique": true, "multiple": false, "field_metadata": {"_default": true}}, + {"display_name": "URL", "uid": "url", "data_type": "text", "mandatory": false, "unique": false, "multiple": false}, + {"display_name": "Composable UID", "uid": "composable_uid", "data_type": "text", "mandatory": true, "unique": true, "multiple": false}, + {"display_name": "UI Preview", "uid": "ui_preview", "data_type": "file", "mandatory": false, "unique": false, "multiple": false}, + {"display_name": "Connected Content Type", "uid": "connected_content_type", "data_type": "text", "mandatory": false, "unique": false, "multiple": false}, + {"display_name": "UI", "uid": "ui", "data_type": "text", "mandatory": false, "unique": false, "multiple": false, "field_metadata": {"multiline": true}}, + {"display_name": "Data Sources", "uid": "data_sources", "data_type": "text", "mandatory": false, "unique": false, "multiple": false, "field_metadata": {"multiline": true}}, + {"display_name": "Static Value", "uid": "static_value", "data_type": "group", "mandatory": false, "unique": false, "multiple": false, "schema": [ + {"display_name": "text", "uid": "text", "data_type": "group", "mandatory": false, "unique": false, "multiple": true, "schema": [ + {"display_name": "Key", "uid": "key", "data_type": "text", "mandatory": false, "unique": false, "multiple": false}, + {"display_name": "Value", "uid": "value", "data_type": "text", "mandatory": false, "unique": false, "multiple": false, "field_metadata": {"multiline": true}} + ]}, + {"display_name": "html_rte", "uid": "html_rte", "data_type": "group", "mandatory": false, "unique": false, "multiple": true, "schema": [ + {"display_name": "Key", "uid": "key", "data_type": "text", "mandatory": false, "unique": false, "multiple": false}, + {"display_name": "Value", "uid": "value", "data_type": "text", "mandatory": false, "unique": false, "multiple": false, "field_metadata": {"multiline": true}} + ]}, + {"display_name": "array", "uid": "array", "data_type": "group", "mandatory": false, "unique": false, "multiple": true, "schema": [ + {"display_name": "Key", "uid": "key", "data_type": "text", "mandatory": false, "unique": false, "multiple": false}, + {"display_name": "Value", "uid": "value", "data_type": "text", "mandatory": false, "unique": false, "multiple": false, "field_metadata": {"multiline": true}} + ]}, + {"display_name": "object", "uid": "object", "data_type": "group", "mandatory": false, "unique": false, "multiple": true, "schema": [ + {"display_name": "Key", "uid": "key", "data_type": "text", "mandatory": false, "unique": false, "multiple": false}, + {"display_name": "Value", "uid": "value", "data_type": "text", "mandatory": false, "unique": false, "multiple": false, "field_metadata": {"multiline": true}} + ]}, + {"display_name": "number", "uid": "number", "data_type": "group", "mandatory": false, "unique": false, "multiple": true, "schema": [ + {"display_name": "Key", "uid": "key", "data_type": "text", "mandatory": false, "unique": false, "multiple": false}, + {"display_name": "Value", "uid": "value", "data_type": "text", "mandatory": false, "unique": false, "multiple": false, "field_metadata": {"multiline": true}} + ]}, + {"display_name": "href", "uid": "href", "data_type": "group", "mandatory": false, "unique": false, "multiple": true, "schema": [ + {"display_name": "Key", "uid": "key", "data_type": "text", "mandatory": false, "unique": false, "multiple": false}, + {"display_name": "Value", "uid": "value", "data_type": "text", "mandatory": false, "unique": false, "multiple": false, "field_metadata": {"multiline": true}} + ]}, + {"display_name": "textarea", "uid": "textarea", "data_type": "group", "mandatory": false, "unique": false, "multiple": true, "schema": [ + {"display_name": "Key", "uid": "key", "data_type": "text", "mandatory": false, "unique": false, "multiple": false}, + {"display_name": "Value", "uid": "value", "data_type": "text", "mandatory": false, "unique": false, "multiple": false, "field_metadata": {"multiline": true}} + ]}, + {"display_name": "any", "uid": "any", "data_type": "group", "mandatory": false, "unique": false, "multiple": true, "schema": [ + {"display_name": "Key", "uid": "key", "data_type": "text", "mandatory": false, "unique": false, "multiple": false}, + {"display_name": "Value", "uid": "value", "data_type": "text", "mandatory": false, "unique": false, "multiple": false, "field_metadata": {"multiline": true}} + ]}, + {"display_name": "json_rte", "uid": "json_rte", "data_type": "group", "mandatory": false, "unique": false, "multiple": true, "schema": [ + {"display_name": "Key", "uid": "key", "data_type": "text", "mandatory": false, "unique": false, "multiple": false}, + {"display_name": "Value", "uid": "value", "data_type": "text", "mandatory": false, "unique": false, "multiple": false, "field_metadata": {"multiline": true}} + ]}, + {"display_name": "datestring", "uid": "datestring", "data_type": "group", "mandatory": false, "unique": false, "multiple": true, "schema": [ + {"display_name": "Key", "uid": "key", "data_type": "text", "mandatory": false, "unique": false, "multiple": false}, + {"display_name": "Value", "uid": "value", "data_type": "text", "mandatory": false, "unique": false, "multiple": false, "field_metadata": {"multiline": true}} + ]}, + {"display_name": "boolean", "uid": "boolean", "data_type": "group", "mandatory": false, "unique": false, "multiple": true, "schema": [ + {"display_name": "Key", "uid": "key", "data_type": "text", "mandatory": false, "unique": false, "multiple": false}, + {"display_name": "Value", "uid": "value", "data_type": "boolean", "mandatory": false, "unique": false, "multiple": false} + ]}, + {"display_name": "imageurl", "uid": "imageurl", "data_type": "group", "mandatory": false, "unique": false, "multiple": true, "schema": [ + {"display_name": "Key", "uid": "key", "data_type": "text", "mandatory": false, "unique": false, "multiple": false}, + {"display_name": "Value", "uid": "value", "data_type": "text", "mandatory": false, "unique": false, "multiple": false, "field_metadata": {"multiline": true}} + ]}, + {"display_name": "choice", "uid": "choice", "data_type": "group", "mandatory": false, "unique": false, "multiple": true, "schema": [ + {"display_name": "Key", "uid": "key", "data_type": "text", "mandatory": false, "unique": false, "multiple": false}, + {"display_name": "Value", "uid": "value", "data_type": "text", "mandatory": false, "unique": false, "multiple": true, "field_metadata": {"multiline": true}} + ]} + ]}, + {"display_name": "Schema Version", "uid": "schema_version", "data_type": "text", "mandatory": false, "unique": false, "multiple": false}, + {"display_name": "Place Composition As", "uid": "place_composition_as", "data_type": "text", "mandatory": false, "unique": false, "multiple": false}, + {"display_name": "Linked Schemas", "uid": "linked_schemas", "data_type": "group", "mandatory": false, "unique": false, "multiple": true, "schema": [ + {"display_name": "Content Type UID", "uid": "content_type_uid", "data_type": "text", "mandatory": true, "unique": false, "multiple": false}, + {"display_name": "Selected Field", "uid": "selected_field", "data_type": "text", "mandatory": false, "unique": false, "multiple": false}, + {"display_name": "Display Name", "uid": "display_name", "data_type": "text", "mandatory": false, "unique": false, "multiple": false}, + {"display_name": "Cardinality", "uid": "cardinality", "data_type": "text", "mandatory": false, "unique": false, "multiple": false} + ]}, + {"display_name": "URL Metadata", "uid": "url_metadata", "data_type": "group", "mandatory": false, "unique": false, "multiple": false, "schema": [ + {"display_name": "URL Source", "uid": "url_source", "data_type": "text", "mandatory": false, "unique": false, "multiple": false}, + {"display_name": "URL Queries", "uid": "url_queries", "data_type": "text", "mandatory": false, "unique": false, "multiple": false} + ]}, + {"display_name": "Linked Sections", "uid": "linked_sections", "data_type": "reference", "reference_to": ["<compositions_ct_uid>"], "field_metadata": {"ref_multiple": true, "ref_multiple_content_types": true}, "mandatory": false, "unique": false, "multiple": true} + ] + } +} +``` -| uid | data_type | mandatory | multiple | Required by | Purpose | -|---|---|---|---|---|---| -| `title` | text | yes | no | CMS UX | Author-visible title. Unique recommended. | -| `url` | text | no | no | Runtime | Composition URL pattern (e.g. `/products/{{entry.url}}`). Empty for freeform. | -| `composable_uid` | text | yes | no | Runtime + editor | Contract key: runtime queries compositions by `composable_uid`, NOT entry uid. Must be unique. | -| `ui_preview` | file | no | no | Editor | Thumbnail shown in Studio's composition list. | -| `connected_content_type` | text | no | no | Editor + runtime | The page CT uid this composition is connected to (Connected Template). Empty for Freeform. | -| `ui` | text (multiline) | no | no | Editor + runtime | `zlib:<base64>`-encoded composition node tree. Written by Studio canvas. Written by [`author-composition-via-api`](author-composition-via-api.md) for headless authoring. | -| `data_sources` | text (multiline) | no | no | Runtime | JSON object holding `resolvedReferences` etc. for reference resolution at render. | -| `static_value` | group | no | no | Editor | Static-value sub-bindings. See § static_value sub-schema below. Required as-is for the picker to enumerate static-value types. | -| `schema_version` | text | no | no | Editor | Composition schema version stamp written by Studio, a semver-shaped string like `"1.0.0"` (also seen: `""`, `"1"`, `"2"`). | -| `place_composition_as` | text | no | no | Editor | `"page"` for top-level template compositions (both Connected and Freeform). `"section"` for section compositions. `"template"` is legal but not what Studio writes for the top-level entry. | -| `linked_schemas` | **group, multiple=true** | no | yes | Editor | Per-section template-binding override. One entry per content type the section accepts. **Must be `group` with `multiple: true`**. The editor expects an array. Sub-fields on the group: `content_type_uid` (text, **required**), `selected_field` (text, **optional**, set when the section restricts to one field), `display_name` (text, **optional**) and `cardinality` (text, **optional**, `single` or `collection`). Runtime stored shape: `[{ "content_type_uid": "blog_post", "_metadata": { "uid": "<system-generated>" } }, …]`. If this is a single `group` (multiple=false) or a flat text field, editor crashes during section-context build. | -| `url_metadata` | group | no | no | Editor | `url_source` (text), `url_queries` (text). Drives the URL pattern editor. **Both sub-fields must exist.** If the group (or either sub-field) is missing, the CMA silently drops `url_metadata` on every composition write: 201 returned, field reads back absent, and every template composition falls back to `legacy_url` semantics with no error anywhere. See [`author-composition-via-api` § `url` + `url_metadata`, a matched pair](author-composition-entry.md#url-metadata-matched-pair). | -| `linked_sections` | **reference, multiple=true, reference_to: <same CT uid>** | no | yes | Editor | Self-reference to other composition entries that are templates this composition links. **Must be a reference field**. The editor's CDA call uses `?include[]=linked_sections`, which only works on reference fields. If modeled as text/group, the editor's Template Did Not Load error appears. | +What each field is for, and the rules that make it fatal to improvise: -### `static_value` sub-schema +- `composable_uid` (mandatory, unique): the runtime queries compositions by it, not by entry uid. +- `ui`, `data_sources` (multiline text): the `zlib:<base64>` tree and the JSON-encoded `resolvedReferences` list. Wire shapes: [`author-composition-entry`](author-composition-entry.md#wire-shapes) § Wire shapes. +- `static_value`: a group of 13 `{ key, value }` sub-groups, one per binding type the picker offers. `value` is multiline text except `boolean` (boolean) and `choice` (text, **`multiple: true`**: exposed choice props write `["center"]`, and a single-value `choice.value` rejects the write with "should be a single value instead of multiple"). `number.value` is text too, so number literals go in as strings (`"49"`). Missing sub-groups produce missing options in the picker. +- `linked_schemas`: **`group` with `multiple: true`**, sub-field `content_type_uid` (never `uid`, which the CMA reserves inside groups). A single group or a text field crashes the editor's section-context build. +- `url_metadata`: both `url_source` and `url_queries` must exist, or the CMA silently drops `url_metadata` on every composition write and templates fall back to `legacy_url` ([`author-composition-entry`](author-composition-entry.md#url-metadata-matched-pair)). +- `linked_sections`: **reference, `multiple: true`, `reference_to` this CT**. The editor loads it with `?include[]=linked_sections`, which only works on a reference field. Anything else gives Template Did Not Load. +- `schema_version`, `place_composition_as`, `connected_content_type`, `url`, `ui_preview`: written by Studio and read by the editor/runtime. Values are documented in [`author-composition-entry`](author-composition-entry.md). -Add a `group` field named `static_value` (multiple=false) with **13 sub-groups (each multiple=true)**, one per scalar binding type the picker offers. Each sub-group has a uniform `{ key: text, value: text }` shape, EXCEPT `boolean` (`value: boolean`) and `choice` (`value: text` but **`multiple: true`**: exposed choice props write an array like `["center"]`. A single-value `choice.value` rejects the write with "should be a single value instead of multiple"): +**By MCP**: `create_a_content_type`, then the PUT over HTTP (the MCP rejects updates to a CT created after it started, see the callout in step 1). The tool's `content_type` argument takes the **whole** JSON above, outer `content_type` key included, so the call's arguments are double-wrapped: -``` -static_value (group) -├─ text (group[]) { key: text, value: text (multiline) } -├─ html_rte (group[]) { key: text, value: text (multiline) } -├─ array (group[]) { key: text, value: text (multiline) } -├─ object (group[]) { key: text, value: text (multiline) } -├─ number (group[]) { key: text, value: text (multiline) } -├─ href (group[]) { key: text, value: text (multiline) } -├─ textarea (group[]) { key: text, value: text (multiline) } -├─ any (group[]) { key: text, value: text (multiline) } -├─ json_rte (group[]) { key: text, value: text (multiline) } -├─ datestring (group[]) { key: text, value: text (multiline) } -├─ boolean (group[]) { key: text, value: boolean } -├─ imageurl (group[]) { key: text, value: text (multiline) } -└─ choice (group[]) { key: text, value: text (multiline, **multiple: true**) } +```json +{ + "content_type": { + "content_type": { "title": "Compositions", "uid": "<compositions_ct_uid>", "options": {"…": "…"}, "schema": ["… every field except linked_sections …"] } + } +} ``` -The picker's "Static Value" branch enumerates these sub-groups by uid. Missing sub-groups produce missing options in the picker. +Passing the inner object alone (`{ "content_type": { "title": …, "schema": … } }` as the arguments) sends the fields without the `content_type` wrapper the CMA expects. The tool's own description says so: "The fields should be wrapped by an content_type object". `update_content_type` takes the same double-wrapped argument, plus `content_type_uid`. -9. **Verify the CT exists.** `GET /v3/content_types/<uid>` and confirm the field list matches the table above. Catch shape errors early. Fixing a wrong cardinality after entries exist requires deleting and re-creating the CT. +9. **Verify the CT exists.** `GET /v3/content_types/<uid>` (HTTP when the CT was created in this MCP session) and confirm the field list matches the schema above. Catch shape errors early. Fixing a wrong cardinality after entries exist requires deleting and re-creating the CT. 10. **Register the Studio project** (Studio API). Two calls. @@ -230,7 +299,7 @@ The picker's "Static Value" branch enumerates these sub-groups by uid. Missing s - [ ] Live Preview enabled: `GET /v3/stacks/settings` returns `stack_settings.live_preview.enabled === true`. - [ ] Environment exists. Delivery token + preview token captured. - [ ] `GET /v1/projects` lists the project with `connectedStackApiKey === <stack api_key>` and the right `contentTypeUid`. -- [ ] `GET /v3/content_types/<uid>` returns all 13 fields from the table with exact `data_type`, `mandatory`, `multiple`. +- [ ] `GET /v3/content_types/<uid>` returns all 13 fields from the schema with exact `data_type`, `mandatory`, `multiple`. - [ ] `linked_schemas` is `group` with `multiple: true`. (Single-group is the most common provisioning bug, re-check.) - [ ] `linked_sections` is `reference` with `multiple: true` and `reference_to: [<this CT uid>]`. - [ ] `composable_uid` is mandatory + unique. @@ -242,17 +311,17 @@ The picker's "Static Value" branch enumerates these sub-groups by uid. Missing s | Pitfall | Why it bites | Fix | |---|---|---| -| Naming a group sub-field `uid` | The CMA rejects the whole `PUT` with `422 schema.N.schema.0.uid: "has a restricted value 'uid', which is not allowed"`. Easy to hit on `linked_schemas`, where the natural guess for the content-type key is `uid` | The sub-field is **`content_type_uid`**, never `uid`. See the `linked_schemas` row above. `uid` is reserved by the CMA inside groups | +| Naming a group sub-field `uid` | The CMA rejects the whole `PUT` with `422 schema.N.schema.0.uid: "has a restricted value 'uid', which is not allowed"`. Easy to hit on `linked_schemas`, where the natural guess for the content-type key is `uid` | The sub-field is **`content_type_uid`**, never `uid`. See the schema above. `uid` is reserved by the CMA inside groups | | `linked_schemas` modelled as `group` with `multiple: false` | Editor expects an array. Section-context build crashes on `.map` of a single object | Re-create CT with `multiple: true`. Or PATCH the CT field if no entries exist yet | | `linked_sections` modelled as text/group instead of reference | Editor's `?include[]=linked_sections` returns nothing. Template Did Not Load on canvas open | Must be `reference` field with `reference_to: [<same CT uid>]`. See [`troubleshoot-canvas`](troubleshoot-canvas.md) Template-Did-Not-Load row | | `composable_uid` left non-unique | Runtime lookup picks an arbitrary entry. One composition renders for many URLs | Mark mandatory + unique at CT creation time | -| Forgetting `static_value` sub-groups | Picker's Static Value root shows fewer (or zero) type options | Add the 13 sub-groups exactly as schema'd above | +| Forgetting `static_value` sub-groups | Picker's Static Value root shows fewer (or zero) type options | Add the 13 sub-groups exactly as in the schema above | | Composition entry not published | CDA returns 404. Canvas blank | Publish to the environment after entry create | | Preview token missing / set to literal `"undefined"` | Canvas iframe gets HTTP 401 with no CORS headers, "Component Loading Error" | See [`troubleshoot-canvas`](troubleshoot-canvas.md) preview-token row | | JSON-RTE static values fail to embed correctly | JSON-RTE embeds carry `attrs.locale`, missing in API-authored payloads | Set `attrs.locale` on every embedded entry/asset in `static_value.json_rte[*].value` | | Wrong region host | CT creation 404s | Use the region-specific CMA and Studio API hosts (Step 10) | | Live Preview not enabled on stack | Canvas iframe hangs on "Loading composition…". Rest-preview CORS preflight fails | `POST /v3/stacks/settings` with `live_preview.enabled`, `default-env` and `default-url` (step 5). `PUT /v3/stacks` answers `200` and changes nothing | -| Studio project configured with environment **name** instead of **uid** | Studio API accepts the PUT but environment binding silently misses. Compositions never resolve | Look up env uid via `GET /v3/environments`, pass `settings.configuration.environment = <uid>` | +| Studio project configured with environment **name** instead of **uid** | Routes work. Studio finds no Base URL, so the canvas uses the Playground | Look up env uid via `GET /v3/environments`, pass `settings.configuration.environment = <uid>` | | Re-using a Studio project across stacks (name match, stale `connectedStackApiKey`) | Project surfaces last stack's compositions. Current stack invisible in canvas | On every provision, `GET` the project and compare its `connectedStackApiKey` with the current stack. If it differs, show the user the before/after and get approval, then PUT to re-bind | | Mixed CMA + Studio API hosts (e.g. NA Studio API with EU CMA) | 401/404s with no clear error | Pick a region and use BOTH region-matched hosts | | Including `linked_sections` in the initial `POST /v3/content_types` | `error_code 115: linked_sections.reference_to: content type does not exist`. CMA validates reference targets at create time. A CT can't self-reference before it exists | Two-step: POST without `linked_sections`, then PUT with the full schema (step 8) | diff --git a/skills/studio/provision-studio-stack.md b/skills/studio/provision-studio-stack.md index cbb90bd..e2a6e26 100644 --- a/skills/studio/provision-studio-stack.md +++ b/skills/studio/provision-studio-stack.md @@ -22,6 +22,8 @@ All operations are CMA-only (`api.contentstack.io` and regional variants). Auth ## Task +**Plan-only requests stop at the plan.** When the user asks for the plan first ("show me the plan", "don't run anything yet"), print it, ask what you need, and end the turn. Their answers to those questions (the stack, the environments, a file path) are not a go-ahead: every step here writes to a shared stack and some can't be undone cleanly, so wait for an explicit "go" before the first create, upload or publish. + 1. **Prerequisites.** Stack exists with delivery + preview tokens. The compositions CT has been created via [`provision-studio-project`](provision-studio-project.md). You have `CS_AUTH_TOKEN`, `CS_API_KEY`, the target environment name, and the region's CMA host. 2. **Create / update global fields first.** Page CTs that reference global fields (e.g. `gf_hero`, `gf_card_list`) must have those GFs in place at CT create time. Contentstack rejects CT POSTs that reference an unknown global field uid. @@ -142,6 +144,7 @@ All operations are CMA-only (`api.contentstack.io` and regional variants). Auth | Pitfall | Why it bites | Fix | |---|---|---| +| Treating the user's answers to the plan's questions as the go-ahead on a plan-only request | Content types and entries get created on a stack the user only wanted a plan for | Answers fill in the plan. Reprint what changed and wait for an explicit "go" before any write | | Creating a CT that references a GF before the GF exists | CMA rejects POST `/v3/content_types` with "global field not found" | Create / update all global fields BEFORE page CTs | | Re-uploading the same asset every provision run | Stack accumulates duplicate assets. Entry references go stale on re-create | Dedupe via `GET /v3/assets?query={"title":"…"}` + persist `.asset-map.json` | | File-field value set to the asset URL or filename instead of uid | CMA accepts the POST but the field reads as broken in CDA | Pass `<asset_uid>` (string) for single-file, `[<uid>, ...]` for multi-file | @@ -155,7 +158,6 @@ All operations are CMA-only (`api.contentstack.io` and regional variants). Auth ## See also -- [`provision-studio-project`](provision-studio-project.md): stack-wide setup, compositions CT, Studio project registration. +- [`provision-studio-project`](provision-studio-project.md): stack-wide setup, compositions CT, Studio project registration, regional hosts and auth headers. - [`author-composition-via-api`](author-composition-via-api.md): composition entries that bind to the CTs created here. - [`build-connected-template`](build-connected-template.md): Template authoring against these page CTs. -- [`provision-studio-project`](provision-studio-project.md): regional hosts and auth headers. diff --git a/skills/studio/register-breakpoints.md b/skills/studio/register-breakpoints.md index af971e3..28b4a41 100644 --- a/skills/studio/register-breakpoints.md +++ b/skills/studio/register-breakpoints.md @@ -33,7 +33,7 @@ The registry tracks unique `id`s and unique `displayName`s. Duplicates throw. Th ## Task -1. **Locate the bootstrap module: by content, not by path.** `grep -rln "registerComponent" src app lib 2>/dev/null` (path lists produce false negatives on `src/app`, route groups, and locale segments). Typical homes are ( `src/studio/bootstrap.ts`, `src/studio/registerComponents.ts`, or whatever the project pulls in from `@/lib/contentstack`). If none exists, create `src/studio/bootstrap.ts` and import it from the app shell (`app/layout.tsx`, `src/main.tsx`, `app/root.tsx`, etc.) for its side effects. +1. **Locate the bootstrap module: by content, not by path.** `grep -rlnE "register(Lazy)?Component" src app lib 2>/dev/null` (path lists produce false negatives on `src/app`, route groups, and locale segments). Typical homes are ( `src/studio/bootstrap.ts`, `src/studio/registerComponents.ts`, or whatever the project pulls in from `@/lib/contentstack`). If none exists, create `src/studio/bootstrap.ts` and import it from the app shell (`app/layout.tsx`, `src/main.tsx`, `app/root.tsx`, etc.) for its side effects. 2. **Add the import** at the top of that file: diff --git a/skills/studio/register-component-pitfalls.md b/skills/studio/register-component-pitfalls.md index 6b6d0b8..0ddf187 100644 --- a/skills/studio/register-component-pitfalls.md +++ b/skills/studio/register-component-pitfalls.md @@ -23,15 +23,20 @@ Part of the component-registration set. Start at [`register-component`](register | Copying a prop type off a built-in node into a `registerComponent` schema | Built-ins use a separate prop-type vocabulary. Some of their strings are not among the 14 registered-component types, and the registration is rejected | Map each one to a registered-component type from `register-component` step 3. Read built-in defs for structure, never for prop-type strings | | Registering a prop that's not actually on the component | The Data Picker offers a binding that crashes at render time | Read the source. Register only props you see | | Duplicate `type:` UID across two components | **First registration wins. Duplicates are silently skipped** (the SDK's default is idempotent so HMR / RSC-plus-client / build-worker re-evaluation doesn't break). A real bug stays hidden because the second definition never lands. | Search the registration file for the UID before writing. Pass `{ strict: true }` as the second arg to `registerComponents([...], { strict: true })` to throw on duplicates instead. | +| Editing a registration while the dev server keeps running | The same skip applies to your own edit. The registry lives on `globalThis`, so hot reload re-runs the module but the server keeps the **old** entry (old props, defaults, component) while the browser loads the new one. Symptom: a hydration mismatch, old markup on the server and new in the browser (seen as 0-px image frames), with no error naming the cause | **Restart the dev server after every change to a registration.** Not a production issue: a fresh process registers once | | Forgetting to import the registration file at startup | Components never appear in the palette | Add `import "./register-components"` to `main.tsx` / `_app.tsx` | | Registering the unwrapped primitive (e.g. `Box` from a UI lib) instead of the customer's component | Loses brand styling. Studio composes pages out of unstyled primitives | Register the customer's wrapped component (e.g. `<Hero>`), not the lib primitive | | Inferring a `string` prop as `choice` because TS uses a union of two literals | Authors can't type freeform text into a free-form field | Use `choice` only when the union is a closed set the user MUST pick from | -| Arity-0 component (`function Header() {…}` as `component: Header`) | SDK treats as lazy loader and calls it outside render, which throws React #321 "Invalid hook call". | Use lazy shape `() => import(...)` or give it a `props` param. | -| Registering eagerly when lazy would do | Eager ships in entry bundle. Many components balloon TTI. | Default to lazy `() => import("./Foo")`. | +| Arity-0 component (`function Header() {…}` as `component: Header`) | SDK treats as lazy loader and calls it outside render, which throws React #321 "Invalid hook call". | Register it lazily with `registerLazyComponent`, or give it a `props` param. | +| Registering eagerly when lazy would do | Eager ships in entry bundle. Many components balloon TTI. | Default to `registerLazyComponent(config, () => import("./Foo").then((m) => m.Foo))`. | +| `component: () => import("./Foo")` inside `registerComponent` | Works at runtime in 2.0.1, but fails `tsc` (`TS2322`: a `Promise` of a module is not a `ReactNode`), so a typed build breaks | Use `registerLazyComponent(config, loader)` | +| `registerLazyComponent` loader resolving to the module (`() => import("./Foo")`) | The loader's result becomes the `default` of a `React.lazy` module, so React receives a module object instead of a component and the render fails | Resolve to the component: `.then((m) => m.Foo)` or `.then((m) => m.default)` | +| `type: "object"` without `properties`, or `type: "array"` without `items` | Both are required in 2.0.1. `tsc` rejects the registration ("Property 'properties' is missing" / "Property 'items' is missing") | Declare `properties: { … }` on every object prop and `items: { type: … }` on every array prop (`{ type: "any" }` if items vary). See `register-component` step 3 | | Component depends on layout ancestor (e.g. `.fs-card` only sized by `.fs-grid`) | Renders full-bleed in a Slot/Repeater that doesn't provide that ancestor. | Decouple. See Layout contract above. Layout lives in parent Section. See `use-section-slot` § Layout container. | | Setting `defaultValue` on a `slot` prop | Slots start empty until authors fill them. A static default doesn't fit the model and is silently ignored. | Omit `defaultValue` on `slot` props. The slot renders as a dashed drop target until an author drops content. | | Naming a slot prop something the component doesn't render | The slot becomes a drop target in Studio but its contents never appear at render time because the component ignores the prop. | The prop name must match what the React component's signature uses, typically `children` (for `<Card>{...}</Card>` style), but any name works as long as the component renders `{propName}` somewhere in its JSX. | | Trying to constrain a slot to "only accept Sections of type X" | Slots accept any registered component or Section. No built-in type filter exists. Adding a constraint isn't supported. | If you need a typed value override (one specific kind of content), use an exposed Section Prop instead. If you need a typed component constraint, surface the expectation in the slot's `displayName` ("Drop a CTA Section here") so authors self-route. | | Component throws on `undefined` / `null` / empty-array props during canvas preview | Bindings resolve at runtime. An unfilled entry field, empty multi-file field, or unbound prop surfaces `undefined` to the component. One thrown access kills the whole canvas render. Author can't recover without re-binding. | See § Null-safe rendering contract above. Optional-chain every nested access (`props.image?.url`). Return `null` or a skeleton when essential data is missing. | | Confusing `slot` (component prop type) with Section Slot (drop region inside a Section) | They are different mechanisms at different levels, both surface drop targets, both can hold the other. | A `slot` PROP is part of a registered component's schema. A Section Slot is a Smart Container carved into a Section. The recommended composition runs in three steps: declare a `slot` prop on the component, carve a Section Slot inside that prop via `use-section-slot` § component-slot-prop placement, then let authors fill that Section Slot at template time. | -| Design tab is empty for this component when selected in the canvas | The registration didn't declare a `styles` block. The Design tab surfaces the sections listed in `styles`, so with no `styles` the tab is empty. | Add `styles: [...]` to the registration listing the categories authors should be able to edit (`size`, `spacing`, `typography`, `background`, etc.). See [`register-component`](register-component.md) § styles. If the Design tab itself is missing (not just empty), the project's Freeform Feature is off (see [`install-studio`](install-studio.md) § After install) if the Design tab is missing / disabled. | +| Design tab is empty for this component when selected in the canvas | The registration didn't declare a `styles` block. The Design tab surfaces the style groups listed in `styles`, so with no `styles` the tab is empty. | Add a `styles` **object** (not an array): one key per group, e.g. `styles: { default: { styleSections: ["size", "spacing", "background"] } }`. Apply what arrives: the `default` group as `className`, every other group as a prop named after its key. See [`register-component`](register-component.md) step 5c. If the Design tab itself is missing (not just empty), the project's Freeform Feature is off (see [`install-studio`](install-studio.md) § After install). | +| Design panel edits saved but nothing changes on the page | The component receives the style group's class (`className`, or the group-named prop) but never puts it on an element | Spread the class on the element the group styles. Same for `design: true` props, which arrive as a generated class name, not a CSS value | diff --git a/skills/studio/register-component-prop-contracts.md b/skills/studio/register-component-prop-contracts.md index 89385b6..512865c 100644 --- a/skills/studio/register-component-prop-contracts.md +++ b/skills/studio/register-component-prop-contracts.md @@ -4,7 +4,7 @@ name: register-component-prop-contracts ## When to use -The prop-shape contracts a registered component must honour: array and object binding rules, tolerant `imageurl` and link signatures, and why `{{entry.title}}` does not resolve outside a Connected template URL pattern. +The prop-shape contracts a registered component must honour: array and object binding rules, why the live site fetches only the fields bindings name, image (`<file_field>.url`) and link signatures, and why `{{entry.title}}` does not resolve outside a Connected template URL pattern. Part of the component-registration set. Start at [`register-component`](register-component.md), which routes here. @@ -27,7 +27,7 @@ Common requirements across all four: This is the **preferred path** for wrapping an existing production component whose interface already takes an array of items. See `build-repeating-section` § When to skip the Repeater entirely: the array-prop alternative. -**Layout fidelity (not just data shape) decides Repeater vs array-prop.** A Repeater renders items **uniformly in sequence**. It cannot reproduce a bespoke or asymmetric layout (a bento mosaic, a 1-big-plus-3-small grid, a carousel). When the design is fixed and non-uniform, or you simply want the production component's own layout verbatim, bind the whole array/object to the real component (`array` / `object`) and let its own markup + CSS run. The render is pixel-identical to the live site. Reach for a Repeater only when the layout genuinely IS a uniform list, or when items are polymorphic and need per-type rendering (Repeater + Condition Block). +**Layout fidelity (not just data shape) decides Repeater vs array-prop.** A Repeater renders items **uniformly in sequence**. It cannot reproduce a bespoke or asymmetric layout (a bento mosaic, a 1-big-plus-3-small grid, a carousel). When the list is interactive, or its layout depends on item position or count (the component computes its values), bind the whole array/object to the real component (`array` / `object`) and let its own markup + CSS run; this is the exception the [Section rule](SKILL.md#section-rule) allows, not a default for keeping the production layout verbatim. The render is pixel-identical to the live site, provided every field the component reads reaches it (see § Studio fetches only the fields your bindings name). Reach for a Repeater only when the layout genuinely IS a uniform list, or when items are polymorphic and need per-type rendering (Repeater + Condition Block). **Bindable: `type: "object"` bound to a matching entry Group / Global Field.** Same idea for a single-entry-object shape (a nested `cta: { label, href }` binding into an entry Group with matching sub-fields). Groups live on the entry, no `resolvedReferences` step. @@ -46,11 +46,33 @@ The narrow non-CMS use for `type: "array"` / `type: "object"` remains: authoring `build-section` Step 5e enforces this at bind time. Catching it at registration saves a round trip. -## Tolerant image signatures: accept `string | { url }` on every `imageurl` prop +## Studio fetches only the fields your bindings name -Every `imageurl`-typed prop must accept both a plain URL string AND a Contentstack asset object with a `.url` field. Contentstack stores assets as objects (`{ url, filename, uid, … }`). The picker sometimes binds the object, sometimes the `.url` sub-field, depending on the depth of the picked path. +On the live site the SDK does not fetch whole entries. It builds the Delivery API query from the composition's bindings: `include[]` for the references they cross, and an `only[]` projection listing the fields they name. **Inside the Studio canvas and Visual Builder it skips the projection and fetches every field.** So a component that reads fields no binding names renders correctly while you author, then receives a stripped object on the published page: a product card bound to a whole referenced entry got `{ uid }` with no title, price or images, and nothing logged an error. -If the component's TypeScript signature demands one shape, the other silently fails: icons "map correctly" but render invisible, or the object gets `.toString()`'d and the URL area shows `[object Object]`. Both bugs ship to production without a warning. +**Prefer one binding per field.** Register the leaves the component renders (`title`, `price`, `image` → `product.image.url`, `imageAlt`) instead of one `object` prop for the whole entry. Each binding adds its field to the projection, and each field gets its own `$`-twin for Visual Editor. + +**When a component genuinely needs the whole entry** (a production card that reads a dozen fields, or a computed price), the options in `@contentstack/studio-react` 2.0.1 are: + +| Option | How | Caveat | +|---|---|---| +| Name the extra fields in `extendQuery` | Pass `{ extendQuery: { "<content_type_uid>": { only: [["<reference_field>", "title"], ["<reference_field>", "price"]] } } }` as the options argument of `fetchCompositionData` / `useCompositionData`. A plain string means a field on the entry itself (`["BASE", "<field>"]`). The key is the content type of the entry being fetched: the template's connected type, or a pinned entry's type | **Deprecated.** 2.0.1 still merges these `only` pairs into the projection, but logs "`extendQuery` is deprecated and will be removed in the next major". Its "tick the references in Studio" advice covers `include[]`, not field projection. No replacement for per-field projection exists in 2.0.1 that we could confirm | +| Fetch the data yourself | Fetch the entry in the app and pass it through `data` ([`wire-external-data`](wire-external-data.md), [`render-with-own-data`](render-with-own-data.md)) | The fields are no longer bindable in Studio for that prop | + +**Verify outside the editor.** Open the published route in a plain browser tab (not the canvas, not Visual Builder) and check the component's props, or the Delivery API request's `only[...]` parameters in the Network tab. An editor-only check cannot catch this. + +## Image props: always bind `<file_field>.url`, alt text separately + +**The binding rule.** Bind every `imageurl` prop to the file field's **`.url`** leaf (`hero_image.url`, or `images.0.url` for the first of a multiple file field), never to the file field itself. A file field is an asset object (`{ uid, url, filename, title, description, … }`), and in 2.0.1 `imageurl` accepts **only a string**: a bound asset object is reported as a type mismatch ("Data Mismatch Found" in the editor, a console warning on the live site) and the image renders nothing. **Alt text is a second prop**: a `string` prop (`imageAlt`) bound to the asset's `description` (or `title`), so the image and its alt are each one binding. + +```ts +props: { + image: { type: "imageurl", displayName: "Image" }, // bind → hero_image.url + imageAlt: { type: "string", displayName: "Image alt" }, // bind → hero_image.description +} +``` + +**Defense in depth: still accept `string | { url }`.** Bindings authored before this rule, or by hand through the API, can still point at the object. If the component's TypeScript signature demands one shape, the other silently fails: icons "map correctly" but render invisible, or the object gets `.toString()`'d and the URL area shows `[object Object]`. Both bugs ship to production without a warning. Contract for every `imageurl` prop: @@ -63,9 +85,7 @@ function coerce(img: ImageProp): string | undefined { } ``` -Every registered component's `imageurl` prop declares its TS type as `string | { url?: string } | null | undefined`, and the component internally calls a coerce helper (or inlines `typeof img === "string" ? img : img?.url`. Never `img?.url ?? img`: an asset object with no `url` falls through to the object itself and renders `[object Object]`). This is a first-class contract, peer of the null-safety and `$`-twin contracts, not a pitfall row. - -Rationale: Studio's picker doesn't ship type-aware binding coercion today. Until it does, the component compensates. +Every registered component's `imageurl` prop declares its TS type as `string | { url?: string } | null | undefined`, and the component internally calls a coerce helper (or inlines `typeof img === "string" ? img : img?.url`. Never `img?.url ?? img`: an asset object with no `url` falls through to the object itself and renders `[object Object]`). This is a first-class contract, peer of the null-safety and `$`-twin contracts, not a pitfall row. The coercion is the safety net. The `.url` binding is the rule. ## Tolerant link signatures: bind the leaves, not the whole link object diff --git a/skills/studio/register-component.md b/skills/studio/register-component.md index 91d497b..e9861bc 100644 --- a/skills/studio/register-component.md +++ b/skills/studio/register-component.md @@ -4,7 +4,7 @@ name: register-component ## When to use -Read a React component, infer a prop schema, and write a `registerComponent` call so Studio's palette uses the customer's component instead of built-in defaults. Lazy by default. +Read a React component, infer a prop schema, and write a `registerLazyComponent` / `registerComponent` call so Studio's palette uses the customer's component instead of built-in defaults. Lazy by default. Use when the user wants Studio's palette to surface their components: "make my Hero available in Studio", "Studio shows defaults instead of mine", "register components in src/components". The BYOC moment. Do NOT use to register design tokens (use `import-design-tokens`). Do NOT invent prop schemas. Only register props visible in source. @@ -22,9 +22,9 @@ Use when the user wants Studio's palette to surface their components: "make my H This skill runs the last three questions of the [four-question decision framework](design-component-library.md). Q1 (classify: atom, layout, or compound?) is answered before you land here. If the input is a compound, `decompose-jsx-to-atomics` or `decompose-design` decomposes it into atoms + layouts before registration. If the input is already a valid atom or layout, proceed. -- **Q2: Does the registry already have it?** Before writing a `registerComponent` call, check whether an existing primitive covers the shape. If yes, reuse (don't re-register). If not, proceed to Q3. -- **Q3: Which props?** For each proposed prop: does it vary per-instance AND represent functional/content intent (not visual polish)? If either is no, refuse the prop. Let a DS token decide. -- **Q4: Which are Exposed?** For each schema prop: is it author-understandable AND DS-portable? If either is no, keep static per-Section. Never expose atom props. +- **Q2** — if an existing primitive covers the shape, reuse it; don't re-register. +- **Q3** — a prop that fails either half is refused here. Let a DS token decide instead. +- **Q4** — a prop that fails either half stays static per-Section. If any answer is ambiguous, halt and ask the user to disambiguate. Never silently register a prop that fails Q3 or expose a prop that fails Q4. @@ -45,7 +45,7 @@ The registration is a `registerComponent` call that names the component, gives i ## Task -1. **Locate the registration entry point.** Look for `src/register-components.tsx`, `src/register-studio.ts`, or `src/lib/contentstack.ts` (whichever file imports `registerComponent` from `@contentstack/studio-react`). If none exists, create `src/register-components.tsx` and ensure it's imported once at app startup (before any `<StudioCanvas />` or `<StudioComponent />` mounts). +1. **Locate the registration entry point.** Look for `src/register-components.tsx`, `src/register-studio.ts`, or `src/lib/contentstack.ts` (whichever file imports `registerComponent` or `registerLazyComponent` from `@contentstack/studio-react`). If none exists, create `src/register-components.tsx` and ensure it's imported once at app startup (before any `<StudioCanvas />` or `<StudioComponent />` mounts). 2. **Read the component at `componentPath`.** Extract the prop names and types. Source of truth ranked by reliability: - Explicit TypeScript interface (`interface HeroProps { … }`), most reliable @@ -62,10 +62,10 @@ The registration is a `registerComponent` call that names the component, gives i | `boolean` | `boolean` | | `string` constrained to known values (union literal, enum) | `choice` (set `options:` to the literal values. Write `defaultValue` as an **array** (`["centered"]`) for both single- and multi-select, per [`wire-component-default-data`](wire-component-default-data.md)) | | URL string used as `href` | `href` | - | URL string used as image `src` | `imageurl` | + | URL string used as image `src` | `imageurl`, always bound to `<file_field>.url` (never the asset object). Alt text is a separate `string` prop bound to the asset's `description` | | ISO date string | `datestring` | - | array | `array`, binds to a **multi-value field** (Reference multi, Group multi, Modular Block, scalar multi). Reference sources need `data_sources.resolvedReferences`. Groups / Modular Blocks / scalar multi-values live on the entry and need no resolution. See [`register-component-prop-contracts`](register-component-prop-contracts.md) § Array & object props: binding rules | - | object | `object`, binds when the shape matches an entry Group / Global Field. See [`register-component-prop-contracts`](register-component-prop-contracts.md) § Array & object props: binding rules | + | array | `array`, binds to a **multi-value field** (Reference multi, Group multi, Modular Block, scalar multi). **`items` is required** (the per-item prop, e.g. `items: { type: "string" }`, or `items: { type: "any" }`). Reference sources need `data_sources.resolvedReferences`. Groups / Modular Blocks / scalar multi-values live on the entry and need no resolution. See [`register-component-prop-contracts`](register-component-prop-contracts.md) § Array & object props: binding rules | + | object | `object`, binds when the shape matches an entry Group / Global Field. **`properties` is required** (a nested `props`-style map, one entry per sub-field). See [`register-component-prop-contracts`](register-component-prop-contracts.md) § Array & object props: binding rules | | React `children` / placeholder for extensible region | `slot`, fillable at template time via `use-section-slot` § component-slot-prop placement. The prop turns that region of the component into an author-controlled drop target. No `defaultValue` (slots start empty). | | rich text (JSON RTE field, HTML payload) | **`json_rte`**, not `string`. RTE renders markup, and `string` would show it as literal `<p>` text. | | unknown / can't infer, and the value is a **primitive** | `any`, **never for object- or array-shaped data.** See the flatten trap below | @@ -107,7 +107,7 @@ The registration is a `registerComponent` call that names the component, gives i 4. **Set `defaultValue` for every prop** where the component has a default value in its source. The default appears as the palette tile preview. Without it, the tile renders blank and authors don't know what the component looks like. Use `defaultValue:` exactly, NOT `default:` (the latter is silently ignored). -5. **Ship a `thumbnailUrl`: every registration, no exceptions.** (This and the `studioAttributes` contract below are two of the six in [`complete-the-build`](complete-the-build.md). Both ship in the same commit as the registration, never as a follow-up.) `thumbnailUrl` defaults to `""` in the registry, and Studio then renders a **text placeholder** instead of a preview. A palette of twenty text placeholders is unusable for the marketers this whole exercise exists to serve, and nothing errors, so it ships unnoticed. +5. **Ship a `thumbnailUrl`: every registration, no exceptions.** (This and the `studioAttributes` contract below are two of the ten in [`complete-the-build`](complete-the-build.md). Both ship in the same commit as the registration, never as a follow-up.) `thumbnailUrl` defaults to `""` in the registry, and Studio then renders a **text placeholder** instead of a preview. A palette of twenty text placeholders is unusable for the marketers this whole exercise exists to serve, and nothing errors, so it ships unnoticed. Rules that matter: @@ -120,19 +120,21 @@ The registration is a `registerComponent` call that names the component, gives i const thumb = (svg: string) => `data:image/svg+xml,${encodeURIComponent(svg)}`; const HeroGlyph = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 80"><rect width="120" height="80" fill="#f4f4f7"/><rect x="12" y="20" width="72" height="10" fill="#6c5ce7"/><rect x="12" y="38" width="52" height="6" fill="#a0a0b0"/><rect x="12" y="54" width="28" height="10" rx="2" fill="#6c5ce7"/></svg>`; - registerComponent({ - type: "site-hero", - displayName: "Hero", - component: () => import("./components/Hero"), - sections: ["Acme · Patterns"], - thumbnailUrl: thumb(HeroGlyph), // data:image/svg+xml,... - props: { /* … */ }, - }); + registerLazyComponent( + { + type: "site-hero", + displayName: "Hero", + sections: ["Acme · Patterns"], + thumbnailUrl: thumb(HeroGlyph), // data:image/svg+xml,... + props: { /* … */ }, + }, + () => import("./components/Hero").then((m) => m.Hero), + ); ``` - **One argument, not two.** `registerComponent(Hero, { name, … })` — component first, config second — **throws `Component type is required.`** Verified against the published `@contentstack/studio-registry@1.9.0`: `registerComponent` is typed as *one element* of the array `registerComponents` takes, so a component passed first is read as the config and `type` comes back undefined. Pass one config object; **`type` and `component` are the only required fields**. `registerComponents([{ … }], options?)` takes an array of the same shape. + **One config object, not component-then-config.** `registerComponent(Hero, { name, … })` **throws `Component type is required.`** Verified against `@contentstack/studio-react@2.0.1`: `registerComponent` takes *one element* of the array `registerComponents` takes, so a component passed first is read as the config and `type` comes back undefined. **`type` and `component` are the only required fields**. `registerComponents([{ … }], options?)` takes an array of the same shape. The one two-argument form is `registerLazyComponent(config, loader)`: the config (everything except `component`) first, then the loader. - **Retrofitting an existing registration file?** Every `registerComponent` call without `thumbnailUrl` is a blank tile today. Sweep them all: `grep -c "registerComponent" <file>` vs `grep -c "thumbnailUrl" <file>`. The counts must match. + **Retrofitting an existing registration file?** Every registration without `thumbnailUrl` is a blank tile today. Sweep them all: `grep -cE "register(Lazy)?Component\(" <file>` vs `grep -c "thumbnailUrl" <file>`. The counts must match. 5b. **Next.js App Router: registration must run in the CLIENT realm.** A registered component that is (or renders) a `"use client"` component cannot be registered from a Server Component module graph. On the server the SDK sees a client reference, and Next throws at render time ("Attempted to call the default export of \<module\> from the server but it's on the client") or the palette silently comes up empty because the registry that got populated was the server-side one. @@ -148,42 +150,57 @@ The registration is a `registerComponent` call that names the component, gives i Do **not** import the registration module from `app/layout.tsx`, a `page.tsx` Server Component, or any non-`"use client"` module. Pages Router / Vite / Remix don't have this split. A module-scope import in `_app.tsx` or `main.tsx` is fine. Related: [`install-studio`](install-studio.md) § App Router init boundary, and [`troubleshoot-ssr-rendering`](troubleshoot-ssr-rendering.md) for the registry-singleton symptoms. -6. **Write the registration: prefer LAZY by default.** Append (or update) the registration file. The recommended shape is **lazy**: the component is downloaded only when it first renders, keeping the initial bundle small and code-splitting each registration automatically. The SDK wraps the dynamic `import()` with `React.lazy` + `Suspense` for you. +6. **Write the registration: prefer LAZY by default.** Append (or update) the registration file. The recommended shape is **lazy**: the component is downloaded only when it first renders, keeping the initial bundle small and code-splitting each registration automatically. `registerLazyComponent` wraps the loader with `React.lazy` + `Suspense` for you. ```ts - import { registerComponent } from "@contentstack/studio-react"; - - registerComponent({ - type: "site-hero", // componentType — unique UID - displayName: "Hero", // componentName — palette label - component: () => // ← LAZY: arity-0 thunk that returns a dynamic import - import("./components/Hero").then(m => ({ default: m.Hero })), - props: { - headline: { type: "string", displayName: "Headline", defaultValue: "Welcome" }, - description: { type: "string", displayName: "Description", defaultValue: "Lorem ipsum" }, - imageUrl: { type: "imageurl", displayName: "Image", defaultValue: "https://…" }, - ctaHref: { type: "href", displayName: "CTA Link", defaultValue: "/get-started" }, - layout: { type: "choice", displayName: "Layout", defaultValue: ["centered"], options: ["centered", "split"] }, + import { registerLazyComponent } from "@contentstack/studio-react"; + + registerLazyComponent( + { + type: "site-hero", // componentType — unique UID + displayName: "Hero", // componentName — palette label + props: { + headline: { type: "string", displayName: "Headline", defaultValue: "Welcome" }, + description: { type: "string", displayName: "Description", defaultValue: "Lorem ipsum" }, + imageUrl: { type: "imageurl", displayName: "Image", defaultValue: "https://…" }, + ctaHref: { type: "href", displayName: "CTA Link", defaultValue: "/get-started" }, + layout: { type: "choice", displayName: "Layout", defaultValue: ["centered"], options: ["centered", "split"] }, + cta: { // object: `properties` is required + type: "object", displayName: "CTA", + properties: { + label: { type: "string", defaultValue: "Get started" }, + href: { type: "href", defaultValue: "/get-started" }, + }, + }, + features: { // array: `items` is required + type: "array", displayName: "Features", + items: { type: "string", defaultValue: "Feature item" }, + defaultValue: ["First feature", "Second feature"], + }, + }, }, - }); + () => import("./components/Hero").then((m) => m.Hero), // ← LAZY loader: resolve to the component itself + ); ``` - If the component is the **default** export: `component: () => import("./components/Hero")` is enough. An explicit form `registerLazyComponent(config, loader)` exists with identical runtime behavior. + **The loader resolves to the component, not the module.** `registerLazyComponent` wraps whatever the promise yields as the `default` of a `React.lazy` module, so `() => import("./components/Hero")` hands React a module object and the render fails. For a default export write `.then((m) => m.default)`. **5a. Exposing extensible regions with `slot` props.** To let template authors drop content into a region of the component (a card body, a sidebar, a CTA area), declare a `slot`-typed prop. The component renders the prop wherever the extensible region should appear. Studio surfaces it as a drop target for any component or Section. ```ts - import { registerComponent } from "@contentstack/studio-react"; - - registerComponent({ - type: "site-card", - displayName: "Card", - component: () => import("./components/Card"), - props: { - title: { type: "string", displayName: "Title", defaultValue: "Card title" }, - body: { type: "slot", displayName: "Body" }, // ← extensible region, no defaultValue + import { registerLazyComponent } from "@contentstack/studio-react"; + + registerLazyComponent( + { + type: "site-card", + displayName: "Card", + props: { + title: { type: "string", displayName: "Title", defaultValue: "Card title" }, + body: { type: "slot", displayName: "Body" }, // ← extensible region, no defaultValue + }, }, - }); + () => import("./components/Card").then((m) => m.default), + ); ``` The React component receives the slot as a prop (or via `children`, depending on what its TypeScript interface declares) and renders it inside its tree: @@ -197,22 +214,64 @@ The registration is a `registerComponent` call that names the component, gives i At template-authoring time, the slot region shows as a dashed drop target. Authors drop a component or Section into it (see `use-section-slot` § component-slot-prop placement for the canonical filling pattern). Slots take ANY component or Section. There is no type constraint. Slot fills are stored per-template, so the same Card can carry different filled content on different templates. - **5b. The two valid `component:` shapes**, decided by arity: + **5b. The two valid registration shapes:** - | Shape | `component:` value | When | + | Shape | Call | When | |---|---|---| - | **Lazy (default)** | arity-0 function returning a `Promise` (dynamic `import()`) | Every BYOC registration unless tiny+hot. | - | **Eager** | function with ≥1 parameter (ordinary React component) | Tiny components on hot paths. | + | **Lazy (default)** | `registerLazyComponent(config, () => import("./Foo").then((m) => m.Foo))` | Every BYOC registration unless tiny+hot. | + | **Eager** | `registerComponent({ …, component: Foo })`, `Foo` a function with ≥1 parameter (an ordinary React component) | Tiny components on hot paths. | + + ```ts + registerLazyComponent(config, () => import("./components/Hero").then((m) => m.Hero)) // ✅ LAZY + registerComponent({ ...config, component: Hero }) // ✅ EAGER — function Hero(props) {…} + registerComponent({ ...config, component: (props) => createElement(Hero, props) }) // ✅ EAGER — wrap arity-0 + ``` + + **`component: () => import("./Foo")` runs, but does not type-check.** In 2.0.1 `registerComponent` still detects a parameter-less function as a lazy thunk at runtime, but `component` is typed `(props?) => ReactNode`, and a `Promise` of a module is not a `ReactNode` (`TS2322` under `@types/react` 19). Use `registerLazyComponent` so `tsc` and the build pass. + + **Arity-0 trap.** A parameter-less function that does NOT return a Promise (e.g. `function Header() {…}`) is treated as a lazy loader, called outside render, and throws React #321 "Invalid hook call". Rule: **lazy goes through `registerLazyComponent`; an eager component takes a `props` parameter.** + + **5c. Design tab (`styles`) and design props.** `styles` is an **object of style groups**, not an array. Each key is one group in the Design panel. The `default` group's classes arrive as `className`, every other group as a prop named after its key, and the component must apply them or the author's edits never reach the DOM. `styleSections` takes `"block"`, `"text"`, `"all"`, `"none"`, or a list of sections (`"size"`, `"spacing"`, `"typography"`, `"background"`, `"border"`, …): ```ts - component: () => import("./components/Hero") // ✅ LAZY - component: Hero // ✅ EAGER — function Hero(props) {…} - component: (props) => createElement(Hero, props) // ✅ EAGER — wrap arity-0 + registerComponent({ + type: "acme-box", + component: Box, + wrap: false, + props: { + paddingBlock: { // design prop: key is a camelCase CSS property + type: "choice", + design: true, + options: [ + { label: "None", value: "0" }, + { label: "Compact", value: "var(--spacing-8)" }, + { label: "Roomy", value: "var(--spacing-16)" }, + ], + defaultValue: "var(--spacing-8)", + }, + children: { type: "slot" }, + }, + styles: { + default: { displayName: "Box", styleSections: ["background", "border"] }, // → className + }, + }); + ``` + + `design: true` (only on `choice` and `string`) makes a prop a per-breakpoint CSS value edited in the Settings tab. The component receives a **generated class name** under that prop, not the CSS value, and applies it (`className={paddingBlock}`). Never name a design prop or a style group after another prop. For when to use design props versus a plain `choice` menu, see [`design-component-library`](design-component-library.md) § Design props. + +7. **Confirm Studio palette shows the new tile.** **Restart the dev server first** if you changed an existing registration (see the restart rule below). Open the canvas, switch the palette accordion to **Registered Components**, find `Hero`. **The tile must show the thumbnail image**. A text placeholder means `thumbnailUrl` is missing or the data URI is malformed. A broken-image icon means you used an external URL and the iframe blocked it. The tile preview itself renders from the `defaultValue`s. If that area is blank, a prop is missing a default or the prop type was wrong. + + **Headless check (no Studio UI, dev only).** The project's registered-components list read from Studio's side can stay empty, so it can't tell you what an author will see in the palette. Read the app's own registry instead: open the canvas route (`/canvas`) in a browser, then in the DevTools console run + + ```js + globalThis.__contentstack_studio_registry_v1.componentRegistry + .getComponents({ registry: "user-defined" }) + .map((c) => [c.type, c.displayName, c.sections, Object.keys(c.props)]) ``` - **Arity-0 trap.** A parameter-less function that does NOT return a Promise (e.g. `function Header() {…}`) is treated as a lazy loader, called outside render, and throws React #321 "Invalid hook call". Rule: **return a `Promise` (lazy), OR give it a `props` parameter (eager).** + That is the same object `getComponentRegistryInstance().getComponents()` returns. `getComponentRegistryInstance` is exported by `@contentstack/studio-registry`, not by `@contentstack/studio-react`, and the global slot name is an SDK internal (verified on 2.0.1), so use this for debugging only and never ship code that depends on either. Every `type` you registered must be listed, with the `sections` and props you expect. -7. **Confirm Studio palette shows the new tile.** Open the canvas, switch the palette accordion to **Registered Components**, find `Hero`. **The tile must show the thumbnail image**. A text placeholder means `thumbnailUrl` is missing or the data URI is malformed. A broken-image icon means you used an external URL and the iframe blocked it. The tile preview itself renders from the `defaultValue`s. If that area is blank, a prop is missing a default or the prop type was wrong. + **Restart the dev server after changing a registration.** The registry lives on `globalThis` and skips a `type` it already holds (see [`register-component-pitfalls`](register-component-pitfalls.md) § duplicate `type`). After you edit a registration (props, defaults, `component`, `styles`), hot reload re-runs the module, but the running server keeps the old entry while the browser loads the new one. The result is a hydration mismatch, old markup from the server and new in the browser, with no hint why. Stop and restart `next dev` (or the equivalent) every time. ## Inputs needed from the user @@ -229,7 +288,7 @@ split out so a task loads only what it touches. | Read this | For | |---|---| -| [`register-component-prop-contracts`](register-component-prop-contracts.md) | Array and object binding rules, tolerant image and link signatures, `{{entry.*}}` resolution | +| [`register-component-prop-contracts`](register-component-prop-contracts.md) | Array and object binding rules, the bound-fields-only fetch, image (`.url`) and link signatures, `{{entry.*}}` resolution | | [`register-component-render-contracts`](register-component-render-contracts.md) | Layout-agnostic rule, null-safe rendering | | [`register-component-cslp`](register-component-cslp.md) | `$` CSLP props and the `studioAttributes` contract. **Both or neither.** | | [`register-component-pitfalls`](register-component-pitfalls.md) | What fails silently, and why. **Read before reporting done.** | @@ -238,7 +297,7 @@ split out so a task loads only what it touches. This skill succeeds only when ALL of the following are true. -- [ ] The registration file contains a `registerComponent` call for the supplied `componentType` that did not exist before this skill ran. +- [ ] The registration file contains a `registerLazyComponent` (or `registerComponent`) call for the supplied `componentType` that did not exist before this skill ran. - [ ] **The component spreads `studioAttributes` on its root element** (or the registration sets `wrap`), verified in the DOM by a `data-cslp` on the root. Without it the node has no CSLP tag and Visual Editor cannot select it. - [ ] **Every bindable prop that renders visible content spreads its `$`-twin** on the element rendering it, verified by a `data-cslp` on each of those elements. - [ ] **An empty bound value still renders its element** when its `$`-twin is present (not `null`), so a new entry's field can be clicked, and decorative overlays over images have `pointer-events: none` ([`make-visual-editor-editable`](make-visual-editor-editable.md) § 4–5). @@ -251,10 +310,12 @@ This skill succeeds only when ALL of the following are true. - [ ] The registration file is imported once at app startup, verified by grep for the import in `main.tsx` / `_app.tsx` / `App.tsx`. - [ ] Studio's **Registered Components** palette accordion shows the new tile with the supplied `displayName`. - [ ] The tile preview renders (not blank) because the defaults are populated. -- [ ] `component:` follows one of the two valid shapes: **lazy** (arity-0 function that returns a `Promise` of the component, typically `() => import("./Foo")`) OR **eager** (function with at least one parameter, i.e. an ordinary React component). The default recommendation is lazy. A zero-parameter function that does NOT return a Promise triggers React #321 / "Invalid hook call" at render. +- [ ] The registration follows one of the two valid shapes: **lazy** (`registerLazyComponent(config, () => import("./Foo").then((m) => m.Foo))`, the loader resolving to the component itself) OR **eager** (`registerComponent` with `component:` a function with at least one parameter, i.e. an ordinary React component). The default recommendation is lazy. A zero-parameter function in `component:` that does NOT return a Promise triggers React #321 / "Invalid hook call" at render. +- [ ] Every `object` prop declares `properties` and every `array` prop declares `items`, and the registration file passes `tsc` against the installed SDK. +- [ ] Dev only: the console registry check (step 7) lists every registered `type` on `/canvas`, read after a dev-server restart. - [ ] The component is **layout-agnostic**, `width: 100%` of its container, no hard-coded `max-width` to "protect" against wrong contexts, no styles that rely on a specific ancestor selector (`.fs-grid > .fs-card { ... }`). Layout sizing is delegated to the parent Section, not baked into the component. - [ ] **Call-site literal sweep**. If this component is already used in production code, grep every JSX use of it. Every literal prop set at a call site (`isInteractive={false}`, `variant="compact"`, `columns={3}`, `isLogoBgWhite={false}`) becomes a `defaultValue` on the corresponding registered prop, or a named preset. Skipping this is the single most common cause of "composed page looks 80% right but wrong on hover/motion/spacing." Sample grep: `rg -t ts -o '<ComponentName[^>]*/?>' src | head`. If the component is new (no production call sites yet), acceptance is trivially satisfied, but state that explicitly. -- [ ] **Tolerant image signatures**: every `imageurl` prop's TS type accepts `string | { url?: string } | null | undefined`, and the component internally coerces. See [`register-component-prop-contracts`](register-component-prop-contracts.md) § Tolerant image signatures. This is a first-class contract, not an optional nicety. Studio's picker binds sometimes the object, sometimes `.url`, and either shape must render correctly. +- [ ] **Image props**: every `imageurl` prop is bound to `<file_field>.url`, alt text is its own `string` prop bound to the asset's `description`, and the prop's TS type still accepts `string | { url?: string } | null | undefined` with internal coercion as a safety net. See [`register-component-prop-contracts`](register-component-prop-contracts.md) § Image props. - [ ] **Tolerant link signatures**: every `href`-typed prop binds against a link field's `href` leaf (never the whole link object). Where the design mandates a single link prop, the component coerces `string | {title, href}` inside via `href()` / `label()` helpers. See [`register-component-prop-contracts`](register-component-prop-contracts.md) § Tolerant link signatures. - [ ] **Palette group + thumbnail**: the registration declares `sections: ["<Brand> · Elements | Patterns | Layouts"]` (never `"Template"` or `"Section"` in the name) and a `thumbnailUrl` data URI that visually previews the tier (step 5). diff --git a/skills/studio/register-json-rte.md b/skills/studio/register-json-rte.md index 7e1bea7..f398957 100644 --- a/skills/studio/register-json-rte.md +++ b/skills/studio/register-json-rte.md @@ -13,6 +13,8 @@ Use for **any** rich-text field bound to a component prop, whatever its storage # Register an RTE renderer > **The name says `json-rte` for historical reasons. The API is not JSON-RTE-only.** `registerRTERenderer` is a single call covering **JSON RTE, HTML RTE and Markdown**. If you arrived here with an HTML RTE or a Markdown field, you are in the right place: everything below applies unchanged. +> +> **Limit: an `embeddedEntry` handler returns an HTML string, so an embedded entry is static markup.** The string is parsed and sanitized into the page: no React component, no state, no event handlers (an embedded product loses its add-to-basket button). If an embed must be interactive, render the rich text in a registered component of your own and draw the embeds with the app's React components. See § Interactive embeds. ## Context @@ -24,9 +26,9 @@ Contentstack stores rich text in more than one shape, and Studio renders all of | **HTML RTE** | an HTML string | embedded refs extracted from the markup | | **Markdown** | a Markdown string | converted, then rendered | -The built-in **`RichText`** component is typed `any` and **auto-detects** which of these it received, so a JSON tree, an HTML string, Markdown, or a plain string all render without the author choosing a variant. +The built-in **`rich-text`** component is typed `any` and **auto-detects** which of these it received, so a JSON tree, an HTML string, Markdown, or a plain string all render without the author choosing a variant. -When Studio resolves a component prop bound to a rich-text field, it serializes the value to HTML and passes it to your component. +**What a `json_rte` prop receives: the raw stored value, not HTML.** Studio passes a JSON RTE field as its JSON document (an object with `type` + `children`) and an HTML RTE or Markdown field as its string, unserialized (verified on `@contentstack/studio-registry` 2.0.1, which accepts either shape for `json_rte`). Serializing to HTML and resolving embeds happens at render time, inside the built-in `rich-text` or `useStudioRichText` (step 9). That render step is where `registerRTERenderer` applies. The default serializer already handles paragraphs, headings, lists, links, marks, images, and tables. Use **`registerRTERenderer`** (the unified, canonical API) when any of these apply: @@ -119,7 +121,7 @@ Registration is a side-effect on the SDK singleton. It must happen **once, at ap 7. **Match keys to node `type` exactly.** Keys in `customElementTypes` must equal the `type` string on the JSON RTE node emitted by Contentstack, byte-for-byte. `callout` will not match `"Callout"` or `"custom-callout"`. Use a standard tag name (`h2`, `a`, `ul`) to override the default rendering for that tag. -8. **Declare the prop in your component schema as `json_rte`.** Studio creates a `JsonRTEProp` and binds it to the field like any other prop: your component code stays JSON-RTE-agnostic. +8. **Declare the prop in your component schema as `json_rte`.** Studio creates a `JsonRTEProp` and binds it to the field like any other prop. The component receives the raw value (JSON object or string, see Context) and renders it through step 9. 9. **Render it: check which path the installed SDK supports.** Two options, and the newer one removes most of this work: @@ -128,7 +130,7 @@ Registration is a side-effect on the SDK singleton. It must happen **once, at ap && echo "hook available" || echo "hook ABSENT — use the manual path" ``` - **`hook available` means one call.** `useStudioRichText` runs the same pipeline as the built-in `RichText`: it detects JSON RTE / HTML RTE / Markdown, resolves embedded entries and assets, and sanitizes before rendering. No `dangerouslySetInnerHTML`, no DOMPurify, no format branching. + **`hook available` means one call.** `useStudioRichText` runs the same pipeline as the built-in `rich-text`: it detects JSON RTE / HTML RTE / Markdown, resolves embedded entries and assets, and sanitizes before rendering. No `dangerouslySetInnerHTML`, no DOMPurify, no format branching. ```tsx import { useStudioRichText } from "@contentstack/studio-react"; @@ -154,7 +156,7 @@ Registration is a side-effect on the SDK singleton. It must happen **once, at ap **Sanitize on this path.** RTE content is author-supplied. Rendering it unsanitized is an XSS vector. The hook does this for you. The manual path does not. - On an SDK without the hook, `json_rte` also accepts **only** JSON RTE (an object). HTML RTE and Markdown are strings and cannot bind to it. Bind those to an `any` prop, or use the built-in `RichText`, until the SDK is upgraded. + On an SDK without the hook, `json_rte` also accepts **only** JSON RTE (an object). HTML RTE and Markdown are strings and cannot bind to it. Bind those to an `any` prop, or use the built-in `rich-text`, until the SDK is upgraded. 10. **(Optional) Inspect current config**: three internal readers used by hooks: - `getJSONRTE()` returns `IJsonToHtmlOptions` (without `embeddedEntry`/`embeddedAsset`) @@ -176,7 +178,7 @@ If none of the three is provided, ask the user whether they actually need this s This skill succeeds only when ALL of the following are true: - [ ] `registerRTERenderer({...})` is called once, in the bootstrap module, before any `<StudioComposition />` or `<StudioComponent />` mounts. -- [ ] If embedded entries are in scope: the `embeddedEntry` handler branches on `contentTypeUid` and/or `displayType` and returns an HTML string per case. +- [ ] If embedded entries are in scope: the `embeddedEntry` handler branches on `contentTypeUid` and/or `displayType` and returns an HTML string per case. Any embed that must be interactive is drawn with React by a registered rich-text component (§ Interactive embeds), not by the handler. - [ ] If embedded assets need custom markup: the `embeddedAsset` handler returns an HTML string. Otherwise the default is acceptable. - [ ] Every declared custom element type has a handler returning an HTML string (not React). - [ ] Handler keys match the raw `type` values in the entry JSON exactly. @@ -186,19 +188,19 @@ This skill succeeds only when ALL of the following are true: ## Which component for which field {#rte-field-to-component} -Rich text is never a `Heading`/`Paragraph` job. Map the CT field to the right component, then register what that component needs: +Rich text is never a `header`/`text` job. Map the CT field to the right component, then register what that component needs: | Contentstack field | Component | Prop type | Must register | |---|---|---|---| -| **JSON RTE** | `RichText` | `json_rte` | `registerRTERenderer`: always, if it embeds entries/assets or custom nodes | -| **HTML RTE** (multiline + rich text editor) | `RichText` | `any` | same call: embedded refs are extracted from the markup | -| **Markdown** (multiline + markdown) | `RichText` | `any` | same call | -| Rich text that must **collapse / "read more"** | `CollapsibleText` | same as above | same call: it accepts the full RTE matrix | -| Single-line / multiline **plain** text | built-in `Text` / `Header` | `plaintext` | nothing | +| **JSON RTE** | `rich-text` | `json_rte` | `registerRTERenderer`: always, if it embeds entries/assets or custom nodes | +| **HTML RTE** (multiline + rich text editor) | `rich-text` | `any` | same call: embedded refs are extracted from the markup | +| **Markdown** (multiline + markdown) | `rich-text` | `any` | same call | +| Rich text that must **collapse / "read more"** | `collapsible-text` | same as above | same call: it accepts the full RTE matrix | +| Single-line / multiline **plain** text | built-in `text` / `Header` | `plaintext` | nothing | -**The line between them:** if an author can apply any formatting (bold, a link, a list, an embed) it is rich text and needs `RichText`. Only genuinely unformatted strings belong on `Text`/`Header`, which render `String(text)` and show markup as literal characters. +**The line between them:** if an author can apply any formatting (bold, a link, a list, an embed) it is rich text and needs `rich-text`. Only genuinely unformatted strings belong on `text`/`header`, which render `String(text)` and show markup as literal characters. -`CollapsibleText` is not a different content model: it is `RichText` plus expand/collapse. Choose it whenever the design truncates long copy behind "read more", an accordion, or a "show details" toggle. +`collapsible-text` is not a different content model: it is `rich-text` plus expand/collapse. Choose it whenever the design truncates long copy behind "read more", an accordion, or a "show details" toggle. ## Embedded entries and embedded assets: the reason this API exists @@ -225,6 +227,66 @@ Returning block markup for an `inline` embed breaks the paragraph it sits in, th Both handlers return **HTML strings**, not React: a returned JSX object stringifies to `[object Object]`. +## Interactive embeds: render them with React in a registered component + +`registerRTERenderer` cannot return React, and nothing in `@contentstack/studio-react` 2.0.1 renders an embedded entry as a React component. When an embed must be interactive (add to basket, a carousel, a form), register your own rich-text component and split the work: the SDK renders each ordinary block, and your app's React component renders each **block-level** embedded entry of the types you list. + +```tsx +"use client"; +import { useMemo, type ReactNode } from "react"; +import { useStudioRichText, type Cslptag, type StudioAttributes } from "@contentstack/studio-react"; +import { ProductCard } from "@/components/ProductCard"; // the app's own interactive component + +type RteNode = { type?: string; attrs?: Record<string, any>; children?: RteNode[] }; + +// content type uid → React component for a BLOCK embed of that type +const EMBEDS: Record<string, (p: { entryUid: string }) => ReactNode> = { + product: ({ entryUid }) => <ProductCard uid={entryUid} />, +}; + +// One top-level block through the SDK pipeline (marks, links, lists, inline embeds, sanitizing). +function RteBlock({ node }: { node: RteNode }) { + const doc = useMemo(() => ({ type: "doc", attrs: {}, children: [node] }), [node]); // stable identity + return <>{useStudioRichText(doc)}</>; +} +function RteWhole({ value }: { value: string }) { + return <>{useStudioRichText(value)}</>; +} + +export function RichBody({ body, $body, studioAttributes }: { body?: RteNode | string; $body?: Cslptag } & StudioAttributes) { + return ( + <div className="prose" {...studioAttributes} {...$body}> + {typeof body === "string" ? ( + <RteWhole value={body} /> // HTML RTE / Markdown: HTML path only + ) : ( + body?.children?.map((node, i) => { + const isEntry = node.type === "reference" && node.attrs?.type === "entry"; + const Embed = isEntry ? EMBEDS[node.attrs?.["content-type-uid"]] : undefined; + return Embed + ? <Embed key={i} entryUid={node.attrs?.["entry-uid"]} /> + : <RteBlock key={i} node={node} />; + }) + )} + </div> + ); +} +``` + +```ts +registerLazyComponent( + { type: "acme-rich-body", displayName: "Rich body", wrap: false, + props: { body: { type: "json_rte", displayName: "Body" } } }, + () => import("./components/RichBody").then((m) => m.RichBody), +); +``` + +How it behaves: + +- **Only JSON RTE, only block embeds.** A block embed is a top-level `reference` node, so the component can swap it for React. An inline or link embed sits inside a paragraph and still goes through `embeddedEntry` (HTML). An HTML RTE or Markdown field arrives as a string and has no node tree to split, so all its embeds stay HTML. +- **The embed component loads its own data.** It gets the entry uid (and you have the content type uid) from the node's `attrs`, then fetches the way the app already does for that component. Studio does not fetch the embedded entry for it. +- **Memoize each block** as above. The SDK hook re-runs its embedded-entry fetch whenever the value's identity changes, so a fresh object on every render fetches on every render. +- Keep `registerRTERenderer` for the remaining HTML embeds (inline, link, other types) and assets. + ## Embedded items: let the fetch you already did do the work Embedded **entries** are stored in the RTE as references, so their content must be fetched. Embedded **assets** are not: their metadata travels inline in the markup and never costs a request. @@ -233,7 +295,7 @@ Two ways the entries get resolved, and the cheaper one is easy to miss: | Route | What happens | Cost | |---|---|---| -| Pass the **`entry`** prop to `RichText` | Resolves `entry._embedded_items[<field>]` via the `$text` CSLP tag | **no extra request** | +| Pass the **`entry`** prop to `rich-text` | Resolves `entry._embedded_items[<field>]` via the `$text` CSLP tag | **no extra request** | | Pass only the RTE value | Refs are extracted, grouped by content type, and fetched in parallel | one query per content type | So if the caller already fetched with **`.includeEmbeddedItems()`**, pass `entry` through: the embedded items are in the payload and re-fetching them is pure waste. Reach for the fetching route only when the RTE value arrives without its parent entry. @@ -253,20 +315,21 @@ So if the caller already fetched with **`.includeEmbeddedItems()`**, pass `entry ## Deprecated built-ins: do not reach for these -`Number`, `JSONRTE` and `Section` are deprecated and no longer appear in `getBasicComponents` / `BASIC_COMPONENTS`. For rich text use **`RichText`** (or `CollapsibleText`, which accepts the same RTE matrix). The built-in `Text` and `Header` now type their text prop as `plaintext` rather than `string`. `Text` renders `String(text)` and no longer parses HTML, so markup passed to it shows as literal characters: that is what `RichText` is for. +`Number`, `JSONRTE` and `Section` are deprecated and no longer appear in `getBasicComponents` / `BASIC_COMPONENTS`. For rich text use **`rich-text`** (or `collapsible-text`, which accepts the same RTE matrix). The built-in `text` and `Header` now type their text prop as `plaintext` rather than `string`. `text` renders `String(text)` and no longer parses HTML, so markup passed to it shows as literal characters: that is what `rich-text` is for. ## Common pitfalls | Pitfall | Why it bites | Fix | | --- | --- | --- | | Assuming this is JSON-RTE-only because of the name | `registerRTERenderer` covers JSON RTE, HTML RTE **and** Markdown in one call. Treating HTML RTE as unsupported leads to hand-rolled parsing | One registration serves every rich-text field, whatever its storage format | -| Putting rich text through the built-in `Text` | `Text` renders `String(text)` and no longer parses HTML, so tags appear as literal `<p>` characters | Use the `RichText` component (typed `any`, auto-detects the payload) | +| Putting rich text through the built-in `text` | `text` renders `String(text)` and no longer parses HTML, so tags appear as literal `<p>` characters | Use the `rich-text` component (typed `any`, auto-detects the payload) | | Re-fetching embedded entries the caller already loaded | Without the `entry` prop the refs are fetched again, one query per content type, on every render path | Pass `entry` so `_embedded_items` is used. Pair with `.includeEmbeddedItems()` at fetch time | | Custom `embeddedAsset` handler that interpolates the URL directly | Drops the default's scheme guard: `javascript:` / `vbscript:` / non-image `data:` URLs become an XSS vector | Keep the scheme check, or extend `defaultAssetRender` rather than replacing it | -| Using the deprecated `Number` / `JSONRTE` / `Section` basics | Removed from `BASIC_COMPONENTS`. The `@deprecated` marker sits on the declarations so IDEs warn | Use `RichText` for rich text. `Number`/`Section` have no replacement in the basics palette | +| Using the deprecated `Number` / `JSONRTE` / `Section` basics | Removed from `BASIC_COMPONENTS`. The `@deprecated` marker sits on the declarations so IDEs warn | Use `rich-text` for rich text. `Number`/`Section` have no replacement in the basics palette | | Using `registerJSONRTE` for new code | Deprecated. `registerRTERenderer` is the unified canonical API | Use `registerRTERenderer`. Existing `registerJSONRTE` calls still work, migrate when you next touch the file. | | Embedded entry renders blank or as a placeholder | No `embeddedEntry` handler is registered (default renders nothing for entry embeds) | Register `embeddedEntry: ({ entry, contentTypeUid, displayType }) => html` | -| `embeddedEntry` returns a React element / JSX | Serializer is HTML-string based. React objects stringify to `[object Object]` | Return a plain HTML string from the handler | +| `embeddedEntry` returns a React element / JSX | Serializer is HTML-string based. React objects stringify to `[object Object]` | Return a plain HTML string from the handler. For React (state, click handlers), use § Interactive embeds | +| Expecting buttons or handlers inside an embedded entry to work | The handler's HTML is parsed and sanitized: no React, no event handlers, no state | Render that content type's embeds with a React component in a registered rich-text component (§ Interactive embeds) | | Calling `registerRTERenderer` more than once | Each call REPLACES the full config. Later calls drop earlier `customElementTypes` / `embeddedEntry` | Build a single merged config and call once at bootstrap. Or use `registerJSONRTE` for serializer-only updates (it preserves existing `embeddedEntry`/`embeddedAsset`), but the right answer is "register everything in one call." | | Registering too late | After props resolve, re-registering does not retroactively re-render | Always register at bootstrap, before any composition mount | | Wrong key name in `customElementTypes` | Keys must match the raw `type` in entry JSON, not the editor's display label | Verify against the actual entry JSON | diff --git a/skills/studio/resolve-reference-depth.md b/skills/studio/resolve-reference-depth.md index 27e9506..b3f4724 100644 --- a/skills/studio/resolve-reference-depth.md +++ b/skills/studio/resolve-reference-depth.md @@ -71,6 +71,8 @@ Never open a checkpoint for any row but the first. Offering to dig deeper for ab Walk the content type. Each segment whose field is `data_type: "reference"` is a hop. Group, blocks and block-uid segments are free. Declare **every** derivable hop in one request rather than one per pass, each costs a path, not a round trip. +**Only paths a binding actually reads.** "Every derivable hop" means every hop on the way to a field some binding in the composition reads, not every reference in the schema and not the app's own `include[]` list. Measured: copying a production app's include list into `resolvedReferences` pulled in a third-level reference no binding used, and the SDK reported `REFERENCES_UNRESOLVED` ("references came back as pointers rather than entries") for it. When you see that warning, first check whether the stubbed path is one any binding reads. If none does, delete the path rather than digging deeper. + Declare the same paths in the composition's `resolvedReferences`. A fetch that includes a path the composition never declared still renders blank, and the two failures are indistinguishable from outside. **Use schema paths, not payload paths.** The payload carries array indices (`sections.1.resources.cards.0.reference_card.card_category.0.author.0`) and `include[]` takes the schema shape with the indices dropped: `sections.resources.cards.reference_card.card_category.author`. Copying the path out of the response is the most common way this silently declares nothing. @@ -166,6 +168,7 @@ A caller may configure more. What automation must never do is treat "no human pr - [ ] `include[]` used **schema** paths with array indices stripped, not paths copied out of the payload. - [ ] `_metadata` objects were excluded before any stub was counted or reported. - [ ] Every derivable hop was declared without prompting. The first prompt came only at a path that could not be derived. +- [ ] Every declared path leads to a field a binding reads. No path was copied from the app's own include list, and a `REFERENCES_UNRESOLVED` warning on an unread path was fixed by deleting the path. - [ ] Each stub's own path was declared before any hop beyond it. - [ ] An empty `entries` array was classified as "no entry in this environment", never counted as zero stubs. - [ ] Every candidate CT of a multi-target reference was expanded, not only the one this entry held. diff --git a/skills/studio/setup-section-preview.md b/skills/studio/setup-section-preview.md index 5e46e6a..891edc1 100644 --- a/skills/studio/setup-section-preview.md +++ b/skills/studio/setup-section-preview.md @@ -51,49 +51,87 @@ Only once 1 to 3 hold, continue. 2. **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 that `throw`s on failure surfaces as a runtime overlay INSIDE Studio's canvas (looks like Studio is broken, isn't). + **The canvas route MUST be isolated from your app's global chrome, but not from its styles.** The route that mounts `<StudioCanvas />` should render 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 that `throw`s on failure surfaces as a runtime overlay INSIDE Studio's canvas (looks like Studio is broken, isn't). + + **What the canvas still needs:** the site's global CSS, fonts, and the React providers your registered components read (theme, i18n, cart, query client). Sections render the app's own components on the canvas; strip those and every component renders unstyled or throws on a missing context. **Framework-specific isolation patterns:** - - **Next.js App Router:** put the canvas at a route group that has its own minimal `layout.tsx` returning `{children}` only, e.g. `app/(studio)/canvas/page.tsx` with `app/(studio)/layout.tsx` doing `export 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 `RootLayout` with a pathname check that skips analytics / third-party scripts when the pathname matches `*/canvas`. + - **Next.js App Router: gate the chrome in the root layout.** A route group can't opt out of the **root** `app/layout.tsx`: every route renders inside it, and that's where most apps put the header, footer, CSS imports and providers. A `(studio)` group with its own `layout.tsx` adds a layer inside the root one; it removes nothing. Instead, keep one root layout and let a small client component drop the chrome on the canvas path: + + ```tsx + // app/site-chrome.tsx + "use client"; + import type { ReactNode } from "react"; + import { usePathname } from "next/navigation"; + + const CANVAS_PATH = "/canvas"; // your <canvasPath> + + export function SiteChrome({ header, footer, children }: { + header: ReactNode; footer: ReactNode; children: ReactNode; + }) { + const pathname = usePathname(); + // endsWith covers a locale-prefixed canvas (/en/canvas). + const onCanvas = pathname === CANVAS_PATH || pathname.endsWith(CANVAS_PATH); + return ( + <> + {onCanvas ? null : header} + {children} + {onCanvas ? null : footer} + </> + ); + } + ``` + + ```tsx + // app/layout.tsx — Server Component; stays the one root layout + import "./globals.css"; // CSS and fonts: kept on the canvas + import { SiteChrome } from "./site-chrome"; + import { StudioInit } from "./studio-init"; // from install-studio step 8 + // Providers, Header, Footer: the app's own, imported as they are today + + export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + <html lang="en"> + <body> + <Providers> {/* kept on the canvas */} + <StudioInit /> + <SiteChrome header={<Header />} footer={<Footer />}>{children}</SiteChrome> + </Providers> + </body> + </html> + ); + } + ``` + + `Header` and `Footer` are passed as props, so they stay Server Components. Gate analytics and third-party `<Script>`s the same way (render them through `SiteChrome`, or check the pathname in their own client wrapper). + - **Heavier alternative: multiple root layouts.** Remove `app/layout.tsx` and give each top-level route group its own root layout with `<html>` and `<body>`, e.g. `app/(site)/layout.tsx` with chrome and `app/(studio)/layout.tsx` without. The studio one must import the same global CSS, fonts and providers itself. Every page moves under a group, and navigating between groups is a full page load. Use it only when the chrome gate can't work. - **Next.js Pages Router:** use `_app.tsx` with 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 to `kit.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: + **Next.js App Router** (`app/<canvasPath>/page.tsx`): ```tsx + // app/canvas/page.tsx "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 }, - ); + import { StudioCanvas } from "@contentstack/studio-react"; export default function CanvasRoute() { - return (<><style>{getSSRStyles(null)}</style><StudioCanvas /></>); + return <StudioCanvas />; } ``` - **Next.js Pages Router** (`pages/<canvasPath>.tsx`), same SSR-mismatch reason, wrap in `dynamic({ ssr: false })`: + **Next.js Pages Router** (`pages/<canvasPath>.tsx`), same body without the directive: ```tsx - import dynamic from "next/dynamic"; - const StudioCanvas = dynamic( - () => import("@contentstack/studio-react").then((m) => m.StudioCanvas), - { ssr: false }, - ); + import { StudioCanvas } from "@contentstack/studio-react"; export default function CanvasRoute() { return <StudioCanvas />; } ``` + No `dynamic({ ssr: false })` needed on studio-react 2.x: `StudioCanvas` is a client component that renders `null` on the server and on the first client render, and builds the canvas only after mount, so there is nothing to mismatch. Older snippets that wrap it in `dynamic()` still work; they're just unnecessary. + **Vite + React Router** (`src/routes/CanvasRoute.tsx` + router config update), Vite is SPA-only, so a plain import is safe: ```tsx import { StudioCanvas } from "@contentstack/studio-react"; @@ -112,7 +150,7 @@ Only once 1 to 3 hold, continue. } ``` - **Every framework: render `<style>{getSSRStyles(null)}</style>` beside `<StudioCanvas />`.** In studio-react 1.9.0 `StudioCanvas` writes no design-token `:root` variables, so token styles (padding, max-width, colors) resolve to nothing on the canvas. + **No token `<style>` on studio-react 2.x.** `StudioCanvas` writes the design-token `:root` variables itself once it mounts. Don't add `<style>{getSSRStyles(null)}</style>` to a client canvas page: the server and the browser hold different token registries, so the string differs between them and React reports a hydration error on every load. Only on studio-react 1.9.x, where `StudioCanvas` wrote no token variables, add them **after mount**: `const [css, setCss] = useState(""); useEffect(() => setCss(getSSRStyles(null)), []);`, then render `{css ? <style>{css}</style> : null}`. Convert `<canvasPath>` to the framework's expected filename (e.g. `/canvas` becomes `canvas/page.tsx` for App Router, or `canvas.tsx` for Pages Router). @@ -158,7 +196,6 @@ Only once 1 to 3 hold, continue. const canvasHeaders = [ // Studio runs on *.contentstack.com (all regions). Add your own origin if you embed the canvas elsewhere. { key: 'Content-Security-Policy', value: "frame-ancestors 'self' https://*.contentstack.com;" }, - { key: 'Access-Control-Allow-Origin', value: '*' }, ]; return [ { source: '/canvas', headers: canvasHeaders }, @@ -168,7 +205,7 @@ Only once 1 to 3 hold, continue. }; ``` - The Vite blocks above are for local use only and allow framing by Studio's origin, nothing wider. Don't add `frame-ancestors *` or `Access-Control-Allow-Origin: *`, and don't ship a global framing policy to production: scope it to the canvas route. + The Vite blocks above are for local use only and allow framing by Studio's origin, nothing wider. Don't add `frame-ancestors *` or `Access-Control-Allow-Origin: *`: framing is governed by `frame-ancestors` alone, and CORS headers on the page play no part in it. Don't ship a global framing policy to production either: scope it to the canvas route. **Why it matters:** the `frame-ancestors` CSP header tells the browser Studio's origin may embed the page; without it, Studio's iframe stays blank with no console error, a particularly hard failure to diagnose. If you reach the server through a non-localhost hostname (e.g. a tunnel) and Vite returns "host not allowed" (403), list that hostname in `allowedHosts`; never set it to `true`. @@ -187,10 +224,11 @@ Only once 1 to 3 hold, continue. - [ ] 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) +- [ ] The file mounts `<StudioCanvas />` and nothing else (no required props, no token `<style>` on studio-react 2.x) +- [ ] Next.js App Router: the canvas path renders without the site header/footer but with the site's CSS, fonts and providers (chrome gate in step 2) - [ ] 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`), AND the framework config sends `frame-ancestors 'self' https://*.contentstack.com` (no wildcard ACAO; `allowedHosts` only lists explicit non-localhost hostnames, never `true`) +- [ ] The canvas route sends `Content-Security-Policy: frame-ancestors 'self' https://*.contentstack.com`, scoped to the canvas route, with no `Access-Control-Allow-Origin` header added for it. `allowedHosts` (Vite) only lists explicit non-localhost hostnames, never `true`. Plain `http://localhost` is fine; HTTPS is needed only when the app itself requires it (`setup-local-https-canvas`) - [ ] 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. diff --git a/skills/studio/setup-template-preview-routes.md b/skills/studio/setup-template-preview-routes.md index 6402f4f..fea41f5 100644 --- a/skills/studio/setup-template-preview-routes.md +++ b/skills/studio/setup-template-preview-routes.md @@ -81,7 +81,7 @@ Fix, in order: (1) a route in the app matches the template's URL, (2) that route | Route found, Canvas URL not set in Studio | Run [`setup-section-preview`](setup-section-preview.md) step 4. The file alone is half the wiring. | | **Nothing found** | **Run [`setup-section-preview`](setup-section-preview.md) now, in full, then come back.** Do not emit any template route first. | - `setup-section-preview` owns this route and everything around it: framework-correct file path, the client-only `dynamic({ ssr: false })` wrapper, chrome isolation, iframe-allowed headers, and the Canvas URL setting in Studio. Don't hand-roll a `/canvas` file here. Delegate. + `setup-section-preview` owns this route and everything around it: framework-correct file path, the client component, chrome isolation (without losing the site's CSS and providers), iframe-allowed headers, and the Canvas URL setting in Studio. Don't hand-roll a `/canvas` file here. Delegate. **Why this is compulsory in wildcard mode specifically.** A catch-all matches `/canvas` like any other URL. With no canvas route registered, `<Route path="*">` / `app/[[...slug]]/page.tsx` claims it, Studio's section iframe loads the catch-all, `fetchCompositionData({ url: "/canvas" })` finds no composition, and the author gets a blank frame, or (once step 5a's 404 handling lands) **your 404 page rendered inside Studio's section-authoring canvas**. Every Section in the project looks broken, and no error message points at routing. In dedicated mode nothing claims `/canvas`, so the shadowing failure doesn't occur. The route is still mandatory, just for the ordinary reason that Section authoring needs it. @@ -146,13 +146,19 @@ Fix, in order: (1) a route in the app matches the template's URL, (2) that route // app/[[...slug]]/page.tsx — Server Component import { sdk } from "@/lib/contentstack"; import { StudioRender } from "./StudioRender"; - export default async function Page({ params, searchParams }) { + + type Props = { + params: Promise<{ slug?: string[] }>; + searchParams: Promise<Record<string, string>>; + }; + + export default async function Page({ params, searchParams }: Props) { // Same fetch for visitor + in-Studio iframe — StudioComponent picks the // right render path internally. Forward the full request searchQuery so // Studio's iframe overrides (locale / variant / preview entry) reach the SDK. const { slug } = await params; // a Promise on Next 15+ const url = "/" + (slug ?? []).join("/"); - const searchQuery = new URLSearchParams((await searchParams) as Record<string, string>).toString(); + const searchQuery = new URLSearchParams(await searchParams).toString(); // Unwrapped for clarity — this call THROWS when no composition matches, so // shipping it as-is returns 500 on every unclaimed URL. Step 5a (iii) has // the production form using resolveComposition. @@ -164,56 +170,31 @@ Fix, in order: (1) a route in the app matches the template's URL, (2) that route ```tsx // app/[[...slug]]/StudioRender.tsx — Client Component "use client"; - import { StudioComponent } from "@contentstack/studio-react"; - - export function StudioRender({ specOptions, editorMode = false }: { specOptions?: any; editorMode?: boolean }) { - // Missing options on a VISITOR render is a real bug — fail loudly rather than - // serving an empty 200. Inside Studio's iframe the same state is legitimate: - // an unsaved template has no published spec yet, so the editor path passes - // `editorMode` and the guard stands down. Throwing there would turn the - // editor exemption below (§ 5a) into a render-time error on every unsaved - // template — the opposite of what that path exists to do. - if (!editorMode && !specOptions?.fetchOptions) throw new Error("Studio render options are missing; verify editor bootstrap for the installed SDK."); - // StudioComponent handles iframe context internally — same component on - // visitor routes and inside Studio's preview iframe. Mount it even with no - // spec: a bare `<div />` returns 200 without starting the editor bridge. - // Confirm the no-spec form against the installed SDK (§ Editor-mode - // detection) instead of fabricating options. + import { StudioComponent, type StudioComponentSpecOptions } from "@contentstack/studio-react"; + + export function StudioRender({ specOptions }: { specOptions: StudioComponentSpecOptions }) { + // Same component on visitor routes and inside Studio's preview iframe: it + // picks the render path itself. `spec: null` (the editor branch in step 5a) + // means "ask Studio for the draft". Mount it even then: a bare `<div />` + // returns 200 without starting the editor bridge. return <StudioComponent specOptions={specOptions} />; } ``` + `specOptions` is required and typed: a visitor render always has the object `resolveComposition` returned, and the editor branch builds a complete one with `spec: null`. Don't loosen it to `any` to make the editor branch compile. + **App Router CSR variant (SPAs only (no SEO requirement)**) emit at `app/_components/LinkedRoute.tsx`, marked `"use client"`. Skip this for any visitor-facing site. The SSR pattern above is the default: ```tsx "use client"; - import { usePathname, useSearchParams } from "next/navigation"; + import { usePathname } from "next/navigation"; import { StudioComponent, useCompositionData } from "@contentstack/studio-react"; export function LinkedRoute() { const pathname = usePathname(); - const searchParams = useSearchParams(); - return <VisitorComposition url={pathname} searchParams={searchParams} />; - } - - function VisitorComposition({ url, searchParams }: { - url: string; - searchParams: URLSearchParams; - }) { - // Pass `searchQuery` as the raw URLSearchParams OBJECT — NOT a stringified - // form and NOT omitted. Studio's editor-mode detection reads editor params - // (see § Editor-mode detection below) directly off this object. The 1.5.x - // .d.ts type says `URLSearchParamsString` but the object is what actually - // works; passing the string form or omitting the arg breaks editing of - // unsaved compositions. - // - // `extendQuery` merges into the CDA fetch. Keeping locale-only avoids - // hydration mismatches from server-included fields. - const { specOptions, isLoading, error } = useCompositionData({ - url, - searchQuery: searchParams, - extendQuery: { blog_post: { only: ["locale"] } }, // one entry per connected CT - }); + // No searchQuery argument: the hook reads Studio's iframe params (editor + // mode, preview hash, locale, variant) off window.location itself. + const { specOptions, isLoading, error } = useCompositionData({ url: pathname }); if (isLoading) return <div>Loading…</div>; if (error) return <div />; // empty div, NOT null if (!specOptions?.spec) return <div />; // canvas host needs a mount target @@ -221,11 +202,11 @@ Fix, in order: (1) a route in the app matches the template's URL, (2) that route } ``` - ### Why the `searchQuery` object + empty `<div>` matter + ### Why the query string and the empty `<div>` matter - - **`searchQuery` = raw URLSearchParams object.** When Studio's iframe loads the template URL, it appends editor-mode params. The SDK reads them off `searchQuery` to switch from "render deployed composition" to "render unsaved working spec." Omit `searchQuery` and the SDK falls through to the deployed path, which returns `null` on a not-yet-saved composition, so you get **"Template Did Not Load."** - - **Return an empty `<div />` (not `null`) when there's no spec.** Studio's canvas host queries the mount target after route render. If there's nothing in the DOM it can't drive it, and Studio shows "SDK Not Initialized" or a blank error. An empty div gives it something to attach to. **In wildcard mode this applies to the editor iframe only**, for real visitors the no-spec branch must render the 404 page instead, or the site never 404s. Step 4a (iii) has the gated version. Use that one. - - **`extendQuery` per connected CT.** Recommended; older docs snippets omit it. Restrict to `only: ["locale"]` to avoid over-fetching. + - **The query string carries editor mode.** When Studio's iframe loads the template URL, it appends its params (`cs-composable-studio`, `hash`, locale, variant). On the server, forward the raw request query string as `searchQuery` (a string or `URLSearchParams`; both type-check), never a subset. In the browser `useCompositionData` takes no `searchQuery`: its first argument accepts only `url`, `composableUid` or `templateContentTypeUid`, and it reads `window.location` itself. + - **Return an empty `<div />` (not `null`) when there's no spec.** Studio's canvas host queries the mount target after route render. If there's nothing in the DOM it can't drive it, and Studio shows "SDK Not Initialized" or a blank error. An empty div gives it something to attach to. **In wildcard mode this applies to the editor iframe only**, for real visitors the no-spec branch must render the 404 page instead, or the site never 404s. Step 5a (iii) has the gated version. Use that one. + - **No `extendQuery`.** It is deprecated in studio-react 2.x: still honoured, but it logs `[studio] extendQuery is deprecated … Tick the references in Studio instead` in development and goes away in the next major. References now travel with the composition, see § Fetching referenced fields below. 5. **Register the catch-all route: pick `[...slug]` vs `[[...slug]]` based on existing app pages.** @@ -237,7 +218,10 @@ Fix, in order: (1) a route in the app matches the template's URL, (2) that route # Does a ROOT page already exist? Any extension, app/ or src/app/, and note that # a locale-prefixed app's root lives at app/[locale]/page.* — which still owns "/". find app src/app -maxdepth 2 -name 'page.*' 2>/dev/null - ls pages/index.* 2>/dev/null + find pages src/pages -maxdepth 1 -name 'index.*' 2>/dev/null # not `ls pages/index.*`: zsh aborts on a glob with no match + + # Does a catch-all ALREADY exist (CMS pages, a legacy router)? Then see § Existing catch-all below. + find app src/app pages src/pages -name '\[*\.\.\.*\]*' 2>/dev/null # What other specific routes exist? (informational — they win by Next precedence) find app src/app pages -name 'page.*' 2>/dev/null | head -20 @@ -251,7 +235,8 @@ Fix, in order: (1) a route in the app matches the template's URL, (2) that route |---|---| | `app/page.tsx` (or `pages/index.tsx`) **exists** | `app/[...slug]/page.tsx`: **non-optional** catch-all. Does NOT match `/`. The existing root page keeps owning `/`. Coexists cleanly. | | No root page found, so greenfield-style | `app/[[...slug]]/page.tsx`: **optional** catch-all. Matches `/` AND all nested URLs. | - | **Locale-prefixed app** (`app/[locale]/page.*` owns `/en`, `/fr`) | `app/[locale]/[...slug]/page.tsx`, **inside** the segment. Strip the prefix before building `url`, pass the mapped stack locale as `{ locale }`, forward `searchQuery` untouched. See [`convert-project-to-studio`](convert-project-to-studio.md) § Step 3 for the map and the Base URL rule. | + | **A catch-all already exists** (`app/[...slug]/page.*`, `app/[[...slug]]/page.*`, `pages/[...slug].*`) | **Don't add a second one.** Chain Studio into the existing file: § Existing catch-all | +| **Locale-prefixed app** (`app/[locale]/page.*` owns `/en`, `/fr`) | `app/[locale]/[...slug]/page.tsx`, **inside** the segment. Strip the prefix before building `url`, pass the mapped stack locale as `{ locale }`, forward `searchQuery` untouched. See [`convert-project-to-studio`](convert-project-to-studio.md) § Step 3 for the map and the Base URL rule. | Tell the user verbatim what you found: @@ -285,6 +270,8 @@ Fix, in order: (1) a route in the app matches the template's URL, (2) that route Do NOT create `app/blog/[slug]/page.tsx`, `app/products/[sku]/page.tsx`, etc. The catch-all handles all of them. Studio's URL-pattern matching runs inside `sdk.fetchCompositionData({ url, searchQuery })`. The router doesn't need to know about each template. + **Caching changes with the route.** The catch-all reads the request query, so it renders dynamically; Studio's reads go through the Delivery SDK (axios), so `fetch`-level revalidation and cache tags don't reach them; and the app's publish webhook doesn't fire for compositions. Tell the user, and keep their caching strategy with [`configure-csr-vs-ssr` § Static generation and caching](configure-csr-vs-ssr.md#static-generation-and-caching). + 5a. **Wildcard mode ONLY: preserve site chrome and 404. Mandatory. Do not skip.** A catch-all takes over every URL that isn't explicitly claimed. Two things silently disappear when it lands in an existing app: **the header/footer that used to be rendered by the pages it replaced**, and **the app's own 404 page** (the catch-all now matches the URLs that used to fall through to it). Neither throws an error. The site just renders bare compositions on every route and never 404s. @@ -313,7 +300,8 @@ Fix, in order: (1) a route in the app matches the template's URL, (2) that route ```bash # Next.js — is chrome in the root layout, or in individual pages? grep -n "Header\|Footer\|Nav" app/layout.tsx 2>/dev/null - grep -rln "Header\|Footer" app/**/page.tsx src/pages/ 2>/dev/null | head + grep -rln --include='page.*' "Header\|Footer" app src/app 2>/dev/null | head # not app/**/page.tsx: zsh aborts when the glob matches nothing + grep -rln "Header\|Footer" src/pages 2>/dev/null | head # React Router — is chrome around <Routes>, or inside each page component? grep -n "Header\|Footer\|Outlet" src/App.tsx src/main.tsx 2>/dev/null ``` @@ -349,31 +337,36 @@ Fix, in order: (1) a route in the app matches the template's URL, (2) that route </Routes> ``` - **`/canvas` outside the chrome is not cosmetic.** `<StudioCanvas />` renders one Section at a time inside Studio's authoring iframe. Wrapped in site chrome, every section is authored inside a full page header and footer. The section's own spacing, sticky behavior, and viewport measurements all read wrong. Same rule for Next.js: if `/canvas` sits under a layout that renders chrome, move it into a route group with a bare layout. + **`/canvas` outside the chrome is not cosmetic.** `<StudioCanvas />` renders one Section at a time inside Studio's authoring iframe. Wrapped in site chrome, every section is authored inside a full page header and footer. The section's own spacing, sticky behavior, and viewport measurements all read wrong. Next.js is different: a route group does not escape the **root** layout, which is where most apps render the header and footer. Use the chrome gate in [`setup-section-preview`](setup-section-preview.md) step 2: a small client component in the root layout hides the chrome on the canvas path and keeps the site's CSS and providers. #### (iii) Wire the 404, and exempt Studio's editor iframe - The catch-all now owns unmatched URLs, so "no composition for this URL" is the site's 404 path. **Reuse the project's existing not-found page** (`app/not-found.tsx`, `pages/404.tsx`, the app's `NotFound` component, or the per-locale one under `app/[locale]/`). Create a new file only when none exists. **But the same no-spec condition happens legitimately inside Studio's iframe** for a template that hasn't been saved yet. 404 unconditionally and every unsaved template shows your 404 page in the canvas ("Template Did Not Load"). Gate on editor mode: reuse `isStudioEditorMode` from § Editor-mode detection below. + The catch-all now owns unmatched URLs, so "no composition for this URL" is the site's 404 path. **Reuse the project's existing not-found page** (`app/not-found.tsx`, `pages/404.tsx`, the app's `NotFound` component, or the per-locale one under `app/[locale]/`). Create a new file only when none exists. **But the same no-spec condition happens legitimately inside Studio's iframe** for a template that hasn't been saved yet. 404 unconditionally and every unsaved template shows your 404 page in the canvas ("Template Did Not Load"). Gate on editor mode with `isStudioEditorMode`, imported from `@contentstack/studio-react` (§ Editor-mode detection below). - **`fetchCompositionData` throws on a miss. It does not resolve with an empty spec.** A guard like `if (!specOptions?.spec) notFound()` never runs on the path it was written for. The `await` rejects first and the visitor gets a **500 instead of your 404 page**. Use the canonical [`resolveComposition`](configure-csr-vs-ssr.md#resolve-composition-helper) wrapper (it classifies known not-found ids as 404 and rethrows real failures), don't hand-roll a second one, and never blanket-catch. + **`fetchCompositionData` throws on a miss. It does not resolve with an empty spec.** A guard like `if (!specOptions?.spec) notFound()` never runs on the path it was written for. The `await` rejects first and the visitor gets a **500 instead of your 404 page**. Use the canonical [`resolveComposition`](configure-csr-vs-ssr.md#resolve-composition-helper) wrapper (it clears a stale preview hash, classifies a miss as 404 and rethrows real failures), don't hand-roll a second one, and never blanket-catch. **Next.js App Router**: 404 for visitors, empty mount target for the editor: ```tsx // app/[...slug]/page.tsx (or [[...slug]]) import { notFound } from "next/navigation"; + import { isStudioEditorMode } from "@contentstack/studio-react"; import { sdk } from "@/lib/contentstack"; import { resolveComposition } from "@/lib/resolve-composition"; import { StudioRender } from "./StudioRender"; - import { isStudioEditorMode } from "@/lib/studio-editor-mode"; // § Editor-mode detection below - export default async function Page({ params, searchParams }) { + type Props = { + params: Promise<{ slug?: string[] }>; + searchParams: Promise<Record<string, string>>; + }; + + export default async function Page({ params, searchParams }: Props) { const { slug } = await params; const url = "/" + (slug ?? []).join("/"); - const query = new URLSearchParams((await searchParams) as Record<string, string>); + const query = new URLSearchParams(await searchParams); // Throws on CDA/token/network failure (correctly → 500); returns notFound - // only for the known "no composition here" error ids. + // only when the SDK established there is no composition here. const { specOptions, notFound: miss } = await resolveComposition(sdk, { url, searchQuery: query.toString(), @@ -383,7 +376,7 @@ Fix, in order: (1) a route in the app matches the template's URL, (2) that route // In Studio's iframe an unsaved template legitimately has no published spec — // 404ing here is what produces "Template Did Not Load" in the canvas. // A null spec (not undefined: that crashes) asks Studio for the draft. - if (isStudioEditorMode(query)) return <StudioRender editorMode specOptions={{ + if (isStudioEditorMode(query)) return <StudioRender specOptions={{ spec: null, hasSpec: false, hasTemplate: false, seo: null, fetchOptions: { config: sdk.getConfig(), compositionQuery: { url }, searchQuery: query.toString(), options: {}, mode: "edit" } }} />; @@ -393,6 +386,8 @@ Fix, in order: (1) a route in the app matches the template's URL, (2) that route } ``` + On the server `isStudioEditorMode` must be handed the request's params, as here. Called with no argument it reads `window.location`, and on the server it always answers `false`. + **Test the 404 with two URLs, not one.** `/definitely-not-a-page` fails at pattern match (`NO_COMPOSITION_FOR_URL`). A bad slug under a connected template (`/blog/zzz-not-an-entry`) matches the pattern and fails later at entry lookup (`URL_MATCHED_NO_ENTRY`). `isCompositionNotFound` covers both; a hand-rolled list that knows only the first still 500s on every mistyped slug. Full taxonomy: [`troubleshoot-composition-resolution` § Runtime error taxonomy](troubleshoot-composition-resolution.md). If `app/not-found.tsx` doesn't exist, create one: `notFound()` falls back to Next's unstyled default otherwise, which won't have the site chrome: @@ -414,30 +409,30 @@ Fix, in order: (1) a route in the app matches the template's URL, (2) that route ```tsx import { useLocation } from "react-router-dom"; - import { StudioComponent, useCompositionData } from "@contentstack/studio-react"; - import { isCompositionNotFound } from "@contentstack/studio-client"; + import { + StudioComponent, useCompositionData, isCompositionNotFound, isStudioEditorMode, + } from "@contentstack/studio-react"; + import { sdk } from "./lib/contentstack"; import NotFound from "./pages/NotFound"; // the app's EXISTING 404 page — reuse it - import { isStudioEditorMode } from "./lib/studio-editor-mode"; // § Editor-mode detection below export function LinkedRoute() { const { pathname, search } = useLocation(); const searchParams = new URLSearchParams(search); - const { specOptions, isLoading, error } = useCompositionData({ - url: pathname, - searchQuery: searchParams, - }); + // The hook reads Studio's iframe params off window.location itself. + const { specOptions, isLoading, error } = useCompositionData({ url: pathname }); if (isLoading) return <div>Loading…</div>; // The hook stores the SDK's rejection verbatim in `error`, so a not-found // and a CDA outage arrive on the same branch. Ask the SDK which it is. - const isMiss = !error || isCompositionNotFound(error); - if (error && !isMiss) throw error; // real failure — don't turn it into a 404 + if (error && !isCompositionNotFound(error)) throw error; // real failure — don't turn it into a 404 if (error || !specOptions?.spec) { // Editor iframe, unsaved template: null-spec options as in App Router (not 404, not <div />). - if (isStudioEditorMode(searchParams)) return <StudioComponent specOptions={{ spec: null, - fetchOptions: { compositionQuery: { url: pathname }, searchQuery: searchParams.toString(), options: {}, mode: "edit" } } as any} />; + if (isStudioEditorMode(searchParams)) return <StudioComponent specOptions={{ + spec: null, hasSpec: false, hasTemplate: false, seo: null, + fetchOptions: { config: sdk.getConfig(), compositionQuery: { url: pathname }, + searchQuery: searchParams.toString(), options: {}, mode: "edit" } }} />; return <NotFound />; } return <StudioComponent specOptions={specOptions} />; @@ -511,6 +506,7 @@ This skill succeeds only when ALL of the following are true. If any fails, do no **Dedicated mode:** every pattern in `studioPatterns` mounts `<StudioComponent />`. Every other route, the existing chrome, and the existing 404 page are unchanged (verify by loading one non-Studio page and one bad URL). - [ ] **ONE catch-all route** is registered (wildcard mode). For Next.js: `app/[...slug]/page.tsx` (non-optional) WHEN the app has an existing `app/page.tsx`, OR `app/[[...slug]]/page.tsx` (optional) WHEN greenfield with no root page. For React Router: `<Route path="*">` registered LAST. The catch-all points at `LinkedRoute`. +- [ ] If the app already had a catch-all, it was extended in the order app page, then Studio composition, then the app's 404 (§ Existing catch-all), not duplicated or replaced. One of its own pages still renders as before, and a bogus URL still renders its 404. - [ ] If the app has an existing `app/page.tsx`: confirm the chosen catch-all does NOT collide (greenfield-style `[[...slug]]` is wrong here, must be `[...slug]`). - [ ] If the user provided opt-outs, those routes are registered ahead of the catch-all and bypass Studio correctly. - [ ] Studio's project-level Canvas URL still points at `/canvas` (not a template route). @@ -519,22 +515,92 @@ This skill succeeds only when ALL of the following are true. If any fails, do no - [ ] Opt-out routes (if any) are NOT served by Studio. - [ ] `npm run dev` (or equivalent) starts without runtime errors. -## Editor-mode detection: for custom renderers +<a id="existing-catch-all"></a> -If you're shipping a custom `StudioRenderer` (rather than importing `<StudioComponent />` directly, e.g. because you're doing per-route swaps in a partial-adoption app and want to gate on editor mode explicitly), detect editor mode by checking Studio's iframe search params: +## Existing catch-all: chain Studio into it -```ts -// lib/studio-editor-mode.ts -export function isStudioEditorMode(searchParams: URLSearchParams): boolean { - return ( - searchParams.get("cs-composable-studio") === "true" || - searchParams.has("cs-composable-uid") || - searchParams.has("hash") - ); +Many apps already own a catch-all (`app/[...slug]/page.tsx` serving CMS pages, a `path="*"` page route). Adding a second one collides, and replacing it breaks every page it served. Extend the existing file instead, in a fixed order: + +1. **The app's own page.** Its existing lookup runs first, unchanged. If it finds a page, render it exactly as before. +2. **A Studio composition.** Only when the app has nothing for this URL. +3. **The app's 404.** Only when Studio has nothing either. Studio's editor iframe stays exempt, as in step 5a (iii). + +```tsx +// app/[...slug]/page.tsx — the app's EXISTING catch-all, with Studio chained in +import { notFound } from "next/navigation"; +import { isStudioEditorMode } from "@contentstack/studio-react"; +import { sdk } from "@/lib/contentstack"; // lib/studio on the minimal-add branch +import { resolveComposition } from "@/lib/resolve-composition"; +import { getCmsPage, CmsPage } from "@/lib/cms"; // the app's own lookup and page — unchanged +import { StudioRender } from "./StudioRender"; + +type Props = { + params: Promise<{ slug: string[] }>; + searchParams: Promise<Record<string, string>>; +}; + +export default async function Page({ params, searchParams }: Props) { + const { slug } = await params; + const url = "/" + slug.join("/"); + + // 1. The app's own page, exactly as before. + const page = await getCmsPage(url); + if (page) return <CmsPage page={page} />; + + // 2. A Studio composition. Read searchParams only here, so the app's own + // pages above never touch it. + const query = new URLSearchParams(await searchParams); + const { specOptions, notFound: miss } = await resolveComposition(sdk, { + url, + searchQuery: query.toString(), + }); + if (!miss) return <StudioRender specOptions={specOptions} />; + + // 3. Neither: Studio's editor iframe gets the draft (step 5a iii), visitors + // get the app's own 404. + if (isStudioEditorMode(query)) return <StudioRender specOptions={{ + spec: null, hasSpec: false, hasTemplate: false, seo: null, + fetchOptions: { config: sdk.getConfig(), compositionQuery: { url }, + searchQuery: query.toString(), options: {}, mode: "edit" } }} />; + notFound(); } ``` -**If you copied a `StudioRenderer` from another project** and editor mode never activates (Studio always shows "Template Did Not Load" for unsaved templates), the reference is likely gated on a different param set (`content_type_uid` / `entry_uid` / `live_preview`) than the public SDK's iframe sends. Replace the check with the one above. Always verify against `document.location.search` inside the live Studio iframe (`window.parent === window ? "standalone" : "iframe"`, then log the search params) if you're unsure which scheme applies to the SDK version you installed. +- **Keep the app's branch byte-for-byte.** Its data fetching, `generateStaticParams` and `generateMetadata` stay as they are. Chain `generateMetadata` the same way: the app's metadata when its page exists, else `specOptions.seo` from `resolveComposition`, else `{}`. +- **Read `searchParams` only in the Studio branch.** Awaiting it opts that render into dynamic rendering. Reading it after the app's branch has returned keeps the app's own pages rendering the way they did (static or ISR); only URLs that fall through to Studio become dynamic ([`configure-csr-vs-ssr` § Static generation and caching](configure-csr-vs-ssr.md#static-generation-and-caching)). +- **The 404 is still the app's.** `notFound()` renders the existing `not-found.tsx`. If the app's branch used to call `notFound()` itself on a miss, move that call to step 3. +- **React Router `path="*"` page route:** same order inside its element. Run the app's lookup, then `useCompositionData({ url: pathname })`, then the existing not-found UI, with the editor exemption from step 5a (iii). + +The app's own route file is modified here, and only by appending steps 2 and 3. Say so in the report: it's the one exception to "no existing route file changes" in [`convert-project-to-studio`](convert-project-to-studio.md). + +## Editor-mode detection: use the SDK's predicate + +Don't hand-write the check. `@contentstack/studio-react` exports it (2.0.1): + +```ts +import { isStudioEditorMode, isStudioCanvas } from "@contentstack/studio-react"; + +isStudioEditorMode(searchQuery?: string | URLSearchParams): boolean +``` + +- **True in any Studio-driven context**: the canvas (`cs-composable-studio` present, or `builder=true`) or a preview pane (`hash` or `live_preview` present). `isStudioCanvas` is the narrower canvas-only check, for gating cookie banners and third-party scripts. +- **Server**: pass the request's params, as step 5a (iii) does. With no argument on the server it returns `false`. +- **Browser**: with no argument it reads the mode the SDK stamped on `window` at load (it survives client-side navigations that drop the query string), else `window.location.search`. + +The param names are an internal contract between Studio and the SDK, which is why the predicate lives in the SDK. A copied `StudioRenderer` that checks its own list (`content_type_uid`, `entry_uid`, `cs-composable-uid`, …) is the usual reason editor mode never activates and unsaved templates show "Template Did Not Load". Replace the list with the import. + +<a id="fetching-referenced-fields"></a> + +## Fetching referenced fields: `extendQuery` is deprecated + +Older snippets passed `extendQuery: { <ct_uid>: { includeReferences, only } }` to widen the entry fetch. In studio-react 2.x it is deprecated and slated for removal in the next major. It still works, but in development it logs once: *"[studio] `extendQuery` is deprecated … Tick the references in Studio instead — they are saved with the composition."* + +What replaces it: + +- **Bindings decide the fetch.** The SDK derives `include[]` paths from what the composition binds, so a bound referenced field is fetched without any app-side option. +- **References the bindings don't name are ticked in Studio** on the data source and saved with the composition. The SDK applies the saved set to the request it belongs to (the template entry, or a pinned entry). +- **Compositions written by API** carry the same saved set in the composition entry's `data_sources`, under the data source's `resolvedReferences`. The canonical shape is in [`author-composition-entry`](author-composition-entry.md) and [`resolve-reference-depth`](resolve-reference-depth.md); this skill doesn't restate it. No API exists to "tick" references other than writing that field. When it's ambiguous for your SDK version, author the reference once in Studio, read the saved entry back with the CMA, and copy that shape. +- **Extra fields, not references** (the `only` half of `extendQuery`): no Studio-side or API replacement is documented as of 2.0.1. Prefer one binding per field, and keep `extendQuery.only` to the components that truly need a whole entry ([`migrate-page-to-studio` § Studio fetches only the fields your bindings name](migrate-page-to-studio.md)). ## Environment Base URL: must be reachable + include locale segment @@ -604,12 +670,14 @@ Analytics / commerce embeds that `throw` on failure show up as scary-but-harmles | Wiring a catch-all into an existing app without asking | It silently claims every URL the app didn't explicitly route, including the ones that used to reach the 404 page. The user expected Studio added, not Studio in front of everything | Ask (step 3). Wildcard with code precedence, `chromeOwner = code` and the existing 404 is the minimal-conversion default. Dedicated when the user wants pattern-level control | | Header/footer vanish on Studio-rendered pages | They were imported per `page.tsx` / per page component, and the catch-all replaced those files | Lift chrome into the layout (Next `app/layout.tsx`) or a React Router layout route wrapping the catch-all, step 5a (ii) | | Header and footer render twice | Code layout renders them AND the template has Header/Footer Sections on it | One owner only. Strip whichever side isn't the chosen `chromeOwner` | -| Site chrome wraps the `/canvas` section-authoring iframe | `/canvas` was nested inside the chrome layout, so every Section is authored inside a full page header + footer: spacing, sticky positioning, and viewport measurements all read wrong | Register `/canvas` outside the chrome layout (React Router) or in a bare-layout route group (Next.js) | +| Site chrome wraps the `/canvas` section-authoring iframe | `/canvas` was nested inside the chrome layout, so every Section is authored inside a full page header + footer: spacing, sticky positioning, and viewport measurements all read wrong | Register `/canvas` outside the chrome layout (React Router). Next.js: the chrome gate in the root layout ([`setup-section-preview`](setup-section-preview.md) step 2), since a route group can't escape the root layout | | Unmatched URLs render blank instead of 404ing | The no-spec branch returns an empty `<div />`, correct for the editor iframe, wrong for visitors, and the catch-all now owns every unmatched URL | Gate on `isStudioEditorMode`: empty div in the editor, 404 page for visitors, step 5a (iii) | -| Unsaved templates show the 404 page inside Studio's canvas ("Template Did Not Load") | The 404 fires unconditionally on no-spec. An unsaved template legitimately has no published spec | Same gate, other direction. Verify `isStudioEditorMode` checks the params the installed SDK actually sends (§ Editor-mode detection) | +| Unsaved templates show the 404 page inside Studio's canvas ("Template Did Not Load") | The 404 fires unconditionally on no-spec. An unsaved template legitimately has no published spec | Same gate, other direction. Import `isStudioEditorMode` from `@contentstack/studio-react` and pass it the request's params on the server (§ Editor-mode detection) | | React Router: the app's `<Route path="*" element={<NotFound />} />` left in place | Both it and the Studio catch-all are `path="*"`. First match wins, so one of them is dead code, usually the 404 | Remove the old route. Render `NotFound` from `LinkedRoute`'s visitor no-spec branch | ## See also - `install-studio`: installs the SDKs this skill depends on. - `setup-section-preview`: adds the `/canvas` section-authoring route. +- [`handle-404-pages`](handle-404-pages.md): the 404 branch for unknown URLs and bad slugs, per framework. +- [`csr-and-ssr-requirements`](csr-and-ssr-requirements.md): what a CSR or SSR route needs for Live Preview and Visual Editor. diff --git a/skills/studio/skills-index.md b/skills/studio/skills-index.md index 0bc9add..bc9be9d 100644 --- a/skills/studio/skills-index.md +++ b/skills/studio/skills-index.md @@ -14,13 +14,13 @@ Every name below is written `<name>` for readability. Apply this rule to each. R If the user's message contains any phrase in the left column, Read the skill in the right column and follow it. -> **Mandatory atomic-and-reusable rule: applies to EVERY row below.** Never register or author a component that renders more than one piece of content. Decompose it first (`decompose-jsx-to-atomics`). Check for an existing atomic or Section before creating one. Header / footer / nav / menus are shared Sections with their menus as **Repeaters over a field**, never hardcoded lists. One atomic prop binds one CT field. See the `studio` router § Standing rule. +> **Mandatory atomic-and-reusable rule: applies to EVERY row below.** One Section per page region, always. Inside a Section, reuse a production component whole only when it is interactive or computes its values. Decompose everything else (`decompose-jsx-to-atomics`) into the primitive library (`design-component-library`). This is the Section rule, defined once in the `studio` router (`SKILL.md` § Standing rule: atomic and reusable): read it there. Check for an existing atomic or Section before creating one. Header / footer / nav / menus are shared Sections with their menus as **Repeaters over a field**, never hardcoded lists. One atomic prop binds one CT field. See the `studio` router § Standing rule. > > **Mandatory plan-first rule: applies before every build row below.** If the request is to build/create/implement anything larger than one already-scoped piece ("build me a rewards page", "implement this design", "add this page to Studio", "build the site", "migrate this route"), Read [`plan-studio-architecture`](plan-studio-architecture.md) **first** and get the plan explicitly approved. Only then run the build skills it names. Skipping this is what produces a single monolithic template with no Sections, no Section Slots and no reuse, forcing the user to re-prompt. Straight to a build skill only when the user is iterating on a piece from an already-approved plan. **Exception: a whole-project ask** ("convert my project to Studio", "make it Studio-compatible") routes to [`convert-project-to-studio`](convert-project-to-studio.md) and its scoping gate first. `plan-studio-architecture` runs only if the user picks the one-page or full-site option there. > **Mandatory auth preflight: applies before every row below.** Settle the credential before the first API call, OAuth-first per `authenticate-cma`: `CS_OAUTH_ACCESS_TOKEN`, else the Contentstack MCP's stored session (**ask the user before reading or refreshing it**: refreshing rotates the refresh token). **Don't ask for a session `authtoken` up front**: it is the last rung, considered only after a freshly refreshed OAuth token has been observed to fail, and only after explaining the risk. If nothing resolves, hand the user `! CONTENTSTACK_REGION=<code> npx @contentstack/mcp --auth` (interactive, so it cannot be run for them). `403 error_code 316` means wrong org, not missing auth. -> **Mandatory done-rule: applies after every build row below.** Eight deliverables are mandatory on every build and all eight fail silently: `cslp.appendTags`, `studioAttributes` + `$`-twins per component, `thumbnailUrl` per registration, `ui_preview` per Section, stack Live Preview + preview token, the `/canvas` route, **publishing every Section and Template** (an edit creates an unpublished version. Delivery keeps serving the old one), and **a template actually composed of Sections**. A single component bound to the whole page body renders perfectly and fails no other check, so count `section-composition` nodes before reporting done. Read `complete-the-build`. It is the handover gate, and `plan-studio-architecture` commits to it up front. Never report a build finished with any row outstanding. +> **Mandatory done-rule: applies after every build row below.** Ten deliverables are mandatory on every build and all ten fail silently. They are listed once, with owners and checks, in `complete-the-build`: read the list there. It is the handover gate, and `plan-studio-architecture` commits to it up front. Never report a build finished with any row outstanding. | If the user says… | Read | |---|---| @@ -60,8 +60,10 @@ If the user's message contains any phrase in the left column, Read the skill in | variant alias, Personalize-driven render, cookie/header variant | `wire-variant-alias` | | share state between sections, cart count in Studio, use my Redux store, Bring Your Own State, BYOS, stateVariable, getState/setState, action prop, author should pick which function the button calls | `wire-studio-state` | | set up canvas route, MISSING_CANVAS_URL, where does StudioCanvas mount, canvas iframe is blank in Studio | `setup-section-preview` | -| make every URL go through Studio, wire catch-all template route, template URL 404s, wildcard or single page, header/footer disappeared after adding Studio, duplicate header on Studio pages, my 404 page stopped working | `setup-template-preview-routes` | +| make every URL go through Studio, wire catch-all template route, template URL 404s, wildcard or single page, header/footer disappeared after adding Studio, duplicate header on Studio pages | `setup-template-preview-routes` | | CSR or SSR, Next App Router setup, hydration mismatch, SEO meta missing, Live Preview broken after switching render mode | `configure-csr-vs-ssr` | +| what does a CSR page need, what does an SSR page need, CSR vs SSR with Live Preview, Visual Editor with SSR, convert to SSR, switch to CSR, Live Preview doesn't update, Visual Editor doesn't show my edits, Edit button shows in Visual Editor, Edit button missing on the preview site, Visual Editor shows Live Preview behaviour | `csr-and-ssr-requirements` | +| 404 page, not found page, unknown URL shows 500 / a blank page / an error, bad slug, my 404 page stopped working after adding Studio, Studio-authored 404 | `handle-404-pages` | | here's the design, here's a Figma link, here's a screenshot, map this design to Studio, decompose this design, how would I build this in Studio, what components do I need for this design | `decompose-design` | | here are all the page designs, decompose this whole site, multi-page decomposition, site-wide plan, build the marketing site, N templates | `decompose-site` | | here's the whole Figma board, build every section in this file, all the hero variants, turn this design file into sections, decompose the whole design file | `decompose-site` | @@ -89,7 +91,7 @@ If the user's message contains any phrase in the left column, Read the skill in | build a blog template, PDP template, connected to a content type | `build-connected-template` | | build a freeform template, static page in Studio, one-off landing page | `build-freeform-template` | | page builder, section picker, authors choose the sections, reorder sections per page, modular page, one template many layouts, let marketing add sections | `build-page-builder-template` | -| pin an entry to freeform, additional entry data | `pin-entry-to-freeform` | +| pin an entry to freeform, additional entry data, pin an entry to a template or Section, header and footer blank on new pages, shared Site Settings entry for a header or footer | `pin-entry-to-freeform` | | pin a query, iterate latest entries, filtered list on freeform | `pin-query-to-freeform` | | expose this prop, let authors override the headline, per-instance override | `expose-section-props` | | repeat per item, list of blocks, iterate references | `use-repeater` | @@ -104,7 +106,7 @@ If the user's message contains any phrase in the left column, Read the skill in | static_value binding by API, static value renders the default, where the literal goes | `author-composition-static-values` | | binding types list, node anatomy, where bindings live in the ui tree, repeater binding shape | `author-composition-bindings` | | compress the ui spec, zlib encode composition, preflight validate the ui tree | `author-composition-encoding` | -| repeat a card over a multi-reference, repeat over group-multiple, repeater recipe by API | `author-composition-repeater-recipes` | +| repeat a card over a multi-reference, repeat over group-multiple, repeater recipe by API, loop over a reference field via the API, a different card per content type via the API, Condition Blocks by API | `author-composition-repeater-recipes` | | nested repeaters by API, list inside each iterated item, repeaterUID chain | `author-composition-nested-repeaters` | | author a section by API, selectedField priority, section scoping, section-slot node shape | `author-composition-sections` | | Pattern B by API, fill a section slot by API, wrapper section with a slot | `author-composition-section-slot-fill` | @@ -126,7 +128,7 @@ If the user's message contains any phrase in the left column, Read the skill in | cookie banner in canvas, chat widget over the panel, hide analytics in Studio, gate GTM, isStudioCanvas | `gate-third-party-scripts` | | URL renders the homepage, two templates match the same pattern, /blog/post-1 404s, wildcard stopped matching | `troubleshoot-composition-resolution` | | binding shows nothing, wrong value rendering, literal {{ | `troubleshoot-data-binding` | -| SSR render failure, use client mismatch, hydration Studio, registry undefined in server | `troubleshoot-ssr-rendering` | +| SSR render failure, use client mismatch, hydration Studio, registry undefined in server, App Router route 500s, "Cannot read properties of null (reading 'useContext')", crash at useData / RepeaterPreview, `transpilePackages` lists `@contentstack/studio-*` | `troubleshoot-ssr-rendering` | | what's a slot, swap point, drop zone | `understand-section-slots` | | what's a Section, Section vs Template, reusable page parts | `understand-sections` | | what's a Template, template vs section, one template N pages | `understand-templates` | @@ -140,7 +142,7 @@ If the user's message contains any phrase in the left column, Read the skill in | give me a Studio tour, walk me through Studio | `studio-tour` | | plan a Studio project, architecture plan, sections vs templates for my app, **build me a page, implement this design in Studio, add this page to Studio, build the site, create a page in Studio** | `plan-studio-architecture` (**first, then the build skills it names**) | -If several rows match, follow the ordering the sequence implies: `analyze-project-fit` first, then provisioning, install, configure, register, author, verify, and troubleshoot. `enable-visual-experience` gates install. `install-studio` gates everything after. +If several rows match, follow the ordering the sequence implies (§ Ordering hints): `analyze-project-fit` first, because it is read-only and every state change after it depends on what it finds. Then Live Preview, provisioning, install, configure, the primitive library, register, author, verify, and troubleshoot. `enable-visual-experience` gates install. `install-studio` gates everything after. --- @@ -179,7 +181,7 @@ Grouped by intent. Use when the user's message is Studio-shaped but no trigger p ### 4. Components — plan and register -- **design-component-library**: meta-planning skill. Apply the atom + layout taxonomy to the user's design system BEFORE any registration is written. Emits a decomposition sheet with the 10-primitive proposal and DS token gaps flagged. Run first when a project is starting fresh with a component library or when the user asks "what should I register". +- **design-component-library**: meta-planning skill. Apply the atom + layout taxonomy to the user's design system BEFORE any registration is written. Emits a decomposition sheet with the 10-primitive proposal and DS token gaps flagged. Required on every build (`plan-studio-architecture` Step 2c), fresh or migration, and whenever the user asks "what should I register". - **classify-existing-component**: diagnostic. Runs Q1 of the four-question framework on **one** component and stops: Atom, Layout, or Compound. Routes to the right next skill (`register-component` for atoms/layouts, `decompose-jsx-to-atomics` for JSX compounds, `decompose-design` for design-input compounds). Use when auditing an existing registry, sanity-checking before registration, or triaging "authors can't rearrange this" reports. - **decompose-jsx-to-atomics**: break an existing React component (often a whole page in one file) into Layer-1 atomics, Layer-2 containers and Layer-3 Sections so its parts become reusable. Run before `discover-sections` on an existing codebase. The code-input twin of `decompose-design`. - **register-component**: read a React component, infer prop schema, emit a `registerComponent` call. @@ -229,14 +231,16 @@ Grouped by intent. Use when the user's message is Studio-shaped but no trigger p - **build-connected-template**: content-type-bound template. Every entry renders at a derived URL. - **build-freeform-template**: static template with Pinned Entries and Pinned Queries. - **build-page-builder-template**: the author-composes-the-page pattern: a Modular Block of section types, one Section per block, one template with a Repeater + a Condition Block per block. One template renders every layout. -- **pin-entry-to-freeform**: pin one entry to a freeform template as a Pinned Entry. +- **pin-entry-to-freeform**: pin one entry as a Pinned Entry on a Freeform or Connected template or a Section (e.g. a header or footer Section reading a shared Site Settings entry). - **pin-query-to-freeform**: pin a CDA query so a Repeater source iterates dynamic content. ### 10. Routes and template URLs - **setup-section-preview**: mount `<StudioCanvas />` in the app + set Canvas URL on the project. -- **setup-template-preview-routes**: wildcard (catch-all) vs dedicated routes. Wildcard also wires site-chrome preservation + 404 handling. +- **setup-template-preview-routes**: wildcard (catch-all) vs dedicated routes. Wildcard also wires site-chrome preservation; the 404 branch itself is `handle-404-pages`. - **configure-csr-vs-ssr**: choose CSR (`useCompositionData`) vs SSR (`sdk.fetchCompositionData`). +- **csr-and-ssr-requirements**: what a CSR or SSR page needs on the visitor site, the Live Preview pane, Visual Editor and the canvas: the five Live Preview settings, `mode`/`ssr`, the Edit button, which refetch to call, converting between modes. +- **handle-404-pages**: unknown URLs and bad slugs render the site's 404 page (real 404 on SSR, `noindex` on CSR), decided by `isCompositionNotFound` only. ### 11. Data binding @@ -274,7 +278,7 @@ Grouped by intent. Use when the user's message is Studio-shaped but no trigger p ### 14. Ship and verify -- **complete-the-build**: the ten always-mandatory deliverables (CSLP tags, `studioAttributes`, component + section thumbnails, Live Preview, `/canvas`, publishing, a template actually composed of Sections, builder mode, and new entries editable in Visual Editor) and the verification block that proves them. Run at the end of every build, and cite it in the plan at the start. +- **complete-the-build**: the definition of done. Lists the ten always-mandatory deliverables, once, and the verification block that proves them. Run at the end of every build, and cite it in the plan at the start. - **verify-setup**: layered smoke test (Delivery / Live Preview / Visual Editor / Studio). - **verify-visual-parity**: brownfield migration: drive a browser to compare composed vs production Section-by-Section, classify drift by cause class, fix at the source (adapter / registration / wrapper CSS). - **deploy-studio-site**: Contentstack Launch project, environments and env vars, composition Deploy, Live Preview gating, region config. @@ -310,7 +314,9 @@ Grouped by intent. Use when the user's message is Studio-shaped but no trigger p Some skills gate others. When multiple are relevant, run in this order: ``` -enable-visual-experience → provision-studio-{stack,project} → analyze-project-fit +analyze-project-fit (read-only: runs before anything that changes state) + ↓ +enable-visual-experience → provision-studio-{stack,project} ↓ convert-project-to-studio (existing app: drives the next four) ↓ @@ -318,6 +324,8 @@ enable-visual-experience → provision-studio-{stack,project} → analyze-pr ↓ configure-studio → register-breakpoints ↓ + design-component-library (the primitive library, every build) + ↓ register-component (+ register-json-rte if json_rte fields) ↓ setup-section-preview → build-section diff --git a/skills/studio/troubleshoot-canvas.md b/skills/studio/troubleshoot-canvas.md index 66d5542..416c72b 100644 --- a/skills/studio/troubleshoot-canvas.md +++ b/skills/studio/troubleshoot-canvas.md @@ -92,12 +92,12 @@ curl -s "<studio-api-host>/v1/projects/<projectId>/compositions/<compositionId>" | **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 `setup-section-preview`. | | **"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 `useCompositionData` returns a non-thrown error with `ComposableStudioError: [Composable Studio SDK] Internal components missing: page` (also seen for `box`, `text`) | **Pre-patch SDK (1.3.1 / 1.5.1 and earlier).** The visitor render path didn't auto-register built-in/system components (`page`, `box`, `text`, …). Only the authoring canvas did. Compositions whose root node is type `page` couldn't render on the public route. | In DevTools, log the return of `useCompositionData` (or whatever hook drives the route), then look for `state: "error"` with `Internal components missing: page` in the body. Compositions with `page` (or another system type) as the root node are affected. | Upgrade `@contentstack/studio-react` to a later 1.3.x / 1.5.x patch, which restores auto-registration of built-ins on the visitor path. | +| **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. | | **"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 section composition is published and fetched along with the composition that places it, so the SDK can register it. 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 (preferred). If it must be scripted and a freshly refreshed OAuth token is rejected (`422 "is not a valid upload."`), explain the risk and use a session authtoken only with the user's explicit agreement for that single call, never persisted (see [`author-composition-pitfalls`](author-composition-pitfalls.md)). Then re-publish + re-open. | -| **"Authentication failed" / 401 / 403** 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. | +| **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`. Other regions and custom / private data centers 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? Don't print token values. Check presence: `grep -qE '^[A-Z_]*PREVIEW_TOKEN=.+' .env.local && echo set \|\| echo missing`. Check for the literal `undefined`: `grep -qE '^[A-Z_]*PREVIEW_TOKEN="?undefined' .env.local && echo stale`. Compare with `.env` by hash, not by value: `for f in .env .env.local; do grep -E '^[A-Z_]*PREVIEW_TOKEN=' "$f" \| cut -d= -f2- \| shasum \| cut -c1-8; done`. Different hashes mean `.env.local` wins with a different value. | 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. | Compare the `API_KEY` lines by hash, without printing them: `for f in .env .env.local; do grep -E '^[A-Z_]*API_KEY=' "$f" \| cut -d= -f2- \| shasum \| cut -c1-8; done`. If `.env.local` hashes differently from the key Studio's project uses, 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. | <!-- style-lint: allow --> @@ -163,7 +163,7 @@ This skill succeeds only when ALL of the following are true. ## 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 ("Composition not found for URL: …") the moment resolution needs a second internal query — which is the common case for anything but an exact literal-URL match. +**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: diff --git a/skills/studio/troubleshoot-composition-resolution.md b/skills/studio/troubleshoot-composition-resolution.md index c93a0f4..42ed24b 100644 --- a/skills/studio/troubleshoot-composition-resolution.md +++ b/skills/studio/troubleshoot-composition-resolution.md @@ -102,7 +102,7 @@ Every other class is real breakage and **never a 404**: `SETUP_INVALID` (a wrong Classify with the predicate: ```ts -import { isCompositionNotFound } from "@contentstack/studio-client"; +import { isCompositionNotFound } from "@contentstack/studio-react"; if (isCompositionNotFound(err)) { /* 404 */ } else { throw err; /* 500 */ } ``` diff --git a/skills/studio/troubleshoot-data-binding.md b/skills/studio/troubleshoot-data-binding.md index 7ea86be..cea801d 100644 --- a/skills/studio/troubleshoot-data-binding.md +++ b/skills/studio/troubleshoot-data-binding.md @@ -63,7 +63,7 @@ The reliable debugging move when bindings misbehave is: capture a working compos | **Browser console warning:** `Data binding type mismatch in component "...": Expected prop "..." to be ... but received ...` | At runtime, the SDK saw a type that doesn't match the registered prop's declared `type`. **Only logged in production-style render**. Editor mode suppresses it. | Open DevTools console on the published page. Filter `Data binding type mismatch`. | Reconcile the schema. Either change the prop's declared type, or change the bound field, or insert a transformation in code. Don't silence the warning: it always indicates a real type drift. | | **Field renders a hardcoded default (or empty string / 0 / false) instead of CMS data** | Binding path didn't resolve in the actual data, so the SDK used the static value / default value (then a type-default if both are absent). | In DevTools, open the Network tab, find the composition fetch, and look for the binding's path in the response. Does the path exist? | (a) If path is wrong: re-bind via picker. (b) If path is correct but value is `null`/missing: fix upstream content. (c) Set a non-empty static value as visible fallback if the field is legitimately optional. | | **A list/grid component shows generic placeholder cards (`ARTICLE ONE / TWO / THREE`, "short teaser", gray image icons) even in Preview Mode** | A whole-array component prop (a `Card Grid` / `Article Grid` that `.map()`s internally) is bound to a field that **doesn't exist on the CT** (or isn't bound at all), so the array resolves to nothing and the component renders its registration `defaultValue`. Classic cause: the page CT (e.g. `Article List`) has no multi-Reference / Modular Block / Pinned Query feeding the list. The items live in a **separate CT** the page never references. | Open the Data Picker on the array prop: is there actually a multi-valued field to bind to? Then in DevTools, open the Network tab and check the composition fetch: is the array field present and populated in the payload? | Add the missing multi-valued source: multi-Reference on the page CT (`migrate-ct-schema`), or a Pinned Query (`pin-query-to-freeform`), then bind. If you want per-item binding visible in Studio instead of one array blob, decompose to Repeater + atomic child Section (`build-repeating-section`). This is the `build-section` Step 5e / `register-component` whole-array trap, caught after the fact. | -| **Console:** `[Composable Studio SDK] useData() was called outside a DataCtxProvider` (dev only, client only) | A basic component (Repeater / ConditionBlock / SectionComposition) rendered without its parent `DataCtxProvider`. Hook returns empty data and warns. | Confirm the component tree wraps the basic component with `<StudioComponent>`. | Always render compositions inside `<StudioComponent>` (it provides `DataCtxProvider` with the spec). If you're using a basic component directly outside that wrapper, wrap it explicitly. This is a render-tree shape issue, not a binding-path issue. | +| **Console:** `[Contentstack Studio] useData() was called outside a DataCtxProvider` (dev only, client only) | A basic component (Repeater / ConditionBlock / SectionComposition) rendered without its parent `DataCtxProvider`. Hook returns empty data and warns. | Confirm the component tree wraps the basic component with `<StudioComponent>`. | Always render compositions inside `<StudioComponent>` (it provides `DataCtxProvider` with the spec). If you're using a basic component directly outside that wrapper, wrap it explicitly. This is a render-tree shape issue, not a binding-path issue. | | **Repeater renders one iteration on the Studio canvas, production renders the correct N** | Repeater's authoring default is to render a single iteration with placeholder/default values so authors can lay out the structure without depending on bound data. Production iterates the real array. | Repeater Preview Mode toggle in Properties. | Select the Repeater node in Layers, toggle **Preview Mode** in Properties: canvas re-renders with real iteration data. (If production also renders one or zero iterations, the binding itself is wrong. Re-check the bound array field in the data payload.) | | **Repeater renders no iterations in production / SSR** | During SSR, `useData()` returns empty data (no items), so the Repeater iterates 0× until hydration attaches real context. | View page source (no JS): Repeater HTML is minimal. | This is expected for SSR. If the items don't appear AFTER hydration either, the issue is upstream: `specOptions.data.dataSources` doesn't contain the items, or the binding path is wrong. Check the Network payload. See `troubleshoot-ssr-rendering` for further SSR-specific failure modes. | | **Condition Block's field picker shows no options** | Parent node (section / repeater / template) has no binding set, or its binding hasn't resolved. Condition is a child of the parent's data context. | Select the parent in Layers. Check its binding in Properties, under Data. | Select the parent in Layers FIRST. In Properties, under Data, verify a binding is set. Then return to the Condition Block: its picker now lists fields from the parent context. | diff --git a/skills/studio/troubleshoot-ssr-rendering.md b/skills/studio/troubleshoot-ssr-rendering.md index 5347007..10c9403 100644 --- a/skills/studio/troubleshoot-ssr-rendering.md +++ b/skills/studio/troubleshoot-ssr-rendering.md @@ -6,7 +6,7 @@ name: troubleshoot-ssr-rendering Symptom-mapped diagnostic for SSR / RSC render failures with `<StudioComponent>`. Covers the "use client" boundary, registry singleton, lazy-component loading, and `useData` fallback. -Use when the app fails to render compositions on the server. Signals: `"Attempted to call Page() from the server"`, `"Element type is invalid"`, `useData()` warning, `"is registered as lazy"`, `"is not registered"` from the server bundle, hydration mismatches. If the symptom is ambiguous, route via `troubleshoot` first. Do NOT use for canvas iframe issues (`troubleshoot-canvas`) or binding issues (`troubleshoot-data-binding`). +Use when the app fails to render compositions on the server. Signals: `"Attempted to call Page() from the server"`, `"Element type is invalid"`, `useData()` warning, `"is registered as lazy"`, `"is not registered"` from the server bundle, hydration mismatches (including right after editing a component registration in dev). If the symptom is ambiguous, route via `troubleshoot` first. Do NOT use for canvas iframe issues (`troubleshoot-canvas`) or binding issues (`troubleshoot-data-binding`). > **Mandatory 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 (**ask the user before reading or refreshing it**: refreshing rotates the refresh token; see that skill for where it lives). **Don't ask for a session `authtoken` up front**: it is the last rung, considered only after a freshly refreshed OAuth token has been observed to fail, and only after explaining the risk. 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. @@ -35,11 +35,12 @@ The SDK ships fixes for each boundary internally (client-only built-ins isolatio | Symptom | Failing boundary | Check first | Fix | |---|---|---|---| | **"Attempted to call Page() from the server"** (or `Section()`, `Text()`, `Repeater()`, any basic) | The basics package landed in the consumer's server module graph as client-references. The renderer's registry lookup then tries to call them via JSX. Server can't. | Trace the import. Does anything in the server-rendered tree transitively import `@contentstack/studio-react-components` (the basics package) outside a `"use client"` boundary? | The SDK isolates the basics behind a `"use client"` boundary internally. Recent `@contentstack/studio-react` versions ship this fix. Upgrade and check the CHANGELOG for the basics-isolation entry. On the consumer side: do not import the basics package from server code, and wrap `<StudioComponent>` in a `"use client"` child component. | -| **Console warning:** `[Composable Studio SDK] useData() was called outside a DataCtxProvider.` (dev only, client only) | A basic component (Repeater / ConditionBlock / SectionComposition) hit the SSR pass before `DataCtxProvider` attached. The hook returns `EMPTY_DATA` and warns. | Confirm: is the warning in dev only? In prod is the page rendering correctly post-hydration? | If post-hydration render is correct, the warning is expected and benign: SSR placeholder, then hydration populates real data. If post-hydration is still wrong, see `troubleshoot-data-binding` (the binding itself is broken). If the warning appears in production console, that's an SDK assertion bug. Escalate. | +| **Console warning:** `[Contentstack Studio] useData() was called outside a DataCtxProvider.` (dev only, client only) | A basic component (Repeater / ConditionBlock / SectionComposition) hit the SSR pass before `DataCtxProvider` attached. The hook returns `EMPTY_DATA` and warns. | Confirm: is the warning in dev only? In prod is the page rendering correctly post-hydration? | If post-hydration render is correct, the warning is expected and benign: SSR placeholder, then hydration populates real data. If post-hydration is still wrong, see `troubleshoot-data-binding` (the binding itself is broken). If the warning appears in production console, that's an SDK assertion bug. Escalate. | | **Hydration mismatch around the composition tree** (React error about HTML mismatching) | `<StudioComponent>` (or its containing component) is being rendered as a Server Component. The renderer uses hooks + client-only CSS-in-JS and is a `"use client"` module. Rendering it server-side produces HTML the client can't reconcile. | `grep -n '"use client"' app/<your-route>/page.tsx`. If absent and you're calling `<StudioComponent>` directly: that's the bug. | Wrap `<StudioComponent>` in a tiny `"use client"` child (per `configure-csr-vs-ssr` [skill](configure-csr-vs-ssr.md)). The parent Server Component does the data fetch (`sdk.fetchCompositionData`) and passes `specOptions` to the wrapper. | -| **Error:** `Component '<name>' is registered as lazy but hasn't been loaded yet. This usually happens when: 1. fetchSpec() was not awaited before rendering` | The component was registered LAZILY. Both lazy entry points hit this same code path: (a) `registerComponent({ component: () => import("./Foo") })`, any arity-0 function in `component:` that returns a Promise is treated as lazy. (b) `registerLazyComponent({...config}, () => import("./Foo"))`, the explicit-signature form. The SDK does **not** auto-load on access. The loader runs explicitly during `fetchSpec`. If `<StudioComponent>` mounts before `fetchSpec` (or `fetchCompositionData`) has been awaited, the registry knows the component exists but the React.lazy bundle isn't downloaded yet, and this throws. | Confirm: is `fetchSpec` (or its wrapper `fetchCompositionData`) awaited before `<StudioComponent>` mounts? Note this affects both lazy-registration shapes. Fix is the same for both. | Always `await sdk.fetchCompositionData(...)` before passing `specOptions` to `<StudioComponent>`. If using `useCompositionData`, render a loading state until `isLoading === false`. The contract is the same whether you registered via the arity-0-thunk form of `registerComponent` or the explicit `registerLazyComponent` form. | +| **Hydration mismatch, or 0-px / collapsed elements, right after editing a `registerComponent` / `registerLazyComponent` call in dev**: server HTML shows the old markup, the browser the new | Not a boundary bug. The registry lives on `globalThis` and skips a `type` it already holds, so hot reload re-runs the registration module but the running dev server keeps the **old** entry (old props, defaults, component) while the browser registers the new one | Did you change a registration since `next dev` (or the dev server) started? Does `view-source` show the old props / markup? | **Restart the dev server.** Do this after every registration change. A fresh process registers once, so production builds are unaffected. See [`register-component-pitfalls`](register-component-pitfalls.md) | +| **Error:** `Component '<name>' is registered as lazy but hasn't been loaded yet. This usually happens when: 1. fetchSpec() was not awaited before rendering` | The component was registered LAZILY. Both lazy entry points hit this same code path: (a) `registerComponent({ component: () => import("./Foo") })`, any arity-0 function in `component:` that returns a Promise is treated as lazy. (b) `registerLazyComponent({...config}, () => import("./Foo").then((m) => m.Foo))`, the explicit-signature form. The SDK does **not** auto-load on access. The loader runs explicitly during `fetchSpec`. If `<StudioComponent>` mounts before `fetchSpec` (or `fetchCompositionData`) has been awaited, the registry knows the component exists but the React.lazy bundle isn't downloaded yet, and this throws. | Confirm: is `fetchSpec` (or its wrapper `fetchCompositionData`) awaited before `<StudioComponent>` mounts? Note this affects both lazy-registration shapes. Fix is the same for both. | Always `await sdk.fetchCompositionData(...)` before passing `specOptions` to `<StudioComponent>`. If using `useCompositionData`, render a loading state until `isLoading === false`. The contract is the same whether you registered via the arity-0-thunk form of `registerComponent` or the explicit `registerLazyComponent` form. | | **Component with type 'X' is not registered** (emitted from the server bundle / RSC fetch) | Pre-fix, the component registry was module-scoped. RSC server and client bundles had separate instances, so server-fetched specs couldn't find user-registered components. The SDK's fix shares the registry across both module graphs at runtime. | Confirm the installed `@contentstack/studio-registry` version is recent (check its CHANGELOG for "shared registry" / "globalThis-backed singleton" entries). | Upgrade `studio-registry` and the packages that depend on it to a version that ships the shared-registry fix. After upgrade, registration done at module-init time is visible to both server and client bundles within the same JS runtime. | -| **Component with type 'X' is not registered** where X is a built-in (`page`, `section`, `text`, `repeater`, `condition-block`, `section-slot`, `box`), on the **visitor render** path (`<StudioComponent>` in SSR/RSC, or any non-canvas public route) | Pre-patch SDK (1.3.1 / 1.5.1 and earlier) didn't auto-register built-ins on the visitor render path. Only the authoring canvas did. Later patch releases restore auto-registration. | `npm ls @contentstack/studio-react` (or the pnpm / yarn equivalent). Is the installed version 1.3.1 / 1.5.1 or earlier? | Upgrade `@contentstack/studio-react` to a later patch release (check its CHANGELOG for built-in auto-registration on the visitor path). Don't hand-register internals as a workaround. Auto-register is back on the visitor path. | +| **Component with type 'X' is not registered**, or `COMPONENT_NOT_REGISTERED: Studio's own components are not registered: X`, where X is a built-in (`page`, `section`, `text`, `repeater`, `condition-block`, `section-slot`, `box`), on the **visitor render** path (`<StudioComponent>` in SSR/RSC, or any non-canvas public route) | The built-ins register themselves when the `@contentstack/studio-react` package root is first imported. The visitor path's fetching code never imported it, or a server-only bundle tree-shook it out. | Does the module that runs the fetch, or one it imports, contain `import "@contentstack/studio-react";`? | Add `import "@contentstack/studio-react";` once, above the fetch, in the bundle that runs it. Don't hand-register internals as a workaround. | | **Component with type 'X' is not registered** where X is a built-in, only on routes that mount `<StudioCanvas>` and not `<StudioComponent>` | Distinct from the visitor-path issue above: older SDK versions also had a shared-registry tree-shake gap where canvas-only routes could load with an empty built-in registry. A separate fix in recent `@contentstack/studio-react` versions covers this case. | Open a canvas-only route in a production build and check console for "is not registered" errors on built-in types. If present on a canvas route (not a visitor route), this is the canvas-side fix you need. | Upgrade `@contentstack/studio-react` to a version that includes the canvas-route built-in fix (check its CHANGELOG). | | **`<StudioCanvas>` import explodes during server build** | `<StudioCanvas>` is the edit-mode iframe overlay, client-only by design. Importing it into a Server Component pulls the visual-editor SDK into the server bundle. | Was the canvas route file marked `"use client"`? Check at the top of the file. | Mark the canvas route `"use client"` (per `setup-template-preview-routes` [skill](setup-template-preview-routes.md) / `setup-section-preview` [skill](setup-section-preview.md)). Or use Next's `dynamic(() => import('...'), { ssr: false })` form. | | **In SSR, fetched composition resolves to the wrong locale / variant / preview entry** | On the server there is no `window.location.search`. `sdk.fetchCompositionData` REQUIRES the consumer to pass `searchQuery` explicitly so iframe/preview parameters flow into the CDA call. Skip it and you get the default fetch. | Inspect the server-side call site. Is `searchQuery` being passed? `await sdk.fetchCompositionData({ url, searchQuery }, { locale })`. | Extract `searchQuery` from the request and pass it. Pattern documented in [`configure-csr-vs-ssr.md`](configure-csr-vs-ssr.md). Not an SDK bug: a consumer integration step. | diff --git a/skills/studio/troubleshoot.md b/skills/studio/troubleshoot.md index ea1d592..900d973 100644 --- a/skills/studio/troubleshoot.md +++ b/skills/studio/troubleshoot.md @@ -32,7 +32,7 @@ Match the user's complaint against the most specific row. **Load only the matchi Two paths: -1. **Ask the user one clarifying question**: "Is the failure on the canvas inside Studio, on the live site, or during a build / SSR step?" That maps to canvas / URL resolution / SSR respectively. If the answer is "the value renders but is wrong", route to data binding. +1. **Ask the user one clarifying question**: "Is the failure on the canvas inside Studio, on the live site, or during a build / SSR step?" That maps to canvas / URL resolution / SSR respectively. If the answer is "the value renders but is wrong", route to data binding. **Then end the turn.** No likely causes, no fixes, no "next steps" until they answer: a guessed cause anchors the user on one surface before you know which one failed, and the specialist's own checks are what find the cause. 2. **Default to canvas-test pre-flight.** If the user is unsure where the failure is, [`troubleshoot-canvas`](troubleshoot-canvas.md) leads with a section-test pre-flight that rules out 80% of misdirections before reading any symptom row. Run that, then route based on what the pre-flight finds. @@ -48,3 +48,4 @@ Skill succeeds when: - [ ] The user's symptom was matched to exactly ONE specialist row (no "load all five"). - [ ] The specialist was invoked / linked. - [ ] If the symptom didn't match, a clarifying question was asked OR the canvas-test pre-flight was run. +- [ ] A turn that asks the clarifying question lists no causes and proposes no fixes. diff --git a/skills/studio/understand-authoring-headlessly.md b/skills/studio/understand-authoring-headlessly.md index abe60b8..6603a2c 100644 --- a/skills/studio/understand-authoring-headlessly.md +++ b/skills/studio/understand-authoring-headlessly.md @@ -28,7 +28,7 @@ API authoring is NOT a replacement for the UI. The Data Picker handles every sha The compositions CT has 13 fields (full schema in [`provision-studio-project`](provision-studio-project.md)). The two that carry the composition itself: - **`ui`**: `zlib:<base64>`-encoded composition node tree. The whole layout (page, sections, components, repeaters, condition blocks, section slots) lives here as one inflated JSON tree. -- **`data_sources`**: JSON object holding `resolvedReferences` (which fields to populate via CDA `?include[]=`), the static-value map, and other resolution metadata. +- **`data_sources`**: JSON object holding `resolvedReferences` (which fields to populate via CDA `?include[]=`) and other resolution metadata. Static values live in the separate `static_value` field. Exact shapes per API: [`author-composition-entry`](author-composition-entry.md#wire-shapes). Other fields (`composable_uid`, `url`, `linked_schemas`, `linked_sections`, `url_metadata`, etc.) are CT-level routing/metadata. The actual composition is in `ui` + `data_sources`. diff --git a/skills/studio/understand-canvas-url.md b/skills/studio/understand-canvas-url.md index b162b43..f5897d8 100644 --- a/skills/studio/understand-canvas-url.md +++ b/skills/studio/understand-canvas-url.md @@ -50,7 +50,7 @@ export default function CanvasRoute() { } ``` -`<StudioCanvas />` is client-only: it reads the post-message channel with Studio's parent frame and `window.location`. For SSR frameworks (Next, Remix), load it client-only via `next/dynamic({ ssr: false })` or `ClientOnly`. `setup-section-preview` emits the right shape per framework. +`<StudioCanvas />` is client-only: it reads the post-message channel with Studio's parent frame and `window.location`. On studio-react 2.x it is a `"use client"` component that renders `null` until mount, so a `"use client"` canvas page is enough; no `next/dynamic({ ssr: false })` or `ClientOnly` wrapper is needed. `setup-section-preview` emits the right shape per framework. The route should NOT include nav, header, footer, or any other layout chrome. Those would push the canvas down and clip Studio's overlays. Bare `<StudioCanvas />` and nothing else. diff --git a/skills/studio/understand-contentstack-mcp.md b/skills/studio/understand-contentstack-mcp.md index 8079d69..4e7d8c5 100644 --- a/skills/studio/understand-contentstack-mcp.md +++ b/skills/studio/understand-contentstack-mcp.md @@ -61,9 +61,9 @@ Three signatures mean the stored session is unusable and the command above is th **Auto-captures `organization_uid` + region.** The OAuth token stores the **org** (your login's default org) and the region, no need to ask for `org_id`. (This corrects the docs' "can't discover" claim: org IS captured. Only the stack isn't.) -**Runs CMA ops as tools** (77 CMA + 22 CMA-extended): `create_a_content_type`, `update_content_type`, `get_all_content_types`, `create_a_global_field`, `update_a_global_field`, `create_an_entry`, `update_an_entry`, `publish_an_entry`, `get_all_entries`, `get_all_environments`, `create_an_environment`, `get_all_assets`, `publish_an_asset`. Covers most of `provision-studio-stack`, the CT-create in `provision-studio-project`, and entry create/publish in `author-composition-via-api`. **Note: asset upload has NO tool** (only get/publish/update/delete of existing assets). Creating a new asset from a binary stays raw multipart `POST /v3/assets`. +**Runs CMA ops as tools** (77 CMA + 22 CMA-extended). **Content types are snapshotted at server start**: its update, read and publish tools reject a content type created later in the session, so those steps go over HTTP until a restart ([`install-contentstack-mcp`](install-contentstack-mcp.md#cts-created-after-start) Step 6b). Tools: `create_a_content_type`, `update_content_type`, `get_all_content_types`, `create_a_global_field`, `update_a_global_field`, `create_an_entry`, `update_an_entry`, `publish_an_entry`, `get_all_entries`, `get_all_environments`, `create_an_environment`, `get_all_assets`, `publish_an_asset`. Covers most of `provision-studio-stack`, the CT-create in `provision-studio-project`, and entry create/publish in `author-composition-via-api`. **Note: asset upload has NO tool** (only get/publish/update/delete of existing assets). Creating a new asset from a binary stays raw multipart `POST /v3/assets`. -**Does NOT auto-capture the stack `api_key`.** The token's `stack_api_key` is empty, no `list_stacks` tool. Before asking, read `CONTENTSTACK_API_KEY` from the MCP's registration (this project's `mcpServers.contentstack.env` in `~/.claude.json`), else [`install-playwright-mcp`](install-playwright-mcp.md) Step 5b's `CS_RECENT_STACK_API_KEY`. Found one: ask "Use stack `<key>`, or give another key?" None: ask for it. It MUST belong to the OAuth'd org (Step 5 new-vs-existing / 403-recovery loop). +**Does NOT auto-capture the stack `api_key`.** The token's `stack_api_key` is empty, no `list_stacks` tool. Before asking, read `CONTENTSTACK_API_KEY` from the MCP's registration (`mcpServers.contentstack.env`: under this project's entry in `~/.claude.json` for the default `local` scope, at the top level of `~/.claude.json` for `user` scope, or in the repo's `.mcp.json` for `project` scope), else [`install-playwright-mcp`](install-playwright-mcp.md) Step 5b's `CS_RECENT_STACK_API_KEY`. Found one: ask "Use stack `<key>`, or give another key?" None: ask for it. It MUST belong to the OAuth'd org (Step 5 new-vs-existing / 403-recovery loop). **Studio API (`/v1/projects`): OAuth FIRST, session authtoken only as fallback.** The MCP has no tool for it, so this is raw HTTP either way. But **attempt OAuth before asking anyone for a session token**, in this order: @@ -81,6 +81,21 @@ Three signatures mean the stored session is unusable and the command above is th **No delivery/preview-token tool, no stack-create, no Live-Preview-enable tool.** All stay raw HTTP (`provision-studio-project` steps 3, 5, and stack-create). +### The hosted connector on claude.ai + +Everything above describes the **local** server, `@contentstack/mcp`, which `claude mcp add` (or an IDE's MCP config) starts on your machine. claude.ai also lists a **Contentstack connector** under Settings, then Connectors. It is a remote MCP server run for you: you sign in through claude.ai, nothing is installed, no `--auth` runs, and nothing is written to the `ContentstackMCP` store above. + +**Does it satisfy the MCP gate?** The gate is the same for any server ([`install-contentstack-mcp`](install-contentstack-mcp.md) § Discovery in the active host): its tools are callable in this session, and a read call on the target stack returns real data. If both hold, it qualifies, for exactly the operations its tools cover. Not being installed locally is not a reason to skip it, and being connected on claude.ai is not proof the tools reach this session. + +**Unconfirmed, so check by discovery rather than assume:** + +- Which tools it exposes, compared with the local server's `GROUPS=cma,cma-extended` set, in particular content-type create/update and entry publish. +- How it chooses the stack (no `CONTENTSTACK_API_KEY` registration to read), and its region handling. +- Whether it has the local server's start-up content-type snapshot (Step 6b). +- Whether it appears in Claude Code sessions as well as on claude.ai. + +**What is certain:** its OAuth session is held by claude.ai, so scripts and `curl` cannot reuse it. HTTP fallbacks still need their own credential: `csdx auth:login --oauth` then [`authenticate-cma`](authenticate-cma.md#export-csdx-token) § Export the `csdx` OAuth token, or the local server's `--auth`. + > **The split:** Contentstack MCP = auth (incl. org) + CMA execution, **standard regions** for the login. Playwright MCP = stack-key discovery from the UI. Raw HTTP = Studio API + tokens + LP-enable + stack-create. A custom / private data center works through the MCP's API calls too, via base-URL overrides (Step 0). No single tool does all of it. ### Custom / private data centers: the one clear path diff --git a/skills/studio/upgrade-studio-sdk.md b/skills/studio/upgrade-studio-sdk.md index b59aa66..933a200 100644 --- a/skills/studio/upgrade-studio-sdk.md +++ b/skills/studio/upgrade-studio-sdk.md @@ -4,9 +4,9 @@ name: upgrade-studio-sdk ## When to use -Bump the Studio SDK packages the host app declares (usually just `@contentstack/studio-react`): pin versions, read CHANGELOGs, smoke-check, roll back if needed. +Bump the Studio SDK packages the host app declares (usually just `@contentstack/studio-react`): pin versions, find what changed (type check, the 1.x → 2.0 table), smoke-check, roll back if needed. -Use when the user wants to upgrade the Studio SDK packages in their host app: "upgrade Studio SDK to latest", "what's new in `@contentstack/studio-react`", "is `studio-client@1.5` safe", "I want the new repeater fix". Do NOT use to install for the first time (`install-studio`). Do NOT use to develop the SDK itself. +Use when the user wants to upgrade the Studio SDK packages in their host app: "upgrade Studio SDK to latest", "what's new in `@contentstack/studio-react`", "is `studio-client@1.5` safe", "I want the new repeater fix", "upgrade to studio-react 2", "what broke in 2.0", "registration stopped type-checking after the upgrade". Do NOT use to install for the first time (`install-studio`). Do NOT use to develop the SDK itself. > **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 (**ask the user before reading or refreshing it**: refreshing rotates the refresh token; see that skill for where it lives). **Don't ask for a session `authtoken` up front**: it is the last rung, considered only after a freshly refreshed OAuth token has been observed to fail, and only after explaining the risk. 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. @@ -27,10 +27,11 @@ The `peerDependencies` are `react ^18.0.0 || ^19.0.0` and `react-dom ^18.0.0 || - List the resolved versions of every `@contentstack/studio-*` package with the project's package manager: `npm ls --all | grep @contentstack/studio`, `pnpm why @contentstack/studio-react`, or `yarn why @contentstack/studio-react`. - To see what's published, run `npm view @contentstack/studio-react versions --json` (or `pnpm view` / `yarn info`). -2. **Read the CHANGELOG delta: actually read it.** - - For each package the host app installs, open its CHANGELOG and read every entry from the installed version up to the target. Do not summarize from imagination. Copy or paraphrase actual entries. - - The studio-client CHANGELOG lives at `node_modules/@contentstack/studio-client/CHANGELOG.md` on the consumer side. Same for the other packages. - - Flag any entry that touches: registry singleton, SSR / `"use client"` boundary, `fetchSpec` / `fetchCompositionData` signatures, URL pattern compilation, design-token / font loading, data-binder type-mismatch. Those are the ones most likely to surface integration friction even when the bump is technically non-breaking. +2. **Find out what changed: from the compiler, not from memory.** + - **No changelog ships in the npm packages.** As of 2.0.1, each `@contentstack/studio-*` tarball holds only `README.md`, `dist/` and `package.json`. The changelogs live in the SDK's source repo, which is private, and its `studio-react` changelog stops at 1.8.0. Don't tell the user to read a `node_modules/.../CHANGELOG.md`, and don't summarize release notes you haven't seen. + - **Crossing a major (1.x → 2.x)?** Apply § [Upgrading from 1.x to 2.0](#upgrade-1-to-2) first. + - **Let TypeScript list the breaks.** After installing the target (step 3), run the project's type check (`npx tsc --noEmit`, or the framework's `build`). Every registration, prop schema and fetch call the new types reject shows up there. A JavaScript project gets no such list: use the grep commands in § Upgrading from 1.x to 2.0 instead. + - Pay extra attention to anything touching: registry singleton, SSR / `"use client"` boundary, `fetchCompositionData` signatures, URL pattern compilation, design-token / font loading, data-binder type-mismatch. Those surface integration friction even when nothing fails to compile. 3. **Upgrade the packages the host declares, together.** Bump every `@contentstack/studio-*` package listed in the host's `package.json` in the same change. Usually that is only `@contentstack/studio-react`, as `install-studio` installs it. `studio-react` declares the other Studio packages as its own dependencies, so upgrading it brings in matching versions. Do **not** add those internal packages to `package.json`: a direct pin that falls outside `studio-react`'s range installs a second copy, which splits the registry singleton (the globalThis-backed singleton landed across packages in coordinated bumps). - Pick the exact versions (avoid `^` for the upgrade commit so the resolved versions are reproducible. You can relax to `^` after smoke passes). @@ -56,22 +57,55 @@ The `peerDependencies` are `react ^18.0.0 || ^19.0.0` and `react-dom ^18.0.0 || - `rm -rf node_modules`, then a clean install from that lockfile with the project's package manager: `npm ci`, `pnpm install --frozen-lockfile`, or `yarn install --frozen-lockfile` (`yarn install --immutable` on Yarn 2+). Don't try to selectively downgrade: lockfile reconciliation across the Studio packages is unreliable. - Re-run the smoke check. If you can't get back to a working state with a hard revert, escalate. +<a id="upgrade-1-to-2"></a> + +## Upgrading from 1.x to 2.0 + +`studio-react` 2.0.0 shipped on 2026-10-12 and 2.0.1 on 2026-10-16. Both lists below were checked by installing 1.9.0 (the last 1.x) and 2.0.1 side by side and comparing their types and runtime code. + +### What 2.0 actually changed + +The public `studio-react` exports gained three names and lost none. Most 1.9.0 code compiles unchanged. + +| Change | 1.9.0 | 2.0.1 | Action | Details | +|---|---|---|---|---| +| Not-found errors | A miss throws `ComposableStudioError` (code `COMPOSITION_NOT_FOUND` / `COMPOSITION_NOT_FOUND_BY_URL`); there is no predicate to call | New exports `isCompositionNotFound`, `ComposableStudioError` and `CompositionNotFoundError` | Decide every 404 with `isCompositionNotFound(error)` instead of matching codes or messages | [`handle-404-pages`](handle-404-pages.md) | +| Composition kind | `hasSpec` and `hasTemplate` on the spec options | Both still present but `@deprecated`; new `connectedContentTypeUid` (`null` for freeform) | Read `connectedContentTypeUid` for the kind; never use either flag for a 404. `grep -rnE 'hasSpec\|hasTemplate' src app` | [`handle-404-pages`](handle-404-pages.md) | +| Canvas tokens | `StudioCanvas` writes no design-token variables | `StudioCanvas` writes the token `:root` variables itself once mounted | Delete any `<style>{getSSRStyles(null)}</style>` on the canvas page: it now only causes a hydration error. `grep -rn 'getSSRStyles(null)' src app` | [`setup-section-preview`](setup-section-preview.md) | +| Internal packages | `studio-client` 3.2, `studio-registry` 1.10 | `studio-client` 4.1, `studio-registry` 2.0 | Nothing, unless `package.json` pins one of them directly: then bump it in the same change (step 3) or you get two registries | Step 3 | + +### Also check: rules older guides and code got wrong + +These behave the same on 1.9.0 and 2.0.1, so they are not 2.0 changes. Code copied from older versions of these guides often breaks them, and an upgrade is a good time to fix it. TypeScript reports the first four. + +| Rule | Wrong | Right | Find it | Details | +|---|---|---|---|---| +| `object` props need `properties` | `{ type: "object" }` | `{ type: "object", properties: { … } }` | `grep -rn 'type: "object"' src` | [`register-component`](register-component.md) step 3 | +| `array` props need `items` | `{ type: "array" }` | `{ type: "array", items: { type: "any" } }` (or the per-item prop) | `grep -rn 'type: "array"' src` | [`register-component`](register-component.md) step 3 | +| Lazy components register through `registerLazyComponent` | `registerComponent({ …, component: () => import("./X") })` (runs, but doesn't type-check) | `registerLazyComponent(config, () => import("./X").then((m) => m.X))`; the loader resolves to the component, not the module | `grep -rn 'component: () => import' src` | [`register-component`](register-component.md) § 5b | +| The hook takes no `searchQuery` | `useCompositionData({ url, searchQuery })` | `useCompositionData({ url })`; it reads `window.location` itself | `grep -rn 'useCompositionData(' src app` | [`setup-template-preview-routes`](setup-template-preview-routes.md) | +| `styles` is an object of style groups | `styles: [ … ]` | `styles: { default: …, <group>: … }`; `default` arrives as `className` | `grep -rn 'styles: \[' src` | [`register-component`](register-component.md) § 5c | +| `extendQuery` is deprecated | relied on silently | Still honoured, warns in development; no documented replacement for its `only` half | `grep -rn 'extendQuery' src app` | [`register-component-prop-contracts`](register-component-prop-contracts.md) | +| `refetchData` needs a connected composition | `refetchData` on every page | Fall back to `refetchSpec` on freeform compositions and before the first load | `grep -rn 'refetchData' src app` | [`csr-and-ssr-requirements`](csr-and-ssr-requirements.md) | + +**After the bump:** restart the dev server (a running server keeps the old registrations), run the type check, then the smoke tests in step 4. Check the Components panel on the canvas: a registration that fails at runtime drops out of the palette without an error on the page. + ## Knowing what actually changed -Each package publishes a CHANGELOG derived from Conventional Commits. Read the CHANGELOG for every package in your delta before upgrading. +There is no public changelog for these packages (step 2). Read the compiler output and § Upgrading from 1.x to 2.0. Within a major, compare the installed `.d.ts` files before and after (`diff -r` two installs of `node_modules/@contentstack/studio-react/dist`) when you need more than the type check gives you. -A non-exhaustive list of changes that historically required consumer-side action (read each from its CHANGELOG before claiming it applies to your delta): +A non-exhaustive list of older changes that required consumer-side action (confirm each applies to your delta before claiming it does): - **Registry singleton moved to `globalThis`** (`studio-registry` / `studio-react` coordinated bump): required for RSC server-side fetch to see registered components. If you're upgrading FROM a pre-singleton version TO a post-singleton version, server-side `fetchSpec` calls that used to return "component not registered" will now resolve. No consumer code change beyond the upgrade. - **Basics moved behind a `"use client"` isolation boundary** (`studio-react`): fixes "Attempted to call Page() from the server" in Next App Router. No consumer code change required. The renderer pulls in the client-only basics automatically. - **Tolerant `useData` fallback** (`studio-react-components`): replaces a throw with an empty-data fallback plus a dev-only warning. SSR pass of basics no longer crashes outside a `DataCtxProvider`. Consumer-side: dev console may show one new warning, production unchanged. - **`fetchCompositionData` requires `searchQuery` on the server**: see [`configure-csr-vs-ssr.md`](configure-csr-vs-ssr.md). Required for locale/variant/preview to flow from Studio's iframe params. Pre-existing as an integration step. -Do NOT extrapolate from this list. **Only the actual CHANGELOG entries for your delta** tell you which of these apply. +Do NOT extrapolate from this list. Confirm each against the installed types or a smoke test before claiming it applies. ## What this skill is NOT - Not a guide to first-time installation (use [`install-studio`](install-studio.md)). - Not a guide to SDK-internal development. -- Not a substitute for reading the CHANGELOG. Always read the actual CHANGELOG entries for your version delta. +- Not a substitute for the type check and smoke test. Run both for every version delta. - Not a guide to upgrading the Contentstack Delivery SDK (`@contentstack/delivery-sdk`) or Live Preview Utils (`@contentstack/live-preview-utils`): those are separate packages with their own CHANGELOGs, only loosely coupled to the Studio SDK. diff --git a/skills/studio/use-compositions-api.md b/skills/studio/use-compositions-api.md index e71e936..7d7c5c0 100644 --- a/skills/studio/use-compositions-api.md +++ b/skills/studio/use-compositions-api.md @@ -4,9 +4,9 @@ name: use-compositions-api ## When to use -Create, read, list, update, or delete Studio compositions through the **managed Composable Studio API service**, the REST layer that validates the layout, compresses the `ui` spec, and forwards the caller's identity, so you don't hand-encode entries against the raw CMA. +Create, read, list, update, or delete Studio compositions through the **managed Studio API service**, the REST layer that validates the layout, compresses the `ui` spec, and forwards the caller's identity, so you don't hand-encode entries against the raw CMA. -Use when the project talks to the Composable Studio API service (`/v1/projects/:projectUid/compositions`) and you want guardrails: server-side Studio validation, automatic `ui` compression, `composable_uid` backfill, a referenced-guard on delete, and list/references endpoints. Phrases: "compositions REST API", "Studio API service", "create composition endpoint", "list compositions", "composition references API", "managed composition API". Do NOT use for: raw-CMA / seed-pipeline / migration authoring where you hold a management token and encode the `ui` yourself. That's `author-composition-via-api`. Do NOT use for interactive UI authoring. That's `build-section` / `build-connected-template`. The `ui`-tree shapes themselves are NOT redefined here. They live in `author-composition-via-api`. +Use when the project talks to the Studio API service (`/v1/projects/:projectUid/compositions`) and you want guardrails: server-side Studio validation, automatic `ui` compression, `composable_uid` backfill, a referenced-guard on delete, and list/references endpoints. Phrases: "compositions REST API", "Studio API service", "create composition endpoint", "list compositions", "composition references API", "managed composition API". Do NOT use for: raw-CMA / seed-pipeline / migration authoring where you hold a management token and encode the `ui` yourself. That's `author-composition-via-api`. Do NOT use for interactive UI authoring. That's `build-section` / `build-connected-template`. The `ui`-tree shapes themselves are NOT redefined here. They live in `author-composition-via-api`. > **Auth preflight: settle the credential before the first API call.** The service accepts OAuth Bearer. Resolve it OAuth-first per [`authenticate-cma`](authenticate-cma.md): `CS_OAUTH_ACCESS_TOKEN`, else the Contentstack MCP's stored session (**ask the user before reading or refreshing it**: refreshing rotates the refresh token; see that skill for where it lives). **Don't ask for a session `authtoken` up front**: it is the last rung, considered only after a freshly refreshed OAuth token has been observed to fail, and only after explaining the risk. 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. @@ -14,7 +14,7 @@ Use when the project talks to the Composable Studio API service (`/v1/projects/: ## Context -The **Composable Studio API service** is a REST wrapper over the CMS. Instead of building a composition entry by hand, you `POST` the layout and the service does the mechanical work: +The **Studio API service** is a REST wrapper over the CMS. Instead of building a composition entry by hand, you `POST` the layout and the service does the mechanical work: - **You send the `ui` tree uncompressed** (a JSON object). The service compresses it to `zlib:<base64>`. You never touch zlib. - **The service validates** the tree against Studio's structural rules and returns all problems at once, field-anchored. @@ -43,8 +43,6 @@ All routes are prefixed with `/v1` and nested under the project. Base URL per re | Create a section | `POST …/compositions/sections` | | List / query | `GET …/compositions` | | Fetch one | `GET …/compositions/{uid}` | -| Fetch, assert template | `GET …/compositions/{uid}/template` | -| Fetch, assert section | `GET …/compositions/{uid}/section` | | List references | `GET …/compositions/{uid}/references` | | Update (partial merge) | `PUT …/compositions/{uid}` | | Delete (guarded) | `DELETE …/compositions/{uid}` | @@ -85,17 +83,34 @@ curl -X POST "$STUDIO_API/v1/projects/$PROJECT/compositions" \ "place_composition_as": "page", "url": "/blog/{{entry.slug}}", "connected_content_type": "blog_post", - "ui": { "uid": "root", "type": "page", "props": {}, "slots": {}, "metadata": {} }, + "ui": { "uid": "root", "type": "page", "attrs": {}, "metadata": {}, + "props": {}, "slots": {}, "styles": {} }, "data_sources": [] }' ``` +Body shapes (`ui` as an object, `data_sources` as an array, no `entry` wrapper) differ from the raw CMA: [`author-composition-entry`](author-composition-entry.md#wire-shapes) § Wire shapes is the one definition. Every node carries all seven keys (`uid`, `type`, `attrs`, `metadata`, `props`, `slots`, `styles`). + On success (`201`) the response is `{ notice, composition, warnings? }`. The returned `composition.ui` is the **compressed** string as stored. `composable_uid` equals the entry uid unless you supplied one. Non-blocking findings ride along in `warnings[]`. ### Step 3: Handle validation Blocking problems return **`422` `composition_invalid`** with every failing path at once: `errors: { "<path>": ["<message>"] }`. Fix each path and resend. Warnings don't block, surface them but proceed. +A common `422`: `Empty slots crash the renderer.` on a `slots` entry holding `[]`, most often a `section-slot` node written as `{ "<own-uid>": [] }`. Write `"slots": {}` ([`author-composition-sections`](author-composition-sections.md) § `section-slot` node shape). + +<a id="expected-validator-warnings"></a> + +#### Expected validator warnings {#expected-validator-warnings} + +Three warnings show up on correct compositions. Report them as expected, don't "fix" them: + +| Warning | Why it appears on a correct composition | Act only if | +|---|---|---| +| `unknown_type` on a registered component's node | Components are registered in the browser (`registerComponent` in the app), so the service never sees the registry. It flags every non-built-in `type` | The `type` string differs from the one in the app's `registerComponent` call. Check that by hand | +| `linked_sections_required` on a template | Raised even when every placed section is already in `linked_sections` | A `section-composition` node's `metadata.compositionUID` is missing from `linked_sections`. Compare the two sets | +| "items won't render" on the item Box under a Repeater | The [canonical list recipe](author-composition-repeater-recipes.md#canonical-list-recipe) puts a Box between the Repeater and its Condition Blocks so Visual Editor gets item tags | The Box holds no Condition Block on a reference or modular-block list | + ### Step 4: Read, list, reference - **Fetch one:** `GET …/{uid}` returns the entry with `ui` compressed by default. Add `?decompression=true` to get the node tree back (and `data_sources` parsed). @@ -139,7 +154,8 @@ A composition created through this API is not finished when the write returns `2 | Pitfall | Why it bites | Fix | |---|---|---| | Sending `ui` pre-compressed | The service compresses for you. A `zlib:` string is treated as the tree and fails validation | Send the **uncompressed node object** | -| `data_sources` as a JSON string | The service stringifies it. A string double-encodes | Send a plain **array** | +| `data_sources` as a JSON string | The service stringifies it. A string answers `400` | Send a plain **array** ([Wire shapes](author-composition-entry.md#wire-shapes)) | +| Treating `unknown_type` or `linked_sections_required` as errors | They fire on correct compositions | See § Expected validator warnings | | Blank `composable_uid` | A whitespace-only value is rejected, not backfilled | Omit it, or send a real slug. A blank value gives `422` | | Duplicate `composable_uid` in another locale | Uniqueness is content-type-wide, not per-locale | Expect `409` at create. Pick a unique slug | | Treating `422 stack_not_found` as "missing" | It's the **access-denied** code (Studio convention, not 403) | Check the caller's rights on the project's stack | diff --git a/skills/studio/use-condition-block.md b/skills/studio/use-condition-block.md index 3d7475f..b794444 100644 --- a/skills/studio/use-condition-block.md +++ b/skills/studio/use-condition-block.md @@ -52,7 +52,7 @@ If `repeaterSource` is a Reference and the user gives only one UID, still create This skill succeeds only when ALL of the following are true. If any fails, do not claim success: surface the failure and stop. -- [ ] In Layers the tree reads `Repeater > [Condition Block × N]`: each Condition Block is a direct child of the Repeater, or of the one Box that is the Repeater's only child (for Visual Editor's item tags: [`make-visual-editor-editable`](make-visual-editor-editable.md) § 2), never wrapping it. +- [ ] In Layers the tree reads `Repeater > [Condition Block × N]`: each Condition Block is a direct child of the Repeater, or of the one Box that is the Repeater's only child (for Visual Editor's item tags: [`make-visual-editor-editable`](make-visual-editor-editable.md) § 2), never wrapping it. See the [canonical list recipe](author-composition-repeater-recipes.md#canonical-list-recipe). - [ ] There is exactly one Condition Block per UID in `branches`. UIDs not in `branches` do not have a Condition Block. - [ ] Each Condition Block's `condition.type` metadata is set to `modular_block` (Modular Block source) or `reference` (Reference source), matched to a specific block / content-type UID, never to a shared field name like `title`. - [ ] Inside each Condition Block, the type-specific component's props are bound via `repeater.<field>` paths. The Data Picker shows the typed fields of that Condition Block's schema, not a generic union. diff --git a/skills/studio/use-repeater.md b/skills/studio/use-repeater.md index da81dba..3745d0e 100644 --- a/skills/studio/use-repeater.md +++ b/skills/studio/use-repeater.md @@ -20,7 +20,7 @@ A Repeater is a Smart Container that renders its child subtree once per item of **The scope rule, in user terms:** clicking inside a Repeater switches the Data Picker roots. Outside the Repeater = the Section's linked-schema fields. **Inside the Repeater = the iteration item's fields, labeled "Repeater Data".** Bind from whichever root the picker shows. The location determines the shape, not your choice. -When the iteration items are heterogeneous (Modular Block lists, or References [single-CT and multi-CT]), the immediate child of the Repeater must be a Condition Block. Studio surfaces an inline hint when you bind a child directly and offers to wrap it for you. For Visual Editor's add / move / delete controls, then put those Condition Blocks inside one Box that is the Repeater's only child: [`make-visual-editor-editable`](make-visual-editor-editable.md) § 2. +When the iteration items are heterogeneous (Modular Block lists, or References [single-CT and multi-CT]), the Repeater's only child is an item Box, and the Condition Blocks sit inside that Box. Studio's inline hint offers to wrap the bound child directly in a Condition Block; that puts it in the wrong place, so move it inside the item Box. Follow the [canonical list recipe](author-composition-repeater-recipes.md#canonical-list-recipe). ## What goes into a repeating Slot: a Section, not a raw component @@ -39,7 +39,7 @@ When the Repeater contains a Section Slot (the common "list of cards" pattern), - For a Pinned Query: pick under **Pinned Queries**. - The picked node must be a list/array. The picker disables scalar leaves for this slot. -5. **Wrap iteration in a Condition Block when required.** If `itemKind` is `modular-block` or `reference` (single-CT or multi-CT), the immediate child of the Repeater must be a Condition Block. The cleanest path: drop the `iterationComponent` directly inside the Repeater first. When you bind any prop on it, Studio shows an inline hint that this source needs a Condition Block. Accept the wrap prompt and Studio inserts the Condition Block around the component, scoping it to the active item type. For `group` and `array` of scalars, skip this step. +5. **Wrap iteration in a Condition Block when required.** If `itemKind` is `modular-block` or `reference` (single-CT or multi-CT), put one item Box inside the Repeater as its only child, then a Condition Block inside that Box around the `iterationComponent`, scoped to the active item type ([canonical list recipe](author-composition-repeater-recipes.md#canonical-list-recipe)). If you accept Studio's wrap prompt instead, it inserts the Condition Block directly under the Repeater; move it inside the item Box. For `group` and `array` of scalars, skip this step. 6. **Drop the iteration content inside the Repeater.** - **Preferred: drop a Section.** If the iteration content is non-trivial (a card with title + image + price + rating, etc.), build it as its own Section bound to the iterated item's CT and drop that Section into the Repeater (or into the Condition Block if step 5 applied, or into a Section Slot inside the Repeater. See the "What goes into a repeating Slot" pattern in Context above). The Section is pre-bound, nothing to wire per drop. diff --git a/skills/studio/verify-setup.md b/skills/studio/verify-setup.md index 16b31f8..bf65885 100644 --- a/skills/studio/verify-setup.md +++ b/skills/studio/verify-setup.md @@ -43,17 +43,30 @@ Use existing compositions for verification where possible. Any temporary authori - Before testing any app-side wiring, ask the user to confirm ALL of: - `Settings → Visual Experience → General → Live Preview → Enable Live Preview` checkbox is **checked + Save clicked** - A `Default Preview Environment` is selected on the same page - - The `Preview URL` sub-tab has a `Custom Preview URL` set (and is NOT showing the "Custom Preview URL Unavailable" lockout banner) + - If authors open Live Preview or Visual Editor **from an entry**, the `Preview URL` sub-tab has a `Custom Preview URL` set (and is NOT showing the "Custom Preview URL Unavailable" lockout banner). The Studio canvas alone doesn't use it: see [`enable-visual-experience`](enable-visual-experience.md#custom-preview-url) - The Delivery Token in `.env` has a Preview Token generated (the **Create Preview Token** button on the token edit page must have been clicked at least once) - **The environment your Studio project targets has a non-empty per-locale URL** for your locale, and for **every** other locale the app serves on a localized project (in the stack, open Settings, then Environments, then `<env>`, and read the URL for `<locale>`). This is the canvas Base URL (origin). Empty = blank canvas with an error that points at the wrong layer. For local dev it must equal your dev origin (e.g. `http://localhost:5173`). - If any of these is unconfirmed (or the user is unsure), STOP and route them through `enable-visual-experience` (Live Preview) or `setup-section-preview` (the env Base URL). Re-run this skill after that completes. - Catches the most common failure: app wired, stack-level flag off, runtime silently broken. Skipping makes Layer 2 failures look like SDK bugs when they're actually web-app toggles. -1. **Layer 1: Delivery SDK reachability.** - - Run: `npx tsx -e "import('@contentstack/delivery-sdk').then(async cs => { const stack = cs.default.stack({ apiKey: process.env.CS_API_KEY, deliveryToken: process.env.CS_DELIVERY_TOKEN, environment: process.env.CS_ENVIRONMENT || 'preview' }); const r = await stack.contentType('home_page').entry().limit(1).find(); console.log('OK entries:', r.entries?.length ?? 0); })"` - - Or, programmatically, call `stack.contentType('<uid>').entry().limit(1).find()` for a known CT in the app. - - Pass: a query returns without throwing. - - Fail modes: 401 means a wrong Delivery Token, 422 a wrong environment name, 404 a wrong CT UID, and ECONNREFUSED a wrong region host. +1. **Layer 1: Delivery SDK reachability.** Probe with the credentials the app itself loads, without printing them and without assuming any content type. + + **Reading the app's credentials.** Find the variable names in the app's Contentstack init module (the file `install-studio` wrote, or the app's own stack factory), and the env file that sets them (`.env.local`, `.env`). Load that file inside a subshell so the values never reach the transcript or your shell history, and print only status lines. Below, `<KEY_VAR>`, `<DELIVERY_VAR>`, `<PREVIEW_VAR>` and `<ENV_VAR>` are those names (for an `install-studio` app, `<PREFIX>_CONTENTSTACK_API_KEY` and so on). If the values come from a secrets manager or runtime config instead of a file, ask the user to run the probe themselves and paste only the output. + + ```bash + ( set -a; . ./.env.local; set +a # the app's own env file + H="<cdn-host>" # cdn.contentstack.io, or the regional CDN host + CT=$(curl -s "https://$H/v3/content_types?limit=1" \ + -H "api_key: ${<KEY_VAR>}" -H "access_token: ${<DELIVERY_VAR>}" | jq -r '.content_types[0].uid // empty') + echo "first content type: ${CT:-none}" + curl -s -o /dev/null -w "entries probe: HTTP %{http_code}\n" \ + "https://$H/v3/content_types/$CT/entries?environment=${<ENV_VAR>}&limit=1" \ + -H "api_key: ${<KEY_VAR>}" -H "access_token: ${<DELIVERY_VAR>}" ) + ``` + + - Any content type will do, so the probe takes the first one the stack returns rather than a guessed uid such as `home_page`. Record it: Layer 6 reuses it. + - Pass: a content type uid prints and the entries probe returns 200. + - Fail modes: `first content type: none` means the API key, Delivery Token or CDN host is wrong (rerun the first `curl` with `-w '%{http_code}'`: 401 is a credential, a connection error is the host); the entries probe then 404s on the empty uid, which says nothing more. A 422 on the entries probe means a wrong environment name. 2. **Layer 2: Live Preview iframe channel.** - Open the host app at the Canvas URL in a browser. @@ -66,6 +79,7 @@ Use existing compositions for verification where possible. Any temporary authori - Click the "Preview" or eye icon, and the Visual Editor opens. - Pass: the entry preview shows, fields have hover-edit highlights, clicking a field opens its edit panel. - Fail modes: a blank iframe means the Canvas URL is wrong. Missing field highlights mean the `data-cslp` tags aren't being injected (the app isn't using `live-preview-utils` correctly). Stuck on "Initializing the editor…" means Live Preview is not in `mode: "builder"`. + - Page check (before opening the editor): the app's Live Preview init sets `cslp.appendTags: true` (or the components emit tags themselves), and a rendered page's HTML has `data-cslp` on each registered component's root and on its bound fields. No tags here means Layer 3 cannot pass. - Lists: the rendered HTML has a `data-cslp="….<field>"` tag and a `….<field>.0` tag for every list, or Visual Editor cannot add, move or delete items ([`make-visual-editor-editable`](make-visual-editor-editable.md) § 2). 4. **Layer 4: Studio canvas.** @@ -74,7 +88,7 @@ Use existing compositions for verification where possible. Any temporary authori - Fail modes: - A blank canvas means the canvas iframe address is wrong. Check both halves: the **Canvas URL** (path) on the Studio project must match the mounted route, AND the **targeted environment's per-locale Base URL** (origin) must be non-empty and reachable. An empty env Base URL is the most common cause and surfaces as a "no base URL"-style error pointing at the Studio project, not the environment. - "Component Loading Error" means the section ref's `compositionUID` doesn't resolve (sections not preloaded or fetched from CDA) - - "Authentication failed" means the Delivery or Preview token in `.env` is wrong or revoked. In the stack, open Settings, then Tokens, then the token itself to read its current values, paste those into `.env`, and restart the dev server + - `STACK_NOT_FOUND_ON_HOST` or 401/412: a key or token in `.env` is wrong. In the stack, open Settings, then Tokens, then the token itself to read its current values, paste those into `.env`, and restart the dev server ### Expected (not-a-failure) console messages when verifying @@ -107,7 +121,7 @@ Use existing compositions for verification where possible. Any temporary authori ```bash curl -I -sS --max-time 5 ${canvasAppUrl} | grep -iE 'content-security-policy|access-control-allow-origin' ``` - Pass: the canvas route's `Content-Security-Policy: frame-ancestors` permits the Studio origin (e.g. `frame-ancestors 'self' https://*.contentstack.com`). Wildcards (`frame-ancestors *`, `Access-Control-Allow-Origin: *`) are acceptable only on a local dev server, never on a deployed site. Fail: missing, or Studio's origin not permitted, so see `setup-local-https-canvas` / `setup-section-preview` for the Vite/Next config block. The Studio iframe will silently fail to embed without it. + Pass: the canvas route sends `Content-Security-Policy: frame-ancestors 'self' https://*.contentstack.com` (or names your Studio host). Framing is decided by `frame-ancestors` alone: an `Access-Control-Allow-Origin` header is not needed for it, and `frame-ancestors *` or `Access-Control-Allow-Origin: *` on the canvas route is a finding to remove, not a pass. Fail: missing, or Studio's origin not permitted, so see `setup-section-preview` step 5 for the Vite/Next config block. The Studio iframe will silently fail to embed without it. - **5d. Arity ≥ 1 on every registered component**: ```bash @@ -120,23 +134,24 @@ Use existing compositions for verification where possible. Any temporary authori - If it fails, immediately apply the section-test diagnostic: open a section composition. If the section renders, the SDK is fine. The failure is the connected template's URL match. See `troubleshoot-composition-resolution` (entry `url` field needs a leading slash, and composition `url` must contain a `{{entry.x}}` pattern). - Also verify the env base URL scheme: in `GET /v3/environments/<env>`, `urls[].url` must be HTTPS if the canvas-app is HTTPS. -6. **Layer 6: Preview Token authenticates with the CDA preview API** (behavior-based, project-agnostic, does NOT assume any specific env file path or env var name). +6. **Layer 6: Preview Token authenticates with the CDA preview API** (behavior-based, project-agnostic: variable names come from the app's init module, not from a guessed convention). The most common silent failure on a fresh setup: the **Preview Token** passed to the Delivery SDK / Live Preview SDK is invalid or empty, the CDA preview API returns 401 without CORS headers, and the browser surfaces a misleading "CORS error" instead of "401 Unauthorized." Hours of CORS chasing for a token paste. Verify the token authenticates **before** opening Studio. - **The check is project-agnostic.** Don't grep `.env.local`, don't assume `VITE_*` / `NEXT_PUBLIC_*` / `REACT_APP_*` env var names: every framework / project layout differs. Ask the user for the API key and host and curl-verify directly. **Never ask the user to paste the Preview Token into chat, and never print it.** Have them export it in their own shell (`export PREVIEW_TOKEN=…`, from wherever the project loads it) so the curl below reads `${PREVIEW_TOKEN}`. + **The check is project-agnostic.** Don't assume `VITE_*` / `NEXT_PUBLIC_*` / `REACT_APP_*` names: read them from the app's init module, and load the env file in a subshell as in Layer 1. **Never ask the user to paste the Preview Token into chat, and never print it.** When the token isn't in a file (secrets manager, runtime config), ask the user to run the curl themselves and paste only the status line. Inputs: - `stackApiKey`: the stack's API key the canvas-app uses - - `PREVIEW_TOKEN` (exported by the user in their shell, not pasted into chat): the Preview Token the canvas-app currently passes to `@contentstack/live-preview-utils` / Delivery SDK at runtime, wherever it's loaded from (env file, runtime config, secrets manager, fetched-from-API, hardcoded, doesn't matter) + - The Preview Token the canvas-app passes to the Delivery SDK's `live_preview` block at runtime, read from the app's env file inside the subshell (Layer 1), never echoed - `previewHost`: region-dependent: `rest-preview.contentstack.com` (NA prod), `eu-rest-preview.contentstack.com` (EU prod), `azure-na-rest-preview.contentstack.com`, etc. (see `install-studio` § Regional host map). For a custom / private data center (hosts supplied by Contentstack), take the explicit host from [`analyze-project-fit`](analyze-project-fit.md) § 2b rather than asking again. - - One `<CT>`, `<env>`, `<locale>` from the project to query against: pick any content type the user knows has at least one entry in that environment. + - The content type Layer 1 found, the app's environment, and its default locale. Run: ```bash - curl -is "https://${previewHost}/v3/content_types/${CT}/entries?environment=${env}&locale=${locale}&limit=1" \ - -H "api_key: ${stackApiKey}" \ - -H "preview_token: ${PREVIEW_TOKEN}" + ( set -a; . ./.env.local; set +a + curl -s -w "\nHTTP %{http_code}\n" \ + "https://<previewHost>/v3/content_types/<CT>/entries?environment=${<ENV_VAR>}&locale=<locale>&limit=1" \ + -H "api_key: ${<KEY_VAR>}" -H "preview_token: ${<PREVIEW_VAR>}" | tail -c 300 ) ``` Outcomes: @@ -154,7 +169,7 @@ Use existing compositions for verification where possible. Any temporary authori Inputs: - `previewUrl`: any visitor URL that resolves to a Connected Template that has sections placed on it (e.g. `https://localhost:3006/products/some-product`, `https://your-staging-site/blog/some-post`). The canvas-app must be running and reachable at this URL. - Run (as a one-off Node script using the canvas-app's already-installed SDK): + Run as a one-off `.mjs` script from the app's root, so it uses the app's installed SDK. Replace the `process.env.*` names with the ones the app's init module reads, and load the app's env file with Node itself: `node --env-file=.env.local verify-sections.mjs` (Node 20.6+). No `tsx`, nothing exported in the user's shell. ```js import contentstack from "@contentstack/delivery-sdk"; @@ -219,20 +234,19 @@ Use existing compositions for verification where possible. Any temporary authori 1. `canvasAppUrl`: used in Layer 2 (open in browser) + Layer 4 (compare against Studio project config). 2. `studioUrl`: used in Layer 4. -3. `stackApiKey`: used in Layer 6 (curl against preview API). -4. `PREVIEW_TOKEN`: used in Layer 6. The user exports it in their own shell (never pasted into chat, never printed) from wherever the project loads it (env file, runtime config, secrets manager, hardcoded). Do NOT assume an env file path or variable name. -5. `previewHost`: used in Layer 6. Region-dependent (see `install-studio` § Regional host map). Custom / private data center (hosts supplied by Contentstack): the explicit host the user gives you. -6. One `<CT>`, `<env>`, `<locale>` triple: used in Layer 6 as a known-good probe target. Any content type the user knows has at least one entry in that environment. -7. `previewUrl`: used in Layer 7. Any visitor URL on the canvas-app that resolves to a Connected Template with sections placed on it (e.g. `http://localhost:3006/products/some-product` for local dev). Skip Layer 7 if the project has no section compositions placed on any template yet. +3. Credentials: none asked for. Layers 1 and 6 read the API key, Delivery Token, Preview Token and environment from the app's own env file inside a subshell, using the variable names in its init module, and print only status lines. Ask the user to run a probe themselves only when the values aren't in a file. +4. `cdnHost` / `previewHost`: used in Layers 1 and 6. Region-dependent (see `install-studio` § Regional host map). Custom / private data center (hosts supplied by Contentstack): the explicit host from `analyze-project-fit` § 2b. +5. `<locale>`: the app's default locale. The content type comes from Layer 1. +6. `previewUrl`: used in Layer 7. Any visitor URL on the canvas-app that resolves to a Connected Template with sections placed on it (e.g. `http://localhost:3006/products/some-product` for local dev). Skip Layer 7 if the project has no section compositions placed on any template yet. -If `.env` exists in the project, read `CANVAS_APP_BASE_URL` and `STUDIO_BASE_URL` as defaults for the first two before asking. Do NOT auto-read or print the Preview Token from any env file: its location is project-specific. +If `.env` exists in the project, read `CANVAS_APP_BASE_URL` and `STUDIO_BASE_URL` as defaults for the first two before asking. Never print a token, and never load an env file outside a subshell. ## Acceptance This skill succeeds only when Layer 0 + ALL seven downstream layers were attempted (Layer 7 may be skipped if the project has no section compositions placed on any template) and a clear pass/fail recorded for each. -- [ ] Layer 0 confirmed (Enable Live Preview checked + Default Preview Environment set + Custom Preview URL set + Preview Token created on the Delivery Token + targeted environment has a non-empty Base URL for every locale the project serves). -- [ ] Layer 1 result reported (delivery SDK call result or HTTP status code). +- [ ] Layer 0 confirmed (Enable Live Preview checked + Default Preview Environment set + Custom Preview URL set where entry-opened preview is used + Preview Token created on the Delivery Token + targeted environment has a non-empty Base URL for every locale the project serves). +- [ ] Layer 1 result reported (the content type found and the entries probe's HTTP status). No content type uid was assumed, and no credential was printed or asked for in chat. - [ ] Layer 2 result reported (live-preview event seen / not seen within timeout). - [ ] Layer 3 result reported (entry preview opens / fails / can't even open the entry). - [ ] Layer 4 result reported (canvas DOM tree present in the iframe / not present / specific error string). diff --git a/skills/studio/wire-external-data.md b/skills/studio/wire-external-data.md index 6b5a95e..53431b5 100644 --- a/skills/studio/wire-external-data.md +++ b/skills/studio/wire-external-data.md @@ -120,7 +120,7 @@ This skill succeeds only when ALL of the following are true. If any fails, surfa | `useSearchParams` in a server component | Build / runtime error | Read `searchParams` prop (App Router) or `context.query` (Pages Router) instead | | Binding chip points at `template.*` or `contentstack.*` | Wrong picker root selected | Re-open picker, choose **Component Default Data** explicitly | | **Binding a `file` (multiple) field directly to an `imageurl` / scalar URL prop**, renders "No image" or shows the array `.toString()` | A multiple-file field is an asset array, not a scalar URL. Binding to `images` yields the whole array. The prop expects a single string. | Bind to the **first asset's URL**: `entryBind("imageurl", "images.0.url")`. If the prop should iterate (multiple images shown), use a **Repeater** bound to the array and bind `imageurl` to `repeater.url` inside the repeater item. `.0.url` is for single-image props only. | -| **Binding a `file` (SINGLE) field directly to an `imageurl` / scalar URL prop**, `<img>` renders broken / `src` is `[object Object]` / built-in `Image` renders the default placeholder | A single-file field is an **asset object** (`{ uid, url, content_type, filename, title, ... }`), NOT a URL string. Binding `featured_image` to a string-expecting prop yields the whole object. The data binder treats `imageurl` as a string. There's no special unwrap. Built-in `Image` then renders `<img src="[object Object]">` (or falls back to the default-placeholder URL when the asset object stringifies to empty). | **Bind the `.url` subfield**: `entryBind("imageurl", "featured_image.url")`. For the built-in `Image` component (prop `src` typed `imageurl`), bind to `<file_field>.url`, e.g. `featured_image.url`. For custom registered components consuming the asset object, you can also accept the full object and read `asset.url` yourself. A small `assetUrl()` helper handling `string \| { url }` is the safest cross-binding signature. | +| **Binding a `file` (SINGLE) field directly to an `imageurl` / scalar URL prop**, `<img>` renders broken / `src` is `[object Object]` / built-in `Image` renders the default placeholder | A single-file field is an **asset object** (`{ uid, url, content_type, filename, title, ... }`), NOT a URL string. Binding `featured_image` to a string-expecting prop yields the whole object. The data binder treats `imageurl` as a string. There's no special unwrap. Built-in `Image` then renders `<img src="[object Object]">` (or falls back to the default-placeholder URL when the asset object stringifies to empty). | **Bind the `.url` subfield**: `entryBind("imageurl", "featured_image.url")`. For the built-in `Image` component (prop `src` typed `imageurl`), bind to `<file_field>.url`, e.g. `featured_image.url`. Same rule for custom registered components: bind `.url`, never the asset object (in 2.0.1 `imageurl` accepts only a string and reports the object as a type mismatch), and bind alt text to a separate `string` prop from `featured_image.description`. Keep an `assetUrl()` helper handling `string \| { url }` in the component as a safety net only. See [`register-component-prop-contracts`](register-component-prop-contracts.md) § Image props. | ## See also diff --git a/skills/studio/wire-studio-state.md b/skills/studio/wire-studio-state.md index de787ba..434fd34 100644 --- a/skills/studio/wire-studio-state.md +++ b/skills/studio/wire-studio-state.md @@ -26,7 +26,7 @@ Do **not** use `node -e "require('@contentstack/studio-react/package.json')"`: t **Availability has two independent halves**: the SDK in your app and the Studio editor. The editor half (declared variables in the Data Picker, the `action` prop picker) is **not yet rolled out on every data center**, so a stack whose editor lacks it can't author BYOS even with a correct SDK build. Check the DC alongside the export: [`analyze-project-fit`](analyze-project-fit.md) § 2b. -**`BYOS ABSENT` means stop.** Releases up to `1.7.0` are ABSENT; `1.9.0` ships it. This chapter applies only to a build that ships it. Either obtain such a build, or fall back to component-local state (a plain `useState` inside one registered component, no author binding, no cross-component sharing) and say so explicitly. Don't emit `useStudioState` against a package that can't import it. +**`BYOS ABSENT` means stop.** Releases up to `1.7.0` are ABSENT; `1.8.0` and later ship it (checked on npm: 1.8.0's `index.d.ts` exports `registerGlobalStateVariables`, `studioState` and `useStudioState`). This chapter applies only to a build that ships it. Either obtain such a build, or fall back to component-local state (a plain `useState` inside one registered component, no author binding, no cross-component sharing) and say so explicitly. Don't emit `useStudioState` against a package that can't import it. ## Context @@ -146,7 +146,7 @@ Registered components give Studio your markup. **BYOS gives it your behavior.** ## Acceptance -- [ ] **Preflight passed**: the installed `@contentstack/studio-react` actually exports the state APIs (≥ 1.7.0). Nothing was written against a version that can't import them. +- [ ] **Preflight passed**: the installed `@contentstack/studio-react` actually exports the state APIs (1.8.0 or later; 1.7.0 does not). Nothing was written against a version that can't import them. - [ ] Every variable authors need is **declared** (init or component tier): nothing relies on an ad-hoc key being bindable. - [ ] The tier choice is deliberate, and no key is declared in both places expecting init to win. - [ ] `storage` is set explicitly, and for `custom` both `getState` and `setState` exist.