Loading...
Loading...
Twig coding standards and conventions for Craft CMS 5 templates. Covers variable naming, null handling, whitespace control, include isolation, Craft Twig helpers ({% tag %}, tag(), attr(), |attr, svg()), and collect() usage. Triggers on: any Twig template creation or review, .twig files, {% include %}, {% extends %}, {% tag %}, collect(), props.get(), .implode(), attr(), |attr filter, svg(), ?? operator, whitespace control, template coding standards, Twig best practices, naming conventions for Twig, currentSite, siteUrl, craft.entries, .eagerly(), .collect. Not for Twig architecture patterns (use craft-site) or PHP code (use craft-php-guidelines). Always use when writing, editing, or reviewing any Craft CMS Twig template code.
npx skill4agent add michtio/craftcms-claude-skills craft-twig-guidelinescraft-sitecraft-php-guidelinesweb_fetch{# 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' %}elementelbuttonbtnnavigationnavdescriptiondeschero_imageheroImage??{# 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 %}?.??{# Can't do this yet #}
{{ entry?.author?.fullName }}
{# Do this instead #}
{{ entry.author.fullName ?? '' }}{%-{{-{%- minify -%}{# Correct — surgical whitespace control #}
{%- set heading = entry.title -%}
{%- if heading -%}
{{- heading -}}
{%- endif -%}
{# Wrong — deprecated minification approach #}
{%- minify -%}
{% set heading = entry.title %}
{%- endminify -%}{% include %}only{# 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,
} -%}only{% macro %}{# Wrong — macro for a component #}
{% macro button(text, url) %}
<a href="{{ url }}">{{ text }}</a>
{% endmacro %}
{# Correct — include with isolation #}
{%- include '_atoms/buttons/button--primary' with {
text: text,
url: url,
} only -%}{# =========================================================================
Component Name
Brief description of what this component does.
========================================================================= #}=========craft-php-guidelines{% tag %}{%- 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 -%}elementheadingwrapperelhdfalsenullfalsenullclassariadataaria-*data-*tag(){{ 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:html:imginputbrattr()<div{{ attr({ class: ['card', active ? 'card--active'], data: { id: entry.id } }) }}>false{% tag %}|attr{{ svg('@webroot/icons/check.svg')|attr({ class: 'w-4 h-4', aria: { hidden: 'true' } }) }}|parseAttr{% set attributes = '<div class="foo" data-id="1">'|parseAttr %}
{# attributes = { class: 'foo', data: { id: '1' } } #}|append{{ svg('@webroot/icons/logo.svg')|append('<title>Company Logo</title>', 'replace') }}svg(){{ svg('@webroot/icons/logo.svg') }}
{{ svg(entry.svgField.one()) }}|attr|appendcollect()collect(){%- 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 }) -%}{%- 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(' ') }}"collect()classes.filter(v => v).implode(' ')implode(' '){# .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 -%}?????iconPositionpositiononly{%- minify -%}{%-elbtnnavdescctris not defined??bg-yellow-600bg-brand-accent'flex ' ~ extraClasscollect({})options.x