# Drupal Canvas Sdc

> Drupal Canvas SDC (Single Directory Components) with Twig templates. Use when creating, modifying, or troubleshooting Twig-based Canvas components in themes/modules, component.yml schemas, Canvas preview issues, or page builder functionality. For React/JSX Code Components, see drupal-canvas-code-components skill. (project)

- Skill: `grasmash/drupal-canvas-sdc` (Agent Skill)
- Install (CLI): `npx skillmds@latest add grasmash/drupal-canvas-sdc`
- Raw SKILL.md: https://api.skillmd.com/api/skills/grasmash/drupal-canvas-sdc/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: grasmash (https://skillmd.com/u/grasmash)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/grasmash/drupal-canvas-sdc

---


# Drupal Canvas SDC Components (Twig)

## Overview

**Drupal Canvas 1.0** (released December 2025) is a React-based visual page builder powered by Single Directory Components (SDC). Canvas consolidates three component types:
- **SDC components** from themes/modules
- **Code Components** written in-browser with React/JSX
- **Traditional Drupal Blocks**

Canvas becomes the default page builder in Drupal CMS 2.0 (January 2026).

## Installation

```bash
composer require 'drupal/canvas:^1.0'
drush en canvas -y
```

**Requirements**: Drupal ^11.2

## Component Discovery

SDC components live in a `components/` directory within any theme or module. Drupal scans enabled themes/modules for directories containing a `.component.yml` file.

**Component ID pattern**: `{provider}:{component-name}` (e.g., `mytheme:hero-banner`)

**Management UI**: Appearance > Components (`/admin/appearance/component`)

## SDC File Structure

Every SDC requires exactly two files. CSS/JS with matching names auto-attach.

```
theme_name/
└── components/
    ├── atoms/
    │   └── button/
    │       ├── button.component.yml   # REQUIRED
    │       ├── button.twig            # REQUIRED
    │       ├── button.css             # Auto-attached
    │       └── button.js              # Auto-attached
    └── molecules/
        └── card/
            ├── card.component.yml
            ├── card.twig
            ├── card.css
            └── thumbnail.png          # Optional preview
```

**Naming Rules**:
- Use **kebab-case** for directories and files
- All files share the same base name as the directory
- Templates use `.twig` extension (NOT `.html.twig`)
- Organizational folders (`atoms/`, `molecules/`) don't affect component IDs

## component.yml Schema

```yaml
$schema: https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json
name: Hero Banner
description: Full-width hero with image background and CTA
status: stable  # experimental, stable, deprecated, obsolete
group: Content

props:
  type: object
  required:
    - heading
  properties:
    heading:
      type: string
      title: Heading
      examples: ['Welcome to our site']  # REQUIRED for Canvas preview
    background_image:
      type: object
      $ref: json-schema-definitions://canvas.module/image
    cta_text:
      type: string
      title: Button Text
      default: Learn More
    size:
      type: string
      title: Size
      enum: ['small', 'medium', 'large']
      default: medium
      meta:enum:
        small: "Compact"
        medium: "Standard"
        large: "Full Screen"

slots:
  content:
    title: Body Content
    description: Main content area below the heading

libraryOverrides:
  dependencies:
    - core/drupal
    - core/once
```

**CRITICAL**: The `examples` key is **required for Canvas** to display helpful placeholders.

## Prop Types and Canvas Widgets

| Schema Definition | Canvas Widget | Use Case |
|-------------------|---------------|----------|
| `type: string` | Text input | Simple text |
| `type: string` + `contentMediaType: text/html` | CKEditor 5 | Rich text |
| `type: boolean` | Checkbox | Toggle options |
| `type: integer` with `minimum`/`maximum` | Number input | Numeric values |
| `type: string` + `enum: [...]` | Dropdown | Preset options |
| `$ref: json-schema-definitions://canvas.module/image` | Media library | Image picker |
| `type: array` with `items:` | List input | Multiple values |

### Rich Text Props

