shopify-theme-development について
Shopify テーマ開発者向けのスキルガイドです。Liquid、Online Store 2.0、Shopify CLI、パフォーマンス最適化に加え、設定可能なセクションやテーマアプリ拡張を扱います。
主な機能
- 01Liquid と JSON テンプレートでのテーマ構築
- 02設定可能なセクションとブロックの作成
- 03Shopify CLI によるテーマ開発
- 04AJAX カートとバリアント選択の実装
- 05パフォーマンス最適化とテーマアプリ拡張
活用シーン
- 01Shopify テーマの新規開発や 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 を遅延読み込みする — 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
コメント
Markdown 対応