mcpField
C
craft-twig-guidelines

craft-twig-guidelines

github.com/michtio开发8744 次安装4 天前更新
代码仓库

关于 craft-twig-guidelines

为 Craft CMS 5 项目提供 Twig 模板编码规范的技能,面向编写和维护模板的开发者。内容涵盖安全输出、组件隔离及 Craft 专用模板工具。

主要功能

  1. 01变量命名与空值处理规范
  2. 02使用 only 隔离模板包含
  3. 03组件渲染避免使用宏
  4. 04安全输出与内联资源规范
  5. 05集合数据处理约定

适用场景

  1. 01编写 Craft CMS 5 模板
  2. 02审查 Twig 组件与布局
  3. 03处理模板中的集合和字段数据

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/michtio/craftcms-claude-skills --skill craft-twig-guidelines

安装后在 Claude Code 中输入/craft-twig-guidelines确认已加载。

详细介绍

Twig 编码规范 — Craft CMS 5

Craft CMS 5 项目中 Twig 模板的编码约定。适用于所有 Twig 代码 — 原子组件、视图、布局、构建器、局部模板。

配套技能 — 始终一并加载

触发此技能时,还应加载:

  • craft-site — 模板架构和组件模式。创建或编辑组件、布局、视图或构建器时必需。
  • craft-content-modeling — 内容架构。模板代码涉及元素查询、字段访问或分区决策时必需。

Twig 架构模式(原子设计、路由、构建器)请参阅 craft-site 技能。PHP 编码规范请参阅 craft-php-guidelines。

文档

如果这里未涵盖相关内容,请使用 WebFetch 查阅具体文档页面。

变量命名

优先使用单词、描述性、全小写的名称。需要多个单词时,使用 camelCase。

