关于 shopify-theme-development
面向 Shopify 主题开发者的技能指南,涵盖 Liquid、Online Store 2.0、Shopify CLI 和性能优化。内容也说明如何构建商家可配置的区块与主题应用扩展。
主要功能
- 01使用 Liquid 和 JSON 模板构建主题
- 02创建可配置的区块与版块
- 03通过 Shopify CLI 开发主题
- 04实现 AJAX 购物车与变体选择
- 05优化加载性能并构建主题应用扩展
适用场景
- 01从头开发 Shopify 主题或修改 Dawn
- 02为主题编辑器添加自定义区块
- 03实现商品页、集合页或购物车功能
- 04优化主题速度与 Core Web Vitals
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ finsilabs/ awesome-ecommerce-skills --skill shopify-theme-development安装后在 Claude Code 中输入/shopify-theme-development确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
tile.jsonevals/collection-grid-with-lazy-images-and-pag/criteria.jsontask.mdevals/performance-optimization-and-ajax-cart/criteria.jsontask.mdevals/section-blocks-with-theme-editor-support/criteria.jsontask.md详细介绍
Shopify 主题开发
概述
使用 Liquid 模板、JSON 模板、区块和分区构建并自定义 Shopify 主题,为商家提供可自定义的布局,并使用主题应用扩展集成应用。本技能涵盖 Shopify 主题架构(Online Store 2.0)、Shopify CLI 开发工作流、通过延迟加载和关键 CSS 优化性能,以及构建灵活分区的模式,让商家能通过主题编辑器进行配置。
何时使用此技能
- 从头构建新 Shopify 主题或基于 Dawn 创建分支时
- 为主题编辑器创建自定义分区和区块时
- 使用 Liquid 实现产品页面、系列网格或购物车功能时
- 针对 Core Web Vitals 和速度优化 Shopify 主题时
- 构建主题应用扩展以将应用内容注入主题时
核心说明
-
使用 Shopify CLI 设置开发环境
# Install Shopify CLI npm install -g @shopify/cli @shopify/theme # Initialize a new theme (or clone Dawn) shopify theme init my-theme # Start development server with hot reload shopify theme dev --store=your-store.myshopify.com主题目录结构(Online Store 2.0):
my-theme/ ├── assets/ # CSS, JS, images ├── config/ # settings_schema.json, settings_data.json ├── layout/ # theme.liquid (main layout) ├── locales/ # Translation files ├── sections/ # Sections (reusable, merchant-configurable) ├── snippets/ # Partials (reusable Liquid fragments) └── templates/ # JSON templates referencing sections ├── product.json ├── collection.json └── index.json -
使用分区创建 JSON 模板
// templates/product.json { "sections": { "main": { "type": "main-product", "settings": {} }, "recommendations": { "type": "product-recommendations", "settings": { "heading": "You may also like", "products_to_show": 4 } }, "reviews": { "type": "product-reviews", "settings": {} } }, "order": ["main", "recommendations", "reviews"] } -
使用区块构建可自定义的产品分区
{% comment %} sections/main-product.liquid {% endcomment %} <section class="product-section" data-section-id="{{ section.id }}"> <div class="product-grid"> <div class="product-media"> {% for media in product.media %} {% case media.media_type %} {% when 'image' %} <div class="product-media-item {% if forloop.first %}active{% endif %}"> {{ media | image_url: width: 800 | image_tag: loading: 'lazy', widths: '200,400,600,800,1000', sizes: '(min-width: 768px) 50vw, 100vw', class: 'product-image' }} </div> {% when 'video' %} <div class="product-media-item"> {{ media | video_tag: autoplay: false, controls: true }} </div> {% endcase %} {% endfor %} </div> <div class="product-info"> {% for block in section.blocks %} {% case block.type %} {% when 'title' %} <h1 class="product-title" {{ block.shopify_attributes }}> {{ product.title }} </h1> {% when 'price' %} <div class="product-price" {{ block.shopify_attributes }}> {% if product.compare_at_price > product.price %} <s class="price-compare">{{ product.compare_at_price | money }}</s> {% endif %} <span class="price-current">{{ product.price | money }}</span> {% if product.compare_at_price > product.price %} <span class="price-badge">Sale</span> {% endif %} </div> {% when 'variant_picker' %} <div class="variant-picker" {{ block.shopify_attributes }}> {% for option in product.options_with_values %} <fieldset class="option-group"> <legend>{{ option.name }}</legend> {% for value in option.values %} <label class="option-label"> <input type="radio" name="{{ option.name }}" value="{{ value }}" {% if option.selected_value == value %}checked{% endif %} > <span>{{ value }}</span> </label> {% endfor %} </fieldset> {% endfor %} </div> {% when 'buy_buttons' %} <div class="buy-buttons" {{ block.shopify_attributes }}> {% form 'product', product %} <input type="hidden" name="id" value="{{ product.selected_or_first_available_variant.id }}"> <div class="quantity-selector"> <label for="quantity">Quantity</label> <input type="number" id="quantity" name="quantity" value="1" min="1"> </div> <button type="submit" class="btn btn-primary add-to-cart" {% unless product.selected_or_first_available_variant.available %}disabled{% endunless %} > {% if product.selected_or_first_available_variant.available %} Add to cart — {{ product.selected_or_first_available_variant.price | money }} {% else %} Sold out {% endif %} </button> {% endform %} </div> {% when 'description' %} <div class="product-description" {{ block.shopify_attributes }}> {{ product.description }} </div> {% when 'custom_text' %} <div class="custom-text" {{ block.shopify_attributes }}> {{ block.settings.text }} </div> {% endcase %} {% endfor %} </div> </div> </section> {% schema %} { "name": "Product Page", "tag": "section", "class": "section-product", "blocks": [ { "type": "title", "name": "Title", "limit": 1 }, { "type": "price", "name": "Price", "limit": 1 }, { "type": "variant_picker", "name": "Variant Picker", "limit": 1 }, { "type": "buy_buttons", "name": "Buy Buttons", "limit": 1 }, { "type": "description", "name": "Description", "limit": 1 }, { "type": "custom_text", "name": "Custom Text", "settings": [ { "type": "richtext", "id": "text", "label": "Text" } ] } ], "presets": [ { "name": "Product Page", "blocks": [ { "type": "title" }, { "type": "price" }, { "type": "variant_picker" }, { "type": "buy_buttons" }, { "type": "description" } ] } ] } {% endschema %} -
使用 Cart API 实现 AJAX 购物车
// assets/cart.js class CartManager { async addItem(variantId, quantity = 1) { const response = await fetch('/cart/add.js', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ items: [{ id: variantId, quantity }], }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.description || 'Could not add to cart'); } const data = await response.json(); this.updateCartUI(); return data; } async updateQuantity(lineKey, quantity) { const response = await fetch('/cart/change.js', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: lineKey, quantity }), }); const cart = await response.json(); this.updateCartUI(cart); return cart; } async getCart() { const response = await fetch('/cart.js'); return response.json(); } async updateCartUI(cart) { cart = cart || await this.getCart(); // Update cart count badge const badge = document.querySelector('[data-cart-count]'); if (badge) badge.textContent = cart.item_count; // Update cart drawer if open const drawer = document.querySelector('cart-drawer'); if (drawer) drawer.render(cart); } } window.cart = new CartManager(); -
优化性能
{% comment %} layout/theme.liquid — Critical performance optimizations {% endcomment %} <!doctype html> <html lang="{{ request.locale.iso_code }}"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="preconnect" href="https://cdn.shopify.com" crossorigin> <link rel="preconnect" href="https://fonts.shopifycdn.com" crossorigin> {% if template.name == 'product' %} {% assign hero_image = product.featured_image %} {% if hero_image %} <link rel="preload" as="image" href="{{ hero_image | image_url: width: 800 }}" imagesrcset="{{ hero_image | image_url: width: 400 }} 400w, {{ hero_image | image_url: width: 800 }} 800w" imagesizes="(min-width: 768px) 50vw, 100vw" > {% endif %} {% endif %} <style> {{ 'critical.css' | asset_url | stylesheet_tag | split: '<link' | first }} </style> <link rel="stylesheet" href="{{ 'theme.css' | asset_url }}" media="print" onload="this.media='all'"> <noscript><link rel="stylesheet" href="{{ 'theme.css' | asset_url }}"></noscript> {{ content_for_header }} </head> <body> {{ content_for_layout }} <script src="{{ 'theme.js' | asset_url }}" defer></script> </body> </html> -
创建主题应用扩展
# Generate a theme app extension block shopify app generate extension --type theme_app_extension --name my-app-block{% comment %} extensions/my-app-block/blocks/product-badge.liquid {% endcomment %} <div class="app-product-badge" {{ block.shopify_attributes }}> {% if block.settings.badge_text != blank %} <span class="badge" style="background-color: {{ block.settings.badge_color }}; color: {{ block.settings.text_color }};" > {{ block.settings.badge_text }} </span> {% endif %} </div> {% schema %} { "name": "Product Badge", "target": "section", "settings": [ { "type": "text", "id": "badge_text", "label": "Badge text", "default": "New" }, { "type": "color", "id": "badge_color", "label": "Badge color", "default": "#FF0000" }, { "type": "color", "id": "text_color", "label": "Text color", "default": "#FFFFFF" } ] } {% endschema %}
示例
使用延迟加载图片的系列网格
{% comment %} sections/collection-grid.liquid {% endcomment %}
<section class="collection-grid">
<h1>{{ collection.title }}</h1>
<div class="product-grid" role="list">
{% paginate collection.products by 24 %}
{% for product in collection.products %}
<div class="product-card" role="listitem">
<a href="{{ product.url }}">
{% if product.featured_image %}
{{ product.featured_image | image_url: width: 400 | image_tag:
loading: 'lazy',
widths: '200,300,400',
sizes: '(min-width: 1024px) 25vw, (min-width: 768px) 33vw, 50vw',
class: 'product-card-image'
}}
{% endif %}
<h2 class="product-card-title">{{ product.title }}</h2>
<p class="product-card-price">{{ product.price | money }}</p>
</a>
</div>
{% endfor %}
{% if paginate.pages > 1 %}
<nav class="pagination" aria-label="Pagination">
{{ paginate | default_pagination: next: 'Next', previous: 'Previous' }}
</nav>
{% endif %}
{% endpaginate %}
</div>
</section>
使用 JavaScript 更改变体
// assets/variant-selector.js
class VariantSelector extends HTMLElement {
connectedCallback() {
this.addEventListener('change', this.onVariantChange.bind(this));
this.productData = JSON.parse(
this.querySelector('[type="application/json"]').textContent
);
}
onVariantChange() {
const selectedOptions = [...this.querySelectorAll('input:checked')].map(
input => input.value
);
const variant = this.productData.variants.find(v =>
v.options.every((opt, i) => opt === selectedOptions[i])
);
if (!variant) return;
// Update URL without reload
const url = new URL(window.location);
url.searchParams.set('variant', variant.id);
window.history.replaceState({}, '', url);
// Update price display
const priceEl = document.querySelector('.price-current');
if (priceEl) {
priceEl.textContent = this.formatMoney(variant.price);
}
// Update add-to-cart button
const addToCart = document.querySelector('.add-to-cart');
const idInput = document.querySelector('input[name="id"]');
if (addToCart && idInput) {
idInput.value = variant.id;
addToCart.disabled = !variant.available;
addToCart.textContent = variant.available ? 'Add to cart' : 'Sold out';
}
}
formatMoney(cents) {
return '$' + (cents / 100).toFixed(2);
}
}
customElements.define('variant-selector', VariantSelector);
最佳实践
- 使用 Online Store 2.0 JSON 模板 — 商家无需编辑代码即可添加、移除和重新排序分区
- 充分利用
image_url和image_tag过滤器 — 它们会根据 Shopify 的 CDN 自动生成响应式srcset属性 - 始终包含
{{ block.shopify_attributes }}— 主题编辑器需要此数据属性来识别和选择区块 - 延迟加载所有 JavaScript — 在脚本标签上使用
defer或type="module";避免使用阻塞渲染的 JS - 使用 Shopify 原生 Liquid 过滤器 —
| money、| image_url、| asset_url经过优化并能处理边缘情况;不要重复造轮子 - 提供分区预设 — 预设定义商家添加分区时的默认状态;没有预设的分区不会出现在“添加分区”中
- 将分区控制在 50KB 的 Liquid 代码以内 — 大型分区会拖慢 Liquid 渲染器;将可复用代码提取到片段中
- 在 Shopify 暂存主题上测试 — 部署到线上主题前,始终先在未发布的主题上预览更改
常见问题
| 问题 | 解决方案 |
|---|---|
| 分区未显示在“添加分区”菜单中 | 确保分区的 {% schema %} 中包含 presets 数组;没有预设的分区只能用于 JSON 模板 |
| 主题编辑器显示“渲染分区时出错” | 检查 Liquid 语法错误;使用 shopify theme check 检查 Liquid 代码 |
| 图片无法从 Shopify CDN 加载 | 使用带有明确 width 参数的 image_url 过滤器;旧的 img_url 过滤器已弃用 |
| AJAX 添加商品后购物车数量徽标未更新 | 每次购物车变更后都获取 /cart.js 并更新徽标;不要只依赖 add.js 的响应 |
| 最大内容绘制 (LCP) 速度慢 | 使用 <link rel="preload" as="image"> 在 <head> 中预加载主图或产品图片;内联关键 CSS |
| 在 Liquid 中无法访问元字段 | 确保已在 Shopify 管理后台创建元字段定义;通过 product.metafields.namespace.key 访问 |
相关技能
- @product-page-design
- @ecommerce-seo
- @ecommerce-caching
- @storefront-performance
- @shopify-app-development
评论
支持 Markdown