```yaml
content:
  type: string
  title: Rich Text Content
  contentMediaType: text/html
  x-formatting-context: block  # or 'inline'
  examples: ['<p>Formatted <strong>content</strong></p>']
```

### Enum Labels (Human-Readable Dropdowns)

```yaml
color:
  type: string
  enum: ['primary', 'secondary', 'danger']
  meta:enum:
    primary: "Primary Brand Color"
    secondary: "Secondary Accent"
    danger: "Warning/Error State"
```

## Validation Patterns

### Required Properties

```yaml
props:
  type: object
  required:
    - title
    - url
  properties:
    title:
      type: string
```

### Numeric Constraints

```yaml
spacing:
  type: integer
  minimum: 0
  maximum: 100

heading_level:
  type: integer
  enum: [2, 3, 4, 5, 6]
```

### Nullable Values

```yaml
subtitle:
  type: ['string', 'null']
  title: Optional Subtitle
```

### Array Constraints

```yaml
tags:
  type: array
  items:
    type: string
  minItems: 1
  maxItems: 5
```

### Default Values in Twig

**IMPORTANT**: Schema `default` values are documentation only. Always use `|default()` in Twig:

```twig
{% set heading_level = heading_level|default(2) %}
{% set color = color|default('primary') %}
```

## Slots for Composition

Slots accept arbitrary markup or nested components. Props handle typed data; slots handle content.

### Define Slots

```yaml
slots:
  header:
    title: Header Content
    description: Optional header area
  body:
    title: Body Content
  footer: {}  # Minimal definition
```

### Render Slots in Twig

```twig
<article class="card">
  {% if header %}
    <header class="card__header">{{ header }}</header>
  {% endif %}
  <div class="card__body">{{ body }}</div>
  {% if footer %}
    <footer class="card__footer">{{ footer }}</footer>
  {% endif %}
</article>
```

### Populate Slots with Embed

```twig
{% embed 'my_theme:card' with { heading: 'Featured Article' } only %}
  {% block body %}
    {{ content.field_image }}
    <p>{{ content.field_summary }}</p>
    {{ include('my_theme:button', { text: 'Read More', url: url }) }}
  {% endblock %}
{% endembed %}
```

### Populate Slots from PHP

```php
$build = [
  '#type' => 'component',
  '#component' => 'my_theme:card',
  '#props' => ['heading' => $title],
  '#slots' => [
    'body' => ['#markup' => $content],
    'footer' => $footer_render_array,
  ],
];
```

## Atomic Design Principles

- **Atoms**: Buttons, icons, labels, form inputs
- **Molecules**: Cards, media objects, search forms
- **Organisms**: Headers, footers, article teasers
- **Templates**: Page layouts with slots for organisms

### Single Responsibility Pattern

```yaml
# ✅ Good: Single component with variants
name: Button
props:
  type: object
  properties:
    variant:
      type: string
      enum: ['primary', 'secondary', 'outline', 'ghost']
    size:
      type: string
      enum: ['small', 'medium', 'large']
```

```yaml
# ❌ Avoid: Separate components per variant
# button-primary.component.yml
# button-secondary.component.yml
```

## CSS Styling with BEM

**Component-Scoped CSS (CRITICAL)**: NEVER scope styles to route/path body classes (e.g., `.alias--masterclasses`, `.path-some-page`). Instead, scope styles to the component's own classes (e.g., `.view-masterclasses`, `.block-views-blockmasterclasses-block-2`). The Canvas editor renders page previews in an iframe without the page's body classes, so route-scoped styles won't appear there. All styles must be componentized and portable -- they should look correct regardless of what route or context they render in.

SDC auto-attaches CSS when file matches component name. Use BEM for scoped styles:

