Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:32:51 +08:00
commit 636e443771
13 changed files with 6987 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
{
"name": "shopify-developer",
"description": "Professional Shopify development toolkit with 6 specialized skills covering Liquid templating, theme development, API integration (GraphQL/REST), custom app development, performance optimization, and debugging. Complete solution for building custom themes, headless storefronts, and Shopify apps.",
"version": "1.0.0",
"author": {
"name": "Henrik Soederlund",
"email": "whom-wealthy.2z@icloud.com"
},
"skills": [
"./skills"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# shopify-developer
Professional Shopify development toolkit with 6 specialized skills covering Liquid templating, theme development, API integration (GraphQL/REST), custom app development, performance optimization, and debugging. Complete solution for building custom themes, headless storefronts, and Shopify apps.

81
plugin.lock.json Normal file
View File

@@ -0,0 +1,81 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:henkisdabro/wookstar-claude-code-plugins:shopify-developer",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "7a3f823834c7b16c1a9c1bb38bc68dcf9a46f074",
"treeHash": "88249c77baf96c63a64ebbd8a890e1fd13cfbb27791bdeb959654f02662032e1",
"generatedAt": "2025-11-28T10:17:25.521776Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "shopify-developer",
"description": "Professional Shopify development toolkit with 6 specialized skills covering Liquid templating, theme development, API integration (GraphQL/REST), custom app development, performance optimization, and debugging. Complete solution for building custom themes, headless storefronts, and Shopify apps.",
"version": "1.0.0"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "e0cc55a830d3bf00cb783a65c17dc733a3629b7e68394cb9aa9f610ddb348054"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "cb68ea8e8f417782ac06da447fee7d911d5d969ea557bebb13eae01d7d2dfffd"
},
{
"path": "skills/shopify-api/SKILL.md",
"sha256": "bc6ed6af4e4673dd03528bd9d9160254f051e74bf70fd3eca1b9abf3445a8118"
},
{
"path": "skills/shopify-performance/SKILL.md",
"sha256": "6125b876ef6a814271efddfe7690d3860523f6d9e201f5f61f0593afd690b77c"
},
{
"path": "skills/shopify-liquid/SKILL.md",
"sha256": "62eb4934e29b2471f1ad3aee2eecb0b053d124dbc141c870969a9c50bbad991a"
},
{
"path": "skills/shopify-liquid/references/syntax.md",
"sha256": "d0b0914ecdcf5a09ac88a09695f0ba03cc6f5d6d8f0da7a4d049197d5e18336b"
},
{
"path": "skills/shopify-liquid/references/filters.md",
"sha256": "c4925b98a10dda6207ff0055b631fe97d2e59d2d226f0f25ba2a8a6a41f8e7ff"
},
{
"path": "skills/shopify-liquid/references/objects.md",
"sha256": "f390710223670f2767a585477b1e0b7b22fce622dce84d7446cf7c0fc3820626"
},
{
"path": "skills/shopify-theme-dev/SKILL.md",
"sha256": "0be49aaab36f716a5be86900219da9fa6a1c8d5da0c643bef4553c75ba719e22"
},
{
"path": "skills/shopify-theme-dev/references/settings-schema.md",
"sha256": "704856af655cb1a8dde8a5db3e9f19500cec05a59ca8c7299d9cd4d2e43528f6"
},
{
"path": "skills/shopify-debugging/SKILL.md",
"sha256": "bee8567f9ef0a7bd1be9cba001414c07e5dc18f5f9aa39790eef688cb73327ac"
},
{
"path": "skills/shopify-app-dev/SKILL.md",
"sha256": "0680bdd00e6f5f7a771bdc84ba3a835e2f3e683384989c0ce34dd149c4c3f4bd"
}
],
"dirSha256": "88249c77baf96c63a64ebbd8a890e1fd13cfbb27791bdeb959654f02662032e1"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

837
skills/shopify-api/SKILL.md Normal file
View File

