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: