mcpField
S
shopify-theme-development

shopify-theme-development

github.com/finsilabs开发60103 次安装5 天前更新
代码仓库

关于 shopify-theme-development

面向 Shopify 主题开发者的技能指南,涵盖 Liquid、Online Store 2.0、Shopify CLI 和性能优化。内容也说明如何构建商家可配置的区块与主题应用扩展。

主要功能

  1. 01使用 Liquid 和 JSON 模板构建主题
  2. 02创建可配置的区块与版块
  3. 03通过 Shopify CLI 开发主题
  4. 04实现 AJAX 购物车与变体选择
  5. 05优化加载性能并构建主题应用扩展

适用场景

  1. 01从头开发 Shopify 主题或修改 Dawn
  2. 02为主题编辑器添加自定义区块
  3. 03实现商品页、集合页或购物车功能
  4. 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.json
evals/collection-grid-with-lazy-images-and-pag/
criteria.json
task.md
evals/performance-optimization-and-ajax-cart/
criteria.json
task.md
evals/section-blocks-with-theme-editor-support/
criteria.json
task.md

详细介绍

Shopify 主题开发

概述

使用 Liquid 模板、JSON 模板、区块和分区构建并自定义 Shopify 主题,为商家提供可自定义的布局,并使用主题应用扩展集成应用。本技能涵盖 Shopify 主题架构(Online Store 2.0)、Shopify CLI 开发工作流、通过延迟加载和关键 CSS 优化性能,以及构建灵活分区的模式,让商家能通过主题编辑器进行配置。

何时使用此技能

  • 从头构建新 Shopify 主题或基于 Dawn 创建分支时
  • 为主题编辑器创建自定义分区和区块时
  • 使用 Liquid 实现产品页面、系列网格或购物车功能时
  • 针对 Core Web Vitals 和速度优化 Shopify 主题时
  • 构建主题应用扩展以将应用内容注入主题时

核心说明

  1. 使用 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
    
  2. 使用分区创建 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"]
    }
    
  3. 使用区块构建可自定义的产品分区

    {% 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 %}
    
  4. 使用 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();
    
  5. 优化性能

    {% 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>
    
  6. 创建主题应用扩展

    # 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