@numbered/docs-to-context 0.1.4 → 0.3.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 +71 -16
- package/docs/liquid/README.mdx +59 -0
- package/docs/liquid/a11y-alt-text.mdx +16 -0
- package/docs/liquid/a11y-semantic-html.mdx +19 -0
- package/docs/liquid/alpine-cleanup.mdx +18 -0
- package/docs/liquid/alpine-debounce.mdx +12 -0
- package/docs/liquid/alpine-defer-heavy.mdx +13 -0
- package/docs/liquid/alpine-separate-scopes.mdx +19 -0
- package/docs/liquid/alpine-static-data.mdx +16 -0
- package/docs/liquid/css-critical-inline.mdx +16 -0
- package/docs/liquid/css-prefer-tailwind.mdx +30 -0
- package/docs/liquid/liquid-assign-over-capture.mdx +17 -0
- package/docs/liquid/liquid-avoid-nested-loops.mdx +21 -0
- package/docs/liquid/liquid-break-continue.mdx +23 -0
- package/docs/liquid/liquid-cache-assigns.mdx +16 -0
- package/docs/liquid/liquid-elsif-chain.mdx +25 -0
- package/docs/liquid/liquid-filter-early.mdx +18 -0
- package/docs/liquid/liquid-limit-loops.mdx +17 -0
- package/docs/liquid/liquid-map-join.mdx +16 -0
- package/docs/liquid/liquid-render-over-include.mdx +13 -0
- package/docs/liquid/liquid-snippet-data.mdx +17 -0
- package/docs/liquid/liquid-whitespace-control.mdx +17 -0
- package/docs/liquid/loading-defer-scripts.mdx +13 -0
- package/docs/liquid/loading-lazy-images.mdx +31 -0
- package/docs/liquid/loading-preload-critical.mdx +24 -0
- package/docs/liquid/loading-responsive-images.mdx +23 -0
- package/docs/liquid/schema-blocks-over-settings.mdx +35 -0
- package/docs/liquid/schema-section-settings.mdx +36 -0
- package/docs/react/README.mdx +99 -0
- package/docs/react/advanced-handler-refs.mdx +15 -0
- package/docs/react/advanced-use-latest.mdx +21 -0
- package/docs/react/async-defer-await.mdx +19 -0
- package/docs/react/async-promise-all.mdx +17 -0
- package/docs/react/async-start-early.mdx +25 -0
- package/docs/react/async-suspense-boundaries.mdx +35 -0
- package/docs/react/bundle-barrel-imports.mdx +25 -0
- package/docs/react/bundle-conditional-loading.mdx +21 -0
- package/docs/react/bundle-defer-third-party.mdx +15 -0
- package/docs/react/bundle-dynamic-imports.mdx +15 -0
- package/docs/react/bundle-preload-intent.mdx +24 -0
- package/docs/react/client-event-listeners.mdx +45 -0
- package/docs/react/client-swr-dedup.mdx +40 -0
- package/docs/react/effect-derive-state.mdx +12 -0
- package/docs/react/effect-event-handlers.mdx +16 -0
- package/docs/react/effect-key-reset.mdx +11 -0
- package/docs/react/effect-no-chains.mdx +22 -0
- package/docs/react/effect-notify-parents.mdx +18 -0
- package/docs/react/effect-use-memo.mdx +17 -0
- package/docs/react/js-batch-css.mdx +23 -0
- package/docs/react/js-cache-property.mdx +17 -0
- package/docs/react/js-cache-storage.mdx +29 -0
- package/docs/react/js-combine-iterations.mdx +20 -0
- package/docs/react/js-early-return.mdx +24 -0
- package/docs/react/js-hoist-regexp.mdx +21 -0
- package/docs/react/js-index-maps.mdx +14 -0
- package/docs/react/js-length-check.mdx +12 -0
- package/docs/react/js-loop-min-max.mdx +14 -0
- package/docs/react/js-set-lookups.mdx +12 -0
- package/docs/react/js-tosorted.mdx +15 -0
- package/docs/react/render-activity.mdx +17 -0
- package/docs/react/render-conditional.mdx +11 -0
- package/docs/react/render-content-visibility.mdx +12 -0
- package/docs/react/render-cx-clsx.mdx +12 -0
- package/docs/react/render-hoist-jsx.mdx +16 -0
- package/docs/react/render-hydration-flicker.mdx +25 -0
- package/docs/react/render-svg-precision.mdx +17 -0
- package/docs/react/render-svg-wrapper.mdx +19 -0
- package/docs/react/rerender-defer-reads.mdx +24 -0
- package/docs/react/rerender-derived-state.mdx +18 -0
- package/docs/react/rerender-inline-objects.mdx +18 -0
- package/docs/react/rerender-isolate-state.mdx +36 -0
- package/docs/react/rerender-lazy-init.mdx +19 -0
- package/docs/react/rerender-memo-extract.mdx +26 -0
- package/docs/react/rerender-narrow-deps.mdx +26 -0
- package/docs/react/rerender-transitions.mdx +29 -0
- package/docs/react/rerender-use-client-down.mdx +34 -0
- package/docs/react/server-lru-cache.mdx +24 -0
- package/docs/react/server-parallel-fetching.mdx +24 -0
- package/docs/react/server-react-cache.mdx +16 -0
- package/docs/react/server-rsc-serialization.mdx +17 -0
- package/package.json +2 -1
- package/scripts/extract.ts +2 -2
- package/scripts/generate.ts +9 -2
- package/scripts/inject.ts +95 -28
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @numbered/docs-to-context
|
|
2
2
|
|
|
3
|
-
Extract component APIs, design system docs, and architecture specs into structured references and inject a compact index into `CLAUDE.md` — so AI agents always know what's available without hallucinating.
|
|
3
|
+
Extract component APIs, design system docs, best practices, and architecture specs into structured references and inject a compact index into `CLAUDE.md` — so AI agents always know what's available without hallucinating.
|
|
4
4
|
|
|
5
5
|
## Usage
|
|
6
6
|
|
|
@@ -12,7 +12,7 @@ Run from the project root. Auto-detects platform (Next.js or Shopify).
|
|
|
12
12
|
|
|
13
13
|
### Options
|
|
14
14
|
|
|
15
|
-
```
|
|
15
|
+
```
|
|
16
16
|
bunx @numbered/docs-to-context [project_root] [options]
|
|
17
17
|
|
|
18
18
|
--platform nextjs|shopify Force platform detection
|
|
@@ -20,32 +20,76 @@ bunx @numbered/docs-to-context [project_root] [options]
|
|
|
20
20
|
--output path Custom output directory
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
### Output
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
@numbered/docs-to-context
|
|
27
|
+
─────────────────────
|
|
28
|
+
|
|
29
|
+
Extract
|
|
30
|
+
● Platform: nextjs
|
|
31
|
+
✓ Extracted 74 components
|
|
32
|
+
/path/to/project/.context/components
|
|
33
|
+
|
|
34
|
+
Inject
|
|
35
|
+
✓ Copied 1 best practice doc(s)
|
|
36
|
+
● Found 15 core doc(s)
|
|
37
|
+
✓ Injected index into CLAUDE.md
|
|
38
|
+
|
|
39
|
+
Done.
|
|
40
|
+
```
|
|
41
|
+
|
|
23
42
|
## What it does
|
|
24
43
|
|
|
25
|
-
1. **Extracts** component APIs from source files into per-component MDX docs at
|
|
26
|
-
2. **
|
|
27
|
-
3. **
|
|
28
|
-
4. **
|
|
44
|
+
1. **Extracts** component APIs from source files into per-component MDX docs at `.context/components/`
|
|
45
|
+
2. **Copies** platform-specific best practice docs to `.context/best-practices/`
|
|
46
|
+
3. **Discovers** core docs if present (design system, grid system, architecture specs)
|
|
47
|
+
4. **Injects** a compact index between `<!-- PROJECT_DOCS_START -->` / `<!-- PROJECT_DOCS_END -->` markers in `CLAUDE.md`
|
|
48
|
+
5. **Adds** `.context/` to `.gitignore`
|
|
29
49
|
|
|
30
50
|
### Injected format
|
|
31
51
|
|
|
32
|
-
Follows the [Vercel compressed folder path convention](https://vercel.com/blog/agents-md-outperforms-skills-in-our-agent-evals):
|
|
52
|
+
Follows the [Vercel compressed folder path convention](https://vercel.com/blog/agents-md-outperforms-skills-in-our-agent-evals). Files are grouped by directory to minimize token usage:
|
|
33
53
|
|
|
34
54
|
```markdown
|
|
35
55
|
<!-- PROJECT_DOCS_START -->
|
|
56
|
+
|
|
36
57
|
## Project Docs
|
|
58
|
+
|
|
37
59
|
|[Frontend]|root: ./docs
|
|
38
60
|
|frontend:{design-system.md,grid-system.md}
|
|
39
|
-
[
|
|
61
|
+
|[Best Practices]|root: ./.context
|
|
62
|
+
|best-practices:{react-rules.mdx}
|
|
63
|
+
|IMPORTANT: Read best practice docs before writing code
|
|
64
|
+
[Component Index]|root: ./.context/components
|
|
40
65
|
|IMPORTANT: Read component MDX before using any component
|
|
41
66
|
|components:{Button.mdx,Carousel.mdx,Container.mdx}
|
|
42
|
-
|If
|
|
67
|
+
|If ./.context/components is missing, run: bunx @numbered/docs-to-context
|
|
43
68
|
|[Entities]|root: ./docs
|
|
44
69
|
|specs/architecture:{README.md,entities.md,entity-relationship-diagram.md}
|
|
45
70
|
|specs/architecture/entities:{about.md,journal.md,happening.md}
|
|
71
|
+
|
|
46
72
|
<!-- PROJECT_DOCS_END -->
|
|
47
73
|
```
|
|
48
74
|
|
|
75
|
+
Four sections:
|
|
76
|
+
|
|
77
|
+
- **Frontend** — design tokens, grid/layout (read before styling)
|
|
78
|
+
- **Best Practices** — platform-specific coding rules (read before writing code)
|
|
79
|
+
- **Components** — per-component MDX with props, variants, defaults
|
|
80
|
+
- **Entities** — content architecture (read before schema/data work)
|
|
81
|
+
|
|
82
|
+
## Best practices
|
|
83
|
+
|
|
84
|
+
The package bundles best-practice docs per platform:
|
|
85
|
+
|
|
86
|
+
| Platform | Folder | Contents |
|
|
87
|
+
| -------- | -------------- | --------------------------------------- |
|
|
88
|
+
| Next.js | `docs/react/` | React rules (re-renders, effects, etc.) |
|
|
89
|
+
| Shopify | `docs/liquid/` | Liquid rules (performance, Alpine.js) |
|
|
90
|
+
|
|
91
|
+
These are copied to `.context/best-practices/` at generation time. More docs can be added to each folder.
|
|
92
|
+
|
|
49
93
|
## Supported platforms
|
|
50
94
|
|
|
51
95
|
### Next.js
|
|
@@ -69,17 +113,28 @@ Scans `snippets/*.liquid` and extracts:
|
|
|
69
113
|
|
|
70
114
|
The following files are automatically included in the index when present:
|
|
71
115
|
|
|
72
|
-
| Section
|
|
73
|
-
|
|
74
|
-
| Frontend | `docs/design-system.md`
|
|
75
|
-
| Frontend | `docs/grid-system.md`
|
|
116
|
+
| Section | Path | Purpose |
|
|
117
|
+
| -------- | --------------------------------- | ----------------------------------------- |
|
|
118
|
+
| Frontend | `docs/design-system.md` | Design tokens, typography, colors |
|
|
119
|
+
| Frontend | `docs/grid-system.md` | Grid system, breakpoints, fluid utilities |
|
|
76
120
|
| Entities | `docs/specs/architecture/**/*.md` | Document types, ER diagrams, entity specs |
|
|
77
121
|
|
|
122
|
+
## Fresh clone
|
|
123
|
+
|
|
124
|
+
Since `.context/` is gitignored, agents on a fresh clone will see:
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
|If ./.context/components is missing, run: bunx @numbered/docs-to-context
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
No plugin install, no auth — just `bun` and the public npm package.
|
|
131
|
+
|
|
78
132
|
## Publishing
|
|
79
133
|
|
|
80
134
|
```bash
|
|
81
|
-
cd packages/
|
|
135
|
+
cd packages/docs-to-context
|
|
82
136
|
npm login --scope=@numbered
|
|
83
|
-
bun run publish:dry
|
|
84
|
-
bun run publish:
|
|
137
|
+
bun run publish:dry # preview
|
|
138
|
+
bun run publish:patch # bump patch + publish
|
|
139
|
+
bun run publish:minor # bump minor + publish
|
|
85
140
|
```
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Liquid & Shopify Best Practices
|
|
2
|
+
|
|
3
|
+
Read this index first. Load only the files relevant to your current task.
|
|
4
|
+
|
|
5
|
+
## 1. Liquid Performance — CRITICAL
|
|
6
|
+
|
|
7
|
+
| File | Pattern |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `liquid-filter-early.mdx` | Filter and assign before loops |
|
|
10
|
+
| `liquid-cache-assigns.mdx` | Assign once, reuse — avoid repeated filters |
|
|
11
|
+
| `liquid-limit-loops.mdx` | Always use limit: on for loops |
|
|
12
|
+
| `liquid-avoid-nested-loops.mdx` | Flatten nested loops with Liquid filters |
|
|
13
|
+
| `liquid-break-continue.mdx` | break/continue for early loop exit |
|
|
14
|
+
| `liquid-map-join.mdx` | map + join instead of string concatenation |
|
|
15
|
+
| `liquid-whitespace-control.mdx` | Strip whitespace with {%- -%} tags |
|
|
16
|
+
| `liquid-render-over-include.mdx` | render is faster and safer than include |
|
|
17
|
+
| `liquid-snippet-data.mdx` | Pass only needed values to snippets |
|
|
18
|
+
| `liquid-elsif-chain.mdx` | elsif chain over multiple if blocks |
|
|
19
|
+
| `liquid-assign-over-capture.mdx` | assign over capture for simple values |
|
|
20
|
+
|
|
21
|
+
## 2. Asset Loading — HIGH
|
|
22
|
+
|
|
23
|
+
| File | Pattern |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `loading-defer-scripts.mdx` | defer/async on non-critical scripts |
|
|
26
|
+
| `loading-lazy-images.mdx` | Native lazy loading for below-fold images |
|
|
27
|
+
| `loading-preload-critical.mdx` | Preload hero images and fonts |
|
|
28
|
+
| `loading-responsive-images.mdx` | srcset for per-viewport image sizes |
|
|
29
|
+
|
|
30
|
+
## 3. CSS & Styling — MEDIUM
|
|
31
|
+
|
|
32
|
+
| File | Pattern |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `css-prefer-tailwind.mdx` | Tailwind utilities over CSS variables |
|
|
35
|
+
| `css-critical-inline.mdx` | Inline critical CSS, async load the rest |
|
|
36
|
+
|
|
37
|
+
## 4. Alpine.js Performance — MEDIUM
|
|
38
|
+
|
|
39
|
+
| File | Pattern |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `alpine-debounce.mdx` | Debounce expensive watchers/handlers |
|
|
42
|
+
| `alpine-static-data.mdx` | Move static data outside reactive scope |
|
|
43
|
+
| `alpine-defer-heavy.mdx` | Defer heavy content with $nextTick |
|
|
44
|
+
| `alpine-separate-scopes.mdx` | Split x-data into separate concerns |
|
|
45
|
+
| `alpine-cleanup.mdx` | Clean up listeners in destroy() |
|
|
46
|
+
|
|
47
|
+
## 5. Schema Design — MEDIUM
|
|
48
|
+
|
|
49
|
+
| File | Pattern |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `schema-section-settings.mdx` | Lean settings, map to Tailwind in template |
|
|
52
|
+
| `schema-blocks-over-settings.mdx` | Blocks for repeatable content, not numbered settings |
|
|
53
|
+
|
|
54
|
+
## 6. Accessibility — MEDIUM
|
|
55
|
+
|
|
56
|
+
| File | Pattern |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `a11y-semantic-html.mdx` | Semantic elements over div+role |
|
|
59
|
+
| `a11y-alt-text.mdx` | Alt text on all images, escaped |
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Always Provide Alt Text
|
|
2
|
+
|
|
3
|
+
Every `<img>` must have an `alt` attribute. Use the Shopify image alt field, with a fallback.
|
|
4
|
+
|
|
5
|
+
```liquid
|
|
6
|
+
{% comment %} BAD: no alt text {% endcomment %}
|
|
7
|
+
<img src="{{ image | image_url: width: 800 }}">
|
|
8
|
+
|
|
9
|
+
{% comment %} GOOD: alt from image object {% endcomment %}
|
|
10
|
+
<img src="{{ image | image_url: width: 800 }}" alt="{{ image.alt | escape }}">
|
|
11
|
+
|
|
12
|
+
{% comment %} For decorative images {% endcomment %}
|
|
13
|
+
<img src="{{ image | image_url: width: 800 }}" alt="" role="presentation">
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Escape alt text to prevent XSS via CMS-injected content.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Use Semantic HTML
|
|
2
|
+
|
|
3
|
+
Use proper HTML elements instead of generic divs with ARIA roles.
|
|
4
|
+
|
|
5
|
+
```liquid
|
|
6
|
+
{% comment %} BAD {% endcomment %}
|
|
7
|
+
<div class="nav" role="navigation">
|
|
8
|
+
<div class="nav-item" onclick="...">Home</div>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
{% comment %} GOOD {% endcomment %}
|
|
12
|
+
<nav>
|
|
13
|
+
<a href="/">Home</a>
|
|
14
|
+
</nav>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Key elements: `<nav>`, `<main>`, `<header>`, `<footer>`, `<article>`, `<section>`, `<button>`, `<a>`.
|
|
18
|
+
|
|
19
|
+
Only use `<div>` for layout wrappers with no semantic meaning.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Clean Up Alpine.js Event Listeners
|
|
2
|
+
|
|
3
|
+
Always clean up manual event listeners and intervals in `destroy()` to prevent memory leaks on section re-render.
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
component: () => ({
|
|
7
|
+
resizeHandler: null,
|
|
8
|
+
init() {
|
|
9
|
+
this.resizeHandler = () => this.onResize()
|
|
10
|
+
window.addEventListener('resize', this.resizeHandler)
|
|
11
|
+
},
|
|
12
|
+
destroy() {
|
|
13
|
+
window.removeEventListener('resize', this.resizeHandler)
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Alpine calls `destroy()` when the element is removed from the DOM (e.g. section reorder in the editor).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Debounce Expensive Alpine.js Operations
|
|
2
|
+
|
|
3
|
+
Wrap watchers and event handlers with debounce to avoid firing on every keystroke/scroll.
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
component: () => ({
|
|
7
|
+
search: '',
|
|
8
|
+
init() {
|
|
9
|
+
this.$watch('search', debounce((value) => this.performSearch(value), 300))
|
|
10
|
+
}
|
|
11
|
+
})
|
|
12
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Defer Heavy Alpine.js Components
|
|
2
|
+
|
|
3
|
+
Delay rendering of heavy content until after initial load using `$nextTick`.
|
|
4
|
+
|
|
5
|
+
```html
|
|
6
|
+
<div x-data="{ loaded: false }" x-init="$nextTick(() => loaded = true)">
|
|
7
|
+
<template x-if="loaded">
|
|
8
|
+
<!-- Heavy content rendered after initial load -->
|
|
9
|
+
</template>
|
|
10
|
+
</div>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Useful for carousels, maps, complex forms, and anything below the fold.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Separate Alpine.js x-data Scopes
|
|
2
|
+
|
|
3
|
+
Avoid monolithic reactive objects. Split concerns into nested `x-data` scopes.
|
|
4
|
+
|
|
5
|
+
```html
|
|
6
|
+
<!-- BAD: large reactive object -->
|
|
7
|
+
<div x-data="{ items: [...], filters: {...}, sort: {...}, pagination: {...} }">
|
|
8
|
+
|
|
9
|
+
<!-- GOOD: separate concerns -->
|
|
10
|
+
<div x-data="productFilters">
|
|
11
|
+
<div x-data="productSort">
|
|
12
|
+
<div x-data="productPagination">
|
|
13
|
+
...
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Each scope only re-evaluates when its own data changes.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Minimize Alpine.js Reactive Data
|
|
2
|
+
|
|
3
|
+
Static data inside `x-data` becomes reactive — Alpine tracks changes to it even though it never changes. Move static data outside.
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
// BAD: static data as reactive
|
|
7
|
+
component: () => ({
|
|
8
|
+
options: ['Option 1', 'Option 2', 'Option 3'],
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
// GOOD: static data outside component
|
|
12
|
+
const OPTIONS = ['Option 1', 'Option 2', 'Option 3']
|
|
13
|
+
component: () => ({
|
|
14
|
+
selectedOption: OPTIONS[0],
|
|
15
|
+
})
|
|
16
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Inline Critical CSS
|
|
2
|
+
|
|
3
|
+
Inline above-the-fold styles to avoid render-blocking stylesheet requests.
|
|
4
|
+
|
|
5
|
+
```liquid
|
|
6
|
+
{% comment %} BAD: external stylesheet blocks first paint {% endcomment %}
|
|
7
|
+
{{ 'section-hero.css' | asset_url | stylesheet_tag }}
|
|
8
|
+
|
|
9
|
+
{% comment %} GOOD: inline critical styles for first paint {% endcomment %}
|
|
10
|
+
<style>
|
|
11
|
+
.hero { min-height: 60vh; display: flex; align-items: center; }
|
|
12
|
+
</style>
|
|
13
|
+
|
|
14
|
+
{% comment %} Load non-critical CSS async {% endcomment %}
|
|
15
|
+
<link rel="stylesheet" href="{{ 'section-hero.css' | asset_url }}" media="print" onload="this.media='all'">
|
|
16
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Prefer Tailwind Over Custom CSS Variables
|
|
2
|
+
|
|
3
|
+
Use Tailwind utilities instead of custom CSS variables.
|
|
4
|
+
|
|
5
|
+
```liquid
|
|
6
|
+
{% comment %} BAD {% endcomment %}
|
|
7
|
+
<style>--card-padding: 1rem;</style>
|
|
8
|
+
<div class="card">
|
|
9
|
+
|
|
10
|
+
{% comment %} GOOD {% endcomment %}
|
|
11
|
+
<div class="p-16">
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Only use CSS variables for:
|
|
15
|
+
|
|
16
|
+
- Dynamic values from CMS settings
|
|
17
|
+
- Theme-wide color schemes
|
|
18
|
+
- Complex calculations not expressible with utilities
|
|
19
|
+
|
|
20
|
+
When necessary, scope to section ID:
|
|
21
|
+
|
|
22
|
+
```liquid
|
|
23
|
+
{%- if section.settings.background_color != blank -%}
|
|
24
|
+
<style>
|
|
25
|
+
#section-{{ section.id }} {
|
|
26
|
+
--section-bg: {{ section.settings.background_color }};
|
|
27
|
+
}
|
|
28
|
+
</style>
|
|
29
|
+
{%- endif -%}
|
|
30
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Prefer assign Over capture
|
|
2
|
+
|
|
3
|
+
`assign` is lighter weight than `capture` for simple values. Reserve `capture` for multi-line HTML blocks.
|
|
4
|
+
|
|
5
|
+
```liquid
|
|
6
|
+
{% comment %} BAD: capture for a simple value {% endcomment %}
|
|
7
|
+
{% capture product_class %}product-card{% endcapture %}
|
|
8
|
+
|
|
9
|
+
{% comment %} GOOD: assign for simple values {% endcomment %}
|
|
10
|
+
{% assign product_class = 'product-card' %}
|
|
11
|
+
|
|
12
|
+
{% comment %} OK: capture for multi-line HTML {% endcomment %}
|
|
13
|
+
{% capture card_content %}
|
|
14
|
+
<h3>{{ product.title }}</h3>
|
|
15
|
+
<p>{{ product.price | money }}</p>
|
|
16
|
+
{% endcapture %}
|
|
17
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Avoid Nested Loops
|
|
2
|
+
|
|
3
|
+
Nested `for` loops multiply iterations. Flatten with assigns or use Liquid filters.
|
|
4
|
+
|
|
5
|
+
```liquid
|
|
6
|
+
{% comment %} BAD: O(n*m) — 50 products x 10 tags = 500 iterations {% endcomment %}
|
|
7
|
+
{% for product in collection.products %}
|
|
8
|
+
{% for tag in product.tags %}
|
|
9
|
+
{% if tag == 'sale' %}
|
|
10
|
+
...
|
|
11
|
+
{% endif %}
|
|
12
|
+
{% endfor %}
|
|
13
|
+
{% endfor %}
|
|
14
|
+
|
|
15
|
+
{% comment %} GOOD: filter directly {% endcomment %}
|
|
16
|
+
{% for product in collection.products %}
|
|
17
|
+
{% if product.tags contains 'sale' %}
|
|
18
|
+
...
|
|
19
|
+
{% endif %}
|
|
20
|
+
{% endfor %}
|
|
21
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Use break and continue in Loops
|
|
2
|
+
|
|
3
|
+
Stop processing once target data is found, or skip irrelevant iterations.
|
|
4
|
+
|
|
5
|
+
```liquid
|
|
6
|
+
{% comment %} GOOD: stop after finding first match {% endcomment %}
|
|
7
|
+
{% for product in collection.products %}
|
|
8
|
+
{% if product.tags contains 'featured' %}
|
|
9
|
+
{% assign featured_product = product %}
|
|
10
|
+
{% break %}
|
|
11
|
+
{% endif %}
|
|
12
|
+
{% endfor %}
|
|
13
|
+
|
|
14
|
+
{% comment %} GOOD: skip unavailable items {% endcomment %}
|
|
15
|
+
{% for product in collection.products %}
|
|
16
|
+
{% unless product.available %}
|
|
17
|
+
{% continue %}
|
|
18
|
+
{% endunless %}
|
|
19
|
+
<div class="product-card">{{ product.title }}</div>
|
|
20
|
+
{% endfor %}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`break` avoids iterating the entire collection when you only need one result. `continue` skips rendering for items that don't qualify.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Cache Expensive Assigns
|
|
2
|
+
|
|
3
|
+
Assign filtered/transformed values once, then reuse. Avoid repeating Liquid filters on the same data.
|
|
4
|
+
|
|
5
|
+
```liquid
|
|
6
|
+
{% comment %} BAD: escape runs twice {% endcomment %}
|
|
7
|
+
<a href="{{ product.url | within: collection }}">{{ product.title | escape }}</a>
|
|
8
|
+
<span>{{ product.title | escape }}</span>
|
|
9
|
+
|
|
10
|
+
{% comment %} GOOD: assign once, reuse {% endcomment %}
|
|
11
|
+
{% assign product_title = product.title | escape %}
|
|
12
|
+
{% assign product_url = product.url | within: collection %}
|
|
13
|
+
|
|
14
|
+
<a href="{{ product_url }}">{{ product_title }}</a>
|
|
15
|
+
<span>{{ product_title }}</span>
|
|
16
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Use elsif Instead of Multiple if Blocks
|
|
2
|
+
|
|
3
|
+
Mutually exclusive conditions should use `elsif` to short-circuit evaluation. Order by most likely condition first.
|
|
4
|
+
|
|
5
|
+
```liquid
|
|
6
|
+
{% comment %} BAD: evaluates all conditions {% endcomment %}
|
|
7
|
+
{% if product.price < 25 %}
|
|
8
|
+
<span>Budget</span>
|
|
9
|
+
{% endif %}
|
|
10
|
+
{% if product.price >= 25 and product.price < 100 %}
|
|
11
|
+
<span>Standard</span>
|
|
12
|
+
{% endif %}
|
|
13
|
+
{% if product.price >= 100 %}
|
|
14
|
+
<span>Premium</span>
|
|
15
|
+
{% endif %}
|
|
16
|
+
|
|
17
|
+
{% comment %} GOOD: stops at first match {% endcomment %}
|
|
18
|
+
{% if product.price < 25 %}
|
|
19
|
+
<span>Budget</span>
|
|
20
|
+
{% elsif product.price < 100 %}
|
|
21
|
+
<span>Standard</span>
|
|
22
|
+
{% else %}
|
|
23
|
+
<span>Premium</span>
|
|
24
|
+
{% endif %}
|
|
25
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Filter Early, Assign Once
|
|
2
|
+
|
|
3
|
+
Avoid complex logic inside loops. Filter and assign before iterating.
|
|
4
|
+
|
|
5
|
+
```liquid
|
|
6
|
+
{% comment %} BAD: complex logic in every iteration {% endcomment %}
|
|
7
|
+
{% for product in collection.products %}
|
|
8
|
+
{% if product.available and product.price > 0 and product.tags contains 'featured' %}
|
|
9
|
+
...
|
|
10
|
+
{% endif %}
|
|
11
|
+
{% endfor %}
|
|
12
|
+
|
|
13
|
+
{% comment %} GOOD: filter early, iterate lean {% endcomment %}
|
|
14
|
+
{% assign featured = collection.products | where: 'available', true %}
|
|
15
|
+
{% for product in featured limit: 8 %}
|
|
16
|
+
...
|
|
17
|
+
{% endfor %}
|
|
18
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Limit Loops
|
|
2
|
+
|
|
3
|
+
Always use `limit:` when you don't need all items. Unbounded loops are the #1 Liquid performance issue.
|
|
4
|
+
|
|
5
|
+
```liquid
|
|
6
|
+
{% comment %} BAD: iterates entire collection {% endcomment %}
|
|
7
|
+
{% for product in collection.products %}
|
|
8
|
+
...
|
|
9
|
+
{% endfor %}
|
|
10
|
+
|
|
11
|
+
{% comment %} GOOD: only what you need {% endcomment %}
|
|
12
|
+
{% for product in collection.products limit: 8 %}
|
|
13
|
+
...
|
|
14
|
+
{% endfor %}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Also applies to `for` over arrays, recommendations, and search results.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Use map + join Instead of String Concatenation
|
|
2
|
+
|
|
3
|
+
Appending strings in a loop creates many intermediate strings. Use `map` and `join` filters instead.
|
|
4
|
+
|
|
5
|
+
```liquid
|
|
6
|
+
{% comment %} BAD: string concatenation in loop {% endcomment %}
|
|
7
|
+
{% assign ids = '' %}
|
|
8
|
+
{% for product in collection.products %}
|
|
9
|
+
{% assign ids = ids | append: product.id | append: ',' %}
|
|
10
|
+
{% endfor %}
|
|
11
|
+
|
|
12
|
+
{% comment %} GOOD: map + join — one operation {% endcomment %}
|
|
13
|
+
{% assign ids = collection.products | map: 'id' | join: ',' %}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Also works for building class lists, data attributes, and comma-separated values.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Use render Instead of include
|
|
2
|
+
|
|
3
|
+
`{% render %}` is faster and safer than `{% include %}`. It creates an isolated scope (no variable leaking) and is parallelizable by Shopify's renderer.
|
|
4
|
+
|
|
5
|
+
```liquid
|
|
6
|
+
{% comment %} BAD: include leaks variables, slower {% endcomment %}
|
|
7
|
+
{% include 'product-card' %}
|
|
8
|
+
|
|
9
|
+
{% comment %} GOOD: render is isolated and faster {% endcomment %}
|
|
10
|
+
{% render 'product-card', product: product, show_price: true %}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Always pass variables explicitly with `render`. The snippet cannot access outer scope.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Pass Only Needed Data to Snippets
|
|
2
|
+
|
|
3
|
+
Send specific values rather than entire objects when rendering snippets. Reduces serialization overhead.
|
|
4
|
+
|
|
5
|
+
```liquid
|
|
6
|
+
{% comment %} BAD: passes entire product object {% endcomment %}
|
|
7
|
+
{% render 'product-price', product: product %}
|
|
8
|
+
|
|
9
|
+
{% comment %} GOOD: passes only the values the snippet uses {% endcomment %}
|
|
10
|
+
{% render 'product-price',
|
|
11
|
+
price: product.price,
|
|
12
|
+
compare_price: product.compare_at_price,
|
|
13
|
+
currency: cart.currency.iso_code
|
|
14
|
+
%}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
This also makes snippets more reusable — they depend on values, not specific object shapes.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Use Whitespace Control Tags
|
|
2
|
+
|
|
3
|
+
Strip unnecessary whitespace from Liquid output to reduce HTML payload.
|
|
4
|
+
|
|
5
|
+
```liquid
|
|
6
|
+
{% comment %} BAD: extra whitespace in output {% endcomment %}
|
|
7
|
+
{% if section.settings.title != blank %}
|
|
8
|
+
<h2>{{ section.settings.title }}</h2>
|
|
9
|
+
{% endif %}
|
|
10
|
+
|
|
11
|
+
{% comment %} GOOD: stripped output {% endcomment %}
|
|
12
|
+
{%- if section.settings.title != blank -%}
|
|
13
|
+
<h2>{{ section.settings.title }}</h2>
|
|
14
|
+
{%- endif -%}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Use `{%-` and `-%}` on control flow tags. Keep `{{` output tags without stripping when the surrounding whitespace is meaningful for layout.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Defer Non-Critical Scripts
|
|
2
|
+
|
|
3
|
+
Use `defer` and `async` attributes to prevent scripts from blocking rendering.
|
|
4
|
+
|
|
5
|
+
```liquid
|
|
6
|
+
{% comment %} Defer for scripts that need DOM ready {% endcomment %}
|
|
7
|
+
<script src="{{ 'analytics.js' | asset_url }}" defer></script>
|
|
8
|
+
|
|
9
|
+
{% comment %} Async for independent scripts {% endcomment %}
|
|
10
|
+
<script src="{{ 'widget.js' | asset_url }}" async></script>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Never use bare `<script>` tags without `defer` or `async` unless the script must block (e.g. critical polyfills).
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Lazy Load Images
|
|
2
|
+
|
|
3
|
+
Use native `loading="lazy"` for below-the-fold images. Never lazy-load above-the-fold (hero) images.
|
|
4
|
+
|
|
5
|
+
```liquid
|
|
6
|
+
{% comment %} BAD: all images load eagerly {% endcomment %}
|
|
7
|
+
<img src="{{ image | image_url: width: 800 }}" alt="{{ image.alt }}">
|
|
8
|
+
|
|
9
|
+
{% comment %} GOOD: lazy load below-fold images {% endcomment %}
|
|
10
|
+
<img
|
|
11
|
+
src="{{ image | image_url: width: 800 }}"
|
|
12
|
+
alt="{{ image.alt }}"
|
|
13
|
+
loading="lazy"
|
|
14
|
+
width="{{ image.width }}"
|
|
15
|
+
height="{{ image.height }}"
|
|
16
|
+
>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Always include `width` and `height` to prevent layout shift (CLS).
|
|
20
|
+
|
|
21
|
+
For hero images, use `fetchpriority="high"` instead:
|
|
22
|
+
|
|
23
|
+
```liquid
|
|
24
|
+
<img
|
|
25
|
+
src="{{ image | image_url: width: 1200 }}"
|
|
26
|
+
alt="{{ image.alt }}"
|
|
27
|
+
fetchpriority="high"
|
|
28
|
+
width="{{ image.width }}"
|
|
29
|
+
height="{{ image.height }}"
|
|
30
|
+
>
|
|
31
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Preload Critical Assets
|
|
2
|
+
|
|
3
|
+
Preload fonts, hero images, and critical CSS to improve LCP.
|
|
4
|
+
|
|
5
|
+
```liquid
|
|
6
|
+
{% comment %} Preload hero image {% endcomment %}
|
|
7
|
+
<link
|
|
8
|
+
rel="preload"
|
|
9
|
+
as="image"
|
|
10
|
+
href="{{ section.settings.hero_image | image_url: width: 1200 }}"
|
|
11
|
+
fetchpriority="high"
|
|
12
|
+
>
|
|
13
|
+
|
|
14
|
+
{% comment %} Preload critical font {% endcomment %}
|
|
15
|
+
<link
|
|
16
|
+
rel="preload"
|
|
17
|
+
as="font"
|
|
18
|
+
type="font/woff2"
|
|
19
|
+
href="{{ 'custom-font.woff2' | asset_url }}"
|
|
20
|
+
crossorigin
|
|
21
|
+
>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Only preload 2-3 assets max — over-preloading has diminishing returns.
|