@@ -0,0 +1,837 @@
---
name: shopify-api
description: Complete API integration guide for Shopify including GraphQL Admin API, REST Admin API, Storefront API, Ajax API, OAuth authentication, rate limiting, and webhooks. Use when making API calls to Shopify, authenticating apps, fetching product/order/customer data programmatically, implementing cart operations, handling webhooks, or working with API version 2025-10. Requires fetch or axios for JavaScript implementations.
---
# Shopify API Integration
Expert guidance for all Shopify APIs including GraphQL Admin API, REST Admin API, Storefront API, Ajax API, authentication, and webhooks.
## When to Use This Skill
Invoke this skill when:
- Making GraphQL or REST API calls to Shopify
- Implementing OAuth 2.0 authentication for apps
- Fetching product, collection, order, or customer data programmatically
- Using Storefront API for headless commerce
- Implementing Ajax cart operations in themes
- Setting up webhooks for event handling
- Working with API version 2025-10
- Handling rate limiting and API errors
- Building custom integrations with Shopify
- Using Shopify Admin API from Node.js, Python, or other languages
## Core Capabilities
### 1. GraphQL Admin API
Modern API for Shopify Admin operations with efficient data fetching.
**Endpoint:**
```
POST https://{store}.myshopify.com/admin/api/2025-10/graphql.json
```
**Headers:**
```javascript
{
'X-Shopify-Access-Token': 'shpat_...',
'Content-Type': 'application/json'
}
```
**Basic Query:**
```graphql
query GetProducts($first: Int!) {
products(first: $first) {
edges {
node {
id
title
handle
status
vendor
productType
# Pricing
priceRange {
minVariantPrice { amount currencyCode }
maxVariantPrice { amount currencyCode }
}
# Images
images(first: 5) {
edges {
node {
id
url
altText
}
}
}
# Variants
variants(first: 10) {
edges {
node {
id
title
sku
price
inventoryQuantity
available: availableForSale
}
}
}
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
```
**Variables:**
```json
{
"first": 10
}
```
**JavaScript Example:**
```javascript
async function getProducts(accessToken, store, limit = 10) {
const query = `
query GetProducts($first: Int!) {
products(first: $first) {
edges {
node {
id
title
handle
priceRange {
minVariantPrice { amount currencyCode }
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
const response = await fetch(
`https://${store}.myshopify.com/admin/api/2025-10/graphql.json`,
{
method: 'POST',
headers: {
'X-Shopify-Access-Token': accessToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query,
variables: { first: limit },
}),
}
);
const { data, errors } = await response.json();
if (errors) {
console.error('GraphQL Errors:', errors);
throw new Error(errors[0].message);
}
return data.products;
}
```
**Common Mutations:**
Create product:
```graphql
mutation CreateProduct($input: ProductInput!) {
productCreate(input: $input) {
product {
id
title
handle
}
userErrors {
field
message
}
}
}
```
Update product:
```graphql
mutation UpdateProduct($input: ProductInput!) {
productUpdate(input: $input) {
product {
id
title
status
}
userErrors {
field
message
}
}
}
```
Set metafield:
```graphql
mutation SetMetafield($input: MetafieldInput!) {
metafieldSet(input: $input) {
metafield {
id
namespace
key
value
type
}
userErrors {
field
message
}
}
}
```
### 2. REST Admin API
Traditional REST API for Shopify Admin operations.
**Base URL:**
```
https://{store}.myshopify.com/admin/api/2025-10/
```
**Authentication:**
```javascript
headers: {
'X-Shopify-Access-Token': 'shpat_...'
}
```
**Common Endpoints:**
Get products:
```javascript
GET /admin/api/2025-10/products.json?limit=50&status=active
// JavaScript
const response = await fetch(
`https://${store}.myshopify.com/admin/api/2025-10/products.json?limit=50`,
{
headers: {
'X-Shopify-Access-Token': accessToken,
},
}
);
const { products } = await response.json();
```
Get single product:
```javascript
GET /admin/api/2025-10/products/{product_id}.json
```
Create product:
```javascript
POST /admin/api/2025-10/products.json
// Body
{
"product": {
"title": "New Product",
"body_html": "<p>Description</p>",
"vendor": "My Vendor",
"product_type": "Shoes",
"status": "draft"
}
}
```
Update product:
```javascript
PUT /admin/api/2025-10/products/{product_id}.json
// Body
{
"product": {
"id": 123456789,
"title": "Updated Title"
}
}
```
Get orders:
```javascript
GET /admin/api/2025-10/orders.json?status=any&limit=50
```
Get customers:
```javascript
GET /admin/api/2025-10/customers.json?limit=50
```
### 3. OAuth 2.0 Authentication
Complete OAuth flow for custom apps.
**Step 1: Authorization Request**
```
GET https://{shop}.myshopify.com/admin/oauth/authorize?
client_id={api_key}&
redirect_uri={redirect_uri}&
scope={scopes}&
state={random_state}
```
**Scopes:**
```javascript
const scopes = [
'read_products',
'write_products',
'read_orders',
'write_orders',
'read_customers',
'write_customers',
'read_inventory',
'write_inventory',
'read_metafields',
'write_metafields',
].join(',');
```
**Step 2: Handle Callback**
```javascript
// User approves, Shopify redirects to:
GET {redirect_uri}?code={auth_code}&state={state}&hmac={hmac}&shop={shop}
// Verify HMAC for security
function verifyHmac(query, secret) {
const { hmac, ...params } = query;
const message = Object.keys(params)
.sort()
.map(key => `${key}=${params[key]}`)
.join('&');
const hash = crypto
.createHmac('sha256', secret)
.update(message)
.digest('hex');
return hash === hmac;
}
```
**Step 3: Exchange Code for Token**
```javascript
POST https://{shop}.myshopify.com/admin/oauth/access_token
// Body
{
"client_id": "{api_key}",
"client_secret": "{api_secret}",
"code": "{auth_code}"
}
// Response
{
"access_token": "shpat_...",
"scope": "write_products,read_orders"
}
// Node.js example
async function getAccessToken(shop, code, apiKey, apiSecret) {
const response = await fetch(
`https://${shop}/admin/oauth/access_token`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: apiKey,
client_secret: apiSecret,
code,
}),
}
);
const { access_token, scope } = await response.json();
return { access_token, scope };
}
```
### 4. Rate Limiting
GraphQL uses points-based rate limiting.
**Rate Limits:**
- 50 cost points per second maximum
- Bucket refills at 50 points/second
- Each query has a calculated cost
**Check Rate Limit:**
```javascript
const response = await fetch(graphqlEndpoint, options);
const rateLimitHeader = response.headers.get('X-Shopify-GraphQL-Admin-Api-Call-Limit');
// Example: "42/50" (42 points used, 50 max)
const [used, limit] = rateLimitHeader.split('/').map(Number);
if (used > 40) {
// Approaching limit, slow down
await delay(1000);
}
```
**Implement Retry Logic:**
```javascript
async function fetchWithRetry(url, options, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
const response = await fetch(url, options);
if (response.status === 429) {
// Rate limited
const retryAfter = response.headers.get('Retry-After') || 2;
await delay(retryAfter * 1000 * Math.pow(2, i)); // Exponential backoff
continue;
}
return response;
}
throw new Error('Max retries exceeded');
}
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
```
### 5. Storefront API
Public API for headless/custom storefronts.
**Endpoint:**
```
POST https://{store}.myshopify.com/api/2025-10/graphql.json
```
**Headers (Public Access):**
```javascript
{
'Content-Type': 'application/json',
'X-Shopify-Storefront-Access-Token': '{public_token}' // Optional for public stores
}
```
**Query Products:**
```graphql
query GetProducts($first: Int!) {
products(first: $first) {
edges {
node {
id
title
handle
description
priceRange {
minVariantPrice { amount currencyCode }
maxVariantPrice { amount currencyCode }
}
images(first: 3) {
edges {
node {
url
altText
}
}
}
variants(first: 10) {
edges {
node {
id
title
price { amount currencyCode }
availableForSale
sku
}
}
}
}
}
}
}
```
**Cart Operations:**
Create cart:
```graphql
mutation CreateCart($input: CartInput!) {
cartCreate(input: $input) {
cart {
id
checkoutUrl
lines(first: 10) {
edges {
node {
id
quantity
merchandise {
... on ProductVariant {
id
title
price { amount }
}
}
}
}
}
cost {
totalAmount { amount currencyCode }
subtotalAmount { amount }
totalTaxAmount { amount }
}
}
}
}
```
Add to cart:
```graphql
mutation AddToCart($cartId: ID!, $lines: [CartLineInput!]!) {
cartLinesAdd(cartId: $cartId, lines: $lines) {
cart {
id
lines(first: 10) {
edges {
node {
id
quantity
}
}
}
}
}
}
```
### 6. Ajax API (Theme-Only)
JavaScript API for cart operations in themes.
**Get Cart:**
```javascript
fetch('/cart.js')
.then(response => response.json())
.then(cart => {
console.log('Cart:', cart);
console.log('Item count:', cart.item_count);
console.log('Total:', cart.total_price);
console.log('Items:', cart.items);
});
```
**Add to Cart:**
```javascript
fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: variantId, // Required: variant ID
quantity: 1, // Optional: default 1
properties: { // Optional: custom data
'Gift wrap': 'Yes',
'Note': 'Happy Birthday!'
}
})
})
.then(response => response.json())
.then(item => {
console.log('Added to cart:', item);
})
.catch(error => {
console.error('Error:', error);
});
```
**Update Cart:**
```javascript
fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
line: 1, // Line item index (1-based)
quantity: 2 // New quantity (0 = remove)
})
})
.then(response => response.json())
.then(cart => console.log('Updated cart:', cart));
```
**Clear Cart:**
```javascript
fetch('/cart/clear.js', { method: 'POST' })
.then(response => response.json())
.then(cart => console.log('Cart cleared'));
```
**Update Cart Attributes:**
```javascript
fetch('/cart/update.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
attributes: {
'gift_wrap': 'true',
'gift_message': 'Happy Birthday!'
},
note: 'Please handle with care'
})
})
.then(response => response.json())
.then(cart => console.log('Cart updated'));
```
### 7. Webhooks
Event-driven notifications for app integrations.
**Common Webhooks:**
```javascript
// Product events
'products/create'
'products/update'
'products/delete'
// Order events
'orders/create'
'orders/updated'
'orders/paid'
'orders/fulfilled'
'orders/cancelled'
// Customer events
'customers/create'
'customers/update'
'customers/delete'
// Cart events
'carts/create'
'carts/update'
// Inventory events
'inventory_levels/update'
// App events
'app/uninstalled'
```
**Register Webhook (GraphQL):**
```graphql
mutation CreateWebhook($input: WebhookSubscriptionInput!) {
webhookSubscriptionCreate(input: $input) {
webhookSubscription {
id
topic
endpoint {
__typename
... on WebhookHttpEndpoint {
callbackUrl
}
}
}
userErrors {
field
message
}
}
}
```
**Variables:**
```json
{
"input": {
"topic": "ORDERS_CREATE",
"webhookSubscription": {
"callbackUrl": "https://your-app.com/webhooks/orders",
"format": "JSON"
}
}
}
```
**Handle Webhook (Node.js/Express):**
```javascript
app.post('/webhooks/orders', async (req, res) => {
// Verify webhook HMAC
const hmac = req.headers['x-shopify-hmac-sha256'];
const body = JSON.stringify(req.body);
const hash = crypto
.createHmac('sha256', SHOPIFY_WEBHOOK_SECRET)
.update(body)
.digest('base64');
if (hash !== hmac) {
return res.status(401).send('Invalid HMAC');
}
// Process order
const order = req.body;
console.log('New order:', order.id, order.email);
// Respond quickly (within 5 seconds)
res.status(200).send('OK');
// Process in background
await processOrder(order);
});
```
## Common Patterns
### Pagination (GraphQL)
```javascript
async function getAllProducts(accessToken, store) {
let allProducts = [];
let hasNextPage = true;
let cursor = null;
while (hasNextPage) {
const query = `
query GetProducts($first: Int!, $after: String) {
products(first: $first, after: $after) {
edges {
node { id title }
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'X-Shopify-Access-Token': accessToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query,
variables: { first: 50, after: cursor },
}),
});
const { data } = await response.json();
allProducts.push(...data.products.edges.map(e => e.node));
hasNextPage = data.products.pageInfo.hasNextPage;
cursor = data.products.pageInfo.endCursor;
}
return allProducts;
}
```
### Error Handling
```javascript
async function safeApiCall(query, variables) {
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'X-Shopify-Access-Token': accessToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({ query, variables }),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const { data, errors } = await response.json();
if (errors) {
console.error('GraphQL Errors:', errors);
throw new Error(errors[0].message);
}
return data;
} catch (error) {
console.error('API Error:', error);
throw error;
}
}
```
## Best Practices
1. **Always check API version** - Use latest stable (2025-10)
2. **Implement rate limit handling** - Use exponential backoff
3. **Verify webhook HMAC** - Security critical
4. **Use GraphQL over REST** when possible - More efficient
5. **Request only needed fields** - Reduce response size
6. **Handle errors gracefully** - Check `errors` and `userErrors`
7. **Store access tokens securely** - Never expose in client code
8. **Use minimum necessary scopes** - Security best practice
9. **Implement retry logic** - Handle transient failures
10. **Respond to webhooks quickly** - Within 5 seconds
## Detailed References
- **[references/graphql-queries.md](references/graphql-queries.md)** - Complete query examples
- **[references/rest-endpoints.md](references/rest-endpoints.md)** - All REST endpoints
- **[references/webhook-payloads.md](references/webhook-payloads.md)** - Webhook payload structures
## Integration with Other Skills
- **shopify-app-dev** - Use when building custom Shopify apps
- **shopify-liquid** - Use when displaying API data in theme templates
- **shopify-debugging** - Use when troubleshooting API errors
- **shopify-performance** - Use when optimizing API request patterns
## Quick Reference
```javascript
// GraphQL Admin API
POST https://{store}.myshopify.com/admin/api/2025-10/graphql.json
Headers: { 'X-Shopify-Access-Token': 'shpat_...' }
// REST Admin API
GET https://{store}.myshopify.com/admin/api/2025-10/products.json
Headers: { 'X-Shopify-Access-Token': 'shpat_...' }
// Storefront API
POST https://{store}.myshopify.com/api/2025-10/graphql.json
Headers: { 'X-Shopify-Storefront-Access-Token': 'token' }
// Ajax API (theme)
fetch('/cart.js')
fetch('/cart/add.js', { method: 'POST', body: ... })
fetch('/cart/change.js', { method: 'POST', body: ... })
```

View File

@@ -0,0 +1,743 @@
---
name: shopify-app-dev
description: Custom Shopify app development using Shopify CLI, app architecture, OAuth authentication, app extensions, admin UI, Hydrogen/Remix frameworks, and deployment. Use when creating Shopify apps, setting up Shopify CLI, building app extensions, implementing OAuth flows, creating admin UI components, working with Hydrogen or Remix, deploying to Cloudflare Workers, or integrating third-party services with Shopify stores.
---
# Shopify App Development
Expert guidance for building custom Shopify apps using Shopify CLI, modern frameworks, and best practices.
## When to Use This Skill
Invoke this skill when:
- Creating custom Shopify apps with Shopify CLI
- Setting up app development environment
- Implementing OAuth authentication for apps
- Building app extensions (admin blocks, theme app extensions)
- Creating admin UI components and pages
- Working with Hydrogen or Remix for headless storefronts
- Deploying apps to Cloudflare Workers or other platforms
- Integrating third-party APIs with Shopify
- Creating app proxies for custom functionality
- Implementing app billing and subscription plans
- Building public or custom apps
## Core Capabilities
### 1. Shopify CLI Setup
Install and configure Shopify CLI for app development.
**Install Shopify CLI:**
```bash
# Using npm
npm install -g @shopify/cli @shopify/app
# Using Homebrew (macOS)
brew tap shopify/shopify
brew install shopify-cli
# Verify installation
shopify version
```
**Create New App:**
```bash
# Create app with Node.js/React
shopify app init
# Choose template:
# - Remix (recommended)
# - Node.js + React
# - PHP
# - Ruby
# App structure created:
my-app/
├── app/ # Remix app routes
├── extensions/ # App extensions
├── shopify.app.toml # App configuration
├── package.json
└── README.md
```
**App Configuration (shopify.app.toml):**
```toml
# This file stores app configuration
name = "my-app"
client_id = "your-client-id"
application_url = "https://your-app.com"
embedded = true
[access_scopes]
# API access scopes
scopes = "write_products,read_orders,read_customers"
[auth]
redirect_urls = [
"https://your-app.com/auth/callback",
"https://your-app.com/auth/shopify/callback"
]
[webhooks]
api_version = "2025-10"
[[webhooks.subscriptions]]
topics = ["products/create", "products/update"]
uri = "/webhooks"
```
### 2. Development Workflow
**Start Development Server:**
```bash
# Start dev server with tunneling
shopify app dev
# Server starts with:
# - Local development URL: http://localhost:3000
# - Public tunnel URL: https://random-subdomain.ngrok.io
# - App installed in development store
```
**Deploy App:**
```bash
# Deploy to production
shopify app deploy
# Generate app version and deploy extensions
```
**Environment Variables (.env):**
```bash
SHOPIFY_API_KEY=your_api_key
SHOPIFY_API_SECRET=your_api_secret
SCOPES=write_products,read_orders
HOST=your-app-domain.com
SHOPIFY_APP_URL=https://your-app.com
DATABASE_URL=postgresql://...
```
### 3. App Architecture (Remix)
Modern Shopify app using Remix framework.
**app/routes/app._index.jsx (Home Page):**
```javascript
import { useLoaderData } from "@remix-run/react";
import { authenticate } from "../shopify.server";
import {
Page,
Layout,
Card,
DataTable,
Button,
} from "@shopify/polaris";
export async function loader({ request }) {
const { admin, session } = await authenticate.admin(request);
// Fetch products using GraphQL
const response = await admin.graphql(`
query {
products(first: 10) {
edges {
node {
id
title
handle
status
}
}
}
}
`);
const { data } = await response.json();
return {
products: data.products.edges.map(e => e.node),
shop: session.shop,
};
}
export default function Index() {
const { products, shop } = useLoaderData();
const rows = products.map((product) => [
product.title,
product.handle,
product.status,
]);
return (
<Page title="Products">
<Layout>
<Layout.Section>
<Card>
<DataTable
columnContentTypes={["text", "text", "text"]}
headings={["Title", "Handle", "Status"]}
rows={rows}
/>
</Card>
</Layout.Section>
</Layout>
</Page>
);
}
```
**app/routes/app.product.$id.jsx (Product Detail):**
```javascript
import { json } from "@remix-run/node";
import { useLoaderData, useSubmit } from "@remix-run/react";
import { authenticate } from "../shopify.server";
import {
Page,
Layout,
Card,
Form,
FormLayout,
TextField,
Button,
} from "@shopify/polaris";
import { useState } from "react";
export async function loader({ request, params }) {
const { admin } = await authenticate.admin(request);
const response = await admin.graphql(`
query GetProduct($id: ID!) {
product(id: $id) {
id
title
description
status
vendor
}
}
`, {
variables: { id: `gid://shopify/Product/${params.id}` },
});
const { data } = await response.json();
return json({ product: data.product });
}
export async function action({ request, params }) {
const { admin } = await authenticate.admin(request);
const formData = await request.formData();
const title = formData.get("title");
const description = formData.get("description");
const response = await admin.graphql(`
mutation UpdateProduct($input: ProductInput!) {
productUpdate(input: $input) {
product {
id
title
}
userErrors {
field
message
}
}
}
`, {
variables: {
input: {
id: `gid://shopify/Product/${params.id}`,
title,
description,
},
},
});
const { data } = await response.json();
if (data.productUpdate.userErrors.length > 0) {
return json({ errors: data.productUpdate.userErrors }, { status: 400 });
}
return json({ success: true });
}
export default function ProductDetail() {
const { product } = useLoaderData();
const submit = useSubmit();
const [title, setTitle] = useState(product.title);
const [description, setDescription] = useState(product.description);
const handleSubmit = () => {
const formData = new FormData();
formData.append("title", title);
formData.append("description", description);
submit(formData, { method: "post" });
};
return (
<Page title="Edit Product" backAction={{ url: "/app" }}>
<Layout>
<Layout.Section>
<Card>
<FormLayout>
<TextField
label="Title"
value={title}
onChange={setTitle}
autoComplete="off"
/>
<TextField
label="Description"
value={description}
onChange={setDescription}
multiline={4}
autoComplete="off"
/>
<Button primary onClick={handleSubmit}>
Save
</Button>
</FormLayout>
</Card>
</Layout.Section>
</Layout>
</Page>
);
}
```
### 4. App Extensions
Extend Shopify functionality with various extension types.
**Admin Action Extension:**
Create button in admin product page:
```bash
shopify app generate extension
# Choose: Admin action
# Name: Export Product
```
**extensions/export-product/src/index.jsx:**
```javascript
import { extend, AdminAction } from "@shopify/admin-ui-extensions";
extend("Admin::Product::SubscriptionAction", (root, { data }) => {
const { id, title } = data.selected[0];
const button = root.createComponent(AdminAction, {
title: "Export Product",
onPress: async () => {
// Call your app API
const response = await fetch("/api/export", {
method: "POST",
body: JSON.stringify({ productId: id }),
headers: { "Content-Type": "application/json" },
});
if (response.ok) {
root.toast.show("Product exported successfully!");
} else {
root.toast.show("Export failed", { isError: true });
}
},
});
root.append(button);
});
```
**Theme App Extension:**
Add app block to themes:
```bash
shopify app generate extension
# Choose: Theme app extension
# Name: Product Reviews
```
**extensions/product-reviews/blocks/reviews.liquid:**
```liquid
{% schema %}
{
"name": "Product Reviews",
"target": "section",
"settings": [
{
"type": "text",
"id": "heading",
"label": "Heading",
"default": "Customer Reviews"
},
{
"type": "range",
"id": "reviews_to_show",
"label": "Reviews to Show",
"min": 1,
"max": 10,
"default": 5
}
]
}
{% endschema %}
<div class="product-reviews">
<h2>{{ block.settings.heading }}</h2>
{% comment %}
Fetch reviews from your app API
{% endcomment %}
<div id="reviews-container" data-product-id="{{ product.id }}"></div>
</div>
<script>
// Fetch and render reviews
fetch(`/apps/reviews/api/reviews?product_id={{ product.id }}&limit={{ block.settings.reviews_to_show }}`)
.then(r => r.json())
.then(reviews => {
const container = document.getElementById('reviews-container');
container.innerHTML = reviews.map(review => `
<div class="review">
<div class="rating">${'⭐'.repeat(review.rating)}</div>
<h3>${review.title}</h3>
<p>${review.content}</p>
<p class="author">- ${review.author}</p>
</div>
`).join('');
});
</script>
{% stylesheet %}
.product-reviews {
padding: 2rem;
}
.review {
margin-bottom: 1.5rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid #eee;
}
.rating {
color: #ffa500;
margin-bottom: 0.5rem;
}
{% endstylesheet %}
```
### 5. Webhooks in Apps
Handle Shopify events in your app.
**app/routes/webhooks.jsx:**
```javascript
import { authenticate } from "../shopify.server";
import db from "../db.server";
export async function action({ request }) {
const { topic, shop, session, admin, payload } = await authenticate.webhook(request);
console.log(`Webhook received: ${topic} from ${shop}`);
switch (topic) {
case "APP_UNINSTALLED":
// Clean up app data
await db.session.deleteMany({ where: { shop } });
break;
case "PRODUCTS_CREATE":
// Handle new product
console.log("New product created:", payload.id, payload.title);
await handleProductCreated(payload);
break;
case "PRODUCTS_UPDATE":
// Handle product update
console.log("Product updated:", payload.id);
await handleProductUpdated(payload);
break;
case "ORDERS_CREATE":
// Handle new order
console.log("New order:", payload.id, payload.email);
await handleOrderCreated(payload);
break;
case "CUSTOMERS_CREATE":
// Handle new customer
await handleCustomerCreated(payload);
break;
default:
console.log("Unhandled webhook topic:", topic);
}
return new Response("OK", { status: 200 });
}
async function handleProductCreated(product) {
// Process new product
await db.product.create({
data: {
shopifyId: product.id,
title: product.title,
handle: product.handle,
},
});
}
async function handleOrderCreated(order) {
// Send email notification, update inventory, etc.
console.log(`Order ${order.id} received for ${order.email}`);
}
```
**Register Webhooks (app/shopify.server.js):**
```javascript
import "@shopify/shopify-app-remix/adapters/node";
import {
ApiVersion,
AppDistribution,
shopifyApp,
DeliveryMethod,
} from "@shopify/shopify-app-remix/server";
const shopify = shopifyApp({
apiKey: process.env.SHOPIFY_API_KEY,
apiSecretKey: process.env.SHOPIFY_API_SECRET,
scopes: process.env.SCOPES?.split(","),
appUrl: process.env.SHOPIFY_APP_URL,
authPathPrefix: "/auth",
sessionStorage: new SQLiteSessionStorage(),
distribution: AppDistribution.AppStore,
apiVersion: ApiVersion.October25,
webhooks: {
APP_UNINSTALLED: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
PRODUCTS_CREATE: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
PRODUCTS_UPDATE: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
ORDERS_CREATE: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
},
});
export default shopify;
export const authenticate = shopify.authenticate;
```
### 6. App Proxy
Create custom storefront routes that access your app.
**Setup in Partner Dashboard:**
```
Subpath prefix: apps
Subpath: reviews
Proxy URL: https://your-app.com/api/proxy
```
**Result:**
```
https://store.com/apps/reviews → proxies to → https://your-app.com/api/proxy
```
**Handle Proxy Requests (app/routes/api.proxy.jsx):**
```javascript
import { json } from "@remix-run/node";
export async function loader({ request }) {
const url = new URL(request.url);
// Verify proxy request
const signature = url.searchParams.get("signature");
const shop = url.searchParams.get("shop");
if (!verifyProxySignature(signature, request)) {
return json({ error: "Invalid signature" }, { status: 401 });
}
// Handle different paths
const path = url.searchParams.get("path_prefix");
if (path === "/apps/reviews/product") {
const productId = url.searchParams.get("product_id");
const reviews = await getProductReviews(productId);
return json({ reviews });
}
return json({ message: "App Proxy" });
}
function verifyProxySignature(signature, request) {
// Verify HMAC signature
// Implementation depends on your setup
return true;
}
```
### 7. Polaris UI Components
Use Shopify's design system for consistent admin UI.
**Common Components:**
```javascript
import {
Page,
Layout,
Card,
Button,
TextField,
Select,
Checkbox,
Badge,
Banner,
DataTable,
Modal,
Toast,
Frame,
} from "@shopify/polaris";
export default function MyPage() {
return (
<Page
title="Settings"
primaryAction={{ content: "Save", onAction: handleSave }}
secondaryActions={[{ content: "Cancel", onAction: handleCancel }]}
>
<Layout>
<Layout.Section>
<Card title="General Settings" sectioned>
<TextField
label="App Name"
value={name}
onChange={setName}
/>
<Select
label="Status"
options={[
{ label: "Active", value: "active" },
{ label: "Draft", value: "draft" },
]}
value={status}
onChange={setStatus}
/>
<Checkbox
label="Enable notifications"
checked={notifications}
onChange={setNotifications}
/>
</Card>
</Layout.Section>
<Layout.Section secondary>
<Card title="Status" sectioned>
<Badge status="success">Active</Badge>
</Card>
</Layout.Section>
</Layout>
</Page>
);
}
```
### 8. Deployment
Deploy Shopify apps to production.
**Deploy to Cloudflare Workers:**
**wrangler.toml:**
```toml
name = "shopify-app"
compatibility_date = "2025-11-10"
main = "build/index.js"
[vars]
SHOPIFY_API_KEY = "your_api_key"
[[kv_namespaces]]
binding = "SESSIONS"
id = "your_kv_namespace_id"
```
**Deploy:**
```bash
# Build app
npm run build
# Deploy to Cloudflare
wrangler deploy
```
**Environment Secrets:**
```bash
# Add secrets
wrangler secret put SHOPIFY_API_SECRET
wrangler secret put DATABASE_URL
```
## Best Practices
1. **Use Shopify CLI** for app scaffolding and development
2. **Implement proper OAuth** with HMAC verification
3. **Handle webhook events** for real-time updates
4. **Use Polaris** for consistent admin UI
5. **Test in development store** before production
6. **Implement error handling** for all API calls
7. **Store session data securely** (encrypted database)
8. **Follow Shopify app requirements** for listing
9. **Implement app billing** for monetization
10. **Use app extensions** to enhance merchant experience
## Integration with Other Skills
- **shopify-api** - Use when making API calls from your app
- **shopify-liquid** - Use when creating theme app extensions
- **shopify-debugging** - Use when troubleshooting app issues
- **shopify-performance** - Use when optimizing app performance
## Quick Reference
```bash
# Create app
shopify app init
# Start development
shopify app dev
# Generate extension
shopify app generate extension
# Deploy app
shopify app deploy
# Configure webhooks
# Edit shopify.app.toml
```

View File

@@ -0,0 +1,705 @@
---
name: shopify-debugging
description: Complete debugging and troubleshooting guide for Shopify including Liquid errors, theme preview debugging, API error handling, JavaScript console debugging, network request inspection, cart issues, checkout problems, and common error codes. Use when debugging Liquid syntax errors, troubleshooting theme rendering issues, fixing API errors, debugging JavaScript, investigating cart problems, or resolving webhook failures.
---
# Shopify Debugging & Troubleshooting
Expert guidance for debugging Shopify themes, apps, and API integrations with practical solutions to common issues.
## When to Use This Skill
Invoke this skill when:
- Debugging Liquid template errors or syntax issues
- Troubleshooting theme rendering problems
- Fixing API errors (GraphQL, REST)
- Debugging JavaScript errors in themes or apps
- Investigating cart or checkout issues
- Resolving webhook delivery failures
- Troubleshooting OAuth authentication problems
- Debugging theme preview or customizer issues
- Investigating slow page loads or performance problems
- Fixing metafield or metaobject access errors
- Resolving CORS or network request issues
## Core Capabilities
### 1. Liquid Debugging
Debug Liquid template errors and rendering issues.
**Enable Theme Preview:**
```
1. Go to Online Store > Themes
2. Click "Customize" on your theme
3. Open browser DevTools (F12)
4. Check Console for Liquid errors
```
**Common Liquid Errors:**
**Syntax Error:**
```liquid
{# ❌ Error: Missing endif #}
{% if product.available %}
<button>Add to Cart</button>
{# Missing {% endif %} #}
{# ✅ Fixed #}
{% if product.available %}
<button>Add to Cart</button>
{% endif %}
```
**Undefined Variable:**
```liquid
{# ❌ Error: product undefined on collection page #}
{{ product.title }}
{# ✅ Fixed: Check context #}
{% if product %}
{{ product.title }}
{% else %}
{# Not on product page #}
{% endif %}
```
**Invalid Filter:**
```liquid
{# ❌ Error: Unknown filter #}
{{ product.price | format_money }}
{# ✅ Fixed: Correct filter name #}
{{ product.price | money }}
```
**Debug Output:**
```liquid
{# Output variable as JSON #}
{{ product | json }}
{# Check variable type #}
{{ product.class }}
{# Check if variable exists #}
{% if product %}
Product exists
{% else %}
Product is nil
{% endif %}
{# Output all properties #}
<pre>{{ product | json }}</pre>
```
**Console Logging from Liquid:**
```liquid
<script>
console.log('Product ID:', {{ product.id }});
console.log('Product data:', {{ product | json }});
console.log('Cart:', {{ cart | json }});
</script>
```
### 2. JavaScript Debugging
Debug JavaScript errors in themes and apps.
**Browser Console:**
```javascript
// Log to console
console.log('Debug:', variable);
console.error('Error:', error);
console.warn('Warning:', warning);
// Log object properties
console.table(data);
// Group related logs
console.group('Cart Operations');
console.log('Cart ID:', cartId);
console.log('Items:', items);
console.groupEnd();
// Time operations
console.time('API Call');
await fetch('/api/data');
console.timeEnd('API Call');
// Stack trace
console.trace('Execution path');
```
**Breakpoints:**
```javascript
// Programmatic breakpoint
debugger;
// Set in browser DevTools:
// Sources tab > Click line number
```
**Error Handling:**
```javascript
// ❌ Unhandled error
const data = await fetch('/api/data').then(r => r.json());
// ✅ Proper error handling
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
console.log('Data:', data);
} catch (error) {
console.error('Failed to fetch data:', error);
// Show user-friendly error message
alert('Failed to load data. Please try again.');
}
```
**Network Debugging:**
```
1. Open DevTools > Network tab
2. Filter by XHR or Fetch
3. Click request to see:
- Request headers
- Request payload
- Response headers
- Response body
- Timing information
```
### 3. API Error Debugging
Debug GraphQL and REST API errors.
**GraphQL Errors:**
Error response format:
```json
{
"errors": [
{
"message": "Field 'invalidField' doesn't exist on type 'Product'",
"locations": [{ "line": 3, "column": 5 }],
"path": ["product", "invalidField"],
"extensions": {
"code": "FIELD_NOT_FOUND",
"typeName": "Product"
}
}
],
"data": null
}
```
**Check for errors BEFORE accessing data:**
```javascript
const response = await fetch(graphqlEndpoint, {
method: 'POST',
headers: {
'X-Shopify-Access-Token': accessToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({ query, variables }),
});
const { data, errors } = await response.json();
// ✅ Always check errors first
if (errors) {
console.error('GraphQL Errors:');
errors.forEach(error => {
console.error('Message:', error.message);
console.error('Location:', error.locations);
console.error('Path:', error.path);
console.error('Code:', error.extensions?.code);
});
throw new Error(errors[0].message);
}
// Now safe to use data
console.log('Products:', data.products);
```
**Common GraphQL Errors:**
**Authentication Error:**
```json
{
"errors": [{
"message": "Access denied",
"extensions": { "code": "UNAUTHENTICATED" }
}]
}
```
**Fix:** Check access token:
```javascript
// Verify token is valid
const token = 'shpat_...';
// Check token format (should start with shpat_)
if (!token.startsWith('shpat_')) {
console.error('Invalid token format');
}
// Verify in headers
headers: {
'X-Shopify-Access-Token': token, // ✅ Correct header
'Authorization': `Bearer ${token}`, // ❌ Wrong for Admin API
}
```
**Field Not Found:**
```json
{
"errors": [{
"message": "Field 'invalidField' doesn't exist on type 'Product'"
}]
}
```
**Fix:** Check field name in API docs:
```graphql
# ❌ Wrong field name
query {
product(id: "gid://shopify/Product/123") {
invalidField
}
}
# ✅ Correct field name
query {
product(id: "gid://shopify/Product/123") {
title
handle
status
}
}
```
**Rate Limit Error:**
```javascript
// Check rate limit header
const response = await fetch(graphqlEndpoint, options);
const rateLimit = response.headers.get('X-Shopify-GraphQL-Admin-Api-Call-Limit');
console.log('Rate limit:', rateLimit); // "42/50"
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
console.log(`Rate limited. Retry after ${retryAfter} seconds`);
}
```
**REST API Errors:**
**404 Not Found:**
```javascript
const response = await fetch(`https://${shop}/admin/api/2025-10/products/999999.json`, {
headers: { 'X-Shopify-Access-Token': token },
});
if (response.status === 404) {
console.error('Product not found');
// Check:
// 1. Product ID is correct
// 2. Product exists in store
// 3. Using correct endpoint
}
```
**422 Unprocessable Entity:**
```json
{
"errors": {
"title": ["can't be blank"],
"price": ["must be greater than 0"]
}
}
```
**Fix:** Validate input:
```javascript
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'X-Shopify-Access-Token': token,
'Content-Type': 'application/json',
},
body: JSON.stringify({
product: {
title: '', // ❌ Empty title
price: -10, // ❌ Negative price
},
}),
});
if (response.status === 422) {
const { errors } = await response.json();
console.error('Validation errors:', errors);
// Fix data
const validProduct = {
title: 'Product Name', // ✅ Valid title
price: 19.99, // ✅ Valid price
};
}
```
### 4. Cart Debugging
Debug cart and Ajax API issues.
**Cart Not Updating:**
```javascript
// ❌ Common mistake: Wrong variant ID
fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: '123', // ❌ Wrong: using product ID instead of variant ID
quantity: 1,
}),
});
// ✅ Fixed: Use variant ID
fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: 123456789, // ✅ Variant ID (numeric)
quantity: 1,
}),
})
.then(response => {
if (!response.ok) {
return response.json().then(err => {
console.error('Cart error:', err);
throw err;
});
}
return response.json();
})
.then(item => {
console.log('Added to cart:', item);
// Update cart UI
})
.catch(error => {
console.error('Failed to add to cart:', error);
});
```
**Get Current Cart:**
```javascript
// Debug current cart state
fetch('/cart.js')
.then(r => r.json())
.then(cart => {
console.log('Cart:', cart);
console.log('Item count:', cart.item_count);
console.log('Total:', cart.total_price);
console.log('Items:', cart.items);
cart.items.forEach(item => {
console.log('Item:', item.product_id, item.variant_id, item.quantity);
});
});
```
**Cart AJAX Errors:**
```javascript
// Common error: Insufficient inventory
{
"status": 422,
"message": "You can only add 5 of this item to your cart",
"description": "Cannot add more than 5 to cart"
}
// Fix: Check inventory before adding
const variant = product.variants.find(v => v.id === variantId);
if (variant.inventory_quantity < quantity) {
alert(`Only ${variant.inventory_quantity} available`);
} else {
// Add to cart
}
```
### 5. Theme Preview Debugging
Debug issues in the theme customizer.
**Theme Editor Console:**
```
1. Open theme customizer
2. Open DevTools (F12)
3. Check Console for errors
4. Look for:
- Liquid errors (red text)
- JavaScript errors
- Network failures
```
**Section Not Rendering:**
```liquid
{# Check section schema #}
{% schema %}
{
"name": "My Section",
"settings": [...] {# ✅ Must have settings #}
}
{% endschema %}
{# ❌ Missing schema = won't show in customizer #}
```
**Settings Not Updating:**
```liquid
{# ❌ Wrong: Using hardcoded value #}
<h1>Hardcoded Title</h1>
{# ✅ Fixed: Use setting #}
<h1>{{ section.settings.title }}</h1>
{% schema %}
{
"name": "Hero",
"settings": [
{
"type": "text",
"id": "title",
"label": "Title",
"default": "Welcome"
}
]
}
{% endschema %}
```
**Block Attributes Missing:**
```liquid
{# ❌ Missing shopify_attributes #}
<div class="block">
{{ block.settings.text }}
</div>
{# ✅ Fixed: Add shopify_attributes for theme editor #}
<div class="block" {{ block.shopify_attributes }}>
{{ block.settings.text }}
</div>
```
### 6. Webhook Debugging
Debug webhook delivery and processing.
**Webhook Not Received:**
Check in Shopify Admin:
```
1. Settings > Notifications > Webhooks
2. Click webhook
3. Check "Recent deliveries"
4. Look for delivery status:
- ✅ Success (200 OK)
- ❌ Failed (4xx/5xx errors)
```
**Verify Webhook HMAC:**
```javascript
import crypto from 'crypto';
function verifyWebhook(body, hmac, secret) {
const hash = crypto
.createHmac('sha256', secret)
.update(body, 'utf8')
.digest('base64');
return hash === hmac;
}
// Express example
app.post('/webhooks/orders', (req, res) => {
const hmac = req.headers['x-shopify-hmac-sha256'];
const body = JSON.stringify(req.body);
if (!verifyWebhook(body, hmac, process.env.SHOPIFY_WEBHOOK_SECRET)) {
console.error('Invalid webhook HMAC');
return res.status(401).send('Unauthorized');
}
console.log('Webhook verified');
// Process webhook
const order = req.body;
console.log('Order:', order.id, order.email);
// Respond quickly (< 5 seconds)
res.status(200).send('OK');
});
```
**Webhook Timeout:**
```javascript
// ❌ Processing takes too long (> 5 seconds)
app.post('/webhooks/orders', async (req, res) => {
await processOrder(req.body); // Slow operation
res.send('OK'); // Response delayed
});
// ✅ Respond immediately, process async
app.post('/webhooks/orders', async (req, res) => {
const order = req.body;
// Respond quickly
res.status(200).send('OK');
// Process in background
processOrder(order).catch(console.error);
});
```
### 7. Common Error Messages
**Liquid Errors:**
```
Error: Liquid syntax error: Unknown tag 'section'
Fix: Use {% section %} only in JSON templates, not .liquid files
```
```
Error: undefined method 'title' for nil:NilClass
Fix: Variable is nil. Add {% if %} check or provide default:
{{ product.title | default: "No title" }}
```
```
Error: Exceeded maximum number of allowed iterations
Fix: Infinite loop detected. Check loop conditions.
```
**JavaScript Errors:**
```
TypeError: Cannot read property 'forEach' of undefined
Fix: Array is undefined. Check:
if (items && Array.isArray(items)) {
items.forEach(item => { ... });
}
```
```
ReferenceError: $ is not defined
Fix: jQuery not loaded or script runs before jQuery loads
```
```
SyntaxError: Unexpected token <
Fix: API returned HTML error page instead of JSON. Check API endpoint.
```
**API Errors:**
```
Access denied - check your access scopes
Fix: App needs additional permissions. Update scopes in Partner Dashboard.
```
```
Throttled: Exceeded API rate limit
Fix: Implement rate limit handling with exponential backoff.
```
```
Field doesn't exist on type
Fix: Check API version and field availability in docs.
```
## Debugging Toolkit
**Browser DevTools:**
```
- Console: View errors and logs
- Network: Inspect API requests
- Sources: Set breakpoints
- Application: View cookies, localStorage
- Performance: Profile page load
```
**Shopify Tools:**
```
- Theme Preview: Test changes before publishing
- Theme Inspector: View section data
- API Explorer: Test GraphQL queries
- Webhook Logs: Check delivery status
```
**Useful Console Commands:**
```javascript
// Get all form data
new FormData(document.querySelector('form'))
// View all cookies
document.cookie
// Check localStorage
localStorage
// View all global variables
console.log(window)
// Get computed styles
getComputedStyle(element)
```
## Best Practices
1. **Always check for errors** before accessing data (API responses)
2. **Use try-catch blocks** for all async operations
3. **Log meaningful messages** with context
4. **Verify HMAC** for all webhooks
5. **Test in theme preview** before publishing
6. **Monitor API rate limits** and implement backoff
7. **Handle edge cases** (nil values, empty arrays)
8. **Use browser DevTools** for network debugging
9. **Check API version** compatibility
10. **Validate input** before API calls
## Integration with Other Skills
- **shopify-liquid** - Use when debugging Liquid template code
- **shopify-api** - Use when debugging API calls
- **shopify-theme-dev** - Use when debugging theme structure issues
- **shopify-app-dev** - Use when debugging custom apps
- **shopify-performance** - Use when debugging slow performance
## Quick Reference
```javascript
// Debug Liquid
{{ variable | json }}
{% if variable %}{{ variable }}{% endif %}
// Debug JavaScript
console.log('Debug:', data);
debugger;
// Debug API
const { data, errors } = await response.json();
if (errors) console.error(errors);
// Debug Cart
fetch('/cart.js').then(r => r.json()).then(console.log);
// Debug Webhooks
const valid = verifyHmac(body, hmac, secret);
console.log('Webhook valid:', valid);
```

View File

@@ -0,0 +1,345 @@
---
name: shopify-liquid
description: Complete Liquid templating language reference including syntax, filters, objects, control flow, loops, and conditionals for Shopify themes. Use when working with .liquid files, creating theme templates, implementing dynamic content, debugging Liquid code, working with sections and snippets, or rendering product/collection/cart data in Shopify stores.
---
# Shopify Liquid Templating
Expert guidance for Shopify's Liquid templating language including complete syntax reference, filters, objects, and best practices.
## When to Use This Skill
Invoke this skill when:
- Working with `.liquid`, `.css.liquid`, or `.js.liquid` files
- Creating or modifying theme templates (product, collection, cart, etc.)
- Implementing dynamic content rendering
- Using Liquid filters to format data (money, dates, strings)
- Accessing Shopify objects (product, collection, cart, customer)
- Writing conditional logic or loops in templates
- Debugging Liquid syntax errors or output issues
- Creating sections or snippets with Liquid logic
- Formatting prices, dates, or other data
## Core Capabilities
### 1. Liquid Syntax Fundamentals
Three core syntax types:
**Output (display values):**
```liquid
{{ product.title }}
{{ product.price | money }}
{{ collection.products.size }}
```
**Logic (conditionals and control):**
```liquid
{% if product.available %}
<button>Add to Cart</button>
{% else %}
<p>Sold Out</p>
{% endif %}
```
**Assignment (variables):**
```liquid
{% assign sale_price = product.price | times: 0.8 %}
{% capture full_title %}{{ collection.title }} - {{ product.title }}{% endcapture %}
```
**Whitespace control:**
```liquid
{%- if condition -%}
Content (strips whitespace)
{%- endif -%}
```
### 2. Control Flow Tags
**Conditionals:**
- `if/elsif/else/endif` - Standard conditionals
- `unless/endunless` - Negated if
- `case/when/else/endcase` - Switch statements
**Logical operators:**
- `and`, `or` - Combine conditions
- `==`, `!=`, `>`, `<`, `>=`, `<=` - Comparisons
- `contains` - Substring/array search
**Example:**
```liquid
{% if product.available and product.price < 100 %}
Affordable and in stock
{% elsif product.available %}
Available but pricey
{% else %}
Out of stock
{% endif %}
```
### 3. Iteration (Loops)
**for loop:**
```liquid
{% for product in collection.products %}
{{ product.title }}
{% endfor %}
{# With modifiers #}
{% for product in collection.products limit: 5 offset: 10 reversed %}
{{ product.title }}
{% endfor %}
```
**forloop object:**
```liquid
{% for item in array %}
{{ forloop.index }} {# 1-based index #}
{{ forloop.index0 }} {# 0-based index #}
{{ forloop.first }} {# Boolean: first item #}
{{ forloop.last }} {# Boolean: last item #}
{{ forloop.length }} {# Total items #}
{% endfor %}
```
**Pagination:**
```liquid
{% paginate collection.products by 12 %}
{% for product in paginate.collection.products %}
{% render 'product-card', product: product %}
{% endfor %}
{% if paginate.pages > 1 %}
{{ paginate | default_pagination }}
{% endif %}
{% endpaginate %}
```
### 4. Essential Filters
**Money formatting:**
```liquid
{{ 1000 | money }} {# $10.00 #}
{{ 1000 | money_without_currency }} {# 10.00 #}
{{ 1000 | money_without_trailing_zeros }} {# $10 #}
```
**String manipulation:**
```liquid
{{ "hello" | upcase }} {# HELLO #}
{{ "hello" | capitalize }} {# Hello #}
{{ "hello world" | truncate: 8 }} {# hello... #}
{{ "a,b,c" | split: "," }} {# ["a","b","c"] #}
{{ text | strip_html }} {# Remove HTML tags #}
```
**Array/collection:**
```liquid
{{ array | first }} {# First element #}
{{ array | last }} {# Last element #}
{{ array | size }} {# Count #}
{{ products | map: "title" }} {# Extract property #}
{{ products | where: "vendor", "Nike" }} {# Filter #}
{{ products | sort: "price" }} {# Sort #}
{{ array | join: ", " }} {# Join with separator #}
```
**Date formatting:**
```liquid
{{ order.created_at | date: '%B %d, %Y' }} {# November 10, 2025 #}
{{ order.created_at | date: '%m/%d/%Y' }} {# 11/10/2025 #}
{{ order.created_at | date: '%H:%M %p' }} {# 12:39 PM #}
```
**Image handling:**
```liquid
{{ product.image | img_url: '500x500' }} {# Resize image #}
{{ product.image | img_url: 'medium' }} {# Named size #}
{{ 'logo.png' | asset_url }} {# Theme asset CDN #}
```
**Math operations:**
```liquid
{{ 5 | plus: 3 }} {# 8 #}
{{ 5 | minus: 3 }} {# 2 #}
{{ 5 | times: 3 }} {# 15 #}
{{ 10 | divided_by: 2 }} {# 5 #}
{{ 1.567 | round: 2 }} {# 1.57 #}
```
**Chaining filters:**
```liquid
{{ collection.products | where: "available" | map: "title" | sort | first }}
```
### 5. Key Shopify Objects
**Product object:**
```liquid
{{ product.title }}
{{ product.price | money }}
{{ product.available }} {# Boolean #}
{{ product.vendor }}
{{ product.type }}
{{ product.images }} {# Array #}
{{ product.variants }} {# Array #}
{{ product.selected_variant }}
{{ product.metafields.custom.field }}
```
**Collection object:**
```liquid
{{ collection.title }}
{{ collection.products }} {# Array #}
{{ collection.products_count }}
{{ collection.all_tags }}
{{ collection.sort_by }}
{{ collection.filters }}
```
**Cart object (global):**
```liquid
{{ cart.item_count }}
{{ cart.total_price | money }}
{{ cart.items }} {# Array of line items #}
{{ cart.empty? }} {# Boolean #}
```
**Customer object:**
```liquid
{{ customer.name }}
{{ customer.email }}
{{ customer.orders_count }}
{{ customer.total_spent | money }}
{{ customer.default_address }}
```
**Global objects:**
```liquid
{{ shop.name }}
{{ shop.currency }}
{{ shop.url }}
{{ request.path }}
{{ request.page_type }} {# "product", "collection", etc. #}
{{ settings.color_primary }} {# Theme settings #}
```
### 6. Template Inclusion
**render (isolated scope - PREFERRED):**
```liquid
{% render 'product-card', product: product, show_price: true %}
{# Render for each item #}
{% render 'product-card' for collection.products as item %}
```
**include (shared scope - LEGACY):**
```liquid
{% include 'product-details' %}
```
**section (dynamic sections):**
```liquid
{% section 'featured-product' %}
```
## Common Patterns
### Product availability check
```liquid
{% if product.available %}
<button type="submit">Add to Cart</button>
{% elsif product.selected_variant.incoming %}
<p>Coming {{ product.selected_variant.incoming_date | date: '%B %d' }}</p>
{% else %}
<p class="sold-out">Sold Out</p>
{% endif %}
```
### Price display with sale
```liquid
{% if product.compare_at_price > product.price %}
<span class="sale-price">{{ product.price | money }}</span>
<span class="original-price">{{ product.compare_at_price | money }}</span>
<span class="savings">Save {{ product.compare_at_price | minus: product.price | money }}</span>
{% else %}
<span class="price">{{ product.price | money }}</span>
{% endif %}
```
### Loop through variants
```liquid
{% for variant in product.variants %}
<option
value="{{ variant.id }}"
{% unless variant.available %}disabled{% endunless %}
>
{{ variant.title }} - {{ variant.price | money }}
</option>
{% endfor %}
```
### Check collection tags
```liquid
{% if collection.all_tags contains 'sale' %}
<div class="sale-banner">Sale items available!</div>
{% endif %}
```
## Best Practices
1. **Use whitespace control** (`{%-` and `-%}`) to keep HTML clean
2. **Prefer `render` over `include`** for better performance and isolation
3. **Cache expensive operations** by assigning to variables
4. **Use descriptive variable names** for clarity
5. **Leverage filters** instead of complex logic when possible
6. **Check for existence** before accessing nested properties
7. **Use `default` filter** for fallback values: `{{ product.metafield | default: "N/A" }}`
## Detailed References
For comprehensive documentation:
- **[references/syntax.md](references/syntax.md)** - Complete syntax reference with all tags
- **[references/filters.md](references/filters.md)** - All 60+ filters with examples
- **[references/objects.md](references/objects.md)** - Complete object property reference
## Integration with Other Skills
- **shopify-theme-dev** - Use when working with theme file structure and sections
- **shopify-api** - Use when fetching data via Ajax or GraphQL to display in Liquid
- **shopify-debugging** - Use when troubleshooting Liquid rendering issues
- **shopify-performance** - Use when optimizing Liquid template performance
## Quick Syntax Reference
```liquid
{# Output #}
{{ variable }}
{{ product.title | upcase }}
{# Conditionals #}
{% if condition %}...{% elsif %}...{% else %}...{% endif %}
{% unless condition %}...{% endunless %}
{% case variable %}{% when value %}...{% endcase %}
{# Loops #}
{% for item in array %}...{% endfor %}
{% for item in array limit: 5 offset: 10 %}...{% endfor %}
{% break %} / {% continue %}
{# Variables #}
{% assign var = value %}
{% capture var %}content{% endcapture %}
{# Inclusion #}
{% render 'snippet', param: value %}
{% section 'section-name' %}
{# Comments #}
{% comment %}...{% endcomment %}
{# Single line #}
```

View File

@@ -0,0 +1,873 @@
# Liquid Filters - Complete Reference
Filters modify output using pipe syntax: `{{ value | filter: parameter }}`
## String Filters
### upcase
Convert to uppercase:
```liquid
{{ "hello world" | upcase }}
{# Output: HELLO WORLD #}
```
### downcase
Convert to lowercase:
```liquid
{{ "HELLO WORLD" | downcase }}
{# Output: hello world #}
```
### capitalize
Capitalize first letter only:
```liquid
{{ "hello world" | capitalize }}
{# Output: Hello world #}
```
### reverse
Reverse string or array:
```liquid
{{ "hello" | reverse }}
{# Output: olleh #}
{{ array | reverse }}
{# Reverses array order #}
```
### size
Get character count or array length:
```liquid
{{ "hello" | size }}
{# Output: 5 #}
{{ collection.products | size }}
{# Output: number of products #}
```
### remove
Remove all occurrences of substring:
```liquid
{{ "hello world world" | remove: "world" }}
{# Output: hello #}
```
### remove_first
Remove first occurrence only:
```liquid
{{ "hello world world" | remove_first: "world" }}
{# Output: hello world #}
```
### replace
Replace all occurrences:
```liquid
{{ "hello" | replace: "l", "L" }}
{# Output: heLLo #}
```
### replace_first
Replace first occurrence only:
```liquid
{{ "hello" | replace_first: "l", "L" }}
{# Output: heLlo #}
```
### split
Split string into array:
```liquid
{{ "a,b,c,d" | split: "," }}
{# Output: ["a", "b", "c", "d"] #}
{% assign tags = "sale,new,featured" | split: "," %}
{% for tag in tags %}
{{ tag }}
{% endfor %}
```
### strip
Remove leading and trailing whitespace:
```liquid
{{ " hello " | strip }}
{# Output: hello #}
```
### lstrip
Remove leading whitespace only:
```liquid
{{ " hello " | lstrip }}
{# Output: hello #}
```
### rstrip
Remove trailing whitespace only:
```liquid
{{ " hello " | rstrip }}
{# Output: hello #}
```
### truncate
Limit string length with ellipsis:
```liquid
{{ "hello world" | truncate: 8 }}
{# Output: hello... #}
{{ "hello world" | truncate: 10, "!" }}
{# Output: hello worl! #}
{{ "hello world" | truncate: 50 }}
{# Output: hello world (no truncation if shorter) #}
```
### truncatewords
Limit by word count:
```liquid
{{ "hello world testing" | truncatewords: 2 }}
{# Output: hello world... #}
{{ "hello world testing" | truncatewords: 2, "--" }}
{# Output: hello world-- #}
```
### append
Add string to end:
```liquid
{{ "hello" | append: " world" }}
{# Output: hello world #}
{% assign file_name = "image" | append: ".jpg" %}
{# file_name: image.jpg #}
```
### prepend
Add string to beginning:
```liquid
{{ "world" | prepend: "hello " }}
{# Output: hello world #}
```
### newline_to_br
Convert newlines to `<br>` tags:
```liquid
{{ product.description | newline_to_br }}
{# Converts \n to <br> #}
```
### strip_html
Remove all HTML tags:
```liquid
{{ "<p>Hello <strong>world</strong></p>" | strip_html }}
{# Output: Hello world #}
```
### escape
Escape HTML special characters:
```liquid
{{ "<div>Test</div>" | escape }}
{# Output: &lt;div&gt;Test&lt;/div&gt; #}
```
### escape_once
Escape HTML but don't double-escape:
```liquid
{{ "&lt;div&gt;" | escape_once }}
{# Output: &lt;div&gt; (not double-escaped) #}
```
### url_encode
URL-encode string:
```liquid
{{ "hello world" | url_encode }}
{# Output: hello+world #}
{{ "foo@bar.com" | url_encode }}
{# Output: foo%40bar.com #}
```
### url_decode
Decode URL-encoded string:
```liquid
{{ "hello+world" | url_decode }}
{# Output: hello world #}
```
### base64_encode
Encode to base64:
```liquid
{{ "hello" | base64_encode }}
{# Output: aGVsbG8= #}
```
### base64_decode
Decode from base64:
```liquid
{{ "aGVsbG8=" | base64_decode }}
{# Output: hello #}
```
### slice
Extract substring or array slice:
```liquid
{{ "hello" | slice: 0, 3 }}
{# Output: hel #}
{{ "hello" | slice: -3, 3 }}
{# Output: llo #}
```
## Numeric Filters
### abs
Absolute value:
```liquid
{{ -5 | abs }}
{# Output: 5 #}
{{ 5 | abs }}
{# Output: 5 #}
```
### ceil
Round up to nearest integer:
```liquid
{{ 1.2 | ceil }}
{# Output: 2 #}
{{ 1.9 | ceil }}
{# Output: 2 #}
```
### floor
Round down to nearest integer:
```liquid
{{ 1.9 | floor }}
{# Output: 1 #}
{{ 1.1 | floor }}
{# Output: 1 #}
```
### round
Round to specified decimal places:
```liquid
{{ 1.5 | round }}
{# Output: 2 #}
{{ 1.567 | round: 2 }}
{# Output: 1.57 #}
{{ 1.234 | round: 1 }}
{# Output: 1.2 #}
```
### plus
Addition:
```liquid
{{ 5 | plus: 3 }}
{# Output: 8 #}
{{ product.price | plus: 1000 }}
{# Add $10.00 (prices in cents) #}
```
### minus
Subtraction:
```liquid
{{ 5 | minus: 3 }}
{# Output: 2 #}
```
### times
Multiplication:
```liquid
{{ 5 | times: 3 }}
{# Output: 15 #}
{{ product.price | times: 0.8 }}
{# 20% discount #}
```
### divided_by
Integer division:
```liquid
{{ 10 | divided_by: 2 }}
{# Output: 5 #}
{{ 10 | divided_by: 3 }}
{# Output: 3 (integer division) #}
{{ 10.0 | divided_by: 3 }}
{# Output: 3.33... (float division) #}
```
### modulo
Get remainder:
```liquid
{{ 10 | modulo: 3 }}
{# Output: 1 #}
{# Check if even #}
{% if forloop.index | modulo: 2 == 0 %}
Even row
{% endif %}
```
### at_least
Ensure minimum value:
```liquid
{{ 1 | at_least: 5 }}
{# Output: 5 #}
{{ 10 | at_least: 5 }}
{# Output: 10 #}
```
### at_most
Ensure maximum value:
```liquid
{{ 100 | at_most: 50 }}
{# Output: 50 #}
{{ 10 | at_most: 50 }}
{# Output: 10 #}
```
## Array/Collection Filters
### first
Get first element:
```liquid
{{ collection.products | first }}
{# Returns first product #}
{{ "a,b,c" | split: "," | first }}
{# Output: a #}
```
### last
Get last element:
```liquid
{{ collection.products | last }}
{# Returns last product #}
```
### join
Join array with separator:
```liquid
{{ product.tags | join: ", " }}
{# Output: sale, new, featured #}
```
### map
Extract property from each object:
```liquid
{{ collection.products | map: "title" }}
{# Returns array of product titles #}
{{ collection.products | map: "title" | join: ", " }}
{# Output: Product 1, Product 2, Product 3 #}
```
### sort
Sort array by property:
```liquid
{{ collection.products | sort: "price" }}
{# Sort by price ascending #}
{{ collection.products | sort: "title" }}
{# Sort alphabetically #}
```
### sort_natural
Case-insensitive sort:
```liquid
{{ collection.products | sort_natural: "title" }}
{# Sorts: Apple, banana, Cherry (natural order) #}
```
### where
Filter array by property value:
```liquid
{{ collection.products | where: "vendor", "Nike" }}
{# Only Nike products #}
{{ collection.products | where: "available", true }}
{# Only available products #}
{{ collection.products | where: "type", "shoes" | map: "title" }}
{# Combine with map #}
```
### uniq
Remove duplicates:
```liquid
{{ collection.all_vendors | uniq }}
{# Unique vendor names #}
```
### limit
Limit array to N items:
```liquid
{{ collection.products | limit: 5 }}
{# First 5 products #}
```
### offset
Skip first N items:
```liquid
{{ collection.products | offset: 10 }}
{# Products from 11th onward #}
```
### concat
Merge two arrays:
```liquid
{% assign array1 = "a,b,c" | split: "," %}
{% assign array2 = "d,e,f" | split: "," %}
{{ array1 | concat: array2 | join: ", " }}
{# Output: a, b, c, d, e, f #}
```
### compact
Remove nil values from array:
```liquid
{{ array | compact }}
{# Removes nil/null elements #}
```
## Shopify-Specific Filters
### money
Format as currency with symbol:
```liquid
{{ 1000 | money }}
{# Output: $10.00 #}
{{ 1599 | money }}
{# Output: $15.99 #}
```
### money_without_currency
Format without currency symbol:
```liquid
{{ 1000 | money_without_currency }}
{# Output: 10.00 #}
```
### money_without_trailing_zeros
Remove unnecessary decimals:
```liquid
{{ 1000 | money_without_trailing_zeros }}
{# Output: $10 #}
{{ 1050 | money_without_trailing_zeros }}
{# Output: $10.50 #}
```
### weight_with_unit
Add weight unit:
```liquid
{{ 500 | weight_with_unit }}
{# Output: 500 g #}
{{ product.variants.first.weight | weight_with_unit }}
```
### asset_url
Get theme asset CDN URL:
```liquid
{{ 'logo.png' | asset_url }}
{# Output: //cdn.shopify.com/s/files/1/0000/0000/t/1/assets/logo.png #}
```
### img_url
Generate image URL with size:
```liquid
{{ product.featured_image | img_url: '500x500' }}
{# Resize to 500x500 #}
{{ product.featured_image | img_url: 'large' }}
{# Named size: pico, icon, thumb, small, compact, medium, large, grande, 1024x1024, 2048x2048 #}
{{ product.featured_image | img_url: '500x500', crop: 'center' }}
{# With crop #}
```
### link_to_type
Create link to product type collection:
```liquid
{{ product.type | link_to_type }}
{# Output: <a href="/collections/types?q=Shoes">Shoes</a> #}
```
### link_to_vendor
Create link to vendor collection:
```liquid
{{ product.vendor | link_to_vendor }}
{# Output: <a href="/collections/vendors?q=Nike">Nike</a> #}
```
### link_to_tag
Create link to tag filter:
```liquid
{{ tag | link_to_tag: tag }}
{# Output: <a href="/collections/all/sale">sale</a> #}
```
### highlight
Highlight search terms:
```liquid
{{ product.title | highlight: search.terms }}
{# Wraps search terms in <strong class="highlight"> tags #}
```
### highlight_active_tag
Highlight current tag:
```liquid
{{ tag | highlight_active_tag: tag }}
{# Wraps current tag in <span class="active"> #}
```
### payment_type_img_url
Get payment icon URL:
```liquid
{{ 'visa' | payment_type_img_url }}
{# Returns Shopify-hosted Visa icon URL #}
```
### placeholder_svg_tag
Generate placeholder SVG:
```liquid
{{ 'product-1' | placeholder_svg_tag }}
{# Generates placeholder product image SVG #}
{{ 'collection-1' | placeholder_svg_tag: 'custom-class' }}
{# With custom CSS class #}
```
### color_to_rgb
Convert hex to RGB:
```liquid
{{ '#ff0000' | color_to_rgb }}
{# Output: rgb(255, 0, 0) #}
```
### color_to_hsl
Convert hex to HSL:
```liquid
{{ '#ff0000' | color_to_hsl }}
{# Output: hsl(0, 100%, 50%) #}
```
### color_extract
Extract color component:
```liquid
{{ '#ff0000' | color_extract: 'red' }}
{# Output: 255 #}
```
### color_brightness
Calculate brightness:
```liquid
{{ '#ff0000' | color_brightness }}
{# Output: brightness value 0-255 #}
```
### color_modify
Modify color properties:
```liquid
{{ '#ff0000' | color_modify: 'alpha', 0.5 }}
{# Adjust alpha channel #}
```
## Date Filters
### date
Format date using strftime:
```liquid
{{ order.created_at | date: '%B %d, %Y' }}
{# Output: November 10, 2025 #}
{{ order.created_at | date: '%m/%d/%Y' }}
{# Output: 11/10/2025 #}
{{ order.created_at | date: '%Y-%m-%d %H:%M:%S' }}
{# Output: 2025-11-10 14:30:00 #}
```
**Common format codes:**
- `%Y` - 4-digit year (2025)
- `%y` - 2-digit year (25)
- `%m` - Month number (11)
- `%B` - Full month (November)
- `%b` - Short month (Nov)
- `%d` - Day of month (10)
- `%e` - Day without leading zero (10)
- `%A` - Full weekday (Monday)
- `%a` - Short weekday (Mon)
- `%H` - Hour 24-hour (14)
- `%I` - Hour 12-hour (02)
- `%M` - Minutes (30)
- `%S` - Seconds (45)
- `%p` - AM/PM
- `%z` - Timezone offset (+0000)
**Examples:**
```liquid
{{ "now" | date: "%Y-%m-%d" }}
{# Current date: 2025-11-10 #}
{{ article.published_at | date: "%B %d, %Y at %I:%M %p" }}
{# November 10, 2025 at 02:30 PM #}
```
## URL Filters
### url_for_type
Get collection URL for product type:
```liquid
{{ product.type | url_for_type }}
{# Output: /collections/types?q=Shoes #}
```
### url_for_vendor
Get collection URL for vendor:
```liquid
{{ product.vendor | url_for_vendor }}
{# Output: /collections/vendors?q=Nike #}
```
### within
Scope URL within collection:
```liquid
{{ product.url | within: collection }}
{# Output: /collections/sale/products/product-handle #}
```
### default_pagination
Generate pagination HTML:
```liquid
{{ paginate | default_pagination }}
{# Outputs complete pagination HTML #}
```
## Utility Filters
### default
Provide fallback value:
```liquid
{{ product.metafield | default: "N/A" }}
{# If metafield is nil, outputs "N/A" #}
{{ variant.title | default: "Default" }}
```
### json
Convert to JSON:
```liquid
{{ product | json }}
{# Outputs product object as JSON string #}
<script>
var productData = {{ product | json }};
</script>
```
## Filter Chaining
Filters execute left-to-right and can be chained:
```liquid
{{ "hello world" | upcase | replace: "WORLD", "SHOPIFY" }}
{# Output: HELLO SHOPIFY #}
{{ collection.products | where: "available" | map: "title" | sort | join: ", " }}
{# Filter → extract → sort → join #}
{{ product.price | times: 0.8 | round: 2 | money }}
{# Calculate 20% discount, round, format as money #}
{{ product.description | strip_html | truncatewords: 50 | escape }}
{# Strip HTML → truncate → escape for safety #}
```
## Performance Tips
1. **Cache filtered results** if used multiple times:
```liquid
{# ❌ Inefficient: #}
{% for i in (1..10) %}
{{ collection.products | where: "available" | size }}
{% endfor %}
{# ✅ Efficient: #}
{% assign available_products = collection.products | where: "available" %}
{% for i in (1..10) %}
{{ available_products.size }}
{% endfor %}
```
2. **Use `limit` and `offset` filters** instead of manual iteration control
3. **Combine filters intelligently** to reduce operations:
```liquid
{# ❌ Less efficient: #}
{% assign titles = collection.products | map: "title" %}
{% assign sorted = titles | sort %}
{% assign limited = sorted | limit: 5 %}
{# ✅ More efficient: #}
{% assign limited_titles = collection.products | map: "title" | sort | limit: 5 %}
```

View File

@@ -0,0 +1,695 @@
# Liquid Objects - Complete Reference
## Global Objects (Available Everywhere)
### shop
Store-level information:
```liquid
{{ shop.name }} {# Store name #}
{{ shop.url }} {# Store URL (https://...) #}
{{ shop.description }} {# Store tagline/description #}
{{ shop.currency }} {# ISO currency code: USD, GBP, EUR #}
{{ shop.money_format }} {# Money format string: ${{amount}} #}
{{ shop.permanent_domain }} {# Domain: store.myshopify.com #}
{{ shop.domain }} {# Primary domain #}
{{ shop.email }} {# Support email #}
{{ shop.phone }} {# Support phone #}
{{ shop.address }} {# Store address object #}
{{ shop.address.city }}
{{ shop.address.province }}
{{ shop.address.country }}
{{ shop.address.zip }}
{{ shop.enabled_payment_types }} {# Array of enabled payment methods #}
{{ shop.checkout.privacy_policy_url }}
{{ shop.checkout.terms_of_service_url }}
{{ shop.checkout.refund_policy_url }}
```
### request
Request context and routing:
```liquid
{{ request.path }} {# Current URL path: /products/handle #}
{{ request.host }} {# Current domain #}
{{ request.origin }} {# Protocol + host #}
{{ request.page_type }} {# "product", "collection", "index", etc. #}
{{ request.locale.iso_code }} {# Language: "en", "fr", "es" #}
{{ request.locale.root_url }} {# Root URL for locale #}
{{ request.design_mode }} {# Boolean: theme editor active #}
{{ request.visual_preview_mode }} {# Boolean: theme preview active #}
{# Query parameters #}
{{ request.query_string }} {# Full query string #}
{# Build canonical URL #}
{{ request.canonical_url }} {# Full canonical URL #}
{{ request.path_with_query }} {# Path + query string #}
```
### settings
Theme settings (from settings_schema.json):
```liquid
{# Colors #}
{{ settings.color_primary }}
{{ settings.color_secondary }}
{{ settings.color_body_bg }}
{# Typography #}
{{ settings.type_header_font }}
{{ settings.type_body_font }}
{# Layout #}
{{ settings.layout_container_width }}
{{ settings.layout_sidebar_enabled }}
{# Media #}
{{ settings.logo }} {# Image object #}
{{ settings.logo.src }}
{{ settings.logo.width }}
{{ settings.logo.height }}
{{ settings.logo.alt }}
{# Text content #}
{{ settings.announcement_text }}
{{ settings.footer_text }}
{# Boolean settings #}
{% if settings.show_breadcrumbs %}
{# Render breadcrumbs #}
{% endif %}
{# URL settings #}
{{ settings.social_twitter_link }}
{{ settings.social_facebook_link }}
```
### routes
URL routes to standard pages:
```liquid
{{ routes.root_url }} {# / #}
{{ routes.account_url }} {# /account #}
{{ routes.account_login_url }} {# /account/login #}
{{ routes.account_logout_url }} {# /account/logout #}
{{ routes.account_register_url }} {# /account/register #}
{{ routes.account_addresses_url }} {# /account/addresses #}
{{ routes.collections_url }} {# /collections #}
{{ routes.all_products_collection_url }} {# /collections/all #}
{{ routes.search_url }} {# /search #}
{{ routes.cart_url }} {# /cart #}
{{ routes.cart_add_url }} {# /cart/add #}
{{ routes.cart_change_url }} {# /cart/change #}
{{ routes.cart_clear_url }} {# /cart/clear #}
{{ routes.cart_update_url }} {# /cart/update #}
```
### section
Current section context (within sections):
```liquid
{{ section.id }} {# Unique ID: "section-1234567890" #}
{{ section.settings.title }} {# Section setting #}
{{ section.settings.background_color }}
{{ section.index }} {# Position on page #}
{{ section.location }} {# Where section appears #}
{# Blocks #}
{{ section.blocks }} {# Array of blocks #}
{{ section.blocks.size }} {# Number of blocks #}
{% for block in section.blocks %}
{{ block.id }}
{{ block.type }}
{{ block.settings.text }}
{{ block.shopify_attributes }} {# Required for theme editor #}
{% endfor %}
{# Blocks by type #}
{{ section.blocks_by_type }} {# Organized by type #}
```
### block
Current block context (within section blocks):
```liquid
{{ block.id }} {# Unique ID: "block-9876543210" #}
{{ block.type }} {# Block type name #}
{{ block.settings.text }} {# Block setting #}
{{ block.shopify_attributes }} {# Required for theme editor #}
{# Example usage in section #}
{% for block in section.blocks %}
<div {{ block.shopify_attributes }}>
{% case block.type %}
{% when 'heading' %}
<h2>{{ block.settings.title }}</h2>
{% when 'text' %}
<p>{{ block.settings.content }}</p>
{% endcase %}
</div>
{% endfor %}
```
## Page Context Objects
### product
Product object (on product pages):
```liquid
{# Core properties #}
{{ product.id }} {# Numeric ID #}
{{ product.title }} {# Product name #}
{{ product.handle }} {# URL slug #}
{{ product.description }} {# Full HTML description #}
{{ product.vendor }} {# Brand/manufacturer #}
{{ product.type }} {# Category #}
{{ product.url }} {# Product URL #}
{{ product.available }} {# Boolean: any variant in stock #}
{{ product.published_at }} {# Publication timestamp #}
{{ product.created_at }} {# Creation timestamp #}
{{ product.updated_at }} {# Last modified timestamp #}
{# Pricing (in cents) #}
{{ product.price }} {# Current variant price #}
{{ product.price_min }} {# Cheapest variant #}
{{ product.price_max }} {# Most expensive variant #}
{{ product.price_varies }} {# Boolean: different prices #}
{{ product.compare_at_price }} {# Original price for sales #}
{{ product.compare_at_price_min }}
{{ product.compare_at_price_max }}
{{ product.compare_at_price_varies }}
{# Images #}
{{ product.featured_image }} {# Primary image object #}
{{ product.featured_image.src }}
{{ product.featured_image.width }}
{{ product.featured_image.height }}
{{ product.featured_image.alt }}
{{ product.featured_image | img_url: '500x500' }}
{{ product.images }} {# Array of all images #}
{{ product.images.size }} {# Image count #}
{% for image in product.images %}
<img src="{{ image | img_url: '300x300' }}" alt="{{ image.alt }}">
{% endfor %}
{{ product.media }} {# Array of all media (images, videos, 3D) #}
{# Variants #}
{{ product.variants }} {# Array of variants #}
{{ product.variants.size }} {# Variant count #}
{{ product.selected_variant }} {# Currently selected variant #}
{{ product.selected_or_first_available_variant }}
{{ product.first_available_variant }}
{{ product.has_only_default_variant }} {# Boolean: single variant #}
{# Options #}
{{ product.options }} {# Array: ["Size", "Color"] #}
{{ product.options_with_values }} {# Array of option objects #}
{% for option in product.options_with_values %}
<label>{{ option.name }}</label>
<select>
{% for value in option.values %}
<option>{{ value }}</option>
{% endfor %}
</select>
{% endfor %}
{# Collections #}
{{ product.collections }} {# Array of collections #}
{{ product.collections.size }}
{# Tags #}
{{ product.tags }} {# Array of tags #}
{{ product.tags | join: ", " }}
{# Custom data #}
{{ product.metafields.namespace.key }}
{{ product.metafields.custom.field_name }}
{# Template #}
{{ product.template_suffix }} {# Template variant: "alternate" #}
```
### variant
Variant object (product.variants, product.selected_variant):
```liquid
{{ variant.id }} {# Variant ID #}
{{ variant.product_id }} {# Parent product ID #}
{{ variant.title }} {# "Red / Medium" #}
{{ variant.price }} {# Price in cents #}
{{ variant.compare_at_price }} {# Original price #}
{{ variant.sku }} {# SKU code #}
{{ variant.barcode }} {# Barcode #}
{{ variant.weight }} {# Weight in grams #}
{{ variant.weight_unit }} {# "kg", "lb", etc. #}
{{ variant.weight_in_unit }} {# Weight in configured unit #}
{# Availability #}
{{ variant.available }} {# Boolean: in stock #}
{{ variant.inventory_quantity }} {# Current stock level #}
{{ variant.inventory_policy }} {# "continue" or "deny" #}
{{ variant.inventory_management }} {# "shopify" or null #}
{# Options #}
{{ variant.option1 }} {# First option value: "Red" #}
{{ variant.option2 }} {# Second option value: "Medium" #}
{{ variant.option3 }} {# Third option value #}
{{ variant.options }} {# Array: ["Red", "Medium"] #}
{# Image #}
{{ variant.featured_image }} {# Variant-specific image #}
{{ variant.image }} {# Same as featured_image #}
{# URL #}
{{ variant.url }} {# Product URL with variant param #}
{# Metafields #}
{{ variant.metafields.namespace.key }}
```
### collection
Collection object (on collection pages):
```liquid
{# Core properties #}
{{ collection.id }} {# Numeric ID #}
{{ collection.title }} {# Collection name #}
{{ collection.handle }} {# URL slug #}
{{ collection.description }} {# HTML description #}
{{ collection.url }} {# Collection URL #}
{{ collection.published_at }} {# Publication date #}
{# Image #}
{{ collection.image }} {# Featured image object #}
{{ collection.image.src }}
{{ collection.image | img_url: '1024x1024' }}
{# Products #}
{{ collection.products }} {# Array of products #}
{{ collection.products_count }} {# Current page count #}
{{ collection.all_products_count }}{# Total count #}
{# Filtering & sorting #}
{{ collection.all_tags }} {# All tags (max 1000) #}
{{ collection.all_types }} {# All product types #}
{{ collection.all_vendors }} {# All vendors #}
{{ collection.current_type }} {# Active type filter #}
{{ collection.current_vendor }} {# Active vendor filter #}
{{ collection.sort_by }} {# Current sort method #}
{{ collection.default_sort_by }} {# Default sort #}
{{ collection.sort_options }} {# Available sort methods #}
{# Filters (Storefront Filtering) #}
{{ collection.filters }} {# Array of filter objects #}
{% for filter in collection.filters %}
{{ filter.label }} {# Filter name #}
{{ filter.type }} {# "list", "price_range" #}
{{ filter.active_values }} {# Currently active #}
{{ filter.values }} {# Available values #}
{% endfor %}
{# Navigation (on product pages within collection) #}
{{ collection.next_product }} {# Next product in collection #}
{{ collection.previous_product }} {# Previous product #}
{# Metafields #}
{{ collection.metafields.namespace.key }}
{# Template #}
{{ collection.template_suffix }}
```
### cart
Cart object (global - always available):
```liquid
{# Cart state #}
{{ cart.item_count }} {# Total line items #}
{{ cart.total_price }} {# Total in cents #}
{{ cart.total_weight }} {# Weight sum #}
{{ cart.empty? }} {# Boolean: is cart empty #}
{# Items #}
{{ cart.items }} {# Array of line items #}
{{ cart.items.size }} {# Number of line items #}
{% for item in cart.items %}
{{ item.product_id }}
{{ item.variant_id }}
{{ item.title }}
{{ item.quantity }}
{{ item.price }}
{{ item.line_price }} {# price × quantity #}
{{ item.image }}
{% endfor %}
{# Notes and attributes #}
{{ cart.note }} {# Customer note #}
{{ cart.attributes }} {# Custom cart attributes #}
{# Access specific attribute #}
{% if cart.attributes.gift_wrap %}
Gift wrap requested
{% endif %}
{# Discounts #}
{{ cart.cart_level_discount_applications }}
{{ cart.total_discount }} {# Total discount amount #}
{# Checkout #}
{{ cart.requires_shipping }} {# Boolean #}
```
### line_item
Line item object (cart.items):
```liquid
{% for item in cart.items %}
{{ item.id }} {# Line item ID #}
{{ item.key }} {# Unique key #}
{{ item.product_id }} {# Product ID #}
{{ item.variant_id }} {# Variant ID #}
{# Product info #}
{{ item.product }} {# Product object #}
{{ item.variant }} {# Variant object #}
{{ item.title }} {# Product title #}
{{ item.product_title }} {# Same as title #}
{{ item.variant_title }} {# Variant options #}
{# Pricing #}
{{ item.quantity }} {# Quantity ordered #}
{{ item.price }} {# Price per unit (cents) #}
{{ item.line_price }} {# Total: price × quantity #}
{{ item.original_price }} {# Before discounts #}
{{ item.original_line_price }}
{{ item.final_price }} {# After discounts #}
{{ item.final_line_price }}
{# Images #}
{{ item.image }} {# Line item image #}
{{ item.featured_image.src }}
{# URL #}
{{ item.url }} {# Link to product #}
{# SKU #}
{{ item.sku }} {# Variant SKU #}
{# Properties (custom line item data) #}
{{ item.properties }} {# Hash of properties #}
{% for property in item.properties %}
{{ property.first }}: {{ property.last }}
{% endfor %}
{# Discounts #}
{{ item.discount_allocations }}
{% for discount in item.discount_allocations %}
{{ discount.amount }}
{{ discount.discount_application.title }}
{% endfor %}
{# Fulfillment #}
{{ item.requires_shipping }} {# Boolean #}
{{ item.taxable }} {# Boolean #}
{% endfor %}
```
### customer
Customer object (when logged in):
```liquid
{% if customer %}
{{ customer.id }} {# Numeric ID #}
{{ customer.email }} {# Email address #}
{{ customer.first_name }}
{{ customer.last_name }}
{{ customer.name }} {# Full name #}
{{ customer.phone }}
{# Account status #}
{{ customer.has_account }} {# Boolean: registered #}
{{ customer.accepts_marketing }} {# Email marketing opt-in #}
{{ customer.email_marketing_consent }}
{# Addresses #}
{{ customer.addresses }} {# Array of addresses #}
{{ customer.addresses_count }}
{{ customer.default_address }} {# Primary address #}
{% for address in customer.addresses %}
{{ address.first_name }}
{{ address.last_name }}
{{ address.address1 }}
{{ address.address2 }}
{{ address.city }}
{{ address.province }}
{{ address.province_code }} {# State/region code #}
{{ address.country }}
{{ address.country_code }} {# Country code: US, CA, etc. #}
{{ address.zip }}
{{ address.phone }}
{{ address.company }}
{% endfor %}
{# Orders #}
{{ customer.orders }} {# Array of orders #}
{{ customer.orders_count }} {# Total orders #}
{{ customer.total_spent }} {# Lifetime value (cents) #}
{# Tags #}
{{ customer.tags }} {# Array of customer tags #}
{# Metafields #}
{{ customer.metafields.namespace.key }}
{% endif %}
```
### order
Order object (order confirmation, customer account):
```liquid
{{ order.id }} {# Numeric ID #}
{{ order.name }} {# Order name: "#1001" #}
{{ order.order_number }} {# 1001 #}
{{ order.confirmation_number }} {# Unique confirmation #}
{{ order.email }} {# Customer email #}
{{ order.phone }} {# Customer phone #}
{{ order.customer_url }} {# Link to view order #}
{# Timestamps #}
{{ order.created_at }} {# Order date/time #}
{{ order.updated_at }}
{{ order.cancelled_at }} {# If cancelled #}
{{ order.processed_at }}
{# Customer #}
{{ order.customer }} {# Customer object #}
{{ order.customer.name }}
{# Items #}
{{ order.line_items }} {# Array of line items #}
{{ order.line_items_count }}
{% for item in order.line_items %}
{{ item.title }}
{{ item.quantity }}
{{ item.price }}
{{ item.line_price }}
{% endfor %}
{# Pricing #}
{{ order.subtotal_price }} {# Before tax/shipping #}
{{ order.total_price }} {# Grand total #}
{{ order.tax_price }} {# Total tax #}
{{ order.shipping_price }} {# Shipping cost #}
{{ order.total_discounts }} {# Discount amount #}
{# Status #}
{{ order.financial_status }} {# "paid", "pending", "refunded" #}
{{ order.fulfillment_status }} {# "fulfilled", "partial", null #}
{{ order.cancelled }} {# Boolean #}
{{ order.cancel_reason }}
{# Addresses #}
{{ order.shipping_address }}
{{ order.billing_address }}
{# Shipping #}
{{ order.shipping_method.title }} {# Shipping method name #}
{{ order.shipping_method.price }}
{# Discounts #}
{{ order.discount_applications }}
{% for discount in order.discount_applications %}
{{ discount.title }}
{{ discount.total_allocated_amount }}
{% endfor %}
{# Notes #}
{{ order.note }} {# Customer note #}
{{ order.attributes }} {# Custom attributes #}
{# Tags #}
{{ order.tags }}
```
### article
Article object (blog post pages):
```liquid
{{ article.id }} {# Numeric ID #}
{{ article.title }} {# Article headline #}
{{ article.handle }} {# URL slug #}
{{ article.content }} {# Full HTML content #}
{{ article.excerpt }} {# Summary/teaser #}
{{ article.excerpt_or_content }} {# Excerpt if set, else content #}
{# Author #}
{{ article.author }} {# Author name #}
{{ article.author_url }} {# Author profile URL #}
{# Dates #}
{{ article.published_at }} {# Publication date #}
{{ article.created_at }}
{{ article.updated_at }}
{# URL #}
{{ article.url }} {# Article URL #}
{# Image #}
{{ article.image }} {# Featured image #}
{{ article.image.src }}
{{ article.image | img_url: 'large' }}
{# Comments #}
{{ article.comments }} {# Array of comments #}
{{ article.comments_count }}
{{ article.comments_enabled }} {# Boolean #}
{{ article.moderated }} {# Comment moderation enabled #}
{# Tags #}
{{ article.tags }} {# Array of tags #}
{# Blog reference #}
{{ article.blog }} {# Parent blog object #}
{{ article.blog.title }}
{# Metafields #}
{{ article.metafields.namespace.key }}
```
### blog
Blog object (blog listing page):
```liquid
{{ blog.id }} {# Numeric ID #}
{{ blog.title }} {# Blog name #}
{{ blog.handle }} {# URL slug #}
{{ blog.url }} {# Blog URL #}
{# Articles #}
{{ blog.articles }} {# Array of articles #}
{{ blog.articles_count }} {# Total articles #}
{# Tags #}
{{ blog.all_tags }} {# All article tags #}
{# Metafields #}
{{ blog.metafields.namespace.key }}
```
### search
Search results object:
```liquid
{{ search.performed }} {# Boolean: search executed #}
{{ search.results }} {# Results array #}
{{ search.results_count }} {# Number of results #}
{{ search.terms }} {# Search query #}
{{ search.types }} {# Resource types found #}
{% for item in search.results %}
{% case item.object_type %}
{% when 'product' %}
{{ item.title }}
{{ item.price | money }}
{% when 'article' %}
{{ item.title }}
{{ item.excerpt }}
{% when 'page' %}
{{ item.title }}
{{ item.content | strip_html | truncatewords: 50 }}
{% endcase %}
{% endfor %}
```
## Metafields
Access custom data on any object:
```liquid
{# Product metafields #}
{{ product.metafields.namespace.key }}
{{ product.metafields.custom.warranty_info }}
{{ product.metafields.specifications.material }}
{# Collection metafields #}
{{ collection.metafields.seo.custom_title }}
{# Customer metafields #}
{{ customer.metafields.loyalty.points }}
{# Shop metafields #}
{{ shop.metafields.global.announcement }}
{# Check for existence #}
{% if product.metafields.custom.size_guide %}
{{ product.metafields.custom.size_guide }}
{% endif %}
{# Use default filter for safety #}
{{ product.metafields.custom.field | default: "Not specified" }}
```
## Metaobjects
Access metaobject definitions:
```liquid
{# Access metaobject by handle #}
{% assign testimonial = shop.metaobjects.testimonials['customer-review-1'] %}
{{ testimonial.name }}
{{ testimonial.rating }}
{{ testimonial.content }}
{# Loop through metaobjects #}
{% for testimonial in shop.metaobjects.testimonials.values %}
{{ testimonial.fields.author }}
{{ testimonial.fields.quote }}
{% endfor %}
```

View File

@@ -0,0 +1,551 @@
# Liquid Syntax - Complete Reference
## Tag Categories
### Control Flow Tags
#### if/elsif/else/endif
```liquid
{% if product.available %}
<button>Add to Cart</button>
{% elsif product.coming_soon %}
<p>Coming Soon</p>
{% else %}
<p>Sold Out</p>
{% endif %}
```
**Operators:**
- `==` - equals
- `!=` - not equals
- `>` - greater than
- `<` - less than
- `>=` - greater than or equal
- `<=` - less than or equal
- `contains` - substring or array contains
- `and` - logical AND
- `or` - logical OR
**Examples:**
```liquid
{% if product.price > 100 and product.available %}
Premium item in stock
{% endif %}
{% if product.tags contains 'sale' or product.type == 'clearance' %}
On sale!
{% endif %}
```
#### unless
Negated if statement:
```liquid
{% unless customer.name == blank %}
Hello, {{ customer.name }}
{% endunless %}
{# Equivalent to: #}
{% if customer.name != blank %}
Hello, {{ customer.name }}
{% endif %}
```
#### case/when
Switch-case statement:
```liquid
{% case product.type %}
{% when 'shoes' %}
<icon>👟</icon>
{% when 'boots' %}
<icon>👢</icon>
{% when 'sneakers' %}
<icon>👟</icon>
{% else %}
<icon>📦</icon>
{% endcase %}
```
### Iteration Tags
#### for loop
```liquid
{% for product in collection.products %}
{{ product.title }}
{% endfor %}
```
**Modifiers:**
```liquid
{# Limit to first 5 #}
{% for product in collection.products limit: 5 %}
{{ product.title }}
{% endfor %}
{# Skip first 10 #}
{% for product in collection.products offset: 10 %}
{{ product.title }}
{% endfor %}
{# Reverse order #}
{% for product in collection.products reversed %}
{{ product.title }}
{% endfor %}
{# Combine modifiers #}
{% for product in collection.products limit: 5 offset: 10 %}
{# Items 11-15 #}
{% endfor %}
```
**forloop object (available inside loops):**
```liquid
{% for item in array %}
{{ forloop.index }} {# 1-based: 1, 2, 3, ... #}
{{ forloop.index0 }} {# 0-based: 0, 1, 2, ... #}
{{ forloop.rindex }} {# Reverse 1-based: 3, 2, 1 #}
{{ forloop.rindex0 }} {# Reverse 0-based: 2, 1, 0 #}
{{ forloop.first }} {# true on first iteration #}
{{ forloop.last }} {# true on last iteration #}
{{ forloop.length }} {# Total number of items #}
{% endfor %}
```
**Example usage:**
```liquid
{% for product in collection.products %}
{% if forloop.first %}
<h2>Featured Product</h2>
{% endif %}
<div class="product-{{ forloop.index }}">
{{ product.title }}
</div>
{% if forloop.index == 3 %}
<hr> {# Divider after 3rd item #}
{% endif %}
{% if forloop.last %}
<p>Showing {{ forloop.length }} products</p>
{% endif %}
{% endfor %}
```
#### break and continue
```liquid
{% for product in collection.products %}
{% if product.handle == 'target' %}
{% break %} {# Exit loop entirely #}
{% endif %}
{% if product.available == false %}
{% continue %} {# Skip to next iteration #}
{% endif %}
{{ product.title }}
{% endfor %}
```
#### tablerow
Creates HTML table rows:
```liquid
{% tablerow product in collection.products cols: 3 %}
{{ product.title }}
{% endtablerow %}
{# Output: #}
<table>
<tr class="row1">
<td class="col1">Product 1</td>
<td class="col2">Product 2</td>
<td class="col3">Product 3</td>
</tr>
<tr class="row2">
<td class="col1">Product 4</td>
...
</tr>
</table>
```
**tablerow object:**
```liquid
{% tablerow product in products cols: 3 limit: 12 %}
{{ tablerow.col }} {# Current column (1-based) #}
{{ tablerow.col0 }} {# Current column (0-based) #}
{{ tablerow.row }} {# Current row (1-based) #}
{{ tablerow.index }} {# Item index (1-based) #}
{{ tablerow.first }} {# true on first item #}
{{ tablerow.last }} {# true on last item #}
{{ tablerow.col_first }} {# true on first column #}
{{ tablerow.col_last }} {# true on last column #}
{% endtablerow %}
```
#### paginate
For paginating large collections:
```liquid
{% paginate collection.products by 12 %}
{% for product in paginate.collection.products %}
{% render 'product-card', product: product %}
{% endfor %}
{# Pagination controls #}
{% if paginate.pages > 1 %}
{{ paginate | default_pagination }}
{% endif %}
{% endpaginate %}
```
**paginate object:**
```liquid
{{ paginate.current_page }} {# Current page number #}
{{ paginate.pages }} {# Total pages #}
{{ paginate.items }} {# Total items #}
{{ paginate.page_size }} {# Items per page #}
{{ paginate.previous.url }} {# Previous page URL (if exists) #}
{{ paginate.previous.title }} {# Previous page title #}
{{ paginate.previous.is_link }} {# Boolean #}
{{ paginate.next.url }} {# Next page URL (if exists) #}
{{ paginate.next.title }} {# Next page title #}
{{ paginate.next.is_link }} {# Boolean #}
{{ paginate.parts }} {# Array of page links #}
```
**Custom pagination:**
```liquid
{% paginate collection.products by 20 %}
<div class="pagination">
{% if paginate.previous %}
<a href="{{ paginate.previous.url }}">← Previous</a>
{% endif %}
{% for part in paginate.parts %}
{% if part.is_link %}
<a href="{{ part.url }}">{{ part.title }}</a>
{% else %}
<span class="current">{{ part.title }}</span>
{% endif %}
{% endfor %}
{% if paginate.next %}
<a href="{{ paginate.next.url }}">Next →</a>
{% endif %}
</div>
{% endpaginate %}
```
### Variable Assignment
#### assign
Single-line variable assignment:
```liquid
{% assign sale_price = product.price | times: 0.8 %}
{% assign is_available = product.available %}
{% assign product_count = collection.products.size %}
{% assign full_name = customer.first_name | append: ' ' | append: customer.last_name %}
```
#### capture
Multi-line content capture:
```liquid
{% capture product_title %}
{{ collection.title }} - {{ product.title }}
{% endcapture %}
{{ product_title }} {# "Summer Sale - Blue T-Shirt" #}
{% capture greeting %}
<h1>Welcome, {{ customer.name }}!</h1>
<p>You have {{ customer.orders_count }} orders.</p>
{% endcapture %}
{{ greeting }}
```
#### liquid (multi-statement)
Cleaner syntax for multiple statements:
```liquid
{% liquid
assign product_type = product.type
assign is_on_sale = product.on_sale
assign sale_percentage = product.discount_percent
if is_on_sale
assign status = 'SALE'
else
assign status = 'REGULAR'
endif
echo status
%}
```
### Template Inclusion
#### render
Isolated scope (preferred method):
```liquid
{# Basic usage #}
{% render 'product-card', product: product %}
{# Multiple parameters #}
{% render 'product-card',
product: product,
show_price: true,
show_vendor: false,
css_class: 'featured'
%}
{# Render for each item #}
{% render 'product-card' for collection.products as item %}
{# Pass arrays #}
{% render 'gallery', images: product.images %}
```
**Inside product-card.liquid:**
```liquid
{# Only has access to passed parameters #}
<div class="product {% if css_class %}{{ css_class }}{% endif %}">
<h3>{{ product.title }}</h3>
{% if show_price %}
<p>{{ product.price | money }}</p>
{% endif %}
{% if show_vendor %}
<p>{{ product.vendor }}</p>
{% endif %}
</div>
```
#### include
Shared scope (legacy, avoid in new code):
```liquid
{% include 'product-details' %}
{# Can access all parent template variables #}
{# Harder to debug and reason about #}
```
#### section
Load dynamic sections:
```liquid
{% section 'featured-product' %}
{% section 'newsletter-signup' %}
```
### Utility Tags
#### comment
Multi-line comments:
```liquid
{% comment %}
This entire block is ignored
by the Liquid renderer.
Use for documentation.
{% endcomment %}
{# Single-line comment #}
```
#### echo
Output shorthand (alternative to `{{ }}`):
```liquid
{% echo product.title %}
{# Equivalent to: {{ product.title }} #}
```
#### raw
Output Liquid code without processing:
```liquid
{% raw %}
{{ This will be output as-is }}
{% Liquid tags won't be processed %}
{% endraw %}
```
Useful for documentation or code examples.
## Whitespace Control
Strip whitespace using hyphens:
```liquid
{%- if condition -%}
Content (whitespace stripped on both sides)
{%- endif -%}
{{ "hello" -}}world
{# Output: helloworld (no space) #}
{{- product.title }}
{# Strips whitespace before output #}
{{ product.title -}}
{# Strips whitespace after output #}
```
**Example:**
```liquid
{# Without whitespace control: #}
{% for item in array %}
{{ item }}
{% endfor %}
{# Output has newlines and indentation #}
{# With whitespace control: #}
{%- for item in array -%}
{{ item }}
{%- endfor -%}
{# Output is compact #}
```
## Operator Precedence
**Order of evaluation (right-to-left):**
```liquid
{% if true or false and false %}
{# Evaluates as: true or (false and false) = true #}
{% endif %}
```
**IMPORTANT:** No parentheses support in Liquid. Break complex conditions into variables:
```liquid
{# ❌ DOESN'T WORK: #}
{% if (x > 5 and y < 10) or z == 0 %}
{# ✅ WORKS: #}
{% assign condition1 = false %}
{% if x > 5 and y < 10 %}
{% assign condition1 = true %}
{% endif %}
{% if condition1 or z == 0 %}
{# Logic here #}
{% endif %}
```
## Performance Tips
1. **Cache repeated calculations:**
```liquid
{# ❌ Inefficient: #}
{% for i in (1..10) %}
{{ collection.products.size }} {# Calculated 10 times #}
{% endfor %}
{# ✅ Efficient: #}
{% assign product_count = collection.products.size %}
{% for i in (1..10) %}
{{ product_count }}
{% endfor %}
```
2. **Use `limit` and `offset` instead of iterating full arrays:**
```liquid
{# ❌ Inefficient: #}
{% for product in collection.products %}
{% if forloop.index <= 5 %}
{{ product.title }}
{% endif %}
{% endfor %}
{# ✅ Efficient: #}
{% for product in collection.products limit: 5 %}
{{ product.title }}
{% endfor %}
```
3. **Prefer `render` over `include`** for better performance and variable scoping
4. **Use `liquid` tag** for cleaner multi-statement blocks
## Common Gotchas
1. **No parentheses in conditions** - Use variables instead
2. **Right-to-left evaluation** - Be careful with operator precedence
3. **String concatenation** - Use `append` filter or `capture` tag
4. **Array/object mutation** - Not possible; create new variables
5. **Integer division** - `{{ 5 | divided_by: 2 }}` returns `2`, not `2.5`
6. **Truthy/falsy values:**
- `false` and `nil` are falsy
- Everything else (including `0`, `""`, `[]`) is truthy
## Debugging Tips
1. **Output variable types:**
```liquid
{{ product | json }} {# Output entire object as JSON #}
{{ product.class }} {# Output object type #}
{{ variable.size }} {# Check array/string length #}
```
2. **Check for nil/existence:**
```liquid
{% if product.metafield %}
Metafield exists
{% else %}
Metafield is nil
{% endif %}
```
3. **Use default filter for safety:**
```liquid
{{ product.metafield.value | default: "Not set" }}
```
4. **Enable theme preview console** to see Liquid errors in real-time

View File

@@ -0,0 +1,655 @@
---
name: shopify-performance
description: Performance optimization for Shopify stores including theme speed optimization, image optimization, JavaScript and CSS minification, lazy loading, CDN usage, caching strategies, Liquid template performance, and Core Web Vitals improvement. Use when optimizing store speed, reducing load times, improving Lighthouse scores, optimizing images, implementing lazy loading, reducing JavaScript bloat, or improving Core Web Vitals metrics (LCP, FID, CLS).
---
# Shopify Performance Optimization
Expert guidance for optimizing Shopify store performance including theme speed, asset optimization, and Core Web Vitals.
## When to Use This Skill
Invoke this skill when:
- Optimizing Shopify theme performance and load times
- Improving Lighthouse or PageSpeed Insights scores
- Reducing Time to Interactive (TTI) or Largest Contentful Paint (LCP)
- Optimizing images for faster loading
- Implementing lazy loading for images and videos
- Minifying and optimizing JavaScript and CSS
- Reducing JavaScript bundle sizes
- Improving Core Web Vitals metrics (LCP, FID, CLS)
- Implementing caching strategies
- Optimizing Liquid template rendering
- Reducing server response times
- Improving mobile performance
## Core Capabilities
### 1. Image Optimization
Images are typically the largest assets - optimize aggressively.
**Use Shopify CDN Image Sizing:**
```liquid
{# ❌ Don't load full-size images #}
<img src="{{ product.featured_image.src }}" alt="{{ product.title }}">
{# ✅ Use img_url filter with appropriate size #}
<img
src="{{ product.featured_image | img_url: '800x800' }}"
alt="{{ product.featured_image.alt | escape }}"
loading="lazy"
width="800"
height="800"
>
```
**Responsive Images:**
```liquid
<img
src="{{ image | img_url: '800x' }}"
srcset="
{{ image | img_url: '400x' }} 400w,
{{ image | img_url: '800x' }} 800w,
{{ image | img_url: '1200x' }} 1200w,
{{ image | img_url: '1600x' }} 1600w
"
sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
alt="{{ image.alt | escape }}"
loading="lazy"
width="800"
height="800"
>
```
**Modern Image Formats:**
```liquid
<picture>
{# WebP for modern browsers #}
<source
type="image/webp"
srcset="
{{ image | img_url: '400x', format: 'pjpg' }} 400w,
{{ image | img_url: '800x', format: 'pjpg' }} 800w
"
>
{# Fallback to JPEG #}
<img
src="{{ image | img_url: '800x' }}"
srcset="
{{ image | img_url: '400x' }} 400w,
{{ image | img_url: '800x' }} 800w
"
alt="{{ image.alt | escape }}"
loading="lazy"
>
</picture>
```
**Lazy Loading:**
```liquid
{# Native lazy loading #}
<img
src="{{ image | img_url: '800x' }}"
alt="{{ image.alt | escape }}"
loading="lazy"
decoding="async"
>
{# Eager load above-the-fold images #}
{% if forloop.index <= 3 %}
<img src="{{ image | img_url: '800x' }}" loading="eager">
{% else %}
<img src="{{ image | img_url: '800x' }}" loading="lazy">
{% endif %}
```
**Preload Critical Images:**
```liquid
{# In <head> for hero images #}
<link
rel="preload"
as="image"
href="{{ section.settings.hero_image | img_url: '1920x' }}"
imagesrcset="
{{ section.settings.hero_image | img_url: '800x' }} 800w,
{{ section.settings.hero_image | img_url: '1920x' }} 1920w
"
imagesizes="100vw"
>
```
### 2. JavaScript Optimization
Reduce JS payload and execution time.
**Defer Non-Critical JavaScript:**
```html
{# ❌ Blocking JavaScript #}
<script src="{{ 'theme.js' | asset_url }}"></script>
{# ✅ Deferred JavaScript #}
<script src="{{ 'theme.js' | asset_url }}" defer></script>
{# ✅ Async for independent scripts #}
<script src="{{ 'analytics.js' | asset_url }}" async></script>
```
**Inline Critical JavaScript:**
```liquid
{# Inline small, critical scripts #}
<script>
// Critical initialization code
document.documentElement.classList.remove('no-js');
document.documentElement.classList.add('js');
</script>
```
**Code Splitting:**
```javascript
// Load features only when needed
async function loadCart() {
const { Cart } = await import('./cart.js');
return new Cart();
}
// Load on interaction
document.querySelector('.cart-icon').addEventListener('click', async () => {
const cart = await loadCart();
cart.open();
}, { once: true });
```
**Remove Unused JavaScript:**
```javascript
// ❌ Don't load libraries you don't use
// Example: Don't include entire jQuery if you only need a few functions
// ✅ Use native alternatives
// Instead of: $('.selector').hide()
// Use: document.querySelector('.selector').style.display = 'none';
// Instead of: $.ajax()
// Use: fetch()
```
**Minify JavaScript:**
```bash
# Use build tools to minify
npm install terser --save-dev
# Minify
terser theme.js -o theme.min.js -c -m
```
### 3. CSS Optimization
Optimize stylesheets for faster rendering.
**Critical CSS:**
```liquid
{# Inline critical above-the-fold CSS in <head> #}
<style>
/* Critical CSS only (header, hero) */
.header { /* ... */ }
.hero { /* ... */ }
.button { /* ... */ }
</style>
{# Load full CSS deferred #}
<link
rel="preload"
href="{{ 'theme.css' | asset_url }}"
as="style"
onload="this.onload=null;this.rel='stylesheet'"
>
<noscript>
<link rel="stylesheet" href="{{ 'theme.css' | asset_url }}">
</noscript>
```
**Remove Unused CSS:**
```bash
# Use PurgeCSS to remove unused styles
npm install @fullhuman/postcss-purgecss --save-dev
# Configure in postcss.config.js
module.exports = {
plugins: [
require('@fullhuman/postcss-purgecss')({
content: ['./**/*.liquid'],
defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || [],
}),
],
};
```
**Minify CSS:**
```bash
# Use cssnano
npm install cssnano --save-dev
# Minify
npx cssnano style.css style.min.css
```
**Avoid @import:**
```css
/* ❌ Don't use @import (blocks rendering) */
@import url('fonts.css');
/* ✅ Use multiple <link> tags instead */
```
```liquid
<link rel="stylesheet" href="{{ 'main.css' | asset_url }}">
<link rel="stylesheet" href="{{ 'fonts.css' | asset_url }}">
```
### 4. Font Optimization
Optimize web fonts for faster text rendering.
**Font Loading:**
```liquid
{# Preload fonts #}
<link
rel="preload"
href="{{ 'font.woff2' | asset_url }}"
as="font"
type="font/woff2"
crossorigin
>
{# Font face with font-display #}
<style>
@font-face {
font-family: 'CustomFont';
src: url('{{ 'font.woff2' | asset_url }}') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap; /* Show fallback font immediately */
}
</style>
```
**System Font Stack:**
```css
/* Use system fonts for instant rendering */
body {
font-family:
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
"Helvetica Neue",
Arial,
sans-serif;
}
```
**Subset Fonts:**
```css
/* Load only required characters */
@font-face {
font-family: 'CustomFont';
src: url('font-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153;
}
```
### 5. Liquid Template Optimization
Optimize Liquid rendering for faster server response.
**Cache Expensive Operations:**
```liquid
{# ❌ Repeated calculations #}
{% for i in (1..10) %}
{{ collection.products.size }} {# Calculated 10 times #}
{% endfor %}
{# ✅ Cache result #}
{% assign product_count = collection.products.size %}
{% for i in (1..10) %}
{{ product_count }}
{% endfor %}
```
**Use limit and offset:**
```liquid
{# ❌ Iterate full array and break #}
{% for product in collection.products %}
{% if forloop.index > 5 %}{% break %}{% endif %}
{{ product.title }}
{% endfor %}
{# ✅ Use limit #}
{% for product in collection.products limit: 5 %}
{{ product.title }}
{% endfor %}
```
**Avoid Nested Loops:**
```liquid
{# ❌ O(n²) complexity #}
{% for product in collection.products %}
{% for variant in product.variants %}
{# Expensive nested loop #}
{% endfor %}
{% endfor %}
{# ✅ Flatten or preprocess #}
{% assign all_variants = collection.products | map: 'variants' | flatten %}
{% for variant in all_variants limit: 50 %}
{{ variant.title }}
{% endfor %}
```
**Prefer render over include:**
```liquid
{# ❌ include (slower, shared scope) #}
{% include 'product-card' %}
{# ✅ render (faster, isolated scope) #}
{% render 'product-card', product: product %}
```
**Use section-specific stylesheets:**
```liquid
{# Scope CSS to section for better caching #}
{% stylesheet %}
.my-section { /* ... */ }
{% endstylesheet %}
{# Scope JavaScript to section #}
{% javascript %}
class MySection { /* ... */ }
{% endjavascript %}
```
### 6. Third-Party Script Optimization
Minimize impact of external scripts.
**Defer Third-Party Scripts:**
```liquid
{# ❌ Blocking third-party script #}
<script src="https://external.com/script.js"></script>
{# ✅ Async or defer #}
<script src="https://external.com/script.js" async></script>
{# ✅ Load on user interaction #}
<script>
let gaLoaded = false;
function loadGA() {
if (gaLoaded) return;
const script = document.createElement('script');
script.src = 'https://www.googletagmanager.com/gtag/js?id=GA_ID';
script.async = true;
document.head.appendChild(script);
gaLoaded = true;
}
// Load on scroll or after delay
window.addEventListener('scroll', loadGA, { once: true });
setTimeout(loadGA, 3000);
</script>
```
**Use Facade Pattern:**
```html
{# Show placeholder instead of embedding heavy iframe #}
<div class="video-facade" data-video-id="abc123">
<img src="thumbnail.jpg" alt="Video">
<button onclick="loadVideo(this)">Play Video</button>
</div>
<script>
function loadVideo(btn) {
const facade = btn.parentElement;
const videoId = facade.dataset.videoId;
const iframe = document.createElement('iframe');
iframe.src = `https://www.youtube.com/embed/${videoId}?autoplay=1`;
facade.replaceWith(iframe);
}
</script>
```
### 7. Caching Strategies
Leverage browser and CDN caching.
**Asset Versioning:**
```liquid
{# Shopify auto-versions assets #}
<link rel="stylesheet" href="{{ 'theme.css' | asset_url }}">
{# Outputs: /cdn/.../theme.css?v=12345678 #}
```
**Long Cache Headers:**
```liquid
{# Shopify CDN sets appropriate cache headers #}
{# CSS/JS: 1 year #}
{# Images: 1 year #}
```
**Service Worker (Advanced):**
```javascript
// sw.js - Cache static assets
self.addEventListener('install', event => {
event.waitUntil(
caches.open('v1').then(cache => {
return cache.addAll([
'/cdn/.../theme.css',
'/cdn/.../theme.js',
'/cdn/.../logo.png',
]);
})
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request);
})
);
});
```
### 8. Core Web Vitals Optimization
Improve Google's Core Web Vitals metrics.
**Largest Contentful Paint (LCP):**
```liquid
{# Optimize largest element load time #}
{# 1. Preload hero image #}
<link rel="preload" as="image" href="{{ hero_image | img_url: '1920x' }}">
{# 2. Use priority hint #}
<img src="{{ hero_image | img_url: '1920x' }}" fetchpriority="high">
{# 3. Optimize server response time (use Shopify CDN) #}
{# 4. Remove render-blocking resources #}
<script src="theme.js" defer></script>
```
**First Input Delay (FID) / Interaction to Next Paint (INP):**
```javascript
// 1. Reduce JavaScript execution time
// 2. Break up long tasks
function processItems(items) {
// ❌ Long task
items.forEach(item => processItem(item));
// ✅ Break into smaller chunks
async function processInChunks() {
for (let i = 0; i < items.length; i++) {
processItem(items[i]);
// Yield to main thread every 50 items
if (i % 50 === 0) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
}
processInChunks();
}
// 3. Use requestIdleCallback
requestIdleCallback(() => {
// Non-critical work
});
```
**Cumulative Layout Shift (CLS):**
```liquid
{# 1. Always set width and height on images #}
<img
src="{{ image | img_url: '800x' }}"
width="800"
height="600"
alt="Product"
>
{# 2. Reserve space for dynamic content #}
<div style="min-height: 400px;">
{# Content loads here #}
</div>
{# 3. Use aspect-ratio for responsive images #}
<style>
.image-container {
aspect-ratio: 16 / 9;
}
</style>
```
### 9. Performance Monitoring
Track performance metrics.
**Measure Core Web Vitals:**
```javascript
// Load web-vitals library
import { getCLS, getFID, getLCP } from 'web-vitals';
function sendToAnalytics({ name, value, id }) {
// Send to analytics
gtag('event', name, {
event_category: 'Web Vitals',
event_label: id,
value: Math.round(name === 'CLS' ? value * 1000 : value),
});
}
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);
```
**Performance Observer:**
```javascript
// Monitor long tasks
const observer = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
console.warn('Long task detected:', entry.duration, 'ms');
}
});
observer.observe({ entryTypes: ['longtask'] });
```
## Performance Checklist
**Images:**
- [ ] Use `img_url` filter with appropriate sizes
- [ ] Implement responsive images with `srcset`
- [ ] Add `loading="lazy"` to below-fold images
- [ ] Set explicit `width` and `height` attributes
- [ ] Preload critical hero images
- [ ] Use modern formats (WebP)
**JavaScript:**
- [ ] Defer or async all non-critical scripts
- [ ] Minify and bundle JavaScript
- [ ] Code-split large bundles
- [ ] Remove unused code
- [ ] Lazy load features on interaction
**CSS:**
- [ ] Inline critical CSS
- [ ] Defer non-critical CSS
- [ ] Remove unused styles
- [ ] Minify stylesheets
- [ ] Avoid `@import`
**Fonts:**
- [ ] Preload critical fonts
- [ ] Use `font-display: swap`
- [ ] Consider system font stack
- [ ] Subset fonts when possible
**Third-Party:**
- [ ] Audit all third-party scripts
- [ ] Load scripts async or on interaction
- [ ] Use facade pattern for heavy embeds
- [ ] Monitor third-party impact
**Liquid:**
- [ ] Cache expensive calculations
- [ ] Use `limit` instead of manual breaks
- [ ] Prefer `render` over `include`
- [ ] Avoid nested loops
**Core Web Vitals:**
- [ ] LCP < 2.5s
- [ ] FID < 100ms (INP < 200ms)
- [ ] CLS < 0.1
## Best Practices
1. **Test on real devices** - Mobile 3G performance matters
2. **Use Lighthouse** for performance audits
3. **Monitor Core Web Vitals** in production
4. **Optimize above-the-fold** content first
5. **Lazy load everything else** below the fold
6. **Minimize main thread work** for better interactivity
7. **Use Shopify CDN** for all assets
8. **Version assets** for effective caching
9. **Compress images** before uploading
10. **Regular performance audits** to catch regressions
## Integration with Other Skills
- **shopify-liquid** - Use when optimizing Liquid template code
- **shopify-theme-dev** - Use when organizing theme assets
- **shopify-debugging** - Use when troubleshooting performance issues
- **shopify-api** - Use when optimizing API request patterns
## Quick Reference
```liquid
{# Images #}
<img src="{{ image | img_url: '800x' }}" loading="lazy" width="800" height="800">
{# Scripts #}
<script src="{{ 'theme.js' | asset_url }}" defer></script>
{# Fonts #}
<link rel="preload" href="{{ 'font.woff2' | asset_url }}" as="font" crossorigin>
{# Critical CSS #}
<style>/* Critical CSS */</style>
<link rel="preload" href="{{ 'theme.css' | asset_url }}" as="style" onload="this.rel='stylesheet'">
{# Responsive images #}
<img srcset="{{ image | img_url: '400x' }} 400w, {{ image | img_url: '800x' }} 800w">
```

View File

@@ -0,0 +1,734 @@
---
name: shopify-theme-dev
description: Complete theme development guide including file structure, JSON templates, sections, snippets, settings schema, and Online Store 2.0 architecture. Use when creating Shopify themes, organizing theme files, building sections and blocks, working with .json template files, configuring settings_schema.json, creating snippets, or implementing theme customization features.
---
# Shopify Theme Development
Expert guidance for Shopify theme development including file structure, Online Store 2.0 architecture, sections, snippets, and configuration.
## When to Use This Skill
Invoke this skill when:
- Creating or modifying Shopify themes
- Working with `.json` template files (Online Store 2.0)
- Building theme sections with schema definitions
- Creating reusable snippets
- Organizing theme file structure
- Configuring `settings_schema.json` for theme settings
- Implementing section blocks and settings
- Setting up theme assets (CSS, JavaScript, images)
- Working with layout files (`theme.liquid`, `password.liquid`)
- Creating template variations with suffixes
## Core Capabilities
### 1. Theme File Structure
Complete directory organization for Shopify themes:
```
theme/
├── assets/ {# Static resources #}
│ ├── style.css {# Main stylesheet #}
│ ├── style.css.liquid {# Dynamic CSS with Liquid #}
│ ├── theme.js {# Main JavaScript #}
│ ├── theme.js.liquid {# Dynamic JS with Liquid #}
│ ├── logo.png {# Images #}
│ └── fonts/ {# Custom fonts #}
├── config/ {# Configuration #}
│ ├── settings_schema.json {# Theme settings UI #}
│ └── settings_data.json {# Default values #}
├── layout/ {# Master templates #}
│ ├── theme.liquid {# Main wrapper #}
│ ├── password.liquid {# Password protection #}
│ └── checkout.liquid {# Checkout (Plus only) #}
├── locales/ {# Translations #}
│ ├── en.default.json {# English #}
│ └── fr.json {# French #}
├── sections/ {# Reusable sections #}
│ ├── header.liquid
│ ├── hero-banner.liquid
│ ├── product-card.liquid
│ └── footer.liquid
├── snippets/ {# Reusable partials #}
│ ├── product-price.liquid
│ ├── product-rating.liquid
│ └── icon.liquid
└── templates/ {# Page templates #}
├── index.json {# Homepage (JSON) #}
├── product.json {# Product page (JSON) #}
├── collection.json {# Collection page (JSON) #}
├── product.liquid {# Product (Liquid - legacy) #}
├── cart.liquid
├── search.liquid
├── page.liquid
├── 404.liquid
└── customers/
├── account.liquid
├── login.liquid
└── register.liquid
```
### 2. JSON Templates (Online Store 2.0)
Modern template format using JSON configuration:
**templates/index.json (Homepage):**
```json
{
"sections": {
"hero": {
"type": "hero-banner",
"settings": {
"heading": "Summer Collection",
"subheading": "New arrivals",
"button_text": "Shop Now",
"button_link": "/collections/all"
}
},
"featured": {
"type": "featured-products",
"blocks": {
"block_1": {
"type": "product",
"settings": {
"product": "snowboard"
}
},
"block_2": {
"type": "product",
"settings": {
"product": "skateboard"
}
}
},
"block_order": ["block_1", "block_2"],
"settings": {
"title": "Featured Products",
"products_to_show": 4
}
}
},
"order": ["hero", "featured"]
}
```
**templates/product.json:**
```json
{
"sections": {
"main": {
"type": "main-product",
"settings": {
"show_vendor": true,
"show_quantity": true,
"enable_zoom": true
}
},
"recommendations": {
"type": "product-recommendations",
"settings": {
"heading": "You may also like",
"products_to_show": 4
}
}
},
"order": ["main", "recommendations"]
}
```
### 3. Section Architecture
Sections are reusable content blocks with schema configuration:
**sections/hero-banner.liquid:**
```liquid
<div class="hero" style="background-color: {{ section.settings.background_color }}">
{% if section.settings.image %}
<img
src="{{ section.settings.image | img_url: '1920x' }}"
alt="{{ section.settings.heading }}"
loading="lazy"
>
{% endif %}
<div class="hero__content">
{% if section.settings.heading != blank %}
<h1>{{ section.settings.heading }}</h1>
{% endif %}
{% if section.settings.subheading != blank %}
<p>{{ section.settings.subheading }}</p>
{% endif %}
{% if section.settings.button_text != blank %}
<a href="{{ section.settings.button_link }}" class="button">
{{ section.settings.button_text }}
</a>
{% endif %}
</div>
</div>
{% stylesheet %}
.hero {
position: relative;
min-height: 500px;
display: flex;
align-items: center;
justify-content: center;
padding: var(--spacing, 2rem);
}
.hero img {
position: absolute;
width: 100%;
height: 100%;
object-fit: cover;
z-index: -1;
}
.hero__content {
text-align: center;
max-width: 600px;
}
{% endstylesheet %}
{% javascript %}
console.log('Hero banner loaded');
{% endjavascript %}
{% schema %}
{
"name": "Hero Banner",
"tag": "section",
"class": "hero-section",
"settings": [
{
"type": "text",
"id": "heading",
"label": "Heading",
"default": "Welcome"
},
{
"type": "textarea",
"id": "subheading",
"label": "Subheading",
"default": "Discover our collection"
},
{
"type": "image_picker",
"id": "image",
"label": "Background Image"
},
{
"type": "color",
"id": "background_color",
"label": "Background Color",
"default": "#000000"
},
{
"type": "text",
"id": "button_text",
"label": "Button Text",
"default": "Shop Now"
},
{
"type": "url",
"id": "button_link",
"label": "Button Link"
}
],
"presets": [
{
"name": "Hero Banner"
}
]
}
{% endschema %}
```
### 4. Sections with Blocks
Sections can contain dynamic blocks for flexible layouts:
**sections/featured-products.liquid:**
```liquid
<div class="featured-products" {{ section.shopify_attributes }}>
<h2>{{ section.settings.title }}</h2>
<div class="product-grid">
{% for block in section.blocks %}
<div class="product-item" {{ block.shopify_attributes }}>
{% case block.type %}
{% when 'product' %}
{% assign product = all_products[block.settings.product] %}
{% render 'product-card', product: product %}
{% when 'collection' %}
{% assign collection = collections[block.settings.collection] %}
<h3>{{ collection.title }}</h3>
{% for product in collection.products limit: block.settings.products_to_show %}
{% render 'product-card', product: product %}
{% endfor %}
{% when 'heading' %}
<h3>{{ block.settings.heading }}</h3>
{% when 'text' %}
<div class="text-block">
{{ block.settings.text }}
</div>
{% endcase %}
</div>
{% endfor %}
</div>
</div>
{% schema %}
{
"name": "Featured Products",
"tag": "section",
"settings": [
{
"type": "text",
"id": "title",
"label": "Section Title",
"default": "Featured Products"
},
{
"type": "range",
"id": "products_per_row",
"label": "Products per Row",
"min": 2,
"max": 5,
"step": 1,
"default": 4
}
],
"blocks": [
{
"type": "product",
"name": "Product",
"settings": [
{
"type": "product",
"id": "product",
"label": "Product"
}
]
},
{
"type": "collection",
"name": "Collection",
"settings": [
{
"type": "collection",
"id": "collection",
"label": "Collection"
},
{
"type": "range",
"id": "products_to_show",
"label": "Products to Show",
"min": 1,
"max": 12,
"step": 1,
"default": 4
}
]
},
{
"type": "heading",
"name": "Heading",
"settings": [
{
"type": "text",
"id": "heading",
"label": "Heading Text"
}
]
},
{
"type": "text",
"name": "Text Block",
"settings": [
{
"type": "richtext",
"id": "text",
"label": "Text Content"
}
]
}
],
"presets": [
{
"name": "Featured Products",
"blocks": [
{
"type": "product"
},
{
"type": "product"
},
{
"type": "product"
}
]
}
],
"max_blocks": 12
}
{% endschema %}
```
### 5. Snippets
Reusable template partials:
**snippets/product-card.liquid:**
```liquid
{% comment %}
Usage: {% render 'product-card', product: product, show_vendor: true %}
{% endcomment %}
<div class="product-card">
<a href="{{ product.url }}">
{% if product.featured_image %}
<img
src="{{ product.featured_image | img_url: '400x400' }}"
alt="{{ product.featured_image.alt | escape }}"
loading="lazy"
>
{% else %}
{{ 'product-1' | placeholder_svg_tag: 'placeholder' }}
{% endif %}
</a>
<div class="product-card__info">
{% if show_vendor and product.vendor != blank %}
<p class="product-card__vendor">{{ product.vendor }}</p>
{% endif %}
<h3 class="product-card__title">
<a href="{{ product.url }}">{{ product.title }}</a>
</h3>
<div class="product-card__price">
{% render 'product-price', product: product %}
</div>
{% unless product.available %}
<p class="sold-out">Sold Out</p>
{% endunless %}
</div>
</div>
```
**snippets/product-price.liquid:**
```liquid
{% comment %}
Usage: {% render 'product-price', product: product %}
{% endcomment %}
{% if product.compare_at_price > product.price %}
<span class="price price--sale">
{{ product.price | money }}
</span>
<span class="price price--compare">
{{ product.compare_at_price | money }}
</span>
<span class="price__badge">
Save {{ product.compare_at_price | minus: product.price | money }}
</span>
{% else %}
<span class="price">
{{ product.price | money }}
</span>
{% endif %}
{% if product.price_varies %}
<span class="price__from">from</span>
{% endif %}
```
### 6. Settings Schema
Complete theme customization interface:
**config/settings_schema.json:**
```json
[
{
"name": "theme_info",
"theme_name": "My Theme",
"theme_version": "1.0.0",
"theme_author": "Your Name",
"theme_documentation_url": "https://...",
"theme_support_url": "https://..."
},
{
"name": "Colors",
"settings": [
{
"type": "header",
"content": "Color Scheme"
},
{
"type": "color",
"id": "color_primary",
"label": "Primary Color",
"default": "#000000"
},
{
"type": "color",
"id": "color_secondary",
"label": "Secondary Color",
"default": "#ffffff"
},
{
"type": "color_background",
"id": "color_body_bg",
"label": "Body Background"
}
]
},
{
"name": "Typography",
"settings": [
{
"type": "font_picker",
"id": "type_header_font",
"label": "Heading Font",
"default": "helvetica_n7"
},
{
"type": "font_picker",
"id": "type_body_font",
"label": "Body Font",
"default": "helvetica_n4"
},
{
"type": "range",
"id": "type_base_size",
"label": "Base Font Size",
"min": 12,
"max": 24,
"step": 1,
"default": 16,
"unit": "px"
}
]
},
{
"name": "Layout",
"settings": [
{
"type": "select",
"id": "layout_style",
"label": "Layout Style",
"options": [
{ "value": "boxed", "label": "Boxed" },
{ "value": "full-width", "label": "Full Width" },
{ "value": "wide", "label": "Wide" }
],
"default": "full-width"
},
{
"type": "checkbox",
"id": "layout_sidebar_enabled",
"label": "Enable Sidebar",
"default": true
}
]
},
{
"name": "Header",
"settings": [
{
"type": "image_picker",
"id": "logo",
"label": "Logo"
},
{
"type": "range",
"id": "logo_max_width",
"label": "Logo Width",
"min": 50,
"max": 300,
"step": 10,
"default": 150,
"unit": "px"
},
{
"type": "link_list",
"id": "main_menu",
"label": "Main Menu"
},
{
"type": "checkbox",
"id": "header_sticky",
"label": "Sticky Header",
"default": false
}
]
},
{
"name": "Social Media",
"settings": [
{
"type": "header",
"content": "Social Accounts"
},
{
"type": "url",
"id": "social_twitter",
"label": "Twitter URL",
"info": "https://twitter.com/username"
},
{
"type": "url",
"id": "social_facebook",
"label": "Facebook URL"
},
{
"type": "url",
"id": "social_instagram",
"label": "Instagram URL"
}
]
}
]
```
### 7. Layout Files
Master template wrappers:
**layout/theme.liquid:**
```liquid
<!doctype html>
<html lang="{{ request.locale.iso_code }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>
{{ page_title }}
{%- if current_tags %} &ndash; {{ 'general.meta.tags' | t: tags: current_tags.join(', ') }}{% endif -%}
{%- if current_page != 1 %} &ndash; {{ 'general.meta.page' | t: page: current_page }}{% endif -%}
{%- unless page_title contains shop.name %} &ndash; {{ shop.name }}{% endunless -%}
</title>
{{ content_for_header }}
<link rel="stylesheet" href="{{ 'style.css' | asset_url }}">
<script src="{{ 'theme.js' | asset_url }}" defer></script>
</head>
<body class="template-{{ request.page_type }}">
{% section 'header' %}
<main role="main">
{{ content_for_layout }}
</main>
{% section 'footer' %}
</body>
</html>
```
## Common Settings Input Types
All 28+ input types for theme customization:
- `text` - Single line text
- `textarea` - Multi-line text
- `html` - HTML editor
- `richtext` - WYSIWYG editor
- `number` - Numeric input
- `range` - Slider
- `checkbox` - Boolean toggle
- `select` - Dropdown menu
- `radio` - Radio buttons
- `color` - Color picker
- `color_background` - Color with gradient
- `image_picker` - Upload image
- `media` - Image or video
- `url` - URL input
- `font_picker` - Font selector
- `product` - Product picker
- `collection` - Collection picker
- `page` - Page picker
- `blog` - Blog picker
- `article` - Article picker
- `link_list` - Menu picker
- `date` - Date picker
- `video_url` - Video URL (YouTube, Vimeo)
See [references/settings-schema.md](references/settings-schema.md) for complete examples.
## Best Practices
1. **Use JSON templates** for Online Store 2.0 compatibility
2. **Make sections dynamic** with blocks for merchant flexibility
3. **Add `shopify_attributes`** to section/block containers for theme editor
4. **Provide sensible defaults** in schema settings
5. **Use snippets** for repeated UI components
6. **Add `{% stylesheet %}` and `{% javascript %}`** blocks in sections for scoped styles
7. **Include accessibility** attributes (ARIA labels, alt text)
8. **Test in theme editor** to ensure live preview works
9. **Document snippet parameters** with comments
10. **Use semantic HTML** for better SEO
## Detailed References
- **[references/settings-schema.md](references/settings-schema.md)** - Complete input type reference
- **[references/section-patterns.md](references/section-patterns.md)** - Common section architectures
- **[references/template-examples.md](references/template-examples.md)** - JSON template patterns
## Integration with Other Skills
- **shopify-liquid** - Use when working with Liquid code within theme files
- **shopify-performance** - Use when optimizing theme load times and asset delivery
- **shopify-api** - Use when fetching data via Ajax for dynamic sections
- **shopify-debugging** - Use when troubleshooting theme editor or rendering issues
## Quick Reference
```liquid
{# Section with settings #}
{% schema %}
{
"name": "Section Name",
"settings": [...],
"blocks": [...],
"presets": [...]
}
{% endschema %}
{# Access section settings #}
{{ section.settings.setting_id }}
{# Loop through blocks #}
{% for block in section.blocks %}
{{ block.settings.text }}
{{ block.shopify_attributes }}
{% endfor %}
{# Render snippet with parameters #}
{% render 'snippet-name', param: value %}
{# Access theme settings #}
{{ settings.color_primary }}
{# Section attributes for theme editor #}
<div {{ section.shopify_attributes }}>...</div>
<div {{ block.shopify_attributes }}>...</div>
```

View File

@@ -0,0 +1,753 @@
# Settings Schema - Complete Input Type Reference
All 28+ input types for `settings_schema.json` with examples.
## Text Inputs
### text
Single-line text input:
```json
{
"type": "text",
"id": "store_name",
"label": "Store Name",
"default": "My Store",
"placeholder": "Enter store name",
"info": "This appears in the header"
}
```
### textarea
Multi-line text input:
```json
{
"type": "textarea",
"id": "footer_text",
"label": "Footer Text",
"default": "© 2025 My Store. All rights reserved.",
"placeholder": "Enter footer text"
}
```
### html
HTML code editor:
```json
{
"type": "html",
"id": "custom_html",
"label": "Custom HTML",
"default": "<p>Welcome to our store!</p>",
"info": "Add custom HTML code"
}
```
### richtext
WYSIWYG rich text editor:
```json
{
"type": "richtext",
"id": "announcement_content",
"label": "Announcement Bar Content",
"default": "<p>Free shipping on orders over $50!</p>"
}
```
## Numeric Inputs
### number
Numeric input field:
```json
{
"type": "number",
"id": "products_per_page",
"label": "Products Per Page",
"default": 12,
"min": 1,
"max": 100,
"step": 1,
"info": "Number of products to show per page"
}
```
### range
Slider input:
```json
{
"type": "range",
"id": "columns",
"label": "Number of Columns",
"min": 2,
"max": 6,
"step": 1,
"default": 4,
"unit": "columns",
"info": "Adjust the grid layout"
}
```
**Common units:**
- `px` - Pixels
- `%` - Percentage
- `em` - Em units
- `rem` - Root em units
- Custom text (like "columns", "items")
## Boolean Inputs
### checkbox
Toggle checkbox:
```json
{
"type": "checkbox",
"id": "show_search",
"label": "Show Search Bar",
"default": true,
"info": "Display search in header"
}
```
### boolean
Boolean setting (same as checkbox):
```json
{
"type": "boolean",
"id": "enable_feature",
"label": "Enable Feature",
"default": false
}
```
## Selection Inputs
### select
Dropdown menu:
```json
{
"type": "select",
"id": "layout_style",
"label": "Layout Style",
"options": [
{
"value": "boxed",
"label": "Boxed"
},
{
"value": "full-width",
"label": "Full Width"
},
{
"value": "wide",
"label": "Wide"
}
],
"default": "full-width",
"info": "Choose your layout style"
}
```
### radio
Radio button selection:
```json
{
"type": "radio",
"id": "text_alignment",
"label": "Text Alignment",
"options": [
{ "value": "left", "label": "Left" },
{ "value": "center", "label": "Center" },
{ "value": "right", "label": "Right" }
],
"default": "center"
}
```
## Color Inputs
### color
Color picker:
```json
{
"type": "color",
"id": "primary_color",
"label": "Primary Color",
"default": "#000000",
"info": "Main brand color"
}
```
### color_background
Color with gradient support:
```json
{
"type": "color_background",
"id": "section_background",
"label": "Section Background",
"default": "linear-gradient(#ffffff, #000000)"
}
```
**Supports:**
- Solid colors: `#ffffff`
- Linear gradients: `linear-gradient(#fff, #000)`
- Radial gradients
- With opacity
## Media Inputs
### image_picker
Image upload and selection:
```json
{
"type": "image_picker",
"id": "logo",
"label": "Logo Image",
"info": "Recommended size: 300x100px"
}
```
Access in Liquid:
```liquid
{% if settings.logo %}
<img src="{{ settings.logo | img_url: '300x' }}" alt="{{ shop.name }}">
{% endif %}
{{ settings.logo.width }}
{{ settings.logo.height }}
{{ settings.logo.alt }}
{{ settings.logo.src }}
```
### media
Image or video picker:
```json
{
"type": "media",
"id": "hero_media",
"label": "Hero Media",
"accept": ["image", "video"],
"info": "Upload image or video"
}
```
### video_url
Video URL input (YouTube, Vimeo):
```json
{
"type": "video_url",
"id": "promo_video",
"label": "Promo Video",
"accept": ["youtube", "vimeo"],
"placeholder": "https://www.youtube.com/watch?v=...",
"info": "YouTube or Vimeo URL"
}
```
Access in Liquid:
```liquid
{% if settings.promo_video %}
{{ settings.promo_video.type }} {# youtube or vimeo #}
{{ settings.promo_video.id }} {# Video ID #}
{% endif %}
```
## Typography Inputs
### font_picker
Google Fonts selector:
```json
{
"type": "font_picker",
"id": "heading_font",
"label": "Heading Font",
"default": "helvetica_n7",
"info": "Font for headings"
}
```
**Font format:** `family_weight`
- `n4` - Normal 400
- `n7` - Bold 700
- `i4` - Italic 400
Access in Liquid:
```liquid
{{ settings.heading_font.family }}
{{ settings.heading_font.weight }}
{{ settings.heading_font.style }}
{# CSS font face #}
<style>
{{ settings.heading_font | font_face }}
h1, h2, h3 {
font-family: {{ settings.heading_font.family }}, {{ settings.heading_font.fallback_families }};
font-weight: {{ settings.heading_font.weight }};
font-style: {{ settings.heading_font.style }};
}
</style>
```
## Resource Pickers
### product
Product selector:
```json
{
"type": "product",
"id": "featured_product",
"label": "Featured Product",
"info": "Select a product to feature"
}
```
Access in Liquid:
```liquid
{% assign product = all_products[settings.featured_product] %}
{{ product.title }}
{{ product.price | money }}
```
### collection
Collection selector:
```json
{
"type": "collection",
"id": "featured_collection",
"label": "Featured Collection",
"info": "Select a collection to feature"
}
```
Access in Liquid:
```liquid
{% assign collection = collections[settings.featured_collection] %}
{{ collection.title }}
{% for product in collection.products limit: 4 %}
{{ product.title }}
{% endfor %}
```
### page
Page selector:
```json
{
"type": "page",
"id": "about_page",
"label": "About Page",
"info": "Link to about page"
}
```
Access in Liquid:
```liquid
{% assign page = pages[settings.about_page] %}
<a href="{{ page.url }}">{{ page.title }}</a>
{{ page.content }}
```
### blog
Blog selector:
```json
{
"type": "blog",
"id": "main_blog",
"label": "Main Blog",
"info": "Select your primary blog"
}
```
Access in Liquid:
```liquid
{% assign blog = blogs[settings.main_blog] %}
{{ blog.title }}
{% for article in blog.articles limit: 3 %}
{{ article.title }}
{% endfor %}
```
### article
Article (blog post) selector:
```json
{
"type": "article",
"id": "featured_article",
"label": "Featured Article",
"info": "Select an article to feature"
}
```
### link_list
Menu/navigation selector:
```json
{
"type": "link_list",
"id": "main_menu",
"label": "Main Navigation",
"default": "main-menu",
"info": "Select menu for header"
}
```
Access in Liquid:
```liquid
{% assign menu = linklists[settings.main_menu] %}
{% for link in menu.links %}
<a href="{{ link.url }}">{{ link.title }}</a>
{% if link.links.size > 0 %}
{# Nested links #}
{% for child_link in link.links %}
<a href="{{ child_link.url }}">{{ child_link.title }}</a>
{% endfor %}
{% endif %}
{% endfor %}
```
## URL Inputs
### url
URL input field:
```json
{
"type": "url",
"id": "twitter_url",
"label": "Twitter URL",
"placeholder": "https://twitter.com/username",
"info": "Your Twitter profile URL"
}
```
## Date & Time Inputs
### date
Date picker:
```json
{
"type": "date",
"id": "sale_end_date",
"label": "Sale End Date",
"info": "When the sale ends"
}
```
Access in Liquid:
```liquid
{{ settings.sale_end_date | date: '%B %d, %Y' }}
```
## Organization Elements
### header
Visual separator with heading:
```json
{
"type": "header",
"content": "Color Scheme Settings",
"info": "Configure your color palette"
}
```
Not a setting, just a visual divider in the settings panel.
### paragraph
Informational text block:
```json
{
"type": "paragraph",
"content": "These settings control the appearance of your product cards. Make sure to preview changes on different screen sizes."
}
```
## Advanced Inputs
### liquid
Liquid code editor:
```json
{
"type": "liquid",
"id": "custom_liquid",
"label": "Custom Liquid Code",
"info": "Add custom Liquid code"
}
```
### inline_richtext
Inline rich text (no `<p>` wrapper):
```json
{
"type": "inline_richtext",
"id": "banner_text",
"label": "Banner Text",
"default": "Welcome to <strong>our store</strong>!",
"info": "Text without paragraph wrapper"
}
```
## Complete Example
Full settings schema with multiple sections:
```json
[
{
"name": "theme_info",
"theme_name": "Professional Theme",
"theme_version": "1.0.0",
"theme_author": "Your Name",
"theme_documentation_url": "https://docs.example.com",
"theme_support_url": "https://support.example.com"
},
{
"name": "Colors",
"settings": [
{
"type": "header",
"content": "Brand Colors"
},
{
"type": "color",
"id": "color_primary",
"label": "Primary Brand Color",
"default": "#2196F3"
},
{
"type": "color",
"id": "color_secondary",
"label": "Secondary Color",
"default": "#FFC107"
},
{
"type": "header",
"content": "Background Colors"
},
{
"type": "color_background",
"id": "color_body_bg",
"label": "Body Background",
"default": "#FFFFFF"
},
{
"type": "color_background",
"id": "color_header_bg",
"label": "Header Background",
"default": "linear-gradient(#FFFFFF, #F5F5F5)"
}
]
},
{
"name": "Typography",
"settings": [
{
"type": "paragraph",
"content": "Select fonts for your store. Changes preview in real-time."
},
{
"type": "font_picker",
"id": "type_header_font",
"label": "Heading Font",
"default": "helvetica_n7",
"info": "Font for all headings"
},
{
"type": "font_picker",
"id": "type_body_font",
"label": "Body Font",
"default": "helvetica_n4",
"info": "Font for body text"
},
{
"type": "range",
"id": "type_base_size",
"label": "Base Font Size",
"min": 12,
"max": 24,
"step": 1,
"default": 16,
"unit": "px"
}
]
},
{
"name": "Header",
"settings": [
{
"type": "image_picker",
"id": "logo",
"label": "Logo Image"
},
{
"type": "range",
"id": "logo_width",
"label": "Logo Width",
"min": 50,
"max": 300,
"step": 10,
"default": 150,
"unit": "px"
},
{
"type": "link_list",
"id": "main_menu",
"label": "Main Menu",
"default": "main-menu"
},
{
"type": "checkbox",
"id": "header_sticky",
"label": "Sticky Header",
"default": true,
"info": "Header stays visible while scrolling"
},
{
"type": "select",
"id": "header_style",
"label": "Header Style",
"options": [
{ "value": "minimal", "label": "Minimal" },
{ "value": "classic", "label": "Classic" },
{ "value": "centered", "label": "Centered" }
],
"default": "classic"
}
]
},
{
"name": "Product Pages",
"settings": [
{
"type": "checkbox",
"id": "product_show_vendor",
"label": "Show Vendor",
"default": true
},
{
"type": "checkbox",
"id": "product_show_sku",
"label": "Show SKU",
"default": false
},
{
"type": "select",
"id": "product_image_size",
"label": "Image Size",
"options": [
{ "value": "small", "label": "Small" },
{ "value": "medium", "label": "Medium" },
{ "value": "large", "label": "Large" }
],
"default": "medium"
},
{
"type": "product",
"id": "related_product",
"label": "Related Product"
}
]
},
{
"name": "Social Media",
"settings": [
{
"type": "header",
"content": "Social Media Accounts"
},
{
"type": "url",
"id": "social_twitter",
"label": "Twitter",
"placeholder": "https://twitter.com/username"
},
{
"type": "url",
"id": "social_facebook",
"label": "Facebook",
"placeholder": "https://facebook.com/username"
},
{
"type": "url",
"id": "social_instagram",
"label": "Instagram",
"placeholder": "https://instagram.com/username"
}
]
}
]
```
## Best Practices
1. **Group related settings** into logical sections
2. **Provide clear labels and info text** for guidance
3. **Set sensible defaults** for all settings
4. **Use appropriate input types** for each setting
5. **Add placeholder text** for URL and text inputs
6. **Use headers and paragraphs** to organize complex sections
7. **Limit range values** to reasonable min/max
8. **Test in theme customizer** to ensure good UX
9. **Document dependencies** between settings
10. **Consider mobile experience** when choosing input types