```css
/* card.css */
.card {
  --card-padding: 1.5rem;
  --card-radius: 0.5rem;
  --card-shadow: 0 2px 8px rgba(0,0,0,0.1);

  display: flex;
  flex-direction: column;
  padding: var(--card-padding);
  border-radius: var(--card-radius);
  box-shadow: var(--card-shadow);
}

.card__header {
  margin-bottom: 1rem;
  font-weight: 700;
}

.card__body {
  flex: 1;
}

.card--featured {
  --card-shadow: 0 4px 16px rgba(0,0,0,0.15);
  border: 2px solid var(--color-primary);
}

.card--compact {
  --card-padding: 1rem;
}
```

### External Dependencies

```yaml
libraryOverrides:
  css:
    component:
      card.css: {}
      additional-styles.css: {}
  dependencies:
    - core/normalize
```

## JavaScript Behaviors

Use Drupal's behavior pattern with `once()`:

```javascript
// accordion.js
((Drupal, once) => {
  Drupal.behaviors.accordion = {
    attach(context) {
      once('accordion', '.accordion__trigger', context).forEach((trigger) => {
        trigger.addEventListener('click', (e) => {
          const panel = document.getElementById(trigger.getAttribute('aria-controls'));
          const expanded = trigger.getAttribute('aria-expanded') === 'true';

          trigger.setAttribute('aria-expanded', !expanded);
          panel.hidden = expanded;
        });
      });
    },
    detach(context, settings, trigger) {
      if (trigger === 'unload') {
        once.remove('accordion', '.accordion__trigger', context);
      }
    }
  };
})(Drupal, once);
```

**Key Patterns**:
- `once()` prevents duplicate event binding
- `context` scopes to newly added DOM
- `attach()` runs on page load and AJAX updates
- `detach()` handles cleanup

## Accessibility (WCAG 2.2 AA)

### Semantic HTML First

```twig
<nav aria-label="Main navigation">
  <ul>
    <li><a href="{{ url }}">{{ label }}</a></li>
  </ul>
</nav>

{# Accessible button with states #}
<button
  type="button"
  aria-expanded="{{ expanded ? 'true' : 'false' }}"
  aria-controls="panel-{{ id }}"
>
  {{ trigger_text }}
</button>

{# Modal dialog #}
<div
  role="dialog"
  aria-modal="true"
  aria-labelledby="dialog-title-{{ id }}"
>
  <h2 id="dialog-title-{{ id }}">{{ title }}</h2>
  {{ content }}
</div>

{# Live region for dynamic updates #}
<div aria-live="polite" class="visually-hidden">
  {{ status_message }}
</div>
```

### Keyboard Navigation Checklist

- All interactive elements focusable via Tab
- Visible focus indicators (never `outline: none` without alternative)
- Enter/Space activate buttons and links
- Escape closes modals and dropdowns
- Arrow keys navigate within composite widgets

### Dynamic Announcements

```javascript
Drupal.announce('Form submitted successfully');
Drupal.announce('Error: Please fix the highlighted fields', 'assertive');
```

## Canvas Preview for Traditional Blocks

**CRITICAL**: Canvas renders block previews in iframes that only include CSS from explicitly attached libraries.

### Problem

Traditional Drupal blocks relying on theme global SCSS won't show styles in Canvas previews.

### Solution Pattern

1. **Create standalone CSS file** in module's `css/` directory
2. **Add library** to module's `.libraries.yml`
3. **Attach library** via `#attached` in block's `build()` method

```php
// In BlockClass.php build() method
public function build() {
  return [
    '#theme' => 'my_block_template',
    '#variables' => $variables,
    '#attached' => [
      'library' => [
        'my_module/my_block_styles',
      ],
    ],
  ];
}
```

```yaml
# my_module.libraries.yml
my_block_styles:
  version: VERSION
  css:
    theme:
      css/my-block.css: {}
```

### CSS Compilation for Standalone Files

When extracting from theme SCSS, compile variables to hardcoded values:

```css
/* Compiled from SCSS - replace variables */
.my-component {
  max-width: 1440px;  /* was $max-content-width */
  color: #7d11ff;     /* was $primary-color */
}
```

### Blocks That Return Empty

