Install
$ agentstack add skill-chrisrowe-craftcms-claude-skills-craft-twig-guidelines ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Twig Coding Standards — Craft CMS 5
Coding conventions for Twig templates in Craft CMS 5 projects. These apply to all Twig code — atomic components, views, layouts, builders, partials.
For Twig architecture patterns (atomic design, routing, builders), see the craft-site skill. For PHP coding standards, see craft-php-guidelines.
Documentation
- Twig in Craft: https://craftcms.com/docs/5.x/development/twig.html
- Template tags: https://craftcms.com/docs/5.x/reference/twig/tags.html
- Template functions: https://craftcms.com/docs/5.x/reference/twig/functions.html
- Twig 3 docs: https://twig.symfony.com/doc/3.x/
Use web_fetch on specific doc pages when something isn't covered here.
Variable Naming
Single-word, descriptive, lowercase. No camelCase in Twig unless absolutely unavoidable.
{# Correct #}
{% set heading = entry.title %}
{% set image = entry.heroImage.one() %}
{% set items = navigation.links.all() %}
{% set element = props.get('url') ? 'a' : 'span' %}
{# Wrong #}
{% set heroHeading = entry.title %}
{% set heroImg = entry.heroImage.one() %}
{% set navItems = navigation.links.all() %}
{% set el = props.get('url') ? 'a' : 'span' %}
No abbreviations: element not el, button not btn, navigation not nav, description not desc.
If you need a multi-word variable, reconsider whether you're over-specifying. If truly unavoidable, use snake_case over camelCase: hero_image over heroImage.
Null Handling
?? only. Always. No exceptions.
{# Correct #}
{% set heading = entry.heading ?? '' %}
{% set image = entry.heroImage.one() ?? null %}
{{ props.get('label') ?? 'Default' }}
{# Wrong — custom Twig extension, not portable #}
{% set heading = entry.heading ??? '' %}
{# Wrong — verbose, unnecessary #}
{% if entry.heading is defined and entry.heading is not null %}
{% if entry.heading is not defined %}
Twig 3.21.x (Craft 5) does not have the nullsafe operator (?.). That requires Twig 3.23+. Use ?? and ternaries instead:
{# Can't do this yet #}
{{ entry?.author?.fullName }}
{# Do this instead #}
{{ entry.author.fullName ?? '' }}
Whitespace Control
Use {%- and {{- for whitespace trimming. Never use {%- minify -%}.
{# Correct — surgical whitespace control #}
{%- set heading = entry.title -%}
{%- if heading -%}
{{- heading -}}
{%- endif -%}
{# Wrong — deprecated minification approach #}
{%- minify -%}
{% set heading = entry.title %}
{%- endminify -%}
Apply whitespace control on tags that produce unwanted blank lines in output. Not every tag needs it — use where visible output whitespace matters.
Include Isolation
Every {% include %} MUST use only. No exceptions.
{# Correct — explicit, isolated #}
{%- include '_atoms/buttons/button--primary' with {
text: entry.title,
url: entry.url,
} only -%}
{# Wrong — ambient variables leak in #}
{%- include '_atoms/buttons/button--primary' with {
text: entry.title,
url: entry.url,
} -%}
Without only, a component can silently depend on variables from its parent scope, creating invisible coupling.
No Macros for Components
Never use {% macro %} for UI components. Macros don't support extends/block and their scoping model differs from includes.
{# Wrong — macro for a component #}
{% macro button(text, url) %}
{{ text }}
{% endmacro %}
{# Correct — include with isolation #}
{%- include '_atoms/buttons/button--primary' with {
text: text,
url: url,
} only -%}
Macros are acceptable for utility functions that return strings (e.g., formatting helpers), not for rendering UI.
Comment Headers
Every component file gets a section header comment:
{# =========================================================================
Component Name
Brief description of what this component does.
========================================================================= #}
Props files, variant files, views, layouts — all get headers. The ========= separator matches the PHP convention from craft-php-guidelines.
Craft Twig Helpers
{% tag %} — Polymorphic Elements
Primary tool for rendering elements whose tag name depends on props.
{%- set element = props.get('url') ? 'a' : 'span' -%}
{%- tag element with {
class: classes.implode(' '),
href: props.get('url') ?? false,
target: props.get('target') ?? false,
rel: props.get('rel') ?? false,
aria: {
label: props.get('label') ?? false,
},
} -%}
{{ props.get('text') }}
{%- endtag -%}
Rules:
- Variable name must be descriptive:
element,heading,wrapper. Neverel,hd. falseomits an attribute entirely from the rendered HTML.nullalso omits. Usefalsewhen explicitly excluding,nullwhen absent.classaccepts arrays with automatic falsy filtering.ariaanddataaccept nested hashes that expand toaria-*/data-*attributes.
tag() — Inline Element Function
For simple elements without complex inner content:
{{ tag('span', { class: 'sr-only', text: '(opens in new window)' }) }}
{{ tag('img', { src: image.url, alt: image.title, loading: 'lazy' }) }}
{{ tag('i', { class: ['fa-solid', icon], aria: { hidden: 'true' } }) }}
text:key = HTML-encoded content.html:key = raw HTML content (trusted input only).- Self-closing elements (
img,input,br) handled automatically.
attr() — Attribute Strings
For building attributes in non-tag contexts:
Returns a space-prefixed attribute string. Same false-means-omit and class array filtering as {% tag %}.
|attr Filter
For merging attributes onto existing HTML strings:
{{ svg('@webroot/icons/check.svg')|attr({ class: 'w-4 h-4', aria: { hidden: 'true' } }) }}
|parseAttr Filter
For extracting attributes from an HTML string into a hash for manipulation:
{% set attributes = ''|parseAttr %}
{# attributes = { class: 'foo', data: { id: '1' } } #}
|append Filter
For adding content to an element string:
{{ svg('@webroot/icons/logo.svg')|append('Company Logo', 'replace') }}
svg() Function
{{ svg('@webroot/icons/logo.svg') }}
{{ svg(entry.svgField.one()) }}
Combine with |attr for classes and aria attributes. Use |append for accessible labels inside the SVG.
collect() Conventions
collect() wraps a Twig hash into a Collection object. Primary use cases:
Props collection
{%- set props = collect({
heading: heading ?? null,
content: content ?? null,
utilities: utilities ?? null,
}) -%}
{# Access with get() #}
{{ props.get('heading') }}
{{ props.get('size', 'text-base') }}
{# Merge additional props #}
{%- set props = props.merge({ icon: icon ?? null }) -%}
Class collection (named keys)
{%- set classes = collect({
layout: 'flex items-center gap-2',
color: 'bg-brand-primary text-white',
hover: 'hover:bg-brand-accent',
utilities: props.get('utilities'),
}) -%}
class="{{ classes.implode(' ') }}"
Null values in collect() produce harmless extra spaces when joined — browsers normalize whitespace in class attributes. Use classes.filter(v => v).implode(' ') if you want pristine output for devMode inspection, but plain implode(' ') is fine for production.
Entry queries as Collections
{# .collect instead of .all() when you need Collection methods #}
{%- set entries = craft.entries.section('blog').eagerly().collect -%}
{%- set featured = entries.filter(e => e.featured).first -%}
Common Pitfalls
???operator — custom Twig extension from older projects. Not portable. Use??.- camelCase variables —
iconPosition→ justposition. Single descriptive words. - Missing
only— silent variable leaking, invisible coupling. {%- minify -%}— deprecated. Use{%-whitespace control.- Abbreviations —
el,btn,nav,desc,ctr→ spell it out. is not defined— verbose null checking.??handles it.- Macros as components — wrong scoping, no extends/block support.
- Hardcoded colors in class strings —
bg-yellow-600→bg-brand-accent. - String concatenation for classes —
'flex ' ~ extraClass→ usecollect({})with named keys. options.xpattern — old macro convention. Use direct variable names.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: chrisrowe
- Source: chrisrowe/craftcms-claude-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.