mcpField
L
liquid-theme-standards

liquid-theme-standards

github.com/benjaminsehl开发1212.9k 次安装15 周前更新
代码仓库

关于 liquid-theme-standards

为 Shopify Liquid 主题制定 CSS、JavaScript 和 HTML 编写规范的参考技能,面向主题开发者。内容涵盖统一编码约定、渐进增强,以及安全修改 JSON 主题文件的方法。

主要功能

  1. 01CSS 组织与 BEM 命名
  2. 02设计令牌与防御式样式
  3. 03原生浏览器 API 和 Web Components
  4. 04语义化 HTML 与渐进增强
  5. 05图像无障碍与布局稳定性
  6. 06使用 jq 修改 JSON 模板

适用场景

  1. 01组织 Liquid 主题中的 CSS
  2. 02用原生 JavaScript 构建主题组件
  3. 03完善 RTL 与减少动态效果支持
  4. 04安全编辑 Shopify JSON 模板

安装

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

$npx skills add https://github.com/benjaminsehl/liquid-skills --skill liquid-theme-standards

安装后在 Claude Code 中输入/liquid-theme-standards确认已加载。

附带文件references/css-patterns.mdreferences/javascript-patterns.md

详细介绍

Shopify Liquid 主题的 CSS、JS 和 HTML 标准

核心原则

  1. 渐进增强——先使用语义化 HTML,其次是 CSS,最后是 JS
  2. 无外部依赖——JavaScript 仅使用浏览器原生 API
  3. 设计令牌——绝不硬编码颜色、间距或字体
  4. BEM 命名——始终使用一致的类命名方式
  5. 防御性 CSS——妥善处理边缘情况

Liquid 主题中的 CSS

CSS 的位置
位置Liquid?用途
{% stylesheet %}否组件作用域样式(每个文件一个)
{% style %}是需要使用 Liquid 的动态值(例如颜色设置)
assets/*.css否共享/全局样式

重要:{% stylesheet %} 不会处理 Liquid。动态值请使用内联 style 属性:

{%- comment -%} Do: inline variables {%- endcomment -%}
<div
  class="hero"
  style="--bg-color: {{ section.settings.bg_color }}; --padding: {{ section.settings.padding }}px;"
>

{%- comment -%} Don't: Liquid inside stylesheet {%- endcomment -%}
{% stylesheet %}
  .hero { background: {{ section.settings.bg_color }}; } /* Won't work */
{% endstylesheet %}
BEM 命名约定
.block                      → Component root: .product-card
.block__element             → Child: .product-card__title
.block--modifier            → Variant: .product-card--featured
.block__element--modifier   → Element variant: .product-card__title--large

规则:

  • 使用连字符分隔单词:.product-card,而不是 .productCard
  • 元素只允许单层:.block__element,绝不使用 .block__el1__el2
  • 修饰符始终与基础类配对:class="btn btn--primary",绝不单独使用 class="btn--primary"
  • 子元素可能独立存在时,开始新的 BEM 作用域

<div class="product-card">
  <h3 class="product-card__title">{{ product.title }}</h3>
  <span class="product-card__button-label">{{ 'add_to_cart' | t }}</span>
</div>


<div class="product-card">
  <button class="button button--primary">
    <span class="button__label">{{ 'add_to_cart' | t }}</span>
  </button>
</div>
特异性
  • 尽可能使用 0 1 0(单个类)
  • 复杂的父子关系最多使用 0 4 0
  • 绝不将 ID 用作选择器
  • 绝不使用 !important(若确实不得不使用,请注释说明原因)
  • 避免使用元素选择器,改用类
CSS 嵌套
/* Do: media queries inside selectors */
.header {
  width: 100%;

  @media screen and (min-width: 750px) {
    width: auto;
  }
}

/* Do: state modifiers with & */
.button {
  background: var(--color-primary);

  &:hover { background: var(--color-primary-hover); }
  &:focus-visible { outline: 2px solid var(--color-focus); }
  &[disabled] { opacity: 0.5; }
}

