climaybe 3.4.8 → 3.6.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 +14 -0
- package/bin/version.txt +1 -1
- package/package.json +1 -1
- package/src/commands/init.js +7 -0
- package/src/commands/update-workflows.js +3 -1
- package/src/cursor/rules/00-rule-index.mdc +1 -0
- package/src/cursor/rules/global-rules-reference.mdc +1 -0
- package/src/cursor/rules/performance-guide.mdc +436 -0
- package/src/lib/config.js +8 -0
- package/src/lib/prompts.js +14 -0
- package/src/lib/workflows.js +10 -2
- package/src/workflows/profile/liquid-performance.yml +47 -0
- package/src/workflows/profile/reusable-liquid-profile.yml +199 -0
package/README.md
CHANGED
|
@@ -169,6 +169,7 @@ The CLI writes config into `climaybe.config.json`:
|
|
|
169
169
|
"default_store": "voldt-staging.myshopify.com",
|
|
170
170
|
"preview_workflows": true,
|
|
171
171
|
"build_workflows": true,
|
|
172
|
+
"profile_workflows": true,
|
|
172
173
|
"lighthouse_workflows": true,
|
|
173
174
|
"commitlint": true,
|
|
174
175
|
"cursor_skills": true,
|
|
@@ -263,6 +264,19 @@ Optional package, enabled via the `climaybe init` prompt (`Enable preview + clea
|
|
|
263
264
|
| `reusable-cleanup-themes.yml` | workflow_call | `cleanup_mode: by_pr` deletes names ending with `-PR{padded}`; `orphan_pr` deletes `-PR<n>` when PR `n` is not open. Optional `result_artifact_prefix` for matrix fan-in on `pr-close` |
|
|
264
265
|
| `reusable-extract-pr-number.yml` | workflow_call | Extracts padded/unpadded PR number outputs for naming and API-safe usage |
|
|
265
266
|
|
|
267
|
+
### Optional Liquid performance profiling package
|
|
268
|
+
|
|
269
|
+
Enabled via `climaybe init` prompt (`Enable Liquid performance profiling workflows?`; default: yes).
|
|
270
|
+
|
|
271
|
+
Runs on every push to `main` (skipping store-sync and hotfix-backport commits). Creates a shared preview theme, measures TTFB for four core templates (index, collection, product, cart) over 4 iterations each, discards the first (warm-up) run, and reports the average of the last 3 in the GitHub Actions job summary.
|
|
272
|
+
|
|
273
|
+
Requires `SHOPIFY_STORE_URL` and `SHOPIFY_THEME_ACCESS_TOKEN` secrets. When secrets are missing, the workflow skips gracefully.
|
|
274
|
+
|
|
275
|
+
| Workflow | Trigger | What it does |
|
|
276
|
+
|----------|---------|-------------|
|
|
277
|
+
| `liquid-performance.yml` | Push to `main` | Gate check for secrets, then calls reusable profiler |
|
|
278
|
+
| `reusable-liquid-profile.yml` | workflow_call | Shares theme, measures TTFB per template × 4 iterations, reports avg of last 3, cleans up theme |
|
|
279
|
+
|
|
266
280
|
### Build and Lighthouse workflows
|
|
267
281
|
|
|
268
282
|
Optional package. `climaybe init` asks two separate questions: **build workflows** (bundle `_scripts` JS + compile Tailwind in CI; default: yes) and, if build is on, **Lighthouse CI** (performance + a11y budget on the `staging` branch; default: yes, stored as `lighthouse_workflows`). Lighthouse runs inside the build pipeline, so it requires build workflows.
|
package/bin/version.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.6.0
|
package/package.json
CHANGED
package/src/commands/init.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
promptStoreLoop,
|
|
6
6
|
promptPreviewWorkflows,
|
|
7
7
|
promptBuildWorkflows,
|
|
8
|
+
promptProfileWorkflows,
|
|
8
9
|
promptLighthouseWorkflows,
|
|
9
10
|
promptDevKit,
|
|
10
11
|
promptVSCodeDevTasks,
|
|
@@ -99,6 +100,9 @@ async function runInitFlow() {
|
|
|
99
100
|
enableLighthouseWorkflows = await promptLighthouseWorkflows();
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
hint('CI that measures Liquid TTFB for core templates on every push to main.');
|
|
104
|
+
const enableProfileWorkflows = await promptProfileWorkflows();
|
|
105
|
+
|
|
102
106
|
hint('Local config + ignore defaults for theme work (Theme Check, Prettier, Lighthouse, gitignore).', 'theme-dev-kit');
|
|
103
107
|
const enableDevKit = await promptDevKit();
|
|
104
108
|
let enableVSCodeTasks = false;
|
|
@@ -157,6 +161,7 @@ async function runInitFlow() {
|
|
|
157
161
|
default_store: stores[0].domain,
|
|
158
162
|
preview_workflows: enablePreviewWorkflows,
|
|
159
163
|
build_workflows: enableBuildWorkflows,
|
|
164
|
+
profile_workflows: enableProfileWorkflows,
|
|
160
165
|
lighthouse_workflows: enableBuildWorkflows ? enableLighthouseWorkflows : undefined,
|
|
161
166
|
build_entrypoints_ready: enableBuildWorkflows ? missingBuildFiles?.length === 0 : undefined,
|
|
162
167
|
dev_kit: enableDevKit,
|
|
@@ -213,6 +218,7 @@ async function runInitFlow() {
|
|
|
213
218
|
scaffoldWorkflows(mode, {
|
|
214
219
|
includePreview: enablePreviewWorkflows,
|
|
215
220
|
includeBuild: enableBuildWorkflows,
|
|
221
|
+
includeProfile: enableProfileWorkflows,
|
|
216
222
|
});
|
|
217
223
|
|
|
218
224
|
// 7. Optional commitlint + Husky and the AI ruleset (rules, skills, agents + editor bridges)
|
|
@@ -259,6 +265,7 @@ async function runInitFlow() {
|
|
|
259
265
|
}
|
|
260
266
|
console.log(pc.dim(` Preview workflows: ${enablePreviewWorkflows ? 'enabled' : 'disabled'}`));
|
|
261
267
|
console.log(pc.dim(` Build workflows: ${enableBuildWorkflows ? 'enabled' : 'disabled'}`));
|
|
268
|
+
console.log(pc.dim(` Profile workflows: ${enableProfileWorkflows ? 'enabled' : 'disabled'}`));
|
|
262
269
|
if (enableBuildWorkflows) {
|
|
263
270
|
console.log(pc.dim(` Lighthouse CI: ${enableLighthouseWorkflows ? 'enabled' : 'disabled'}`));
|
|
264
271
|
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
isCursorSkillsEnabled,
|
|
7
7
|
getAiEditors,
|
|
8
8
|
isPreviewWorkflowsEnabled,
|
|
9
|
+
isProfileWorkflowsEnabled,
|
|
9
10
|
readConfig,
|
|
10
11
|
} from '../lib/config.js';
|
|
11
12
|
import { scaffoldWorkflows } from '../lib/workflows.js';
|
|
@@ -28,6 +29,7 @@ export async function updateCommand() {
|
|
|
28
29
|
const mode = getMode();
|
|
29
30
|
const includePreview = isPreviewWorkflowsEnabled();
|
|
30
31
|
const includeBuild = isBuildWorkflowsEnabled();
|
|
32
|
+
const includeProfile = isProfileWorkflowsEnabled();
|
|
31
33
|
|
|
32
34
|
// Keep theme project files in sync (root files, package.json, .gitignore, VS Code tasks).
|
|
33
35
|
scaffoldThemeDevKit({
|
|
@@ -42,7 +44,7 @@ export async function updateCommand() {
|
|
|
42
44
|
scaffoldAiConfig(process.cwd(), { editors: getAiEditors() });
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
scaffoldWorkflows(mode, { includePreview, includeBuild });
|
|
47
|
+
scaffoldWorkflows(mode, { includePreview, includeBuild, includeProfile });
|
|
46
48
|
|
|
47
49
|
console.log(pc.bold(pc.green('\n Project files updated!\n')));
|
|
48
50
|
}
|
|
@@ -13,6 +13,7 @@ When you perform any of the following, **read and apply** the corresponding rule
|
|
|
13
13
|
- **Accessibility, a11y, focus, WCAG, UI behavior** → `accessibility-rules.mdc`
|
|
14
14
|
- **JavaScript, web components, _scripts/, *.js** → `javascript-standards.mdc`
|
|
15
15
|
- **Tailwind CSS, theme tokens, Liquid/CSS classes, _styles/** → `tailwindcss-rules.mdc`
|
|
16
|
+
- **Store performance, LCP/INP/CLS, head scripts, third-party tags, CSS/JS tiering, images, _head*/_body* snippets** → `performance-guide.mdc`
|
|
16
17
|
- **Liquid syntax and usage** → `liquid.mdc`
|
|
17
18
|
- **Section files (sections/*.liquid)** → `sections.mdc`
|
|
18
19
|
- **Snippets (snippets/*.liquid)** → `snippets.mdc`
|
|
@@ -52,6 +52,7 @@ These rules are synced from ~/.claude/rules/:
|
|
|
52
52
|
### Project-Specific Rules
|
|
53
53
|
These rules are unique to this project:
|
|
54
54
|
- `project-overview.mdc` - Electric Maybe theme overview
|
|
55
|
+
- `performance-guide.mdc` - Store performance architecture (CSS/JS tiering, third-party containment, LCP)
|
|
55
56
|
- `js-refactor-tasks.mdc` - Current refactoring tasks
|
|
56
57
|
|
|
57
58
|
## Important Notes
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Store performance architecture for our themes — CSS/JS tiering, third-party containment, images, Section API, and maintenance checklist
|
|
3
|
+
globs:
|
|
4
|
+
- "_scripts/**"
|
|
5
|
+
- "_styles/**"
|
|
6
|
+
- "snippets/_head*.liquid"
|
|
7
|
+
- "snippets/_body*.liquid"
|
|
8
|
+
- "layout/theme.liquid"
|
|
9
|
+
- "snippets/a--image*.liquid"
|
|
10
|
+
alwaysApply: false
|
|
11
|
+
---
|
|
12
|
+
# Store Performance Guide
|
|
13
|
+
|
|
14
|
+
Reference for how this theme optimizes storefront performance. Read this before adding scripts, third-party tags, CSS, or heavy Liquid to `<head>`.
|
|
15
|
+
|
|
16
|
+
## Goals & budgets
|
|
17
|
+
|
|
18
|
+
| Metric | Target | Primary levers |
|
|
19
|
+
|--------|--------|----------------|
|
|
20
|
+
| LCP | < 2.5s | Critical CSS, LCP preloads, font discipline, hero image sizing |
|
|
21
|
+
| INP / TBT | Low main-thread blocking | Deferred JS, idle third-parties, smaller `index.js` |
|
|
22
|
+
| CLS | Stable layout | Explicit image dimensions, `font-display: swap`, reserved space |
|
|
23
|
+
| Third-party cost | Contained | Resource blocker, `content_for_header` sanitization, deferred analytics |
|
|
24
|
+
|
|
25
|
+
**Source of truth for JS:** `_scripts/` → built to `assets/` via `npm run scripts:build`. Never edit `assets/*.js` directly.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Page load timeline
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
┌─ HEAD (critical path) ─────────────────────────────────────────────────────┐
|
|
33
|
+
│ 1. _head-resource-blocker Network + DOM patches (first script) │
|
|
34
|
+
│ 2. _head-urgent-script Geoblock / track-my-order redirect │
|
|
35
|
+
│ 3. _meta-tags, _head-seo Meta, favicons (low fetchpriority) │
|
|
36
|
+
│ 4. _product-preload-slider PDP: up to 5 slider images (portrait) │
|
|
37
|
+
│ 5. _index-preload-hero Homepage: mobile LCP hero preload │
|
|
38
|
+
│ 6. _head-style Fonts, :root tokens, critical.css, style.css │
|
|
39
|
+
│ 7. _script-variables window.theme, routes, icons SVG, Klaviyo cfg │
|
|
40
|
+
│ 8. debug-proxy (conditional) API proxy for preview/dev only │
|
|
41
|
+
│ 9. index.js [+ page bundles] Route-specific modules in head │
|
|
42
|
+
│10. speculationrules Conservative prerender for internal links │
|
|
43
|
+
│11. _head-console-filter Suppresses noisy third-party console (LH) │
|
|
44
|
+
│12. content_for_header Shopify apps — sanitized after capture │
|
|
45
|
+
│13. Post-header cleanup Removes blocked script/link nodes │
|
|
46
|
+
└────────────────────────────────────────────────────────────────────────────┘
|
|
47
|
+
┌─ BODY ─────────────────────────────────────────────────────────────────────┐
|
|
48
|
+
│ Main content (sections/snippets) │
|
|
49
|
+
│ _body-script → deferred.js (module, end of body) │
|
|
50
|
+
│ → scheduleIdle → deferred-integrations.js (conditional) │
|
|
51
|
+
│ → klaviyo-cart-tracking.js, hotjar-deferred.js │
|
|
52
|
+
└────────────────────────────────────────────────────────────────────────────┘
|
|
53
|
+
┌─ IDLE / INTERACTION ───────────────────────────────────────────────────────┐
|
|
54
|
+
│ Hotjar (8s idle or first scroll/click/touch/keydown) │
|
|
55
|
+
│ Rebuy product view analytics (PDP, 2s idle) │
|
|
56
|
+
│ Variant image prefetch (hover; touch: max 3 after 5s idle) │
|
|
57
|
+
│ cart.js injected when cart drawer opens │
|
|
58
|
+
│ electric-section lazy fetch (IntersectionObserver, often 1200px margin) │
|
|
59
|
+
└────────────────────────────────────────────────────────────────────────────┘
|
|
60
|
+
┌─ window.load ──────────────────────────────────────────────────────────────┐
|
|
61
|
+
│ AddShoppers referral widget (async) │
|
|
62
|
+
└────────────────────────────────────────────────────────────────────────────┘
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 1. CSS tiering
|
|
68
|
+
|
|
69
|
+
### Critical bundle (render-blocking)
|
|
70
|
+
|
|
71
|
+
- **Source:** `_styles/critical.css` → `assets/critical.css`
|
|
72
|
+
- **Scope:** Header + homepage hero shell only; narrow `@source` list (~12 Liquid files)
|
|
73
|
+
- **Loaded in:** `snippets/_head-style.liquid` via `stylesheet_tag`
|
|
74
|
+
|
|
75
|
+
When adding above-the-fold UI to header or index hero, ensure Tailwind classes are covered by critical `@source` paths or accept FOUC until full CSS loads.
|
|
76
|
+
|
|
77
|
+
### Full stylesheet (non-blocking)
|
|
78
|
+
|
|
79
|
+
- **Source:** `_styles/main.css` → `assets/style.css`
|
|
80
|
+
- **Pattern:** `media="print" onload="this.media='all'"` + `<noscript>` fallback
|
|
81
|
+
- **Also:** `rel="preload" as="style" fetchpriority="high"` for early discovery
|
|
82
|
+
|
|
83
|
+
Build: `npm run tailwind:build` or `tailwind:watch` (builds both critical + main).
|
|
84
|
+
|
|
85
|
+
### Theme tokens
|
|
86
|
+
|
|
87
|
+
- `_styles/theme-tokens.css` — shared `@theme` imported by both CSS entry points
|
|
88
|
+
- Inline `:root` variables in `_head-style.liquid` for fonts, spacing, buttons
|
|
89
|
+
|
|
90
|
+
### Disabled / deferred CSS ideas
|
|
91
|
+
|
|
92
|
+
- `content-visibility: auto` on sections — **commented out** in `_styles/main.css` (evaluate before enabling)
|
|
93
|
+
- Lazysizes CSS — removed; native `loading="lazy"` only
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 2. Font strategy
|
|
98
|
+
|
|
99
|
+
### Self-hosted fonts (no Typekit / Google Fonts on storefront)
|
|
100
|
+
|
|
101
|
+
| Family | File | Preload? | Usage |
|
|
102
|
+
|--------|------|----------|-------|
|
|
103
|
+
| Grosa Medium | `grosa-medium.woff2` | **Yes** | Above-the-fold body emphasis |
|
|
104
|
+
| Ivy Presto Headline Light | `ivypresto-headline-light.woff2` | **Yes** | Headings (`iheadline`) |
|
|
105
|
+
| Grosa Regular | `grosa-regular.woff2` | No | Body text; loads via `@font-face` |
|
|
106
|
+
|
|
107
|
+
All use `font-display: swap`.
|
|
108
|
+
|
|
109
|
+
### Font blocking
|
|
110
|
+
|
|
111
|
+
`_head-resource-blocker.liquid` + `layout/theme.liquid` sanitization block:
|
|
112
|
+
|
|
113
|
+
- Adobe Typekit (`use.typekit.net`, `p.typekit.net`)
|
|
114
|
+
- Google Fonts
|
|
115
|
+
- Shopify Font CDN webfonts (`fonts.shopifycdn.com`, Nunito)
|
|
116
|
+
- Klaviyo legacy brand fonts (`kl-custom-fonts`, Shopify Files `.woff`)
|
|
117
|
+
- Rebuy onsite CDN (`cdn.rebuyengine.com`, `/onsite/`) — **API** (`rebuyengine.com/api/*`) still allowed
|
|
118
|
+
|
|
119
|
+
### Preconnect budget
|
|
120
|
+
|
|
121
|
+
- `_head-typography.liquid`: `cdn.shopify.com`, `cdn.shopifycloud.com`; dns-prefetch `v.shopify.com`
|
|
122
|
+
- Unused preconnects removed: `fonts.shopifycdn.com`, `shop.app`
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## 3. JavaScript architecture
|
|
127
|
+
|
|
128
|
+
### Build system
|
|
129
|
+
|
|
130
|
+
**File:** `.climaybe/build-scripts.js` (via `npm run scripts:build`)
|
|
131
|
+
|
|
132
|
+
| Entry (`_scripts/`) | Output (`assets/`) | Loaded |
|
|
133
|
+
|---------------------|-------------------|--------|
|
|
134
|
+
| `main.js` | `index.js` | All pages, `<head>` module |
|
|
135
|
+
| `deferred.js` | `deferred.js` | End of `<body>` |
|
|
136
|
+
| `deferred-integrations.js` | `deferred-integrations.js` | Idle inject when DOM matches |
|
|
137
|
+
| `productpage.js` | `productpage.js` | PDP `<head>` |
|
|
138
|
+
| `product--variant-radios.js` | `product--variant-radios.js` | PDP `<head>` |
|
|
139
|
+
| `debug-proxy.js` | `debug-proxy.js` | Conditional `<head>` |
|
|
140
|
+
|
|
141
|
+
**Also in head (not in bundler — manual sync risk):**
|
|
142
|
+
|
|
143
|
+
- `collectionpage.js` ← `_scripts/collectionpage.js`
|
|
144
|
+
- `cart.js` ← `_scripts/cart.js`
|
|
145
|
+
|
|
146
|
+
**Per-section assets** (loaded with `defer` / `type="module"` from sections): `wishlist-feed.js`, `irl.js`, `reviews-custom.js`, `product--form.js`, `product-model.js`, `loading-banner.js`, etc.
|
|
147
|
+
|
|
148
|
+
Production build strips JSDoc and `console.*` from bundled output.
|
|
149
|
+
|
|
150
|
+
### `index.js` (critical bundle)
|
|
151
|
+
|
|
152
|
+
**Entry:** `_scripts/main.js`
|
|
153
|
+
|
|
154
|
+
Includes: `core-components.js`, `header.js`, `electric-section.js`, `electric-slider.js`, `electric-search.js`, `cart-dynamic.js`, `electric-cart-upsell-tabs.js`, pagination, checkout spinner delegation.
|
|
155
|
+
|
|
156
|
+
**Keep lean.** New features belong in `deferred.js`, page bundles, or lazy-loaded section scripts unless needed for first paint.
|
|
157
|
+
|
|
158
|
+
### `deferred.js` (body bundle)
|
|
159
|
+
|
|
160
|
+
**Entry:** `_scripts/deferred.js`
|
|
161
|
+
|
|
162
|
+
Always loads: product card scripts, quick ATC modal, Klaviyo cart tracking, Hotjar deferred, chat button visibility.
|
|
163
|
+
|
|
164
|
+
**Conditionally loads** `deferred-integrations.js` when DOM contains:
|
|
165
|
+
`electric-wish`, `electric-wish-counter`, `promo-banner`, `electric-rebuy-simplify`, `footer-newsletter`, `[data-gorgias-widget]`, `.js-promo-banner-text`
|
|
166
|
+
|
|
167
|
+
Uses `scheduleIdle(..., { timeout: 3000 })` from `_scripts/utils/perf.js`.
|
|
168
|
+
|
|
169
|
+
### `productpage.js` (PDP bundle)
|
|
170
|
+
|
|
171
|
+
Includes: gallery, video player, variant preview, variant prefetch, Rebuy product view, gift card sync.
|
|
172
|
+
|
|
173
|
+
### Idle scheduling utility
|
|
174
|
+
|
|
175
|
+
**Canonical:** `_scripts/utils/perf.js` → `scheduleIdle(callback, { timeout })`
|
|
176
|
+
|
|
177
|
+
Falls back to `setTimeout(1)` when `requestIdleCallback` unavailable.
|
|
178
|
+
|
|
179
|
+
**Consumers:** `deferred.js`, `hotjar-deferred.js`, `rebuy-product-view.js`, `product-variant-prefetch.js`, `recently-viewed.js`
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## 4. Third-party containment
|
|
184
|
+
|
|
185
|
+
### Layer 1: Resource blocker (runtime)
|
|
186
|
+
|
|
187
|
+
`snippets/_head-resource-blocker.liquid` — **must stay first in `<head>`**
|
|
188
|
+
|
|
189
|
+
Patches: `fetch`, `XHR`, `createElement`, `setAttribute`, DOM insertion, inline style mutation, `MutationObserver` for late injections.
|
|
190
|
+
|
|
191
|
+
### Layer 2: Header sanitization (Liquid)
|
|
192
|
+
|
|
193
|
+
`layout/theme.liquid` captures `content_for_header`, replaces blocked host strings with `blocked.invalid/*`, then runs a cleanup script.
|
|
194
|
+
|
|
195
|
+
### Layer 3: Deferred / conditional loading
|
|
196
|
+
|
|
197
|
+
| Service | File | Strategy |
|
|
198
|
+
|---------|------|----------|
|
|
199
|
+
| Hotjar | `_scripts/hotjar-deferred.js` | Idle 8s or first user interaction |
|
|
200
|
+
| Klaviyo cart events | `_scripts/klaviyo-cart-tracking.js` | `deferred.js`; listens `cart:added` |
|
|
201
|
+
| Rebuy analytics (PDP view) | `_scripts/rebuy-product-view.js` | Idle 2s on product pages |
|
|
202
|
+
| Rebuy recommendations | `_scripts/electric-rebuy-simplify.js` | Integrations bundle; IO `rootMargin: 1200px` |
|
|
203
|
+
| AddShoppers | `_head-script.liquid` | `window.load`, async, `loadCss: false` |
|
|
204
|
+
| Affirm | `affirm-messaging` web component | Lazy on first product form |
|
|
205
|
+
| Kimonix | `_body-script.liquid` | Async only if `kimonix_void_script` in header |
|
|
206
|
+
| Gorgias | App embed + `gorgias-widget.js` | App via Shopify; triggers deferred |
|
|
207
|
+
|
|
208
|
+
### Layer 4: Console filter (Lighthouse only)
|
|
209
|
+
|
|
210
|
+
`_head-console-filter.liquid` — suppresses known third-party `console.error/warn` patterns. **Do not rely on this for debugging** — disable mentally when investigating app bugs.
|
|
211
|
+
|
|
212
|
+
### Shopify app embeds (`config/settings_data.json`)
|
|
213
|
+
|
|
214
|
+
| App | Status | Notes |
|
|
215
|
+
|-----|--------|-------|
|
|
216
|
+
| Elevar (GTM/dataLayer) | Enabled | Primary tag orchestration — audit in GTM, not theme |
|
|
217
|
+
| Klaviyo onsite embed | Enabled | Fonts may be blocked; onsite JS from Shopify |
|
|
218
|
+
| Gorgias | Enabled | Chat |
|
|
219
|
+
| Photolock | Enabled | Image protection |
|
|
220
|
+
| Okendo, Checkout Blocks | Disabled | — |
|
|
221
|
+
|
|
222
|
+
### Inactive / dead integrations
|
|
223
|
+
|
|
224
|
+
| File | Status |
|
|
225
|
+
|------|--------|
|
|
226
|
+
| `snippets/_script-blocker.liquid` | Fully commented out |
|
|
227
|
+
| `snippets/_head-rebuy-stylesheet-blocker.liquid` | Not rendered |
|
|
228
|
+
| `snippets/_head-csp.liquid` | Not rendered |
|
|
229
|
+
| `snippets/_criteo-tracking.liquid` | Not rendered |
|
|
230
|
+
| `snippets/shoplift.liquid` | Commented out in `theme.liquid` |
|
|
231
|
+
| instant.page | Removed; `data-instant-allow-query-string` on body is legacy |
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## 5. Images & LCP
|
|
236
|
+
|
|
237
|
+
### Snippets
|
|
238
|
+
|
|
239
|
+
| Snippet | Behavior |
|
|
240
|
+
|---------|----------|
|
|
241
|
+
| `a--image.liquid` | Responsive `srcset`; LCP: `fetchpriority="high"`, `loading="eager"`, `decoding="async"`; else `loading="lazy"`; auto-lazy after 2nd section/item |
|
|
242
|
+
| `a--image-responsive.liquid` | `<picture>` breakpoints; LCP path avoids desktop 2x retina |
|
|
243
|
+
|
|
244
|
+
### Preloads
|
|
245
|
+
|
|
246
|
+
| Snippet | When | What |
|
|
247
|
+
|---------|------|------|
|
|
248
|
+
| `_index-preload-hero.liquid` | `request.page_type == 'index'` | Mobile 420w hero from `settings.homepage_lcp_image` |
|
|
249
|
+
| `_product-preload-slider-images.liquid` | PDP | Up to 5 portrait slider images, `imagesrcset` 480/960 |
|
|
250
|
+
| `_head-seo.liquid` | All | Favicons `fetchpriority="low"` |
|
|
251
|
+
|
|
252
|
+
### No lazysizes
|
|
253
|
+
|
|
254
|
+
Theme uses **native `loading="lazy"`** only. `unveilLazyloadIn()` in `core-components.js` is a no-op kept for API compatibility.
|
|
255
|
+
|
|
256
|
+
### Variant image prefetch (PDP)
|
|
257
|
+
|
|
258
|
+
`_scripts/product-variant-prefetch.js`:
|
|
259
|
+
|
|
260
|
+
- **Desktop:** hover/focus → fetch `?section_id=api--product-images&variant={id}` → preload image URLs from JSON payload
|
|
261
|
+
- **Touch:** after 5s idle, max **3** variants, images section only (no `product--main` fetch)
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## 6. Navigation & prefetch
|
|
266
|
+
|
|
267
|
+
### Speculation Rules (native prerender)
|
|
268
|
+
|
|
269
|
+
`snippets/_head-script.liquid` — `eagerness: "conservative"`
|
|
270
|
+
|
|
271
|
+
Prerenders same-origin links except account, cart, login. Do not add instant.page alongside this.
|
|
272
|
+
|
|
273
|
+
### electric-section (Section Rendering API)
|
|
274
|
+
|
|
275
|
+
`_scripts/electric-section.js`:
|
|
276
|
+
|
|
277
|
+
- Lazy load via `IntersectionObserver` (`data-onload`, configurable `data-onload-root-margin`)
|
|
278
|
+
- Fragment cache (max 10 entries)
|
|
279
|
+
- Events: `electric-section:prefetch`, `electric-section:fetch`
|
|
280
|
+
- Used for collection tabs, IRL, more-to-love, header fragments, etc.
|
|
281
|
+
|
|
282
|
+
### electric-pub (tab prefetch)
|
|
283
|
+
|
|
284
|
+
`_scripts/electric-pub.js` — prefetches tab content on hover/focus or intersection.
|
|
285
|
+
|
|
286
|
+
### Cart script on demand
|
|
287
|
+
|
|
288
|
+
`core-components.js` → `loadCartScriptOnce()` injects `cart.js` with `defer` when cart drawer opens (not on every page upfront).
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## 7. Shopify-specific patterns
|
|
293
|
+
|
|
294
|
+
### Route-conditional head scripts
|
|
295
|
+
|
|
296
|
+
From `_head-script.liquid`:
|
|
297
|
+
|
|
298
|
+
```liquid
|
|
299
|
+
index.js → all pages
|
|
300
|
+
productpage.js → product
|
|
301
|
+
product--variant-radios.js → product
|
|
302
|
+
collectionpage.js → collection, search
|
|
303
|
+
cart.js → cart template
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Geoblock
|
|
307
|
+
|
|
308
|
+
When `settings.geoblock_enabled`:
|
|
309
|
+
|
|
310
|
+
- `html.geoblock-pending` hides body until `/browsing_context_suggestions.json` returns
|
|
311
|
+
- Currently **disabled** in `settings_data.json`
|
|
312
|
+
- **Caveat:** fetch failure only warns — body can stay hidden if enabled without a fallback
|
|
313
|
+
|
|
314
|
+
### Fabric cart restoration
|
|
315
|
+
|
|
316
|
+
Inline in `_head-script.liquid` — cart/product template logic for `_fabric` localStorage flow.
|
|
317
|
+
|
|
318
|
+
### Debug API proxy
|
|
319
|
+
|
|
320
|
+
`_scripts/debug-proxy.js` — loads only when debug cookie, `?debug_proxy*`, or Shopify design mode. Rewrites proxy API fetch URLs.
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## 8. Klaviyo & analytics data flow
|
|
325
|
+
|
|
326
|
+
### Cart tracking (deferred)
|
|
327
|
+
|
|
328
|
+
1. `product--form.js` dispatches `cart:added` with cart payload
|
|
329
|
+
2. `klaviyo-cart-tracking.js` waits for `_learnq`, pushes `Added to Cart v3` + legacy `Added to Cart`
|
|
330
|
+
3. Product page context from `window.theme.klaviyoCart` in `_script-variables.liquid`
|
|
331
|
+
4. Cart page replays `cart:added` from `sessionStorage cart:justAdded`
|
|
332
|
+
|
|
333
|
+
### Viewed Product (not deferred)
|
|
334
|
+
|
|
335
|
+
`sections/product--icons-model.liquid` — large inline Klaviyo script when comparison section renders. **Candidate for future deferral.**
|
|
336
|
+
|
|
337
|
+
### Rebuy
|
|
338
|
+
|
|
339
|
+
- Widget CDN blocked; custom `electric-rebuy-simplify` uses API
|
|
340
|
+
- Product view POST deferred via `rebuy-product-view.js`
|
|
341
|
+
- Global helpers in `_script-variables.liquid` (`rebuyTrackEvent`, session storage for added variants)
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## 9. Adding new features — checklist
|
|
346
|
+
|
|
347
|
+
### New JavaScript
|
|
348
|
+
|
|
349
|
+
- [ ] Default to `_scripts/` source file, not `assets/`
|
|
350
|
+
- [ ] If needed on every page → consider `deferred.js` not `main.js`
|
|
351
|
+
- [ ] If third-party → defer (idle/interaction/load), never sync in head unless unavoidable
|
|
352
|
+
- [ ] If PDP-only → `productpage.js` or section `{% javascript %}`
|
|
353
|
+
- [ ] Add to `.climaybe/build-scripts.js` if new bundle entry
|
|
354
|
+
- [ ] Run `npm run scripts:build`
|
|
355
|
+
- [ ] Use `scheduleIdle` from `utils/perf.js` for non-critical init
|
|
356
|
+
- [ ] AbortController + cleanup in web components
|
|
357
|
+
|
|
358
|
+
### New third-party script
|
|
359
|
+
|
|
360
|
+
- [ ] Can it go through GTM with consent + idle trigger instead of theme?
|
|
361
|
+
- [ ] If app embed: expect `content_for_header` injection — add to blocker list if it conflicts (fonts, duplicate widgets)
|
|
362
|
+
- [ ] Add domain to `_head-console-filter` only if noise is unavoidable
|
|
363
|
+
- [ ] Never add sync analytics in `_head-script.liquid`
|
|
364
|
+
|
|
365
|
+
### New above-the-fold UI
|
|
366
|
+
|
|
367
|
+
- [ ] Extend `critical.css` `@source` if new classes needed at first paint
|
|
368
|
+
- [ ] LCP image: use `fetchpriority="high"`, explicit dimensions, preload in dedicated snippet
|
|
369
|
+
- [ ] Do not preload more than 2 fonts
|
|
370
|
+
|
|
371
|
+
### New section below fold
|
|
372
|
+
|
|
373
|
+
- [ ] `loading="lazy"` on images
|
|
374
|
+
- [ ] Consider `<electric-section data-onload>` instead of bundling in `index.js`
|
|
375
|
+
- [ ] Pass `lazyload: true` to product cards
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
## 10. Testing & measurement
|
|
380
|
+
|
|
381
|
+
### Before shipping perf changes
|
|
382
|
+
|
|
383
|
+
1. **Chrome DevTools** — Performance trace: LCP element, long tasks, third-party cost
|
|
384
|
+
2. **Network** — throttle Slow 4G; verify critical.css → first paint, style.css non-blocking
|
|
385
|
+
3. **Coverage** — how much of `index.js` executes on homepage vs PDP
|
|
386
|
+
4. **Block third-parties test** — block `googletagmanager.com`, `static.klaviyo.com`, `static.hotjar.com`; if TBT collapses, marketing stack is the bottleneck
|
|
387
|
+
5. **Theme Check** — `npm run lint:liquid`
|
|
388
|
+
6. **Build** — `npm run scripts:build && npm run tailwind:build`
|
|
389
|
+
|
|
390
|
+
### Debug modes
|
|
391
|
+
|
|
392
|
+
| URL param | Effect |
|
|
393
|
+
|-----------|--------|
|
|
394
|
+
| `?debug_typekit=1` | Typekit audit (`_head-typekit-debug`) |
|
|
395
|
+
| `?debug_rebuy=1` | Rebuy logging |
|
|
396
|
+
| `?debug_proxy=1` | API proxy prompt |
|
|
397
|
+
| `?debug_proxy_domain=` | Set proxy origin |
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## 11. Known gaps & maintenance risks
|
|
402
|
+
|
|
403
|
+
1. **`collectionpage.js` / `cart.js` not in build targets** — `_scripts` edits may not reach `assets/` until manual copy or build update
|
|
404
|
+
2. **Many section scripts outside bundler** — verify `assets/` sync when editing `_scripts/wishlist-feed.js`, etc.
|
|
405
|
+
3. **Stale LiquidDoc** in `_head-script.liquid`, `_head-urgent-script.liquid` — docs mention Rebuy head prefetch, instant.page, promo anti-flash that moved or were removed
|
|
406
|
+
4. **`index.js` size (~340KB+)** — largest theme-controlled bottleneck; split `core-components.js` further if TBT regresses
|
|
407
|
+
5. **GTM / Elevar** — largest real-world cost; theme cannot fix container bloat
|
|
408
|
+
6. **`_head-console-filter`** — masks errors; misleading during app debugging
|
|
409
|
+
7. **Klaviyo Viewed Product inline** — still heavy on PDP with icons model section
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## 12. File quick reference
|
|
414
|
+
|
|
415
|
+
| Concern | Primary files |
|
|
416
|
+
|---------|---------------|
|
|
417
|
+
| Load order | `layout/theme.liquid` |
|
|
418
|
+
| Resource blocking | `snippets/_head-resource-blocker.liquid` |
|
|
419
|
+
| CSS tiering | `snippets/_head-style.liquid`, `_styles/critical.css`, `_styles/main.css` |
|
|
420
|
+
| JS bootstrap | `snippets/_head-script.liquid`, `snippets/_body-script.liquid` |
|
|
421
|
+
| Globals | `snippets/_script-variables.liquid` |
|
|
422
|
+
| Build | `.climaybe/build-scripts.js`, `package.json` |
|
|
423
|
+
| Idle utils | `_scripts/utils/perf.js` |
|
|
424
|
+
| Images | `snippets/a--image.liquid`, `snippets/a--image-responsive.liquid` |
|
|
425
|
+
| LCP preloads | `snippets/_index-preload-hero.liquid`, `snippets/_product-preload-slider-images.liquid` |
|
|
426
|
+
| Section lazy load | `_scripts/electric-section.js` |
|
|
427
|
+
| Deferred 3P | `_scripts/hotjar-deferred.js`, `_scripts/klaviyo-cart-tracking.js`, `_scripts/rebuy-product-view.js` |
|
|
428
|
+
| App embeds | `config/settings_data.json` |
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## 13. Related rules
|
|
433
|
+
|
|
434
|
+
- `javascript-standards.mdc` — web components, layout thrashing, `scheduleIdle` patterns
|
|
435
|
+
- `tailwindcss-rules.mdc` — static classes, `@source` for critical CSS
|
|
436
|
+
- `project-overview.mdc` — performance targets (< 16ms init, 60fps), build outputs vs source (`_scripts/` / `_styles/`, never `assets/` directly)
|
package/src/lib/config.js
CHANGED
|
@@ -260,6 +260,14 @@ export function isCommitlintEnabled(cwd = process.cwd()) {
|
|
|
260
260
|
return config?.commitlint === true;
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Whether optional Liquid performance profile workflows are enabled.
|
|
265
|
+
*/
|
|
266
|
+
export function isProfileWorkflowsEnabled(cwd = process.cwd()) {
|
|
267
|
+
const config = readConfig(cwd);
|
|
268
|
+
return config?.profile_workflows === true;
|
|
269
|
+
}
|
|
270
|
+
|
|
263
271
|
/**
|
|
264
272
|
* Whether the bundled AI ruleset (rules, skills, subagents) was installed (init or add-cursor).
|
|
265
273
|
*/
|
package/src/lib/prompts.js
CHANGED
|
@@ -149,6 +149,20 @@ export async function promptBuildWorkflows() {
|
|
|
149
149
|
return !!enableBuildWorkflows;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Ask whether Liquid performance profile workflows should be scaffolded.
|
|
154
|
+
*/
|
|
155
|
+
export async function promptProfileWorkflows() {
|
|
156
|
+
const { enableProfileWorkflows } = await prompts({
|
|
157
|
+
type: 'confirm',
|
|
158
|
+
name: 'enableProfileWorkflows',
|
|
159
|
+
message: 'Enable Liquid performance profiling workflows? (TTFB measurement on main)',
|
|
160
|
+
initial: true,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return !!enableProfileWorkflows;
|
|
164
|
+
}
|
|
165
|
+
|
|
152
166
|
/**
|
|
153
167
|
* Ask whether Lighthouse CI should run as part of the build pipeline.
|
|
154
168
|
* Only meaningful when build workflows are enabled (Lighthouse runs after the build).
|
package/src/lib/workflows.js
CHANGED
|
@@ -37,7 +37,7 @@ function copyWorkflow(srcDir, fileName, destDir) {
|
|
|
37
37
|
* but for simplicity we track known filenames instead.
|
|
38
38
|
*/
|
|
39
39
|
function getKnownWorkflowFiles() {
|
|
40
|
-
const dirs = ['shared', 'single', 'multi', 'preview', 'build'];
|
|
40
|
+
const dirs = ['shared', 'single', 'multi', 'preview', 'build', 'profile'];
|
|
41
41
|
const files = new Set();
|
|
42
42
|
for (const dir of dirs) {
|
|
43
43
|
const dirPath = join(TEMPLATES_DIR, dir);
|
|
@@ -72,7 +72,7 @@ function cleanWorkflows(cwd = process.cwd()) {
|
|
|
72
72
|
* - Copies single/ or multi/ (+ single/) based on mode.
|
|
73
73
|
*/
|
|
74
74
|
export function scaffoldWorkflows(mode = 'single', options = {}, cwd = process.cwd()) {
|
|
75
|
-
const { includePreview = false, includeBuild = false } = options;
|
|
75
|
+
const { includePreview = false, includeBuild = false, includeProfile = false } = options;
|
|
76
76
|
const dest = ghWorkflowsDir(cwd);
|
|
77
77
|
mkdirSync(dest, { recursive: true });
|
|
78
78
|
|
|
@@ -113,9 +113,17 @@ export function scaffoldWorkflows(mode = 'single', options = {}, cwd = process.c
|
|
|
113
113
|
ensureBuildWorkflowDefaults(cwd);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
if (includeProfile) {
|
|
117
|
+
const profileDir = join(TEMPLATES_DIR, 'profile');
|
|
118
|
+
for (const f of listYmls(profileDir)) {
|
|
119
|
+
copyWorkflow(profileDir, f, dest);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
116
123
|
const total = readdirSync(dest).filter((f) => f.endsWith('.yml')).length;
|
|
117
124
|
console.log(pc.green(` Scaffolded ${total} workflow(s) → .github/workflows/`));
|
|
118
125
|
console.log(pc.dim(` Mode: ${mode}-store`));
|
|
119
126
|
console.log(pc.dim(` Preview workflows: ${includePreview ? 'enabled' : 'disabled'}`));
|
|
120
127
|
console.log(pc.dim(` Build workflows: ${includeBuild ? 'enabled' : 'disabled'}`));
|
|
128
|
+
console.log(pc.dim(` Profile workflows: ${includeProfile ? 'enabled' : 'disabled'}`));
|
|
121
129
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# climaybe — Liquid Performance (Optional Profile Package)
|
|
2
|
+
# Profiles Liquid rendering performance (TTFB) for core templates on main.
|
|
3
|
+
# Pushes a shared theme, measures TTFB for index/collection/product/cart,
|
|
4
|
+
# runs 4 iterations per template, reports the average of the last 3 runs.
|
|
5
|
+
|
|
6
|
+
name: Liquid Performance
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
branches: [main]
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
profile-gate:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
if: >
|
|
16
|
+
!contains(github.event.head_commit.message, '[hotfix-backport]')
|
|
17
|
+
&& !contains(github.event.head_commit.message, '[stores-to-root]')
|
|
18
|
+
&& !contains(github.event.head_commit.message, '[root-to-stores]')
|
|
19
|
+
outputs:
|
|
20
|
+
run_profile: ${{ steps.check.outputs.run_profile }}
|
|
21
|
+
env:
|
|
22
|
+
SHOPIFY_STORE_URL: ${{ secrets.SHOPIFY_STORE_URL }}
|
|
23
|
+
SHOPIFY_THEME_ACCESS_TOKEN: ${{ secrets.SHOPIFY_THEME_ACCESS_TOKEN }}
|
|
24
|
+
steps:
|
|
25
|
+
- name: Check if profiling should run
|
|
26
|
+
id: check
|
|
27
|
+
run: |
|
|
28
|
+
SKIP_REASONS=()
|
|
29
|
+
if [ -z "$SHOPIFY_STORE_URL" ]; then
|
|
30
|
+
SKIP_REASONS+=("Missing secret: SHOPIFY_STORE_URL")
|
|
31
|
+
fi
|
|
32
|
+
if [ -z "$SHOPIFY_THEME_ACCESS_TOKEN" ]; then
|
|
33
|
+
SKIP_REASONS+=("Missing secret: SHOPIFY_THEME_ACCESS_TOKEN")
|
|
34
|
+
fi
|
|
35
|
+
if [ ${#SKIP_REASONS[@]} -gt 0 ]; then
|
|
36
|
+
echo "run_profile=false" >> $GITHUB_OUTPUT
|
|
37
|
+
echo "Liquid profiling skipped:"
|
|
38
|
+
for reason in "${SKIP_REASONS[@]}"; do echo " - $reason"; done
|
|
39
|
+
else
|
|
40
|
+
echo "run_profile=true" >> $GITHUB_OUTPUT
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
liquid-profile:
|
|
44
|
+
needs: [profile-gate]
|
|
45
|
+
if: needs.profile-gate.outputs.run_profile == 'true'
|
|
46
|
+
uses: ./.github/workflows/reusable-liquid-profile.yml
|
|
47
|
+
secrets: inherit
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
name: Reusable Liquid Profile
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_call:
|
|
5
|
+
inputs:
|
|
6
|
+
iterations:
|
|
7
|
+
required: false
|
|
8
|
+
type: number
|
|
9
|
+
default: 4
|
|
10
|
+
description: "Total profiling iterations per template (first is discarded as warm-up)"
|
|
11
|
+
store_alias_secret:
|
|
12
|
+
required: false
|
|
13
|
+
type: string
|
|
14
|
+
description: "Upper snake-case alias for scoped secret (e.g. VOLDT_STAGING). If set, uses SHOPIFY_*_<this>; else uses SHOPIFY_STORE_URL / SHOPIFY_THEME_ACCESS_TOKEN."
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
profile:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
steps:
|
|
20
|
+
- name: Checkout code
|
|
21
|
+
uses: actions/checkout@v4
|
|
22
|
+
|
|
23
|
+
- name: Validate theme root
|
|
24
|
+
run: |
|
|
25
|
+
if [ ! -f "layout/theme.liquid" ]; then
|
|
26
|
+
echo "layout/theme.liquid not found. Ensure workflow runs at theme repository root."
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
- name: Install Shopify CLI
|
|
31
|
+
run: npm install -g @shopify/cli @shopify/theme
|
|
32
|
+
|
|
33
|
+
- name: Share preview theme
|
|
34
|
+
id: share
|
|
35
|
+
env:
|
|
36
|
+
SHOPIFY_STORE_URL: ${{ inputs.store_alias_secret && secrets[format('SHOPIFY_STORE_URL_{0}', inputs.store_alias_secret)] || secrets.SHOPIFY_STORE_URL }}
|
|
37
|
+
SHOPIFY_THEME_ACCESS_TOKEN: ${{ inputs.store_alias_secret && secrets[format('SHOPIFY_THEME_ACCESS_TOKEN_{0}', inputs.store_alias_secret)] || secrets.SHOPIFY_THEME_ACCESS_TOKEN }}
|
|
38
|
+
run: |
|
|
39
|
+
if [ -z "$SHOPIFY_STORE_URL" ] || [ -z "$SHOPIFY_THEME_ACCESS_TOKEN" ]; then
|
|
40
|
+
echo "Missing Shopify secrets."
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
OUTPUT=$(shopify theme share \
|
|
45
|
+
--store "$SHOPIFY_STORE_URL" \
|
|
46
|
+
--password "$SHOPIFY_THEME_ACCESS_TOKEN" 2>&1)
|
|
47
|
+
STATUS=$?
|
|
48
|
+
|
|
49
|
+
echo "$OUTPUT"
|
|
50
|
+
if [ $STATUS -ne 0 ]; then
|
|
51
|
+
echo "Theme share failed."
|
|
52
|
+
exit $STATUS
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
THEME_ID=$(echo "$OUTPUT" | sed -n 's/.*#\([0-9]*\).*/\1/p' | head -1)
|
|
56
|
+
if [ -z "$THEME_ID" ]; then
|
|
57
|
+
echo "Could not parse theme id from share output."
|
|
58
|
+
exit 1
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
echo "theme_id=$THEME_ID" >> $GITHUB_OUTPUT
|
|
62
|
+
echo "Shared theme ID: $THEME_ID"
|
|
63
|
+
|
|
64
|
+
- name: Resolve profiling URLs
|
|
65
|
+
id: urls
|
|
66
|
+
run: |
|
|
67
|
+
URLS_JSON='[
|
|
68
|
+
{"template": "index", "path": "/"},
|
|
69
|
+
{"template": "collection", "path": "/collections"},
|
|
70
|
+
{"template": "product", "path": "/products"},
|
|
71
|
+
{"template": "cart", "path": "/cart"}
|
|
72
|
+
]'
|
|
73
|
+
echo "urls=$URLS_JSON" >> $GITHUB_OUTPUT
|
|
74
|
+
|
|
75
|
+
- name: Profile Liquid performance
|
|
76
|
+
id: profile
|
|
77
|
+
env:
|
|
78
|
+
SHOPIFY_STORE_URL: ${{ inputs.store_alias_secret && secrets[format('SHOPIFY_STORE_URL_{0}', inputs.store_alias_secret)] || secrets.SHOPIFY_STORE_URL }}
|
|
79
|
+
SHOPIFY_CLI_THEME_TOKEN: ${{ inputs.store_alias_secret && secrets[format('SHOPIFY_THEME_ACCESS_TOKEN_{0}', inputs.store_alias_secret)] || secrets.SHOPIFY_THEME_ACCESS_TOKEN }}
|
|
80
|
+
THEME_ID: ${{ steps.share.outputs.theme_id }}
|
|
81
|
+
ITERATIONS: ${{ inputs.iterations }}
|
|
82
|
+
run: |
|
|
83
|
+
STORE=$(echo "$SHOPIFY_STORE_URL" | sed 's|^https\?://||; s|/.*||')
|
|
84
|
+
REPORT_FILE="liquid-profile-report.md"
|
|
85
|
+
RESULTS_JSON="[]"
|
|
86
|
+
|
|
87
|
+
TEMPLATES=("index" "collection" "product" "cart")
|
|
88
|
+
PATHS=("/" "/collections" "/products" "/cart")
|
|
89
|
+
|
|
90
|
+
echo "## Liquid Performance Report" > "$REPORT_FILE"
|
|
91
|
+
echo "" >> "$REPORT_FILE"
|
|
92
|
+
echo "| Template | Avg TTFB (last 3) | Run 1 (warm-up) | Run 2 | Run 3 | Run 4 |" >> "$REPORT_FILE"
|
|
93
|
+
echo "|----------|-------------------|-----------------|-------|-------|-------|" >> "$REPORT_FILE"
|
|
94
|
+
|
|
95
|
+
HAS_ERROR=false
|
|
96
|
+
|
|
97
|
+
for i in "${!TEMPLATES[@]}"; do
|
|
98
|
+
TEMPLATE="${TEMPLATES[$i]}"
|
|
99
|
+
URL_PATH="${PATHS[$i]}"
|
|
100
|
+
echo ""
|
|
101
|
+
echo "=== Profiling $TEMPLATE ($URL_PATH) ==="
|
|
102
|
+
|
|
103
|
+
TIMES=()
|
|
104
|
+
ALL_TIMES_DISPLAY=()
|
|
105
|
+
|
|
106
|
+
for ((iter=1; iter<=ITERATIONS; iter++)); do
|
|
107
|
+
echo " Iteration $iter/$ITERATIONS for $TEMPLATE..."
|
|
108
|
+
|
|
109
|
+
START_MS=$(date +%s%N)
|
|
110
|
+
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
|
|
111
|
+
--connect-timeout 30 \
|
|
112
|
+
--max-time 60 \
|
|
113
|
+
"https://${STORE}${URL_PATH}?preview_theme_id=${THEME_ID}" \
|
|
114
|
+
-H "Accept: text/html") || true
|
|
115
|
+
END_MS=$(date +%s%N)
|
|
116
|
+
|
|
117
|
+
TTFB_MS=$(( (END_MS - START_MS) / 1000000 ))
|
|
118
|
+
echo " HTTP $HTTP_CODE — TTFB: ${TTFB_MS}ms"
|
|
119
|
+
|
|
120
|
+
TIMES+=("$TTFB_MS")
|
|
121
|
+
ALL_TIMES_DISPLAY+=("${TTFB_MS}ms")
|
|
122
|
+
|
|
123
|
+
if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "301" ] && [ "$HTTP_CODE" != "302" ]; then
|
|
124
|
+
echo " Warning: unexpected HTTP status $HTTP_CODE"
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
sleep 2
|
|
128
|
+
done
|
|
129
|
+
|
|
130
|
+
WARMUP="${ALL_TIMES_DISPLAY[0]}"
|
|
131
|
+
|
|
132
|
+
if [ ${#TIMES[@]} -ge 4 ]; then
|
|
133
|
+
SUM=0
|
|
134
|
+
COUNT=0
|
|
135
|
+
for ((j=1; j<${#TIMES[@]}; j++)); do
|
|
136
|
+
SUM=$((SUM + TIMES[j]))
|
|
137
|
+
COUNT=$((COUNT + 1))
|
|
138
|
+
done
|
|
139
|
+
AVG=$((SUM / COUNT))
|
|
140
|
+
else
|
|
141
|
+
SUM=0
|
|
142
|
+
for t in "${TIMES[@]}"; do
|
|
143
|
+
SUM=$((SUM + t))
|
|
144
|
+
done
|
|
145
|
+
AVG=$((SUM / ${#TIMES[@]}))
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
RUN2="${ALL_TIMES_DISPLAY[1]:-n/a}"
|
|
149
|
+
RUN3="${ALL_TIMES_DISPLAY[2]:-n/a}"
|
|
150
|
+
RUN4="${ALL_TIMES_DISPLAY[3]:-n/a}"
|
|
151
|
+
|
|
152
|
+
echo "| \`$TEMPLATE\` | **${AVG}ms** | $WARMUP | $RUN2 | $RUN3 | $RUN4 |" >> "$REPORT_FILE"
|
|
153
|
+
|
|
154
|
+
RESULTS_JSON=$(echo "$RESULTS_JSON" | jq --arg t "$TEMPLATE" --argjson avg "$AVG" \
|
|
155
|
+
--argjson times "$(printf '%s\n' "${TIMES[@]}" | jq -s '.')" \
|
|
156
|
+
'. + [{"template": $t, "avg_ms": $avg, "times_ms": $times}]')
|
|
157
|
+
|
|
158
|
+
echo " → $TEMPLATE average (last 3): ${AVG}ms"
|
|
159
|
+
done
|
|
160
|
+
|
|
161
|
+
echo "" >> "$REPORT_FILE"
|
|
162
|
+
echo "_${ITERATIONS} iterations per template; average excludes the first (warm-up) run._" >> "$REPORT_FILE"
|
|
163
|
+
echo "_Theme ID: ${THEME_ID} | Store: ${STORE}_" >> "$REPORT_FILE"
|
|
164
|
+
|
|
165
|
+
echo ""
|
|
166
|
+
echo "=== Report ==="
|
|
167
|
+
cat "$REPORT_FILE"
|
|
168
|
+
|
|
169
|
+
{
|
|
170
|
+
echo "report<<REPORT_EOF"
|
|
171
|
+
cat "$REPORT_FILE"
|
|
172
|
+
echo "REPORT_EOF"
|
|
173
|
+
} >> $GITHUB_OUTPUT
|
|
174
|
+
|
|
175
|
+
echo "results_json=$(echo "$RESULTS_JSON" | jq -c '.')" >> $GITHUB_OUTPUT
|
|
176
|
+
|
|
177
|
+
- name: Post results as job summary
|
|
178
|
+
env:
|
|
179
|
+
REPORT: ${{ steps.profile.outputs.report }}
|
|
180
|
+
run: |
|
|
181
|
+
echo "$REPORT" >> $GITHUB_STEP_SUMMARY
|
|
182
|
+
|
|
183
|
+
- name: Cleanup preview theme
|
|
184
|
+
if: always()
|
|
185
|
+
env:
|
|
186
|
+
SHOPIFY_STORE_URL: ${{ inputs.store_alias_secret && secrets[format('SHOPIFY_STORE_URL_{0}', inputs.store_alias_secret)] || secrets.SHOPIFY_STORE_URL }}
|
|
187
|
+
SHOPIFY_THEME_ACCESS_TOKEN: ${{ inputs.store_alias_secret && secrets[format('SHOPIFY_THEME_ACCESS_TOKEN_{0}', inputs.store_alias_secret)] || secrets.SHOPIFY_THEME_ACCESS_TOKEN }}
|
|
188
|
+
THEME_ID: ${{ steps.share.outputs.theme_id }}
|
|
189
|
+
run: |
|
|
190
|
+
if [ -z "$THEME_ID" ]; then
|
|
191
|
+
echo "No theme ID to clean up."
|
|
192
|
+
exit 0
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
echo "Deleting preview theme $THEME_ID..."
|
|
196
|
+
shopify theme delete --theme "$THEME_ID" \
|
|
197
|
+
--store "$SHOPIFY_STORE_URL" \
|
|
198
|
+
--password "$SHOPIFY_THEME_ACCESS_TOKEN" \
|
|
199
|
+
--force 2>&1 || echo "Theme cleanup failed (non-fatal)."
|