Blocks returning empty arrays `[]` won't preview in Canvas. Always render the template structure:

```php
public function build() {
  // Always return template for Canvas preview support
  return [
    '#theme' => 'my_block_template',
    '#content' => $content ?? NULL,
    '#attached' => [
      'library' => ['my_module/my_block'],
    ],
    '#cache' => ['contexts' => ['user']],
  ];
}
```

## SDC vs Traditional Blocks Comparison

| Feature | SDC Component | Traditional Block |
|---------|---------------|-------------------|
| CSS Loading | Auto-attached | Must use `#attached` |
| Canvas Preview | Automatic with `examples` | Requires explicit library |
| Props/Config | JSON Schema in YAML | Block configuration form |
| Template | `.twig` (no `.html.twig`) | `.html.twig` |
| Location | `components/` directory | `src/Plugin/Block/` |
| Reusability | High (theme-agnostic) | Module-specific |

## Testing SDC Components

### PHPUnit Kernel Tests

```php
namespace Drupal\Tests\my_module\Kernel;

use Drupal\KernelTests\KernelTestBase;

class CardComponentTest extends KernelTestBase {
  protected static $modules = ['system', 'sdc'];

  public function testCardRendering() {
    $build = [
      '#type' => 'component',
      '#component' => 'my_theme:card',
      '#props' => ['heading' => 'Test'],
    ];
    $output = \Drupal::service('renderer')->renderRoot($build);
    $this->assertStringContainsString('Test', (string) $output);
  }
}
```

### Storybook Integration

```javascript
// card.stories.js
export default {
  title: 'Molecules/Card',
  component: 'my_theme:card',
};

export const Default = {
  args: {
    heading: 'Card Title',
    body: '<p>Card content goes here</p>',
  },
};

export const Featured = {
  args: {
    heading: 'Featured Card',
    variant: 'featured',
  },
};
```

## Canvas API Stability

| Feature | Status |
|---------|--------|
| SDC component integration | **Stable** |
| Props and slots structure | **Stable** |
| Component discovery | **Stable** |
| Code Components (React/JSX) | **Stable** |
| Block integration | **Stable** |
| Content Templates | **Experimental** |
| AI Assistant | **Evolving** |
| ComponentSource plugin API | **In flux** |

## Canvas Development & Migration Tools

### Canvas Submodules

| Module | Status | Environment | When to Use |
|--------|--------|-------------|-------------|
| `canvas_dev_mode` | **Keep enabled locally** | Local only, NEVER production | Always during development. Unlocks experimental APIs and extensions toolbar. |
| `canvas_vite` | **Keep disabled** | Local only when needed | ONLY when developing the Canvas editor UI itself (the React app). Requires Vite dev server running. Causes `ERR_CONNECTION_REFUSED` errors if Vite is not running. |
| `canvas_ai` | Hidden/internal | Per-environment | AI-assisted page building. Enable if using Canvas AI features. |
| `canvas_oauth` | As needed | Production + local | Only if external apps need authenticated Canvas API access. |
| `canvas_styling_traits` | **Enabled** | All environments | Provides shared styling schema definitions (`spacing-padding`, etc.) for components. Note: has a known non-fatal `#/$defs/spacing-padding` schema error on `drush cr` that doesn't affect functionality. |

**`canvas_dev_mode`** details:
- Removes `Choice` constraint on ComponentSource plugins, allowing unstable plugin types
- Shows the extensions toolbar in the Canvas editor UI
- Enable: `ddev drush en canvas_dev_mode -y`

**`canvas_vite`** details:
- Connects to Vite dev server at `localhost:5173`
- Disables CSS/JS preprocessing so changes are instant (no cache clearing for CSS/JS)
- Injects React Refresh runtime for live component updates
- Set `VITE_SERVER_ORIGIN` env var to customize the Vite server URL
- Enable: `ddev drush en canvas_vite -y` then start Vite: `cd docroot/modules/contrib/canvas/ui && npm run dev`
- Disable when done: `ddev drush pm:uninstall canvas_vite -y`

