browser-validator
Automatically validate implementations in real browsers after code is written or when user says "test this", "test what you built in the browser", "check it in…
Best practices for building Drupal Single Directory Components (SDC) with Twig — including props vs slots, the `attributes` object, `include` vs `embed`, escaping rules, accessibility, schema and validation, and overriding components. Invoke when the user mentions "SDC", "Single
$ npx -y skills add kanopi/cms-cultivator --skill drupal-sdc-twig --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/drupal-sdc-twigContext preview
The summary Claude sees to decide when to auto-load this skill.
Best practices for building Drupal Single Directory Components (SDC) with Twig — including props vs slots, the `attributes` object, `include` vs `embed`, escaping rules, accessibility, schema and validation, and overriding components. Invoke when the user mentions "SDC", "Single
name: drupal-sdc-twig description: Best practices for building Drupal Single Directory Components (SDC) with Twig — including props vs slots, the `attributes` object, `include` vs `embed`, escaping rules, accessibility, schema and validation, and overriding components. Invoke when the user mentions "SDC", "Single Directory Component", "component.yml", working with `components/` folders in a Drupal theme or module, writing or reviewing an SDC Twig template, or asks "how should I structure this Drupal component", "props or slots?", "embed vs include in SDC", or "best practices for Drupal components".
Use this skill whenever you are creating, reviewing, or refactoring a Drupal Single Directory Component, or writing the Twig template that ships inside one. SDC has been part of Drupal core since 10.3, so assume it is available unless the user explicitly states an older version.
Every component lives in its own folder inside a top-level `components/` directory of a theme or module:
components/
└── card/
├── card.component.yml # Metadata + schema (props, slots, libraryOverrides)
├── card.twig # Template — note: .twig, NOT .html.twig
├── card.css # Auto-attached as a library asset
├── card.js # Auto-attached as a library asset
└── thumbnail.png # Optional preview imageSubdirectories are allowed (`components/molecules/card/...`). Components are referenced by namespace `theme_or_module:component_name`, e.g. `my_theme:card`.
Apply these rules when generating or reviewing SDC Twig code. They are derived from the official Drupal SDC docs and the SDC FAQ on drupal.org.
SDC templates use the bare `.twig` extension. This is the one place in Drupal where you do **not** use `.html.twig`. The Twig filename must match the component machine name (the folder name).
This is the most important design decision in an SDC and the source of most refactors. The rule:
If you would ever want to pass a nested component, another Twig render result, or a chunk of HTML, it must be a slot — not a prop. Props that are serialized HTML strings are an anti-pattern.
A schema (`props.type: object` with `properties`) is required for:
For a propless component, use the empty-props pattern:
props:
type: object
additionalProperties: false
properties: {}To enforce schemas across a theme, add `enforce_prop_schemas: true` to `theme.info.yml`.
Every SDC template automatically receives an `attributes` variable (a `\Drupal\Core\Template\Attribute` object). You must use it because Drupal core, SEO modules, accessibility modules, translation, and style utilities inject classes, `lang`, `data-*`, and ARIA attributes through it.
<div{{ attributes.addClass('card') }}>
...
</div>Use `.addClass()`, `.setAttribute()`, `.removeClass()` — do not stringify and concatenate. Do not redeclare `attributes` in the schema; SDC adds it automatically. (You may declare `body_attributes` etc. as `type: 'Drupal\Core\Template\Attribute'` — that's a known escape hatch for passing additional Attribute objects.)
Both work, but use them deliberately:
{# Props only — use include() #}
{{ include('my_theme:button', {
label: 'Sign up'|t,
variant: 'primary',
}, with_context = false) }}
{# Slots — use embed #}
{% embed 'my_theme:card' with { variant: 'feature' } only %}
{% block media %}
{{ include('my_theme:image', { src: node.field_image|file_url }, with_context = false) }}
{% endblock %}
{% block body %}
<p>{{ node.body.summary }}</p>
{% endblock %}
{% endembed %}Use `only` (or `with_context = false`) by default so the child component is isolated from the parent's variables. This makes components portable and prevents surprising regressions.
Twig will escape it. If you need to pass HTML, use a slot. Two patterns:
{# Pattern A: capture markup into a variable, pass to a slot via include() #}
{% set body %}
<p><em>Any</em> HTML stays intact.</p>
{% endset %}
{{ include('my_theme:card', { body: body }, with_context = false) }}
{# Pattern B: use embed with a block (preferred for slots) #}
{% embed 'my_theme:card' only %}
{% block body %}<p><em>Any</em> HTML stays intact.</p>{% endblock %}
{% endembed %}Inside the component template, do not branch on the *contents* of a slot — only on whether it is empty:
{% if heading %}
<h{{ heading_level|default(2) }} class="card__heading">{{ heading }}</h{{ heading_level }}>
{% endif %}
{{ body }}All UI logic (variant switching, conditional classes, ARIA stat
Specialist agents and auto-invoked skills for Drupal/WordPress development. Works in Claude Code, Claude Desktop, and OpenAI Codex. Full documentation: What changed in 2.0? CMS Cultivator now focuses on CMS development workflows.
Repo: kanopi/cms-cultivator
Automatically validate implementations in real browsers after code is written or when user says "test this", "test what you built in the browser", "check it in…
Run the right linting, formatting, and static-analysis commands after changing code, and check it against PHPCS, ESLint, WordPress Coding Standards, or Drupal…
Automatically generate conventional commit messages when user has staged changes and mentions committing. Analyzes git diff and status to create properly…
Generate and maintain patches for Composer-installed packages (Drupal contrib modules, WordPress packages, PHP libraries) using cweagans/composer-patches.…
Automatically analyze test coverage when user asks which code is tested, mentions coverage gaps, or shows code asking about testing. Identifies untested code…
Deterministic cleanup of DDEV and Docker disk usage on OrbStack, Docker Desktop, or any Docker provider. Safely reclaims space by removing orphaned Docker…