mcpField
S
shopify-theme-development

shopify-theme-development

github.com/finsilabs開発60103 回インストール6 日前に更新
リポジトリ

shopify-theme-development について

Shopify テーマ開発者向けのスキルガイドです。Liquid、Online Store 2.0、Shopify CLI、パフォーマンス最適化に加え、設定可能なセクションやテーマアプリ拡張を扱います。

主な機能

  1. 01Liquid と JSON テンプレートでのテーマ構築
  2. 02設定可能なセクションとブロックの作成
  3. 03Shopify CLI によるテーマ開発
  4. 04AJAX カートとバリアント選択の実装
  5. 05パフォーマンス最適化とテーマアプリ拡張

活用シーン

  1. 01Shopify テーマの新規開発や 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 を遅延読み込みする — script タグには defer または type="module" を使用し、レンダリングをブロックする JS は避けてください
  • Shopify 標準の Liquid フィルターを使用する — | money、| image_url、| asset_url は最適化されており、特殊なケースにも対応します。独自に再実装しないでください
  • セクションのプリセットを用意する — プリセットは、マーチャントがセクションを追加したときのデフォルト状態を定義します。プリセットがないと、そのセクションは「セクションを追加」に表示されません
  • セクションの Liquid コードは 50KB 未満に保つ — 大きなセクションは Liquid レンダラーを遅くします。再利用可能なコードはスニペットに切り出してください
  • Shopify のステージングテーマでテストする — ライブテーマにデプロイする前に、必ず未公開のテーマで変更をプレビューしてください

よくある落とし穴

問題解決策
セクションが「セクションを追加」メニューに表示されないセクションの {% schema %} に presets 配列があることを確認してください。プリセットのないセクションは JSON テンプレートでのみ利用できます
テーマエディタに「Error rendering section」と表示されるLiquid の構文エラーを確認してください。shopify theme check を使って Liquid コードを lint してください
Shopify CDN から画像が読み込まれない明示的に width パラメーターを指定して image_url フィルターを使用してください。古い img_url フィルターは非推奨です
AJAX で商品を追加した後、カート数バッジが更新されないカートに変更を加えるたびに /cart.js を取得し、バッジを更新してください。add.js のレスポンスだけに頼らないでください
Largest Contentful Paint(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

コメント