@veyralabs/skills 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -101
- package/bin/cli.js +20 -1
- package/commands/brandaudit.md +3 -0
- package/commands/competitornames.md +3 -0
- package/commands/domainforge.md +3 -0
- package/commands/namingguide.md +3 -0
- package/commands/shopify-dev.md +3 -0
- package/commands/shopify-store.md +3 -0
- package/commands/webcloner.md +5 -0
- package/install.sh +24 -1
- package/package.json +8 -2
- package/skills/shopify-suite/shopify-dev/SKILL.md +409 -0
- package/skills/shopify-suite/shopify-dev/references/app-architecture.md +322 -0
- package/skills/shopify-suite/shopify-dev/references/cli-workflows.md +257 -0
- package/skills/shopify-suite/shopify-dev/references/graphql-queries.md +298 -0
- package/skills/shopify-suite/shopify-dev/references/liquid-patterns.md +286 -0
- package/skills/shopify-suite/shopify-store/SKILL.md +283 -0
- package/skills/shopify-suite/shopify-store/references/app-stack.md +175 -0
- package/skills/shopify-suite/shopify-store/references/audit-framework.md +206 -0
- package/skills/shopify-suite/shopify-store/references/mcp-queries.md +216 -0
- package/skills/shopify-suite/shopify-store/references/product-optimization.md +266 -0
- package/skills/shopify-suite/shopify-store/references/seo-shopify.md +165 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# GraphQL Queries — shopify-dev Reference
|
|
2
|
+
|
|
3
|
+
Ready-to-use queries for Storefront API and Admin API.
|
|
4
|
+
Check API version in your app config before using — Shopify deprecates quarterly.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Storefront API
|
|
9
|
+
|
|
10
|
+
Use for: public-facing data, headless storefronts, Hydrogen.
|
|
11
|
+
Auth: public Storefront API token (safe for frontend).
|
|
12
|
+
|
|
13
|
+
### Products
|
|
14
|
+
|
|
15
|
+
```graphql
|
|
16
|
+
query GetProduct($handle: String!) {
|
|
17
|
+
product(handle: $handle) {
|
|
18
|
+
id
|
|
19
|
+
title
|
|
20
|
+
handle
|
|
21
|
+
description
|
|
22
|
+
descriptionHtml
|
|
23
|
+
vendor
|
|
24
|
+
productType
|
|
25
|
+
tags
|
|
26
|
+
priceRange {
|
|
27
|
+
minVariantPrice { amount currencyCode }
|
|
28
|
+
maxVariantPrice { amount currencyCode }
|
|
29
|
+
}
|
|
30
|
+
compareAtPriceRange {
|
|
31
|
+
minVariantPrice { amount currencyCode }
|
|
32
|
+
}
|
|
33
|
+
featuredImage { url altText width height }
|
|
34
|
+
images(first: 20) {
|
|
35
|
+
nodes { id url altText width height }
|
|
36
|
+
}
|
|
37
|
+
variants(first: 100) {
|
|
38
|
+
nodes {
|
|
39
|
+
id
|
|
40
|
+
title
|
|
41
|
+
availableForSale
|
|
42
|
+
quantityAvailable
|
|
43
|
+
price { amount currencyCode }
|
|
44
|
+
compareAtPrice { amount currencyCode }
|
|
45
|
+
selectedOptions { name value }
|
|
46
|
+
image { url altText }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
seo { title description }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Collections
|
|
55
|
+
|
|
56
|
+
```graphql
|
|
57
|
+
query GetCollection($handle: String!, $cursor: String) {
|
|
58
|
+
collection(handle: $handle) {
|
|
59
|
+
id
|
|
60
|
+
title
|
|
61
|
+
handle
|
|
62
|
+
description
|
|
63
|
+
image { url altText }
|
|
64
|
+
products(first: 24, after: $cursor, sortKey: BEST_SELLING) {
|
|
65
|
+
pageInfo { hasNextPage endCursor }
|
|
66
|
+
nodes {
|
|
67
|
+
id
|
|
68
|
+
title
|
|
69
|
+
handle
|
|
70
|
+
featuredImage { url altText }
|
|
71
|
+
priceRange {
|
|
72
|
+
minVariantPrice { amount currencyCode }
|
|
73
|
+
}
|
|
74
|
+
availableForSale
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Cart
|
|
82
|
+
|
|
83
|
+
```graphql
|
|
84
|
+
mutation CartCreate($lines: [CartLineInput!], $buyerIdentity: CartBuyerIdentityInput) {
|
|
85
|
+
cartCreate(input: { lines: $lines, buyerIdentity: $buyerIdentity }) {
|
|
86
|
+
cart {
|
|
87
|
+
id
|
|
88
|
+
checkoutUrl
|
|
89
|
+
totalQuantity
|
|
90
|
+
cost {
|
|
91
|
+
totalAmount { amount currencyCode }
|
|
92
|
+
subtotalAmount { amount currencyCode }
|
|
93
|
+
}
|
|
94
|
+
lines(first: 100) {
|
|
95
|
+
nodes {
|
|
96
|
+
id
|
|
97
|
+
quantity
|
|
98
|
+
merchandise {
|
|
99
|
+
... on ProductVariant {
|
|
100
|
+
id
|
|
101
|
+
title
|
|
102
|
+
price { amount currencyCode }
|
|
103
|
+
product { title handle featuredImage { url } }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
userErrors { field message }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```graphql
|
|
115
|
+
mutation CartLinesAdd($cartId: ID!, $lines: [CartLineInput!]!) {
|
|
116
|
+
cartLinesAdd(cartId: $cartId, lines: $lines) {
|
|
117
|
+
cart { id totalQuantity cost { totalAmount { amount currencyCode } } }
|
|
118
|
+
userErrors { field message }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
```graphql
|
|
124
|
+
mutation CartLinesUpdate($cartId: ID!, $lines: [CartLineUpdateInput!]!) {
|
|
125
|
+
cartLinesUpdate(cartId: $cartId, lines: $lines) {
|
|
126
|
+
cart { id totalQuantity }
|
|
127
|
+
userErrors { field message }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Search (Predictive)
|
|
133
|
+
|
|
134
|
+
```graphql
|
|
135
|
+
query PredictiveSearch($query: String!) {
|
|
136
|
+
predictiveSearch(query: $query, limit: 5) {
|
|
137
|
+
products {
|
|
138
|
+
id
|
|
139
|
+
title
|
|
140
|
+
handle
|
|
141
|
+
featuredImage { url altText }
|
|
142
|
+
priceRange { minVariantPrice { amount currencyCode } }
|
|
143
|
+
}
|
|
144
|
+
collections { id title handle }
|
|
145
|
+
pages { id title handle }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Customer
|
|
151
|
+
|
|
152
|
+
```graphql
|
|
153
|
+
query GetCustomer($customerAccessToken: String!) {
|
|
154
|
+
customer(customerAccessToken: $customerAccessToken) {
|
|
155
|
+
id
|
|
156
|
+
firstName
|
|
157
|
+
lastName
|
|
158
|
+
email
|
|
159
|
+
orders(first: 10, sortKey: PROCESSED_AT, reverse: true) {
|
|
160
|
+
nodes {
|
|
161
|
+
id
|
|
162
|
+
orderNumber
|
|
163
|
+
processedAt
|
|
164
|
+
financialStatus
|
|
165
|
+
fulfillmentStatus
|
|
166
|
+
totalPrice { amount currencyCode }
|
|
167
|
+
lineItems(first: 5) {
|
|
168
|
+
nodes {
|
|
169
|
+
title
|
|
170
|
+
quantity
|
|
171
|
+
variant { image { url } price { amount currencyCode } }
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Admin API
|
|
183
|
+
|
|
184
|
+
Use for: app backends, webhooks, bulk operations.
|
|
185
|
+
Auth: Admin API access token (server-side only, never expose to client).
|
|
186
|
+
|
|
187
|
+
### Product Create
|
|
188
|
+
|
|
189
|
+
```graphql
|
|
190
|
+
mutation ProductCreate($input: ProductInput!) {
|
|
191
|
+
productCreate(input: $input) {
|
|
192
|
+
product {
|
|
193
|
+
id
|
|
194
|
+
title
|
|
195
|
+
handle
|
|
196
|
+
status
|
|
197
|
+
}
|
|
198
|
+
userErrors { field message }
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Variables:
|
|
204
|
+
```json
|
|
205
|
+
{
|
|
206
|
+
"input": {
|
|
207
|
+
"title": "Product Title",
|
|
208
|
+
"descriptionHtml": "<p>Description</p>",
|
|
209
|
+
"vendor": "Brand Name",
|
|
210
|
+
"productType": "Category",
|
|
211
|
+
"status": "DRAFT",
|
|
212
|
+
"variants": [
|
|
213
|
+
{
|
|
214
|
+
"price": "29.99",
|
|
215
|
+
"sku": "SKU-001",
|
|
216
|
+
"inventoryQuantities": [{
|
|
217
|
+
"availableQuantity": 100,
|
|
218
|
+
"locationId": "gid://shopify/Location/YOUR_LOCATION_ID"
|
|
219
|
+
}]
|
|
220
|
+
}
|
|
221
|
+
]
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Metafields Set (bulk)
|
|
227
|
+
|
|
228
|
+
```graphql
|
|
229
|
+
mutation MetafieldsSet($metafields: [MetafieldsSetInput!]!) {
|
|
230
|
+
metafieldsSet(metafields: $metafields) {
|
|
231
|
+
metafields { key value namespace }
|
|
232
|
+
userErrors { field message }
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Bulk Operations (for large datasets)
|
|
238
|
+
|
|
239
|
+
```graphql
|
|
240
|
+
mutation BulkOperationRunQuery($query: String!) {
|
|
241
|
+
bulkOperationRunQuery(query: $query) {
|
|
242
|
+
bulkOperation { id status }
|
|
243
|
+
userErrors { field message }
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Poll for completion:
|
|
249
|
+
```graphql
|
|
250
|
+
query BulkOperationStatus {
|
|
251
|
+
currentBulkOperation {
|
|
252
|
+
id
|
|
253
|
+
status
|
|
254
|
+
errorCode
|
|
255
|
+
objectCount
|
|
256
|
+
fileSize
|
|
257
|
+
url
|
|
258
|
+
partialDataUrl
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Common Patterns
|
|
266
|
+
|
|
267
|
+
### Cursor-based pagination
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
async function fetchAllProducts(storefront) {
|
|
271
|
+
let allProducts = [];
|
|
272
|
+
let cursor = null;
|
|
273
|
+
let hasNextPage = true;
|
|
274
|
+
|
|
275
|
+
while (hasNextPage) {
|
|
276
|
+
const { products } = await storefront.query(PRODUCTS_QUERY, {
|
|
277
|
+
variables: { cursor },
|
|
278
|
+
});
|
|
279
|
+
allProducts.push(...products.nodes);
|
|
280
|
+
hasNextPage = products.pageInfo.hasNextPage;
|
|
281
|
+
cursor = products.pageInfo.endCursor;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return allProducts;
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Error handling for mutations
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
const { data } = await admin.graphql(MUTATION, { variables });
|
|
292
|
+
|
|
293
|
+
if (data.productCreate.userErrors.length > 0) {
|
|
294
|
+
throw new Error(
|
|
295
|
+
data.productCreate.userErrors.map(e => `${e.field}: ${e.message}`).join(', ')
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
```
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# Liquid Patterns — shopify-dev Reference
|
|
2
|
+
|
|
3
|
+
Common Liquid patterns, filters, and gotchas for Shopify theme development.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Objects
|
|
8
|
+
|
|
9
|
+
### Global objects (available everywhere)
|
|
10
|
+
|
|
11
|
+
| Object | What it is |
|
|
12
|
+
|--------|-----------|
|
|
13
|
+
| `shop` | Store settings, name, currency, policies |
|
|
14
|
+
| `cart` | Current cart (items, total, count) |
|
|
15
|
+
| `customer` | Logged-in customer (nil if guest) |
|
|
16
|
+
| `request` | Current request info (page_type, host) |
|
|
17
|
+
| `settings` | Theme settings from settings_data.json |
|
|
18
|
+
| `content_for_header` | Required in `<head>` — Shopify scripts injection |
|
|
19
|
+
| `content_for_layout` | Required in layout — renders template content |
|
|
20
|
+
|
|
21
|
+
### Template-specific objects
|
|
22
|
+
|
|
23
|
+
| Template | Objects available |
|
|
24
|
+
|----------|------------------|
|
|
25
|
+
| product | `product`, `current_variant` |
|
|
26
|
+
| collection | `collection`, `current_tags`, `current_vendor` |
|
|
27
|
+
| article | `article`, `blog` |
|
|
28
|
+
| page | `page` |
|
|
29
|
+
| cart | `cart` |
|
|
30
|
+
| order (customer) | `order` |
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Filters
|
|
35
|
+
|
|
36
|
+
### String filters
|
|
37
|
+
|
|
38
|
+
```liquid
|
|
39
|
+
{{ product.title | upcase }}
|
|
40
|
+
{{ product.title | downcase }}
|
|
41
|
+
{{ product.title | capitalize }}
|
|
42
|
+
{{ product.description | strip_html }}
|
|
43
|
+
{{ product.description | truncate: 150 }}
|
|
44
|
+
{{ product.description | truncate_words: 30 }}
|
|
45
|
+
{{ " padded " | strip }}
|
|
46
|
+
{{ product.handle | replace: "-", " " }}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Number and money filters
|
|
50
|
+
|
|
51
|
+
```liquid
|
|
52
|
+
{{ product.price | money }} → "€29,99"
|
|
53
|
+
{{ product.price | money_without_currency }} → "29,99"
|
|
54
|
+
{{ product.price | money_with_currency }} → "29,99 EUR"
|
|
55
|
+
{{ product.price | divided_by: 100.0 }} → raw float
|
|
56
|
+
{{ 1.5 | round }} → 2
|
|
57
|
+
{{ 1.5 | floor }} → 1
|
|
58
|
+
{{ 1.5 | ceil }} → 2
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Note: Shopify stores prices in cents. `product.price` = 2999 = €29.99. Always use `| money` for display.
|
|
62
|
+
|
|
63
|
+
### URL filters
|
|
64
|
+
|
|
65
|
+
```liquid
|
|
66
|
+
{{ product.featured_image | img_url: '800x' }}
|
|
67
|
+
{{ product.featured_image | img_url: '800x600', crop: 'center' }}
|
|
68
|
+
{{ product.featured_image | image_url: width: 800 }} ← new Liquid syntax
|
|
69
|
+
{{ 'theme.css' | asset_url }}
|
|
70
|
+
{{ 'custom.js' | asset_url | script_tag }}
|
|
71
|
+
{{ product.url | within: collection }} ← collection-scoped URL
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Array filters
|
|
75
|
+
|
|
76
|
+
```liquid
|
|
77
|
+
{{ product.tags | join: ", " }}
|
|
78
|
+
{{ collection.products | size }}
|
|
79
|
+
{{ product.images | first }}
|
|
80
|
+
{{ product.images | last }}
|
|
81
|
+
{{ product.variants | map: "title" | join: ", " }}
|
|
82
|
+
{{ collection.products | where: "available", true }}
|
|
83
|
+
{{ collection.products | sort: "price" }}
|
|
84
|
+
{{ collection.products | reverse }}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Date filters
|
|
88
|
+
|
|
89
|
+
```liquid
|
|
90
|
+
{{ article.created_at | date: "%B %d, %Y" }} → "January 15, 2025"
|
|
91
|
+
{{ article.created_at | date: "%Y-%m-%d" }} → "2025-01-15"
|
|
92
|
+
{{ "now" | date: "%s" | plus: 0 }} → Unix timestamp
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Tags
|
|
98
|
+
|
|
99
|
+
### Conditionals
|
|
100
|
+
|
|
101
|
+
```liquid
|
|
102
|
+
{% if customer %}
|
|
103
|
+
Hello, {{ customer.first_name }}
|
|
104
|
+
{% elsif request.page_type == "product" %}
|
|
105
|
+
Product page
|
|
106
|
+
{% else %}
|
|
107
|
+
Guest
|
|
108
|
+
{% endif %}
|
|
109
|
+
|
|
110
|
+
{% unless product.available %}
|
|
111
|
+
Sold out
|
|
112
|
+
{% endunless %}
|
|
113
|
+
|
|
114
|
+
{% case product.type %}
|
|
115
|
+
{% when "Shirt" %}
|
|
116
|
+
...
|
|
117
|
+
{% when "Pants", "Trousers" %}
|
|
118
|
+
...
|
|
119
|
+
{% else %}
|
|
120
|
+
...
|
|
121
|
+
{% endcase %}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Loops
|
|
125
|
+
|
|
126
|
+
```liquid
|
|
127
|
+
{% for product in collection.products %}
|
|
128
|
+
{{ product.title }}
|
|
129
|
+
{% endfor %}
|
|
130
|
+
|
|
131
|
+
{% for i in (1..5) %}
|
|
132
|
+
{{ i }}
|
|
133
|
+
{% endfor %}
|
|
134
|
+
|
|
135
|
+
{% for tag in product.tags limit: 3 offset: 1 %}
|
|
136
|
+
{{ tag }}
|
|
137
|
+
{% endfor %}
|
|
138
|
+
|
|
139
|
+
{% for product in collection.products %}
|
|
140
|
+
{% if forloop.first %}First{% endif %}
|
|
141
|
+
{% if forloop.last %}Last{% endif %}
|
|
142
|
+
{{ forloop.index }} ← 1-based
|
|
143
|
+
{{ forloop.index0 }} ← 0-based
|
|
144
|
+
{% endfor %}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Content inclusion
|
|
148
|
+
|
|
149
|
+
```liquid
|
|
150
|
+
{% render 'card-product', product: product %}
|
|
151
|
+
{% render 'icon', name: 'cart' %}
|
|
152
|
+
|
|
153
|
+
{% section 'header' %}
|
|
154
|
+
|
|
155
|
+
{% include 'snippet' %} ← deprecated, use render
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`render` is sandboxed — parent variables not accessible unless passed explicitly.
|
|
159
|
+
|
|
160
|
+
### Pagination
|
|
161
|
+
|
|
162
|
+
```liquid
|
|
163
|
+
{% paginate collection.products by 24 %}
|
|
164
|
+
{% for product in collection.products %}
|
|
165
|
+
...
|
|
166
|
+
{% endfor %}
|
|
167
|
+
|
|
168
|
+
{{ paginate | default_pagination }}
|
|
169
|
+
{% endpaginate %}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Custom pagination:
|
|
173
|
+
```liquid
|
|
174
|
+
{% paginate collection.products by 24 %}
|
|
175
|
+
{% if paginate.previous %}
|
|
176
|
+
<a href="{{ paginate.previous.url }}">Previous</a>
|
|
177
|
+
{% endif %}
|
|
178
|
+
|
|
179
|
+
{% for part in paginate.parts %}
|
|
180
|
+
{% if part.is_link %}
|
|
181
|
+
<a href="{{ part.url }}">{{ part.title }}</a>
|
|
182
|
+
{% else %}
|
|
183
|
+
<span>{{ part.title }}</span>
|
|
184
|
+
{% endif %}
|
|
185
|
+
{% endfor %}
|
|
186
|
+
|
|
187
|
+
{% if paginate.next %}
|
|
188
|
+
<a href="{{ paginate.next.url }}">Next</a>
|
|
189
|
+
{% endif %}
|
|
190
|
+
{% endpaginate %}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Gotchas
|
|
196
|
+
|
|
197
|
+
### Blank vs nil vs false
|
|
198
|
+
|
|
199
|
+
```liquid
|
|
200
|
+
{% if product.metafields.custom.badge %} ← true if value exists AND not empty
|
|
201
|
+
{% if product.metafields.custom.badge != blank %} ← safer
|
|
202
|
+
{% if product.metafields.custom.badge == nil %} ← only nil check
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Price display edge case
|
|
206
|
+
|
|
207
|
+
Always check for variant availability before displaying compare_at_price:
|
|
208
|
+
```liquid
|
|
209
|
+
{% if product.compare_at_price_max > product.price_min %}
|
|
210
|
+
<s>{{ product.compare_at_price_max | money }}</s>
|
|
211
|
+
{% endif %}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### render scope
|
|
215
|
+
|
|
216
|
+
```liquid
|
|
217
|
+
{% render 'card', product: product %}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Inside `card.liquid`, only `product` is accessible — not `collection`, `shop`, `settings`, etc. Pass each required variable explicitly.
|
|
221
|
+
|
|
222
|
+
`settings` is an exception — globally accessible in rendered snippets.
|
|
223
|
+
|
|
224
|
+
### Forloop and break
|
|
225
|
+
|
|
226
|
+
Liquid has no `break` in for loops. Use `limit` to cap iterations, or build logic with conditionals and `continue`:
|
|
227
|
+
|
|
228
|
+
```liquid
|
|
229
|
+
{% for product in collection.products limit: 10 %}
|
|
230
|
+
{% if product.available == false %}{% continue %}{% endif %}
|
|
231
|
+
{{ product.title }}
|
|
232
|
+
{% endfor %}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Section settings output in JS
|
|
236
|
+
|
|
237
|
+
```liquid
|
|
238
|
+
<script>
|
|
239
|
+
const threshold = {{ section.settings.countdown_hours | times: 3600 }};
|
|
240
|
+
const color = {{ section.settings.text_color | json }};
|
|
241
|
+
</script>
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`| json` escapes strings correctly. Never interpolate raw strings into JS.
|
|
245
|
+
|
|
246
|
+
### Metafield access
|
|
247
|
+
|
|
248
|
+
```liquid
|
|
249
|
+
{{ product.metafields.custom.size_guide }}
|
|
250
|
+
{{ product.metafields["reviews"]["rating"] }}
|
|
251
|
+
|
|
252
|
+
{% assign guide = product.metafields.custom.size_guide %}
|
|
253
|
+
{% if guide %}
|
|
254
|
+
{{ guide.value }}
|
|
255
|
+
{% endif %}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Metafield types in Liquid: `value` for single values, `.value.items` for list types.
|
|
259
|
+
|
|
260
|
+
### Translation (i18n)
|
|
261
|
+
|
|
262
|
+
```liquid
|
|
263
|
+
{{ 'products.product.add_to_cart' | t }}
|
|
264
|
+
{{ 'cart.items_count' | t: count: cart.item_count }}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Keys defined in `locales/en.default.json`. Always use `| t` for user-facing strings.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Liquid Anti-Patterns
|
|
272
|
+
|
|
273
|
+
**Using `include` instead of `render`:** `include` shares scope (can read parent variables), which creates hidden coupling. Use `render`.
|
|
274
|
+
|
|
275
|
+
**Doing math with price in Liquid:** `product.price | times: 1.21` for VAT display — Liquid math on integers (cents) then format. Don't format first, then calculate.
|
|
276
|
+
|
|
277
|
+
**Nested loops for lookups:** O(n²) in Liquid. Build a hash map instead:
|
|
278
|
+
```liquid
|
|
279
|
+
{% assign variant_map = "" %}
|
|
280
|
+
{% for variant in product.variants %}
|
|
281
|
+
{% assign variant_map = variant_map | append: variant.id | append: ":" | append: variant.title | append: "," %}
|
|
282
|
+
{% endfor %}
|
|
283
|
+
```
|
|
284
|
+
Or pass data to JS and handle there.
|
|
285
|
+
|
|
286
|
+
**Long conditional chains in Liquid:** Move logic to a snippet with `render`, or handle in JS.
|