| name | cms-component-builder |
| description | 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) |
CMS Component Builder
Build production-ready Optimizely CMS components with proper GraphQL integration, daisyUI styling, and TypeScript type safety.
When to Use This Skill
ALWAYS use this skill when the user asks to:
- Create a new component, page, or experience (e.g., "create a testimonial component", "add a new page type")
- Modify or update
.opti-type.jsonfiles (content type definitions) - Modify or update
.opti-style.jsonfiles (style/display template definitions) - Create or modify GraphQL fragments (
.graphqlor.dam.graphqlfiles) - Add components to
allComponents.graphqlor similar aggregation files - Debug component rendering issues
- Fix GraphQL type errors related to components
- Push/sync content types or styles to the CMS
- Understand how the component system works
Critical Instructions
YOU 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.jsonfilesSTYLE-GUIDE.md- For creating.opti-style.jsonfilesGRAPHQL-PATTERNS.md- For creating.graphqland.dam.graphqlfiles
EXAMINE 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 fragment
INTEGRATE PROPERLY - After creating files:
- Add fragment to
src/cms/components/allComponents.graphql - Push to CMS:
yarn type:push ComponentNameandyarn style:push StyleName - Wait 10 seconds for Optimizely Graph sync
- Generate types:
yarn codegen
- Add fragment to
Quick Start Example
Creating 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
Required Files Explained
Every component needs at least 5 files:
1. .astro - Component Template
IMPORTANT: 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:
- Import types from
__generated/sdk(NOT from@/graphql/__generated/graphql) - Include ALL required props:
key,data,displaySettings,displayTemplateKey,contentPayload - Use
data-epi-block-idfor CMS editing context - Use
set:htmlfor rich text fields - Add a component class for CSS targeting
2. .opti-type.json - Content Type
See CONTENTTYPE-GUIDE.md for complete guide on creating content types.
3. .opti-style.json - Style Definitions
See STYLE-GUIDE.md for complete guide on creating style definitions.
4. .graphql + .dam.graphql - GraphQL Fragments
ALWAYS 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
}
}
Component Integration
1. Add to allComponents.graphql
fragment AllComponentsExceptGrid on _IComponent {
...Text
...Button
...Testimonial # Add your component here
}
Important: Add to AllComponentsExceptGrid, NOT AllComponents
2. Sync to CMS & Generate Types
# 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
3. Verify Integration
Check __generated/graphql.ts for your component type.
Component Locations
- Components:
src/cms/components/- Reusable UI (Button, Card, Hero) - Pages:
src/cms/pages/- Page types (ArticlePage, LandingPage) - Experiences:
src/cms/experiences/- Experience templates - Compositions:
src/cms/compositions/- Layout elements (Row, Column)
GraphQL Shared Fragments
Quick reference (full details in GRAPHQL-PATTERNS.md):
LinkUrl- Links with url, title, target, textContentUrl- Content reference URLsLinkCollection- Simple link arraysContentReferenceItem- DAM metadata (.dam.graphqlonly)DisplaySettings- Display/styling settingsPageUrl- Page metadata
Rich text fields: Always use { html }:
Body { html }
Description { html }
Styling with daisyUI + TailwindCSS
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">
Complete Workflow (FOLLOW IN ORDER)
Step 1: Research & Planning
- Ask the user for requirements: What should this component do? What properties does it need?
- Find similar components: Use
Globto find similar components insrc/cms/components/ - Read example files: Study at least one similar component to understand the patterns
Step 2: Create Component Files
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- Reference
CONTENTTYPE-GUIDE.mdfor structure - Look at similar component's
.opti-type.jsonas example - Set
baseType: "component"for components - Set
compositionBehaviors: ["elementEnabled"]for components usable in visual editor
- Reference
DefaultComponentName.opti-style.json- Default style template- Reference
STYLE-GUIDE.mdfor structure - Set
contentTypeto match your component's key - Set
isDefault: truefor the default style - Create meaningful display settings with choices
- Reference
componentName.graphql- Base GraphQL fragment- Reference
GRAPHQL-PATTERNS.md - Fragment name must match component name (PascalCase)
- Include all properties from
.opti-type.json - Use shared fragments:
...LinkUrl,...ContentUrl, etc.
- Reference
componentName.dam.graphql- DAM-enabled GraphQL fragment- Copy from
componentName.graphql - Add
...ContentReferenceItemto content references - This enables DAM metadata for images/videos
- Copy from
ComponentName.astro- Component template- Follow the pattern from existing components (see example above)
- Import types from
__generated/sdk - Include all required props
- Handle display settings for styling
- Use daisyUI classes for styling
Step 3: Integrate with System
Add 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 syncGenerate TypeScript Types
# After waiting for Graph sync yarn codegen
Step 4: Verify & Test
Check generated types in
__generated/sdk.ts:- Look for
ComponentNameFragmenttype - Verify all properties are present
- Look for
Check for TypeScript errors:
yarn tsc --noEmitTest in dev server:
yarn devVerify in CMS:
- Component appears in CMS UI
- All properties are editable
- Style options work correctly
Verification Checklist
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:- Has
key,displayName,baseType - All properties defined correctly
- Uses correct property types
- Has
✓ Style template (
.opti-style.json) is valid:- Has
key,displayName,contentType contentTypematches component key- Has
isDefault: truefor default style - All settings have choices
- Has
✓ Both GraphQL fragments created:
.graphqlfor base version.dam.graphqlwith...ContentReferenceItemfor DAM
✓ Fragment added to
allComponents.graphql:- Added to
AllComponentsExceptGrid(NOTAllComponents)
- Added to
✓ Pushed to CMS successfully:
yarn type:push ComponentNamecompletedyarn style:push StyleNamecompleted- Waited 10-15 seconds for Graph sync
✓ Types generated successfully:
yarn codegencompleted without errorsComponentNameFragmenttype exists in__generated/sdk.ts
✓ Component template (
.astro) follows patterns:- Imports from
__generated/sdk - Has all required props
- Uses
data-epi-block-idfor CMS editing - Uses daisyUI classes for styling
- Handles display settings
- Imports from
✓ No TypeScript errors:
yarn tsc --noEmitpasses
✓ Component works in dev server:
yarn devruns without errors
Resources
All guides are bundled with this skill for quick reference:
GraphQL Patterns: See
GRAPHQL-PATTERNS.mdfor:- Complete fragment syntax and structure
- DAM vs non-DAM differences
- Real-world examples from existing components
- Shared fragment catalog
- Integration patterns
Content Type Creation: See
CONTENTTYPE-GUIDE.mdfor:- Content type structure and syntax
- Property definitions and types
- Display settings configuration
- Real-world examples
Style Creation: See
STYLE-GUIDE.mdfor:- Style definition structure
- Display settings mapping to CSS classes
- daisyUI and TailwindCSS patterns
- Responsive variants
Common Patterns
Component with links:
Links {
...LinkCollection
}
Component with images:
# Base
Image {
...ContentUrl
}
# DAM
Image {
...ContentUrl
...ContentReferenceItem
}
Component with metadata:
_metadata {
types
displayName
}
Common Issues & Troubleshooting
Issue: GraphQL fragment not found after codegen
Solution:
- Check that fragment is added to
allComponents.graphql - Ensure you pushed to CMS:
yarn type:push ComponentName - Wait 10-15 seconds for Optimizely Graph to sync
- Run
yarn codegenagain
Issue: TypeScript errors in .astro file
Solution:
- Verify imports are from
__generated/sdknot@/graphql/__generated/graphql - Check that all Props interface fields are present
- Run
yarn codegento regenerate types - Check that fragment name matches component name exactly (case-sensitive)
Issue: Component doesn't appear in CMS
Solution:
- Verify
yarn type:push ComponentNamecompleted successfully - Check that
baseTypeis set correctly in.opti-type.json - Check that
compositionBehaviors: ["elementEnabled"]is set - Clear CMS cache and refresh
Issue: Display settings not working
Solution:
- Verify
yarn style:push StyleNamecompleted successfully - Check that
contentTypein.opti-style.jsonmatches componentkey - Ensure
isDefault: trueis set for default style - Verify style mapping exists in component's styling helper file
Issue: Rich text fields not rendering HTML
Solution:
- Use
set:html={data.FieldName?.html}instead of{data.FieldName.html} - Ensure GraphQL fragment requests
{ html }for rich text fields
Issue: Content references (images/videos) not loading
Solution:
- Check GraphQL fragment includes
...ContentUrl - For DAM assets, verify
.dam.graphqlincludes...ContentReferenceItem - Check that
url?.defaultorurl?.hierarchicalis used in template
Pages and Experiences
This skill also handles Pages and Experiences, which follow the same patterns:
Pages (src/cms/pages/):
- Set
baseType: "Page"instead of"Component" - Include SEO settings and page admin settings
- Add to appropriate page aggregation file
- May include
mayContainTypesfor content areas
Experiences (src/cms/experiences/):
- Define experience templates
- Include composition structure
- Reference Row, Column, and Section compositions
Using Svelte 5 for Interactive Components
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.
When to Use Svelte
Use Svelte for:
- Admin interfaces (like
/opti-adminpages) - Interactive forms and utilities
- Complex client-side state management
- Real-time features (SSE, WebSockets)
DO NOT use Svelte for:
- CMS components (use
.astroinstead) - Content rendering from Optimizely
- SEO-critical content
Svelte 5 Key Patterns
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:
- Use
$props()instead ofexport let - Use
$state()instead ofletfor reactive variables - Use
$derivedinstead of$:for computed values - Use
$effect()instead of$:for side effects - Use
onclick={}instead ofon:click={} - Use
bind:value={}for two-way binding
Example: Admin Component with TypeScript
See src/pages/opti-admin/components/_CmsSync.svelte for a complete example showing:
- TypeScript interfaces with Props
- State management with
$state()and$derived - Event handling with EventSource (SSE)
- Lifecycle with
onMount() - Conditional rendering with
{#if} - List rendering with
{#each}
Embedding Svelte in Astro
---
// 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 SSR
Remember: Every component must be production-ready with:
- Complete integration into the system
- Proper TypeScript types
- Consistent daisyUI styling
- Both GraphQL fragment versions (.graphql and .dam.graphql)
- Comprehensive verification before completion