/* Do: parent modifier affecting children (single level) */
.card--featured {
  .card__title { font-size: var(--font-size-xl); }
}

/* Don't: nested beyond first level */
.parent {
  .child {
    .grandchild { } /* Too deep */
  }
}
设计令牌

所有值都使用 CSS 自定义属性,绝不硬编码颜色、间距或字体。定义一致的比例,并在各处引用。

比例示例(根据主题需求调整):

:root {
  /* Spacing — use a consistent scale */
  --space-2xs: 0.5rem;    --space-xs: 0.75rem;   --space-sm: 1rem;
  --space-md: 1.5rem;     --space-lg: 2rem;       --space-xl: 3rem;

  /* Typography — relative units */
  --font-size-sm: 0.875rem;  --font-size-base: 1rem;
  --font-size-lg: 1.125rem;  --font-size-xl: 1.25rem;  --font-size-2xl: 1.5rem;
}

关键原则:

  • 间距和排版使用 rem(遵循用户的字体大小偏好)
  • 使用语义化的令牌名称:--space-sm,而不是 --space-16
  • 全局令牌定义在 :root 中,作用域令牌定义在组件根元素上
CSS 变量作用域

全局——在 :root 中定义主题范围的值 组件作用域——在组件根元素上定义,并使用命名空间:

/* Do: namespaced */
.facets {
  --facets-padding: var(--space-md);
  --facets-z-index: 3;
}

/* Don't: generic names that collide */
.facets {
  --padding: var(--space-md);
  --z-index: 3;
}

通过内联样式覆盖分区/区块设置:

<section
  class="hero"
  style="
    --hero-bg: {{ section.settings.bg_color }};
    --hero-padding: {{ section.settings.padding }}px;
  "
>
CSS 属性顺序
  1. 布局——position、display、flex-direction、grid-template-columns
  2. 盒模型——width、margin、padding、border
  3. 排版——font-family、font-size、line-height、color
  4. 视觉效果——background、opacity、border-radius
  5. 动画——transition、animation
逻辑属性(支持 RTL)
/* Do: logical properties */
padding-inline: 2rem;
padding-block: 1rem;
margin-inline: auto;
border-inline-end: 1px solid var(--color-border);
text-align: start;
inset: 0;

/* Don't: physical properties */
padding-left: 2rem;
text-align: left;
top: 0; right: 0; bottom: 0; left: 0;
防御性 CSS
.component {
  overflow-wrap: break-word;        /* Prevent text overflow */
  min-width: 0;                     /* Allow flex items to shrink */
  max-width: 100%;                  /* Constrain images/media */
  isolation: isolate;               /* Create stacking context */
}

.image-container {
  aspect-ratio: 4 / 3;             /* Prevent layout shift */
  background: var(--color-surface); /* Fallback for missing images */
}
现代 CSS 特性
/* Container queries for responsive components */
.product-grid { container-type: inline-size; }
@container (min-width: 400px) {
  .product-card { grid-template-columns: 1fr 1fr; }
}

/* Fluid spacing */
.section { padding: clamp(1rem, 4vw, 3rem); }

/* Intrinsic sizing */
.content { width: min(100%, 800px); }
性能
  • 仅对 transform 和 opacity 设置动画(绝不对布局属性设置动画)
  • 谨慎使用 will-change,动画结束后移除
  • 使用 contain: content 实现隔离渲染
  • 移动设备上使用 dvh 而不是 vh
减少动态效果
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

Liquid 主题中的 JavaScript

