Build Optimizely CMS components for Astro v5. Use when creating/modifying components, pages, experiences, or working with GraphQL fragments, opti-type.json, or opti-style.json files (project)
Build production-ready Optimizely CMS components with proper GraphQL integration, daisyUI styling, and TypeScript type safety.
ALWAYS use this skill when the user asks to:
.opti-type.json files (content type definitions).opti-style.json files (style/display template definitions).graphql or .dam.graphql files)allComponents.graphql or similar aggregation filesYOU MUST FOLLOW THESE STEPS IN ORDER:
READ THE GUIDES FIRST - This skill includes comprehensive guides that YOU MUST reference:
CONTENTTYPE-GUIDE.md - For creating .opti-type.json filesSTYLE-GUIDE.md - For creating .opti-style.json filesGRAPHQL-PATTERNS.md - For creating .graphql and .dam.graphql filesEXAMINE EXISTING COMPONENTS - Always look at similar existing components as examples before creating new ones. Check src/cms/components/ for patterns.
CREATE ALL REQUIRED FILES - A complete component needs 5 files minimum:
.astro - Component template with TypeScript.opti-type.json - Content type definition.opti-style.json - Style definition(s) (can have multiple).graphql - Base GraphQL fragment.dam.graphql - DAM-enabled GraphQL fragmentINTEGRATE PROPERLY - After creating files:
src/cms/components/allComponents.graphqlyarn type:push ComponentName and yarn style:push StyleNameyarn codegenCreating a new "Testimonial" component:
# 1. Create component directory
mkdir -p src/cms/components/TestimonialComponent
# 2. Create required files
src/cms/components/TestimonialComponent/
├── Testimonial.astro # Component template
├── Testimonial.opti-type.json # Content type definition
├── DefaultTestimonial.opti-style.json # Default style
├── testimonial.graphql # Base GraphQL fragment
└── testimonial.dam.graphql # DAM GraphQL fragment
Every component needs at least 5 files:
.astro - Component TemplateIMPORTANT: Always follow the pattern from existing components like Button.astro
---
import type {
TestimonialFragment,
DisplaySettingsFragment,
} from '../../../../__generated/sdk';
import type { ContentPayload } from '../../../graphql/shared/ContentPayload';
import { isEditContext } from '../../shared/utils.ts';
const isCmsEdit = isEditContext(Astro.url);
export interface Props {
key: string;
data: TestimonialFragment;
displaySettings: DisplaySettingsFragment[];
displayTemplateKey: string;
contentPayload: ContentPayload;
}
const { key, data, displaySettings, displayTemplateKey, contentPayload } = Astro.props as Props;
// Your styling logic here - reference STYLE-GUIDE.md
const componentClass = 'component-testimonial';
---
<div data-epi-block-id={isCmsEdit && key || undefined} class={componentClass}>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<p class="text-lg italic" set:html={data.Quote?.html}></p>
<div class="card-actions justify-end">
<p class="font-bold">{data.Author}</p>
</div>
</div>
</div>
</div>
Key Points:
__generated/sdk (NOT from @/graphql/__generated/graphql)key, data, displaySettings, displayTemplateKey, contentPayloaddata-epi-block-id for CMS editing contextset:html for rich text fields.opti-type.json - Content TypeSee CONTENTTYPE-GUIDE.md for complete guide on creating content types.
.opti-style.json - Style DefinitionsSee STYLE-GUIDE.md for complete guide on creating style definitions.
.graphql + .dam.graphql - GraphQL FragmentsALWAYS create both versions. See GRAPHQL-PATTERNS.md for complete details.
Base (testimonial.graphql):
fragment Testimonial on Testimonial {
Quote { html }
Author
AuthorTitle
AuthorImage {
...ContentUrl
}
}
DAM (testimonial.dam.graphql):
fragment Testimonial on Testimonial {
Quote { html }
Author
AuthorTitle
AuthorImage {
...ContentUrl
...ContentReferenceItem
}
}
fragment AllComponentsExceptGrid on _IComponent {
...Text
...Button
...Testimonial # Add your component here
}
Important: Add to AllComponentsExceptGrid, NOT AllComponents
# Push content type and styles to CMS
yarn type:push ComponentName
yarn style:push StyleName
# ⚠️ Wait ~10 seconds for Optimizely Graph to sync
# Then generate TypeScript types
yarn codegen
Check __generated/graphql.ts for your component type.
src/cms/components/ - Reusable UI (Button, Card, Hero)src/cms/pages/ - Page types (ArticlePage, LandingPage)src/cms/experiences/ - Experience templatessrc/cms/compositions/ - Layout elements (Row, Column)Quick reference (full details in GRAPHQL-PATTERNS.md):
LinkUrl - Links with url, title, target, textContentUrl - Content reference URLsLinkCollection - Simple link arraysContentReferenceItem - DAM metadata (.dam.graphql only)DisplaySettings - Display/styling settingsPageUrl - Page metadataRich text fields: Always use { html }:
Body { html }
Description { html }
Prefer daisyUI components:
<button class="btn btn-primary btn-lg">
<div class="card card-compact">
<div class="hero min-h-screen">
Use theme variables:
<div class="bg-base-100 text-base-content">
Mobile-first responsive:
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
Glob to find similar components in src/cms/components/Create the component directory and all required files:
mkdir -p src/cms/components/ComponentNameComponent
cd src/cms/components/ComponentNameComponent
Create these files in order:
ComponentName.opti-type.json - Content type definition
CONTENTTYPE-GUIDE.md for structure.opti-type.json as examplebaseType: "component" for componentscompositionBehaviors: ["elementEnabled"] for components usable in visual editorDefaultComponentName.opti-style.json - Default style template
STYLE-GUIDE.md for structurecontentType to match your component's keyisDefault: true for the default stylecomponentName.graphql - Base GraphQL fragment
GRAPHQL-PATTERNS.md.opti-type.json...LinkUrl, ...ContentUrl, etc.componentName.dam.graphql - DAM-enabled GraphQL fragment
componentName.graphql...ContentReferenceItem to content referencesComponentName.astro - Component template
__generated/sdkAdd to allComponents.graphql
# src/cms/components/allComponents.graphql
fragment AllComponentsExceptGrid on _IComponent {
...ExistingComponent
...YourNewComponent # Add this line
}
Push to CMS
# Push content type
yarn type:push ComponentName
# Push style template
yarn style:push DefaultComponentName
# ⚠️ CRITICAL: Wait 10-15 seconds for Optimizely Graph to sync
Generate TypeScript Types
# After waiting for Graph sync
yarn codegen
Check generated types in __generated/sdk.ts:
ComponentNameFragment typeCheck for TypeScript errors:
yarn tsc --noEmit
Test in dev server:
yarn dev
Verify in CMS:
YOU MUST CHECK ALL OF THESE before completing:
✓ All 5 required files created:
ComponentName.astroComponentName.opti-type.jsonDefaultComponentName.opti-style.jsoncomponentName.graphqlcomponentName.dam.graphql✓ Content type (.opti-type.json) is valid:
key, displayName, baseType✓ Style template (.opti-style.json) is valid:
key, displayName, contentTypecontentType matches component keyisDefault: true for default style✓ Both GraphQL fragments created:
.graphql for base version.dam.graphql with ...ContentReferenceItem for DAM✓ Fragment added to allComponents.graphql:
AllComponentsExceptGrid (NOT AllComponents)✓ Pushed to CMS successfully:
yarn type:push ComponentName completedyarn style:push StyleName completed✓ Types generated successfully:
yarn codegen completed without errorsComponentNameFragment type exists in __generated/sdk.ts✓ Component template (.astro) follows patterns:
__generated/sdkdata-epi-block-id for CMS editing✓ No TypeScript errors:
yarn tsc --noEmit passes✓ Component works in dev server:
yarn dev runs without errorsAll guides are bundled with this skill for quick reference:
GraphQL Patterns: See GRAPHQL-PATTERNS.md for:
Content Type Creation: See CONTENTTYPE-GUIDE.md for:
Style Creation: See STYLE-GUIDE.md for:
Component with links:
Links {
...LinkCollection
}
Component with images:
# Base
Image {
...ContentUrl
}
# DAM
Image {
...ContentUrl
...ContentReferenceItem
}
Component with metadata:
_metadata {
types
displayName
}
Solution:
allComponents.graphqlyarn type:push ComponentNameyarn codegen againSolution:
__generated/sdk not @/graphql/__generated/graphqlyarn codegen to regenerate typesSolution:
yarn type:push ComponentName completed successfullybaseType is set correctly in .opti-type.jsoncompositionBehaviors: ["elementEnabled"] is setSolution:
yarn style:push StyleName completed successfullycontentType in .opti-style.json matches component keyisDefault: true is set for default styleSolution:
set:html={data.FieldName?.html} instead of {data.FieldName.html}{ html } for rich text fieldsSolution:
...ContentUrl.dam.graphql includes ...ContentReferenceItemurl?.default or url?.hierarchical is used in templateThis skill also handles Pages and Experiences, which follow the same patterns:
Pages (src/cms/pages/):
baseType: "Page" instead of "Component"mayContainTypes for content areasExperiences (src/cms/experiences/):
This project uses Svelte 5 (latest version with runes) for client-side interactivity. Svelte components are primarily used for admin/utility interfaces, NOT for CMS components.
Use Svelte for:
/opti-admin pages)DO NOT use Svelte for:
.astro instead)State Management with Runes:
<script lang="ts">
// Props using $props() rune
interface Props {
title: string;
count?: number;
}
let { title, count = 0 }: Props = $props();
// Reactive state using $state() rune
let currentCount = $state(0);
let isLoading = $state(false);
// Derived state using $derived rune
let doubleCount = $derived(currentCount * 2);
// Side effects using $effect rune
$effect(() => {
console.log('Count changed:', currentCount);
});
// Functions
function increment() {
currentCount++;
}
</script>
<div>
<h1>{title}</h1>
<p>Count: {currentCount}</p>
<p>Double: {doubleCount}</p>
<button onclick={increment}>Increment</button>
</div>
Important Svelte 5 Changes:
$props() instead of export let$state() instead of let for reactive variables$derived instead of $: for computed values$effect() instead of $: for side effectsonclick={} instead of on:click={}bind:value={} for two-way bindingSee src/pages/opti-admin/components/_CmsSync.svelte for a complete example showing:
$state() and $derivedonMount(){#if}{#each}---
// MyPage.astro
import MyCoolComponent from './_components/MyCoolComponent.svelte';
const someData = { title: 'Hello', count: 5 };
---
<div>
<MyCoolComponent client:load title={someData.title} count={someData.count} />
</div>
Client Directives:
client:load - Load immediately on page loadclient:idle - Load when browser is idleclient:visible - Load when component is visibleclient:only="svelte" - Only run on client, no SSRRemember: Every component must be production-ready with:
npx skills add kunalshetye/cms-component-builder下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer