Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal 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
3
README.md
Normal 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
81
plugin.lock.json
Normal 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
837
skills/shopify-api/SKILL.md
Normal 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: ... })
|
||||
```
|
||||
743
skills/shopify-app-dev/SKILL.md
Normal file
743
skills/shopify-app-dev/SKILL.md
Normal 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
|
||||
```
|
||||
705
skills/shopify-debugging/SKILL.md
Normal file
705
skills/shopify-debugging/SKILL.md
Normal 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);
|
||||
```
|
||||
345
skills/shopify-liquid/SKILL.md
Normal file
345
skills/shopify-liquid/SKILL.md
Normal 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 #}
|
||||
```
|
||||
873
skills/shopify-liquid/references/filters.md
Normal file
873
skills/shopify-liquid/references/filters.md
Normal 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: <div>Test</div> #}
|
||||
```
|
||||
|
||||
### escape_once
|
||||
|
||||
Escape HTML but don't double-escape:
|
||||
|
||||
```liquid
|
||||
{{ "<div>" | escape_once }}
|
||||
{# Output: <div> (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 %}
|
||||
```
|
||||
695
skills/shopify-liquid/references/objects.md
Normal file
695
skills/shopify-liquid/references/objects.md
Normal 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 %}
|
||||
```
|
||||
551
skills/shopify-liquid/references/syntax.md
Normal file
551
skills/shopify-liquid/references/syntax.md
Normal 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
|
||||
655
skills/shopify-performance/SKILL.md
Normal file
655
skills/shopify-performance/SKILL.md
Normal 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">
|
||||
```
|
||||
734
skills/shopify-theme-dev/SKILL.md
Normal file
734
skills/shopify-theme-dev/SKILL.md
Normal 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 %} – {{ 'general.meta.tags' | t: tags: current_tags.join(', ') }}{% endif -%}
|
||||
{%- if current_page != 1 %} – {{ 'general.meta.page' | t: page: current_page }}{% endif -%}
|
||||
{%- unless page_title contains shop.name %} – {{ 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>
|
||||
```
|
||||
753
skills/shopify-theme-dev/references/settings-schema.md
Normal file
753
skills/shopify-theme-dev/references/settings-schema.md
Normal 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
|
||||
Reference in New Issue
Block a user