### Canvas Admin UI Pages

Use these during migration and development:

| URL | Purpose |
|-----|---------|
| `/admin/appearance/component` | List all Canvas components |
| `/admin/appearance/component/status` | Component status dashboard (enabled/disabled, incompatibilities) |
| `/admin/appearance/component/{component}/audit` | **Audit page** - shows WHERE a component is used (pages, templates, patterns, regions) |
| `/admin/appearance/component/{component}/enable` | Enable a component |
| `/admin/appearance/component/{component}/disable` | Disable a component |
| `/admin/content/pages/add` | Add a new Canvas page |

The **audit page** is the most useful during migration - it shows every canvas page, content template, pattern, and region that uses a given component.

### Canvas API Endpoints (JSON)

Useful for scripting or programmatic migration:

```
GET /canvas/api/v0/usage/component                          # List all component usage (paginated)
GET /canvas/api/v0/usage/component/{component}              # Usage summary for one component
GET /canvas/api/v0/usage/component/{component}/details      # Detailed usage breakdown
GET /canvas/api/v0/config/{type}                            # List all components/content-templates/patterns
GET /canvas/api/v0/config/{type}/{id}                       # Get specific config entity
GET /canvas/api/v0/layout/{entity_type}/{entity}            # Get entity's component tree layout
GET /canvas/api/v0/auto-saves/pending                       # Get pending auto-saves
POST /canvas/api/v0/log-error                               # JS error logging from Canvas UI
```

### Canvas PHP Services (Dependency Injection)

Key services available for custom code:

| Service | Use Case |
|---------|----------|
| `Drupal\canvas\Audit\ComponentAudit` | Find all content/config using a component |
| `Drupal\canvas\ComponentSource\ComponentSourceManager` | Trigger component discovery/regeneration |
| `Drupal\canvas\Storage\ComponentTreeLoader` | Load component trees from entities |
| `Drupal\canvas\CanvasConfigUpdater` | Auto-migrate config structures |
| `Drupal\canvas\ComponentTreeInputExtractor` | Extract inputs from component trees |
| `Drupal\canvas\ShapeMatcher\PropSourceSuggester` | Shape matching for field-to-prop mapping |
| `logger.channel.canvas` | Canvas-specific logging |

### ComponentAudit Service (Migration Key Tool)

```php
$audit = \Drupal::service(Drupal\canvas\Audit\ComponentAudit::class);

// Find all content using a component
$audit->getContentRevisionsUsingComponent($component);

// Find config entities (templates, patterns, regions) using it
$audit->getConfigEntityDependenciesUsingComponent($component);

// Quick boolean check
$audit->hasUsages($component);
```

## Canvas Component Versioning (CRITICAL)

**Understanding component versioning is essential when modifying SDC components.**

### How Versioning Works

Canvas uses a **version-pinning system** for backward compatibility:

1. Each component has an `active_version` (an xxh64 hash)
2. Component instances are **pinned to the version they were created with**
3. When you add/change props, Canvas creates a new `active_version`
4. **Existing instances stay on their old version** - they won't show new fields

### The Problem

When you add a new prop to a component (e.g., adding `gap` to `section`):
- **New** component instances will have the new field
- **Existing** instances created before the change **will NOT show the new field**

This is intentional - Canvas preserves backward compatibility so existing pages don't break.

### Solution: Migrate Instances to Active Version

**IMPORTANT**: After modifying component props, always run the upgrade command to migrate existing instances.

Use the custom drush commands to upgrade existing instances:

```bash
# List components with outdated instances
ddev drush canvas:upgrade-instances

# Preview what would be migrated (dry run)
ddev drush canvas:upgrade-instances sdc.mytheme.section --dry-run

# Migrate a specific component's instances
ddev drush canvas:upgrade-instances sdc.mytheme.section

# Migrate ALL outdated instances at once
ddev drush canvas:upgrade-instances --all

# Show detailed version info for a component
ddev drush canvas:component-info sdc.mytheme.section
```