JS 的位置
位置Liquid?用途
{% javascript %}否组件专用脚本(每个文件一个)
assets/*.js否共享工具、Web Components
Web Component 模式
class ProductCard extends HTMLElement {
  connectedCallback() {
    this.button = this.querySelector('[data-add-to-cart]');
    this.button?.addEventListener('click', this.#handleClick.bind(this));
  }

  disconnectedCallback() {
    // Clean up event listeners, abort controllers
  }

  async #handleClick(event) {
    event.preventDefault();
    this.button.disabled = true;

    try {
      const formData = new FormData();
      formData.append('id', this.dataset.variantId);
      formData.append('quantity', '1');

      const response = await fetch('/cart/add.js', {
        method: 'POST',
        body: formData
      });

      if (!response.ok) throw new Error('Failed');

      this.dispatchEvent(new CustomEvent('cart:item-added', {
        detail: await response.json(),
        bubbles: true
      }));
    } catch (error) {
      console.error('Add to cart error:', error);
    } finally {
      this.button.disabled = false;
    }
  }
}

customElements.define('product-card', ProductCard);
<product-card data-variant-id="{{ product.selected_or_first_available_variant.id }}">
  <button data-add-to-cart>{{ 'products.add_to_cart' | t }}</button>
</product-card>
JavaScript 规则
规则应使用不应使用
循环for (const item of items)items.forEach()
异步async/await.then() 链式调用
变量默认使用 const除非需要重新赋值,否则不要使用 let
条件判断提前返回嵌套的 if/else
URLnew URL() + URLSearchParams字符串拼接
依赖浏览器原生 API外部库
私有方法#methodName()_methodName()
类型JSDoc @typedef、@param、@returns未标注类型
使用 AbortController 处理 Fetch
class DataLoader extends HTMLElement {
  #controller = null;

  async load(url) {
    this.#controller?.abort();
    this.#controller = new AbortController();

    try {
      const response = await fetch(url, { signal: this.#controller.signal });
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return await response.json();
    } catch (error) {
      if (error.name !== 'AbortError') throw error;
      return null;
    }
  }

  disconnectedCallback() {
    this.#controller?.abort();
  }
}
组件间通信

**父级 → 子级:**调用公共方法

this.querySelector('child-component')?.publicMethod(data);

**子级 → 父级:**派发自定义事件

this.dispatchEvent(new CustomEvent('child:action', {
  detail: { value },
  bubbles: true
}));

HTML 标准

优先使用原生元素
需求使用不要使用
可展开内容<details>/<summary>使用 JS 的自定义手风琴
对话框/模态框<dialog>自定义覆盖层 div
工具提示/弹出层popover 属性自定义定位 div
搜索表单<search><div class="search">
表单结果<output><span class="result">
渐进增强
{%- comment -%} Works without JS {%- endcomment -%}
<details class="accordion">
  <summary>{{ block.settings.heading }}</summary>
  <div class="accordion__content">
    {{ block.settings.content }}
  </div>
</details>

{%- comment -%} Enhanced with JS {%- endcomment -%}
{% javascript %}
  // Optional: smooth animation, analytics tracking
{% endjavascript %}
图片
{{ image | image_url: width: 800 | image_tag:
  loading: 'lazy',
  alt: image.alt | escape,
  width: image.width,
  height: image.height
}}
  • 为所有首屏以下的图片设置 loading="lazy"
  • 始终设置 width 和 height,以防止布局偏移
  • 使用描述性 alt 文本;装饰性图片使用空 alt=""

JSON 模板与配置文件

主题模板(templates/*.json)、分区组(sections/*.json)和配置文件(config/settings_data.json)都是 JSON。使用 bash 工具通过 jq 进行精准修改——对于结构化数据,这比基于字符串的查找和替换更安全、更可靠。

常见模式
# Add a section to a template
jq '.sections.new_section = {"type": "hero", "settings": {"heading": "Welcome"}}' templates/index.json > /tmp/out && mv /tmp/out templates/index.json

# Update a setting value
jq '.current.sections.header.settings.logo_width = 200' config/settings_data.json > /tmp/out && mv /tmp/out config/settings_data.json

# Reorder sections
jq '.order += ["new_section"]' templates/index.json > /tmp/out && mv /tmp/out templates/index.json

# Remove a section
jq 'del(.sections.old_banner) | .order -= ["old_banner"]' templates/index.json > /tmp/out && mv /tmp/out templates/index.json

# Read a nested value
jq '.sections.header.settings' templates/index.json

修改任何 .json 文件时,优先使用 jq 而非 edit——它可以验证结构、处理转义,并避免空白和格式问题。

参考资料

评论

支持 Markdown