{# Correct #}
{% set heading = entry.title %}
{% set image = entry.heroImage.one() %}
{% set items = navigation.links.all() %}
{% set element = props.get('url') ? 'a' : 'span' %}
{% set buttonText = entry.callToAction %}
{% set containerClass = 'max-w-3xl' %}

{# Wrong — abbreviations #}
{% set el = props.get('url') ? 'a' : 'span' %}
{% set btn = entry.callToAction %}
{% set nav = navigation.links.all() %}

{# Wrong — snake_case #}
{% set button_text = entry.callToAction %}
{% set container_class = 'max-w-3xl' %}

不要使用缩写:用 element 而不是 el,用 button 而不是 btn,用 navigation 而不是 nav, 用 description 而不是 desc。

如果上下文能明确含义,优先使用单词名称(例如,在组件中使用 heading 比 sectionHeading 更好)。但为了清晰起见,完全可以使用多个单词的 camelCase 名称。

Null 处理

默认使用 ??。始终安全,且具有良好的可移植性。

如果项目已安装 nystudio107/craft-emptycoalesce 或 nystudio107/craft-seomatic,则可以使用 ???(空合并)——这两个插件都提供该运算符。但切勿仅为了使用 ??? 而安装插件。请先检查 composer.json。

{# Always correct #}
{% set heading = entry.heading ?? '' %}
{% set image = entry.heroImage.one() ?? null %}
{{ props.get('label') ?? 'Default' }}

{# OK if empty-coalesce or SEOmatic is installed — checks empty, not just null #}
{% set heading = entry.heading ??? '' %}

{# Wrong — verbose, unnecessary #}
{% if entry.heading is defined and entry.heading is not null %}
{% if entry.heading is not defined %}

Craft 5 支持 nullsafe 运算符(?.)。对于可能包含 null 链接的深层遍历链,请使用它——它能干净地传递 null,无需繁琐地使用 is defined and is not null:

{# Reach for ?. when any link in the chain may be null #}
{{ entry?.author?.fullName ?? 'Anonymous' }}

{# ?? alone is enough when only the leaf is in question #}
{{ entry.title ?? '' }}

对于简单的“值或回退值”情况,?? 仍是正确选择;对于中间链接可能缺失的链,则使用 ?.。不要在单个属性访问上使用 ?.——它只会增加冗余,不会提升安全性。

空白控制

使用 {%- 和 {{- 修剪空白。切勿使用 {%- minify -%}。

{# Correct — surgical whitespace control #}
{%- set heading = entry.title -%}
{%- if heading -%}
    {{- heading -}}
{%- endif -%}

{# Wrong — deprecated minification approach #}
{%- minify -%}
    {% set heading = entry.title %}
{%- endminify -%}

对会在输出中产生多余空行的标签应用空白控制。并非每个标签都需要这样处理——只在输出中的可见空白会产生影响时使用。

Include 隔离

每个 {% 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 %} 构建 UI 组件。宏不支持 extends/block,且其作用域模型与 include 不同。

{# 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 -%}

宏可用于返回字符串的实用函数(例如格式化辅助函数),但不能用于渲染 UI。

注释标题

每个组件文件都要有分节标题注释:

{# =========================================================================
   Component Name
   Brief description of what this component does.
   ========================================================================= #}

Props 文件、变体文件、视图、布局——所有文件都要有标题。========= 分隔符与 craft-php-guidelines 中的 PHP 约定一致。

Craft Twig 辅助工具

{% tag %} — 多态元素

渲染标签名称取决于 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 -%}

规则:

  • 变量名必须具有描述性:element、heading、wrapper。切勿使用 el、hd。
  • false 会从渲染后的 HTML 中完全省略该属性。
  • null 也会省略。显式排除时使用 false,不存在时使用 null。
  • class 支持数组,并会自动过滤假值。
  • aria 和 data 支持嵌套哈希,并会展开为 aria-* / 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' } }) }}

{# Craft 5.10+: pass a string as the second arg as a text-only shortcut #}
{{ tag('span', 'Read more') }}
  • text: 键 = 经 HTML 编码的内容。
  • html: 键 = 原始 HTML 内容(仅限可信输入)。
  • 自动处理自闭合元素(img、input、br)。
attr() — 属性字符串

用于在非标签上下文中构建属性:

<div{{ attr({ class: ['card', active ? 'card--active'], data: { id: entry.id } }) }}>

返回以空格开头的属性字符串。与 {% tag %} 一样,false 表示省略,且会过滤类数组。

|attr 过滤器

用于将属性合并到现有 HTML 字符串:

{{ svg('@webroot/icons/check.svg')|attr({ class: 'w-4 h-4', aria: { hidden: 'true' } }) }}
|parseAttr 过滤器

用于从 HTML 字符串中提取属性,并存入哈希表以便操作:

{% 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 搭配使用来设置类和 aria 属性。可使用 |append 在 SVG 内添加无障碍标签。

url() 中使用 false 参数(Craft 5.11+)

将 false 作为第二个参数传入绝对 URL 时,会移除所有查询字符串参数。适用于规范链接、分享 URL 和“清除筛选条件”链接:

<link rel="canonical" href="{{ url(craft.app.request.absoluteUrl, false) }}">
<a href="{{ url(craft.app.request.absoluteUrl, false) }}">Clear filters</a>

数组仍会合并传入的参数;false 则会移除所有参数。只有传入的 URL 是绝对 URL 时才会移除参数,因此请传入完整 URL,而非路径。PHP 中对应的调用是 UrlHelper::url($url, false),此外还有 UrlHelper::removeParams($url, [...]) 和 UrlHelper::removeAllParams($url),可进行更精细的控制。在 5.11 之前,请改用 siteUrl(craft.app.request.pathInfo) 重新构建不带参数的 URL。

heading() / h() / h1()…h6() — 以编程方式生成标题(Craft 5.10+)

无需拼接字符串,即可根据动态级别构建标题标签。适用于接收 level 属性、需要渲染对应标签的组件,无需手动执行 tag('h' ~ level, text)。

{# heading(level, text-or-attributes) — level is an int 1-6 #}
{{ heading(2, 'Section title') }}                     {# <h2>Section title</h2> #}
{{ heading(3, { class: 'text-xl', text: 'Subsection' }) }}

{# h() is the short alias for heading() #}
{{ h(2, 'Section title') }}

{# Bound-level shortcuts — only attributes/text needed #}
{{ h1('Page title') }}
{{ h2('Section') }}
{{ h6({ class: 'sr-only', text: 'Hidden heading' }) }}

这些是无状态的标签构建器,不会自动递增,也不会跟踪当前级别。需要在嵌套上下文间传递级别的组件仍需将其作为属性传入。若级别超出 1-6,heading() 会抛出 InvalidArgumentException。

过滤器新增项(Craft 5.10+)
{{ price|number(locale: 'de-DE') }}                   {# locale arg #}
{{ entry.postDate|datetime('long', withTimeZone: true) }}
{{ deadline|time('short', withTimeZone: true) }}
{{ maybeNull|timestamp }}                              {# returns "now" for null/empty #}
筛选与映射 — 默认使用 collect()

在模板中操作数据时,默认使用 collect()——通过统一、可链式调用的 API(where、firstWhere、groupBy、keyBy、unique、pluck、map、filter、sortByDesc)替代混用不同的写法:

{% set newsByYear = entries.collect
    .where('type', 'news')
    .sortByDesc('postDate')
    .groupBy(e => e.postDate|date('Y')) %}

若只需单个简单的内联操作,裸用 Twig/Craft 数组过滤器也没问题——例如 {% for e in entries|filter(e => e.enabled) %}——此时 Collection 并无额外作用。但 Twig 的 |filter/|map 返回普通数组,而 Craft 的 |where/|firstWhere/|group/|index 会经由 ArrayHelper 处理,因此无法像 Collection 方法那样链式调用;一旦需要多个步骤,就改用 collect()。不要随意混用这两种写法。

没有 |indexBy 过滤器——用 |index 为列表设置键(或对 Collection 调用 .keyBy()),用 |group 分组(或调用 .groupBy())。(|where、|firstWhere、|contains、|group、|index、|explodeClass/|explodeStyle 均为 Craft 注册的过滤器。)

安全输出与内联资源
  • |t('category'):所有面向用户的字符串都必须通过翻译处理,切勿硬编码显示文案。
  • |purify:对不可信或富 HTML 进行净化,不要使用 |raw。仅对可信字段输出(CKEditor/Redactor)使用 |raw;切勿对用户提交的内容或源自查询字符串的内容使用 |raw。
  • |explodeClass / |explodeStyle:在合并前将类名/样式字符串规范化为数组,不要手动按空格拆分。
  • {% js %} / {% css %} / {% script %}:通过 View 注册内联资源(它会去重并安排位置),不要手动编写 <script>/<style>。({% js %}/{% css %} 通过资源管理器处理;{% script %}/{% html %} 则按原样输出。)
  • {% dd %} / dump():仅用于调试;切勿将其随代码发布,也不要用 {{ x|json_encode }} 作为调试技巧。
  • |e('js') 会进行积极转义:标准 Twig JS 转义器会将每个非字母数字 ASCII 字符(包括空格(0x20))转换为 \uXXXX 序列(空格会变成 )。这在内联 JS 中嵌入字符串时是正确且安全的,但这意味着翻译后的多词字符串不会在输出中原样出现,因此在渲染后的 HTML/JS 中搜索(或用测试 assertSee() 检查)完整短语时会悄悄漏掉。请改为匹配单个词或原始 JS 标识符。关于 QA/搜索部分,请参阅 craftcms 技能中的 testing.md。

collect() 约定

构建 props 和类集合时,需遵循以下样式规则:

  • 键使用 camelCase:heroImage,不要使用 hero_image。
  • 每个关注点使用一个具名键:类集合应为每个样式关注点(layout、color、spacing 等)分别设置一个键,切勿让两个类争用同一个元素。
  • 使用 .implode(' ') 构建类字符串:切勿使用字符串拼接('flex ' ~ extraClass)。
  • 空值/空字符串无妨:implode(' ') 会将它们作为空字符串连接,产生的额外空格会由浏览器在类属性中规范化。
{%- set classes = collect({
    layout: 'flex items-center gap-2',
    color: 'bg-brand-primary text-brand-on-primary',
    hover: 'hover:bg-brand-accent',
    utilities: props.get('utilities'),
}) -%}

class="{{ classes.implode(' ') }}"

有关 collect() 方法的完整参考和架构模式(props 集合、get()/merge()、作为 Collections 的 entry 查询),请参阅 craft-site(references/twig-collections.md);有关具名键 Tailwind 类模式,请参阅 craft-site(references/tailwind-conventions.md)。

文案风格

面向用户的文案中切勿使用 em 短横线(—)或 en 短横线(–),包括模板文本、|t 字符串以及写入模板的任何内容。请改用逗号、句号、冒号或括号;表示范围时,请写作“4 到 10”或使用普通 ASCII 连字符(“4-10”)。普通连字符可以使用。Twig 注释({# … #})不受此限制。完成前,请在模板中搜索 — 和 –。(这是共享规则在前端的部分;craft-php-guidelines 技能对 Craft::t() 字符串、字段标签、CP 通知和文档也有相同规定。)

常见陷阱

  1. 未安装插件却使用 ??? 运算符 — 需要 nystudio107/craft-emptycoalesce 或 nystudio107/craft-seomatic。使用前检查 composer.json。默认使用 ??。
  2. 使用 snake_case 变量 — 使用 camelCase:heroImage,而不是 hero_image。
  3. 缺少 only — 会导致变量悄然泄漏,产生隐式耦合。
  4. {%- minify -%} — 已弃用。使用 {%- 空白控制。
  5. 使用缩写 — el、btn、nav、desc、ctr → 写出完整形式。
  6. 使用 is not defined — 冗长的空值检查。?? 可以处理。
  7. 将宏用作组件 — 作用域错误,且不支持 extends/block。
  8. 在类字符串中硬编码颜色 — bg-yellow-600 → 使用 bg-brand-accent。
  9. 用字符串拼接类名 — 'flex ' ~ extraClass → 使用带命名键的 collect({})。
  10. 在 Craft Model 上使用 is empty / |default(5.10+) — 现在,任何 yii\base\Model(条目、设置、自定义模型)无论其属性值如何,都会被视为非空。这意味着 {{ user|default('Guest') }} 总是会渲染 user 对象;{% if entry is empty %} 总是为 false。检查你关心的具体属性:{% if entry.title is empty %}。
  11. 使用 options.x 模式 — 这是旧式宏约定。请使用直接的变量名。
  12. 在条件语句内定义块 — {% if %}{% block foo %}{% endblock %}{% endif %} 是无效的 Twig。块是编译时结构,不能有条件地定义。将条件判断移到块内:{% block foo %}{% if condition %}...{% endif %}{% endblock %}。
  13. 硬编码 /admin CP URL — cpTrigger 可通过 CRAFT_CP_TRIGGER 环境变量或 general.php 中的 cpTrigger 配置。许多项目使用 cp 而不是 admin。使用 cpUrl() 函数或检查 .env,绝不要硬编码 /admin/。
  14. 面向用户的文案中使用 em/en dash:不要在模板文本或 |t 字符串中使用 — 或 –。请使用逗号、冒号、括号或普通连字符;范围写作“4 到 10”。参见文案风格部分。
  15. Twig 没有 instanceof — 按类限定行为的 PHP(例如控制器中通过 match (true) 对 instanceof 进行检查)没有直接对应的模板写法,而模板尝试模拟时,两者会悄然产生偏差。请根据功能进行分支判断(object.hasProperty('handle')、object.hasMethod(...)),或让 PHP 向模板传入显式判别值(type: 'entry')。
  16. 在 {% cache %} 内输出每个请求独有的内容:该标签会存储正文并重放其中捕获的 {% js %}、{% script %} 和 {% css %},因此其中的 CSP nonce、csrfInput()、aria-current/活动状态类或语言切换器 URL 都会冻结为首次渲染时的值。缓存查询及其标记;将这些内容放在缓存区外。参数只能按一种顺序排列(globally、using key、for 1 hour/until、if/unless),并且没有 tag()。参见 craft-site 技能中的 template-caching.md。
  17. 在输出中搜索经过 |e('js') 转义的短语 — JS 转义器会将每个空格转换为 ,因此翻译后的多词字符串不会原样出现在渲染后的 JS 中。请使用单个词元或原始标识符进行 grep/assert。参见“安全输出与内联资源”部分以及 craftcms 技能中的 testing.md。

评论

支持 Markdown