### Development Workflow for Component Prop Changes

When modifying SDC component props during development:

1. **Modify the component** (`component.yml`, `.twig`, `.css`)
2. **Clear cache**: `ddev drush cr`
3. **Check for outdated instances**: `ddev drush canvas:upgrade-instances`
4. **Migrate if needed**: `ddev drush canvas:upgrade-instances <component_id>`

### canvas_styling_traits Schema Changes

When modifying `canvas_styling_traits/schema.json` (adding/removing/renaming enum values):

1. **Update schema.json** with new enum values
2. **Clear cache**: `ddev drush cr`
3. **Upgrade instances**: `ddev drush canvas:upgrade-instances --all -y`

**CRITICAL**: If you **remove** an enum value (e.g., removing "none" from spacing enums), existing component instances that have that value stored will fail validation at render time with `Does not have a value in the enumeration`. You must also clean the stored data. The `canvas:upgrade-instances` command migrates instances to the new component version but does NOT update stored field values that are no longer valid.

To fix invalid stored values, use a drush script or `hook_update_N()`:
```php
$database = \Drupal::database();
$tables = ['canvas_page__components', 'canvas_page_revision__components'];
foreach ($tables as $table) {
  $results = $database->select($table, 'c')
    ->fields('c')
    ->condition('c.components_component_id', 'sdc.mytheme.page-header')
    ->execute();
  foreach ($results as $row) {
    $inputs = json_decode($row->components_inputs, TRUE);
    $changed = FALSE;
    foreach (['margin_top', 'margin_bottom'] as $field) {
      if (isset($inputs[$field]) && $inputs[$field] === 'none') {
        unset($inputs[$field]); // Or set to a valid value
        $changed = TRUE;
      }
    }
    if ($changed) {
      $database->update($table)
        ->fields(['components_inputs' => json_encode($inputs)])
        ->condition('entity_id', $row->entity_id)
        ->condition('revision_id', $row->revision_id)
        ->condition('delta', $row->delta)
        ->execute();
    }
  }
}
\Drupal::cache('entity')->deleteAll();
\Drupal::cache('render')->deleteAll();
```

**Summary of enum semantics for styling traits:**
- **"- None -"** (Drupal's built-in for non-required `list_string`) = unset, no CSS class applied, element keeps its default styling
- **"zero"** (explicit enum value) = `margin: 0` / `padding: 0`, actively removes spacing via CSS class `cst-mt-zero` etc.

### Production Deployment Workflow

For production deployments where you change component props:
- Write a `hook_update_N()` to migrate instances (see `my_module_update_9015` for example)
- Or run `drush canvas:upgrade-instances --all` as part of deployment scripts

### Canvas Config Entity Structure

Component config entities store versions in `config/default/canvas.component.*.yml`:

```yaml
active_version: c533daa1ccfd0fba  # Current version hash
versioned_properties:
  active:
    settings:
      prop_field_definitions:
        gap:  # New prop - only in active version
          field_type: list_string
          # ...
  2076209c0228c2bb:  # Old version - no gap prop
    settings:
      prop_field_definitions:
        # ...
```

### Instance Storage

Component instances live in the `canvas_page__components` table:
- `components_component_id` - which component (e.g., `sdc.mytheme.section`)
- `components_component_version` - pinned version hash
- `components_inputs` - JSON blob of prop values
- `components_uuid` - unique instance identifier
- Revision data in `canvas_page_revision__components`

## Drush Commands

```bash
# Clear cache after component changes
ddev drush cr

# List all SDC components
ddev drush ev "print_r(array_keys(\Drupal::service('plugin.manager.sdc')->getDefinitions()));"

# Canvas component version management (your project custom)
ddev drush canvas:upgrade-instances          # List outdated
ddev drush canvas:upgrade-instances --all    # Migrate all
ddev drush canvas:component-info             # Show all components
ddev drush canvas:component-info <id>        # Show specific component
```

## Troubleshooting: Component Version Mismatches (500 Errors)

### Symptom

Canvas editor throws 500: `'propName' is not a prop on this version of the Component 'Code component: ComponentName'`

### Root Cause

The Canvas component entity's `active_version` was generated from an older build of the component that didn't include the prop. The source `component.yml` has the prop, but the deployed Canvas entity doesn't.

**Key distinction by component type:**
- **SDC components** (`sdc.*`): Canvas regenerates versions on `drush cr` from the source `component.yml`
- **JS code components** (`js.*`): Canvas versions are set when the component is **uploaded** via CLI. `drush cr` alone does NOT update them.

### Recovery Steps

#### For JS Code Components (`js.*`)

```bash
# 1. Re-upload the component to generate a new version from source component.yml
npx canvas upload --components container -y

# 2. Upgrade existing instances to the new active version
ddev drush canvas:upgrade-instances js.container -y

# 3. Verify
ddev drush canvas:component-info js.container
```

The upload command is configured via `.env` in the canvas-components project:
- `CANVAS_SITE_URL=http://example.ddev.site`
- `CANVAS_CLIENT_ID=canvas_cli`
- `CANVAS_CLIENT_SECRET=canvas_cli_secret`

#### For SDC Components (`sdc.*`)

```bash
# 1. Clear cache to trigger re-discovery from source
ddev drush cr

# 2. Upgrade existing instances
ddev drush canvas:upgrade-instances sdc.mytheme.component-name -y

# 3. Export updated config entity
ddev drush cex -y
```

### Diagnosis Commands

```bash
# Check what props the active version has
ddev drush ev "\$c = \Drupal::entityTypeManager()->getStorage('component')->load('js.container'); echo implode(', ', array_keys(\$c->get('versioned_properties')['active']['settings']['prop_field_definitions'] ?? []));"

# Check what version/inputs an instance has on a specific page
ddev drush ev "\$p = \Drupal::entityTypeManager()->getStorage('canvas_page')->load(6); foreach (\$p->toArray()['components'] ?? [] as \$c) { if (\$c['component_id'] === 'js.container') echo \$c['component_version'] . ': ' . \$c['inputs']; }"

# Compare: does the instance version match the active version?
ddev drush canvas:upgrade-instances  # Lists all mismatches
```

### Stale Auto-Saves

Canvas auto-saves can reference old component versions. If a 500 persists after upgrading instances, check for and delete stale auto-saves:

```bash
# Check pending auto-saves
curl -s -b /tmp/cookies.txt "https://example.ddev.site/canvas/api/v0/auto-saves/pending"

# Delete a stale auto-save (need CSRF token)
CSRF=$(curl -s -b /tmp/cookies.txt "https://example.ddev.site/session/token")
curl -s -b /tmp/cookies.txt -X DELETE -H "X-CSRF-Token: $CSRF" \
  "https://example.ddev.site/canvas/api/v0/auto-saves/canvas_page/{PAGE_ID}"
```

### Prevention

After modifying any component's `component.yml` props:
1. **JS components**: `npx canvas upload --components <name> -y`
2. **SDC components**: `ddev drush cr`
3. **Always**: `ddev drush canvas:upgrade-instances` to check for stale instances
4. **Always**: Test the Canvas editor page before pushing

## Resources

- Canvas project: https://www.drupal.org/project/canvas
- Canvas docs: https://project.pages.drupalcode.org/canvas/
- SDC docs: https://www.drupal.org/docs/develop/theming-drupal/using-single-directory-components
- Canvas SDC Starterkit: https://www.drupal.org/project/canvas_sdc_starterkit
- SDC Examples: https://www.drupal.org/project/sdc_examples

## Apply to Files

- `**/components/**/*.component.yml`
- `**/components/**/*.twig`
- `**/components/**/*.css`
- `**/components/**/*.js`
- `docroot/themes/custom/mytheme/components/**/*`
- `docroot/modules/custom/*/components/**/*`

