@fluid-app/fluid-cli-theme-dev 0.1.20 → 0.1.22

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.
@@ -0,0 +1,707 @@
1
+ ---
2
+ name: themes-review
3
+ description: |
4
+ Iterative reviewer and fixer for Fluid Liquid themes. Themes live in their own Git repos
5
+ (e.g. base-theme) and sync to the Fluid API via the `fluid theme` CLI. Use when reviewing
6
+ a theme PR, auditing a section/component, or working through a theme to fix bad code and
7
+ schema. Companion to the `themes` skill — that one teaches how to *build*, this one
8
+ teaches how to *review and fix*. Works **one section at a time**: review it, fix it,
9
+ re-run `fluid theme lint --json`, and repeat until that section is clean before moving on,
10
+ pausing once in a while to ask the user whether to continue, skip, or stop. Covers: the
11
+ schema validator, every valid setting `type`, singular vs plural selector misuse (e.g.
12
+ multiple `product` settings → one `product_list`), the blocks-first content model, Liquid
13
+ correctness, performance, security, accessibility, and file-size/DRY thresholds. Never
14
+ push to the live theme API without explicit approval.
15
+ ---
16
+
17
+ # Theme Review & Fix
18
+
19
+ This skill is the reviewer counterpart to `themes`. It is opinionated and adversarial: every check has a fix, and every fix has a severity. The agent **edits theme files in place** to fix them (every change is reversible — the user can review or undo it), but it **never writes to the live theme API without explicit approval**.
20
+
21
+ ## How to work — one section at a time
22
+
23
+ The goal is to get the theme healthy **one section at a time** — not to fix the whole theme in a single sweep. Pick the smallest unit (a single section, component, or block file) and stay on it until it's right before touching anything else.
24
+
25
+ For each unit:
26
+
27
+ 1. **Read it** and run every relevant check below — structure, schema, blocks-first content model, selector singular→list, Liquid correctness, performance, security, accessibility.
28
+ 2. **Fix** the findings, one focused change at a time, highest severity first (`blocker` → `should` → `nit`).
29
+ 3. **Re-validate** with `fluid theme lint --json` after each change. Parse the JSON (don't eyeball the text), fix what it flags, and run it again. Keep looping until that file is clean.
30
+ 4. **Confirm the unit is right** — lint is clean _and_ the relevant checklist boxes pass — then **move on** to the next unit.
31
+
32
+ ### Check in with the user
33
+
34
+ Don't run end to end silently. **Once in a while — after finishing a section, or before starting a large or risky change — pause and ask the user how to proceed:** continue to the next section, skip this one, stop here, or look at what changed. Default to pausing at natural boundaries (a section just finished, you're about to touch many files, or you're about to make a change that isn't a clear-cut validator error) rather than interrupting mid-fix.
35
+
36
+ ## Where themes actually live
37
+
38
+ Themes are **separate Git repos**, one repo per theme. The reference starter is `git@github.com:fluid-commerce/base-theme.git`. They are not part of the fluid Rails monorepo and not part of fluid-mono — fluid-mono only ships the _tooling_ (CLI + validator) that operates on them.
39
+
40
+ A typical theme repo looks like this (verified against the `base-theme` starter cloned by `fluid theme init`):
41
+
42
+ ```
43
+ my-theme/
44
+ ├── assets/ ← CSS, JS, images, video — flat at root
45
+ ├── components/{name}/ ← shared partials, at root (no templates/ wrapper)
46
+ │ ├── pagination/index.liquid
47
+ │ ├── cart_template/index.liquid
48
+ │ └── ...
49
+ ├── config/
50
+ │ ├── settings_schema.json ← theme-wide settings
51
+ │ └── settings_data.json ← compiled preset values (managed; rarely hand-edited)
52
+ ├── home_page/default/index.liquid ← page templates: {type}/{variant}/index.liquid
53
+ ├── product/default/index.liquid
54
+ ├── cart_page/default/index.liquid
55
+ ├── collection/default/index.liquid
56
+ ├── collection_page/default/index.liquid
57
+ ├── enrollment_pack/default/index.liquid
58
+ ├── join_page/default/index.liquid
59
+ ├── page/default/index.liquid
60
+ ├── library/default/index.liquid
61
+ ├── navbar/default/index.liquid
62
+ ├── footer/default/index.liquid
63
+ ├── medium/default/index.liquid
64
+ ├── layouts/
65
+ │ └── theme.liquid ← flat layout files, no wrapping dir
66
+ ├── locales/
67
+ │ ├── en.json
68
+ │ ├── de.json
69
+ │ └── ... ← flat one-file-per-locale
70
+ ├── sections/{name}/ ← reusable sections, at root
71
+ │ ├── hero_section/index.liquid
72
+ │ ├── cta_banner/index.liquid
73
+ │ └── ...
74
+ ├── blocks/{name}/ ← standalone (reusable) block templates, at root
75
+ │ ├── review_card/index.liquid
76
+ │ └── ...
77
+ ├── styleguide/index.liquid ← optional dev-only preview
78
+ ├── cover.png ← theme thumbnail for the editor picker
79
+ ├── global_styles.css ← optional theme-wide CSS at root
80
+ ├── .fluid-theme.json ← CLI metadata (themeId, company, checksums)
81
+ ├── .fluidignore ← like .gitignore
82
+ └── README.md
83
+ ```
84
+
85
+ **There is no `templates/` wrapper.** Sections, components, standalone blocks, and page-type folders each sit in their own directory at the theme root — the directory name _is_ the artifact type (`sections/`, `components/`, `blocks/`, `layouts/`, plus page types like `home_page/`, `product/`, `cart_page/`). A hand-rolled `templates/sections/...` or `templates/blocks/...` path is wrong; flatten it to the root.
86
+
87
+ `.fluid-theme.json` is the one piece of CLI state worth reading. It is not human-edited, but it's helpful to look at: it records the remote theme ID, the company subdomain, and a SHA256 checksum per file (the CLI uses these to detect remote drift on push). When a review needs to know which remote theme a repo is wired to, this is the file to check.
88
+
89
+ ## Directory & file structure — validate first
90
+
91
+ Before reading any Liquid, walk the repo root. `fluid theme lint --json` only checks schema JSON — it doesn't enforce layout. The reviewer enforces layout.
92
+
93
+ A valid theme is recognized by the presence of an `assets/` or `config/` directory. In practice every published theme has `assets/`, `config/`, `layouts/`, and the page-type and section folders below.
94
+
95
+ ### Required at the repo root
96
+
97
+ | Path | Severity if missing | Notes |
98
+ | ----------------------------- | --------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------- |
99
+ | `config/settings_schema.json` | `blocker` for any new theme | Theme-wide settings. Even an empty `[]` is acceptable; absence is not. |
100
+ | `layouts/theme.liquid` | `blocker` if the theme renders any page | At least one layout must exist for pages to render. |
101
+ | `assets/` | `should` | Almost every theme needs at least images. |
102
+ | `locales/en.json` | `should` | If the theme uses `{{ 'foo' | t }}`translations anywhere, this is`blocker`. |
103
+ | `cover.png` | `nit` | Used by the editor as the theme thumbnail. |
104
+ | `README.md` | `nit` | Document install / deploy. |
105
+ | `.fluidignore` | `nit` | Exclude `node_modules`, `.DS_Store`, etc. |
106
+
107
+ ### Canonical paths per artifact
108
+
109
+ Every artifact has a fixed depth and shape. **No deeper nesting is allowed under any of these.**
110
+
111
+ | Artifact | Canonical path | Example | Multiple variants? |
112
+ | ----------------- | ----------------------------------------------------------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
113
+ | Page templates | `{page_type}/{variant}/index.liquid` | `home_page/default/index.liquid`, `library/revised/index.liquid` | **Yes** — `default` is conventional, but a page type can have any number of variant directories. |
114
+ | Sections | `sections/{name}/index.liquid` | `sections/hero_section/index.liquid` | No variants. One directory per section type. |
115
+ | Components | `components/{name}/index.liquid` | `components/button/index.liquid` | No variants. |
116
+ | Standalone blocks | `blocks/{name}/index.liquid` | `blocks/review_card/index.liquid` | No variants. Reusable block templates a section opts into via `@theme` or a named-block reference. |
117
+ | Layouts | `layouts/{name}.liquid` (flat file, no wrapping dir) | `layouts/theme.liquid` | Multiple layouts possible; each is a flat `.liquid` file. |
118
+ | Global config | `config/settings_schema.json`, `config/settings_data.json` (flat) | — | — |
119
+ | Locales | `locales/{lang}.json` (flat) | `locales/en.json`, `locales/de.json` | — |
120
+ | Assets | `assets/{file}` (flat) | `assets/featured.css`, `assets/logo.svg` | No sub-folders. |
121
+ | Root-level CSS | `global_styles.css` at the theme root | — | Optional theme-wide stylesheet. |
122
+ | Theme thumbnail | `cover.png` at the theme root | — | — |
123
+ | Styleguide | `styleguide/index.liquid` | — | Optional dev-only preview. |
124
+
125
+ **Page-type folder names recognized** (from the base-theme + reference theme):
126
+ `home_page`, `product`, `cart_page`, `collection`, `collection_page`, `category`, `category_page`, `enrollment_pack`, `join_page`, `page`, `post`, `post_page`, `library`, `medium`, `navbar`, `footer`, `shop_page`, plus any others the engine adds. Each one holds **one or more variant directories** (`default/`, `revised/`, `holiday/`, etc.), each containing an `index.liquid`.
127
+
128
+ **Blocks come in two shapes — prefer standalone.** Fluid themes are **blocks-first** (see [Blocks vs. sections](references/blocks-vs-sections.md)), and standalone blocks are the default shape.
129
+
130
+ 1. **Standalone (theme) blocks** — their own template at `blocks/{name}/index.liquid`, reusable across sections. A section opts in by declaring `@theme` (accept any theme block) or a named-block reference (a `{ "type": "<name>" }` entry with no `name`/`settings`) in its schema's `"blocks"` array; the engine resolves that to the matching `blocks/<name>/index.liquid` template. **This is the preferred shape for almost everything** — reusable, testable on its own, and it keeps the section to a few whole-section settings while each element's settings live on its block.
131
+ 2. **Inline blocks** — defined inside a section's `{% schema %}` `"blocks": [...]` array, local to that one section. **Use only when the block is very small and genuinely one-off.** Extracting an inline block into `blocks/` is always a valid, encouraged recommendation.
132
+
133
+ Blocks can also nest (a block declaring its own `"blocks"`), up to the engine's depth limit. What is **not** allowed is nesting block files _under_ a section or page-variant directory (`sections/x/blocks/...`) — standalone blocks live in the top-level `blocks/` directory, parallel to `sections/` and `components/`.
134
+
135
+ ### The depth rule
136
+
137
+ **No artifact nests deeper than the canonical path above.** Specifically:
138
+
139
+ - ✗ `sections/main_product/blocks/breadcrumb/index.liquid` (block file nested under a section — extract it to top-level `blocks/breadcrumb/index.liquid` instead)
140
+ - ✗ `components/buttons/primary/index.liquid` (sub-grouped components)
141
+ - ✗ `assets/icons/social/twitter.svg` (assets in sub-folders)
142
+ - ✗ `home_page/default/blocks/hero/index.liquid` (block file under a page variant — move to top-level `blocks/hero/index.liquid`)
143
+ - ✓ `sections/main_product/index.liquid`
144
+ - ✓ `blocks/breadcrumb/index.liquid` (standalone block at the top level)
145
+ - ✓ `components/button_primary/index.liquid` (rename to flatten)
146
+ - ✓ `assets/icon-social-twitter.svg` (rename to flatten)
147
+
148
+ If you feel the urge to nest, rename: `assets/icon-social-twitter.svg`, `components/card_product/index.liquid`, etc.
149
+
150
+ ### Layout requirements
151
+
152
+ Every layout file under `layouts/` **must emit both magic drops** for the engine to inject head content and page content:
153
+
154
+ - `{{ content_for_header }}` — placed inside `<head>`. The engine injects meta tags, analytics, editor-mode scripts, asset preloads, and the FairShare runtime initialization here.
155
+ - `{{ content_for_layout }}` — placed where the page content goes (inside `<body>`, typically inside the main container). The selected page template renders into this slot.
156
+
157
+ A layout missing either one is broken. Missing `content_for_header` breaks editor mode, analytics, and head-injected assets. Missing `content_for_layout` renders an empty page.
158
+
159
+ ```liquid
160
+ <!doctype html>
161
+ <html lang="{{ request.locale.iso_code }}">
162
+ <head>
163
+ <meta charset="utf-8">
164
+ <meta name="viewport" content="width=device-width, initial-scale=1">
165
+ <title>{{ page_title }}</title>
166
+
167
+ {{ 'theme.css' | asset_url | stylesheet_tag }}
168
+
169
+ {{ content_for_header }} {# ← required #}
170
+ </head>
171
+ <body>
172
+ {% section 'main_navbar' %}
173
+
174
+ <main>
175
+ {{ content_for_layout }} {# ← required #}
176
+ </main>
177
+
178
+ {% section 'main_footer' %}
179
+ </body>
180
+ </html>
181
+ ```
182
+
183
+ ### Findings to surface
184
+
185
+ | You see | Severity | Fix |
186
+ | -------------------------------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
187
+ | Section file outside `sections/{name}/index.liquid` | `blocker` | Move it. The `{% section 'x' %}` resolver only looks in `sections/`. |
188
+ | Component file with a `{% schema %}` block | `blocker` | Components don't have schemas. Move to `sections/`, or strip the schema. |
189
+ | Section file without `{% schema %}` | `blocker` | A section without a schema is just markup — make it a component (move to `components/{name}/index.liquid`). |
190
+ | Page template at `{type}/index.liquid` (missing variant dir) | `blocker` | Wrap in a variant directory: `{type}/default/index.liquid`. |
191
+ | Asset (`.css`, `.js`, image) outside `assets/` or `global_styles.css` / `cover.png` at root | `blocker` | Move to `assets/` and reference via `\| asset_url`. |
192
+ | Sub-folder inside `assets/` (e.g. `assets/icons/`) | `should` | Flatten: rename file to `assets/icon-{whatever}.svg`. |
193
+ | Sub-folder inside `sections/{name}/` or `components/{name}/` (e.g. `sections/hero/blocks/`) | `blocker` | Sections and components don't nest. A reusable block belongs in the top-level `blocks/{name}/` directory, not under a section. |
194
+ | Co-located `styles.css` / `style.css` next to a section, component, or page template variant | `blocker` for new files, `should` for existing | **Co-located stylesheets are deprecated** — all CSS lives under `assets/`. See [CSS hygiene §1a](references/css-js-hygiene.md). |
195
+ | Hand-rolled `templates/sections/...` or `templates/components/...` paths | `blocker` | The base-theme convention has no `templates/` wrapper. Move to root-level `sections/` / `components/`. |
196
+ | `config/settings_schema.json` missing | `blocker` for new themes | Create it. Start with `[]` if no global settings. |
197
+ | Filename casing mismatch (e.g. `ProductCard/index.liquid`) | `should` | Directory and filenames should be `snake_case`. The engine is case-sensitive on most filesystems. |
198
+ | Two sections with the same directory name in different paths | `blocker` | Section types must be globally unique. |
199
+ | `{% section 'foo' %}` referencing a missing `sections/foo/index.liquid` | `blocker` | Validator catches this — but flag it explicitly with the fix. |
200
+ | Files committed under `assets/` but never referenced | `nit` → `should` | Dead assets bloat downloads. Run the dead-asset grep in [Dead code](references/dead-code.md). |
201
+
202
+ ### Quick structural audit
203
+
204
+ From the theme repo root, this gives a one-shot health view:
205
+
206
+ ```bash
207
+ # Required files
208
+ [ -f config/settings_schema.json ] && echo "OK config/settings_schema.json" || echo "MISSING config/settings_schema.json"
209
+ [ -f layouts/theme.liquid ] && echo "OK layouts/theme.liquid" || echo "MISSING layouts/theme.liquid"
210
+
211
+ # Sections without schemas
212
+ for f in sections/*/index.liquid; do
213
+ grep -q '{% schema %}' "$f" || echo "NO SCHEMA $f"
214
+ done
215
+
216
+ # Components with schemas (anti-pattern)
217
+ for f in components/*/index.liquid; do
218
+ grep -q '{% schema %}' "$f" && echo "HAS SCHEMA $f"
219
+ done
220
+
221
+ # Anything nested deeper than 2 dirs from theme root (excluding hidden dirs)
222
+ find . -mindepth 4 -type f -not -path '*/.*' \
223
+ -not -path './layouts/*' -not -path './assets/*' -not -path './locales/*' -not -path './config/*'
224
+
225
+ # Deprecated co-located stylesheets
226
+ find . -type f \( -name 'styles.css' -o -name 'style.css' \) \
227
+ -not -path './assets/*' -not -path './.git/*'
228
+
229
+ # Hand-rolled `templates/` wrapper (wrong convention)
230
+ [ -d templates ] && echo "WRONG templates/ wrapper found — flatten to root"
231
+ ```
232
+
233
+ If any line of output is unexpected, raise it as a finding before reading the diff line-by-line.
234
+
235
+ ---
236
+
237
+ ## The tooling — `fluid theme` CLI
238
+
239
+ The `fluid theme` command group is how a theme repo syncs with the Fluid API. The reviewer references these commands so the author can reproduce checks locally — it never runs the mutating ones (`push`, `pull`) itself.
240
+
241
+ | Command | What it does | When the reviewer references it |
242
+ | ---------------------- | ----------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
243
+ | `fluid theme init` | Clone the base theme as a new repo | Suggest in comments when a PR is creating a new section from scratch with no structure |
244
+ | `fluid theme dev` | Local dev server on `:9292` with hot-reload, proxied to `{company}.fluid.app` | Suggest when debugging visual issues |
245
+ | `fluid theme push` | Validates the schema, then uploads changed files. **Validation runs automatically unless `--force`.** | Suggest before merge |
246
+ | `fluid theme pull` | Download remote theme to disk | Mention when local is stale |
247
+ | `fluid theme lint --json` | Read-only schema validator. **The agent should run/reference this in every review.** | Always reference: "this is what `fluid theme lint --json` would flag" |
248
+ | `fluid theme navigate` | Opens browser to the dev theme editor | Mention for visual verification |
249
+
250
+ **`fluid theme lint --json` is the official self-check.** Any finding the validator surfaces (see next section) maps directly to a `blocker` severity in your review — it would fail validation.
251
+
252
+ ---
253
+
254
+ ## Operating Rules
255
+
256
+ 1. **One section at a time; fix, re-lint, repeat.** Stay on a single section / component / block until it's clean before moving on. See [How to work](#how-to-work--one-section-at-a-time).
257
+ 2. **No writes to the live API without approval.** When fixing locally you edit theme files in place (changes are reversible — the user can review or undo any of them), but you **never** run `fluid theme push` or otherwise write to the live theme without explicit approval. When reviewing a PR instead of fixing locally, don't push to the author's branch — leave comments or open a follow-up PR.
258
+ 3. **Quote the original line.** Every finding must reference `file:line` and show the offending snippet. No vague "the loop is inefficient".
259
+ 4. **Severity is required.** Every finding is tagged `blocker` / `should` / `nit`. See [Severity ladder](#severity-ladder).
260
+ 5. **Cite the validator.** If `fluid theme lint --json` would flag it, say so — the dev should be able to reproduce the failure locally.
261
+ 6. **Don't restyle.** Pure formatting churn is forbidden — if a file needs both a fix and a tidy, keep them as separate, focused changes.
262
+
263
+ ---
264
+
265
+ ## Severity ladder
266
+
267
+ | Tag | Meaning | Examples |
268
+ | --------- | ----------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
269
+ | `blocker` | The validator would reject it on `fluid theme push`, OR it renders wrong / breaks a page / exposes user data. Must be fixed before merge. | Invalid `type:` value, missing `id`, duplicate `id`, missing `type:` on a block, settings shape is object not array, referenced section not on disk, unescaped user HTML, unclosed `{% if %}` |
270
+ | `should` | Works today but will hurt later: perf cliff, DRY violation, missing default, missing whitespace control in a hot loop. | `asset_url` inside a `for` loop, 3+ identical `{% render %}` snippets, multiple `product` settings playing the same role, missing `default:` on a user-facing setting |
271
+ | `nit` | Cosmetic, style, or low-impact. Leave one comment, do not block. | Comment style, optional `default` on internal-only setting, naming nits |
272
+
273
+ Decision rule: if `fluid theme lint --json` would error on it, it's `blocker`. If it would render wrong but the validator lets it through, it's `blocker`. If it changes nothing visible today, it's `should` or `nit`.
274
+
275
+ ---
276
+
277
+ ## The schema validator (`fluid theme lint --json`)
278
+
279
+ `fluid theme lint --json` is the read-only schema validator. It's the same check the editor runs and the same one `fluid theme push` runs before upload, so anything it rejects is a `blocker`. It runs on every `{% schema %}` block and enforces the rules below.
280
+
281
+ ### Schema-level rules
282
+
283
+ - JSON must be parseable. Syntax errors point to the line number.
284
+ - No duplicate `"blocks"` keys in the same object.
285
+
286
+ ### Settings rules
287
+
288
+ For each entry in `"settings": [...]`:
289
+
290
+ - `id` is required, non-empty, non-whitespace — **except** for `type: "header"` settings, which carry no `id` and use `content:` instead (the validator accepts them without one).
291
+ - `id` must be unique across all settings in the same file (excluding `header` entries, which have no `id`).
292
+ - `type` is required.
293
+ - `type` must be one of the canonical types — see the [full list below](references/setting-types.md).
294
+
295
+ ### Blocks rules
296
+
297
+ For each entry in `"blocks": [...]`:
298
+
299
+ - `type` is required.
300
+ - `name` is required _unless_ the block is `@app`, `@theme`, or a named-block reference (type only, no `name`/`settings` — these point at a standalone `blocks/{name}/index.liquid` template).
301
+ - `settings` (when present) must be an array `[]`, not an object `{}`.
302
+ - Each setting inside is recursively validated (same rules as above).
303
+ - Nested `blocks` are recursively validated.
304
+ - Duplicate `type` values within a blocks array produce a warning.
305
+
306
+ ### Section references
307
+
308
+ - Every `{% section 'name' %}` in a Liquid template must have a matching section file on disk. Missing sections produce errors.
309
+
310
+ ### Template vs section block shape
311
+
312
+ - **Section files** (`sections/{name}/index.liquid`): `blocks` is an **array** `[]`.
313
+ - **Page templates** (`{page_type}/{variant}/index.liquid` — e.g. `home_page/default/index.liquid`, `product/default/index.liquid`): `blocks` is an **object** `{}`.
314
+
315
+ If you see the wrong shape, that's a `blocker`.
316
+
317
+ ---
318
+
319
+
320
+ ## Reference catalogs
321
+
322
+ The deep check catalogs live in `references/` and load only when a review or fix
323
+ touches them. Read the matching file when you hit its topic:
324
+
325
+ - **[Setting types](references/setting-types.md)** — the canonical `type:` list the validator enforces. Read when checking any `{% schema %}` setting `type:`.
326
+ - **[Navigation](references/navigation.md)** — `link_list` menu selector vs. hardcoded `<a>` rows.
327
+ - **[Dynamism](references/dynamism.md)** — settings/locales over hardcoded values.
328
+ - **[Global settings](references/global-settings.md)** — typography/color/spacing tokens, dark mode, `config/settings_schema.json` + `theme.liquid` wiring.
329
+ - **[Blocks vs. sections](references/blocks-vs-sections.md)** — when a setting belongs in a block; settings-panel ergonomics.
330
+ - **[Liquid correctness](references/liquid-correctness.md)** — variable scope, balanced tags, whitespace control, typoed ids.
331
+ - **[Editor attributes](references/editor-attributes.md)** — `section.fluid_attributes` / `block.fluid_attributes`.
332
+ - **[FairShare attributes](references/fairshare-attributes.md)** — `data-fluid-*` cart / add-to-cart behavioral attributes + the CDN script.
333
+ - **[Performance](references/performance.md)** — `asset_url` in loops, render hygiene.
334
+ - **[Security & accessibility](references/security-accessibility.md)** — escaping user content, alt text, semantic clickables.
335
+ - **[Dead code](references/dead-code.md)** — unused sections/components/assets, unhandled block types.
336
+ - **[CSS / JS hygiene](references/css-js-hygiene.md)** — co-located stylesheet deprecation, inline `<style>`/`<script>` thresholds, `defer`.
337
+ - **[Worked examples](references/examples.md)** — full section reviews, end to end.
338
+
339
+ Every reference links directly from here (one level deep). "The validator" always means
340
+ `fluid theme lint --json` — run it after each change and parse the JSON output rather than
341
+ eyeballing the text.
342
+
343
+ ---
344
+ ## Selector heuristic — singular → list
345
+
346
+ This is one of the most common smells.
347
+
348
+ **If a section or block has two or more singular resource pickers playing the _same role_, collapse them to one `*_list` setting.**
349
+
350
+ The test: would a user reasonably want N+1 items in the same role? If yes, it's a list. If the section needs _exactly one_ hero product _and separately_ one upsell product, those are two different roles — keep them singular.
351
+
352
+ ### Bad — six product settings playing the same role
353
+
354
+ ```json
355
+ {
356
+ "settings": [
357
+ { "type": "product", "id": "product_1", "label": "Product 1" },
358
+ { "type": "product", "id": "product_2", "label": "Product 2" },
359
+ { "type": "product", "id": "product_3", "label": "Product 3" },
360
+ { "type": "product", "id": "product_4", "label": "Product 4" },
361
+ { "type": "product", "id": "product_5", "label": "Product 5" },
362
+ { "type": "product", "id": "product_6", "label": "Product 6" }
363
+ ]
364
+ }
365
+ ```
366
+
367
+ ### Good — one list
368
+
369
+ ```json
370
+ {
371
+ "settings": [
372
+ {
373
+ "type": "product_list",
374
+ "id": "products",
375
+ "label": "Products",
376
+ "limit": 6
377
+ }
378
+ ]
379
+ }
380
+ ```
381
+
382
+ ### Rendering changes too
383
+
384
+ ```liquid
385
+ {%- comment -%} Bad: six lookups, six render calls {%- endcomment -%}
386
+ {%- if section.settings.product_1 != blank -%}{% render 'product_card', product: section.settings.product_1 %}{%- endif -%}
387
+ {%- if section.settings.product_2 != blank -%}{% render 'product_card', product: section.settings.product_2 %}{%- endif -%}
388
+ {%- comment -%} ...four more times... {%- endcomment -%}
389
+
390
+ {%- comment -%} Good: one list, one loop {%- endcomment -%}
391
+ {%- for product in section.settings.products -%}
392
+ {% render 'product_card', product: product %}
393
+ {%- endfor -%}
394
+ ```
395
+
396
+ ### Same rule for every resource
397
+
398
+ | Singular smell | List fix |
399
+ | ------------------------------------------------------- | ------------------------------------------- |
400
+ | 2+ `product` (or `products`) settings, same role | `product_list` |
401
+ | 2+ `collection` settings, same role | `collection_list` |
402
+ | 2+ `category` settings, same role | `category_list` |
403
+ | 2+ `post` settings, same role | `posts_list` |
404
+ | 2+ `enrollment` / `enrollment_pack` settings, same role | `enrollment_list` / `enrollment_packs_list` |
405
+ | 2+ `blog` settings, same role | `blog_list` |
406
+
407
+ The benefit isn't just lines of code — a `*_list` setting lets the merchant **add or remove** items without a code change and drops the fixed per-slot count. It does **not** give per-item drag-and-drop reorder, though — that's a *blocks* feature. If the merchant needs to reorder items, model them as blocks instead.
408
+
409
+ ---
410
+
411
+ ## File-size and DRY thresholds — when to extract
412
+
413
+ Sections grow. Past certain thresholds, they stop being readable. Severity for "this section is too big" is `should` (rarely `blocker`), but the fix is mechanical.
414
+
415
+ ### Section line counts
416
+
417
+ | Lines | Action |
418
+ | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
419
+ | < 200 | Fine. |
420
+ | 200–400 | Watch for repetition. 3+ near-identical render snippets → extract a component. |
421
+ | 400–700 | **`should`.** Split section into named blocks via `{% schema %}` `"blocks": [...]` and dispatch in a single `{% for block in section.blocks %}` loop. |
422
+ | > 700 | **`should`** (or `blocker` if it also has bugs). Decompose. Prefer multiple smaller sections or component extraction over one giant section. |
423
+
424
+ ### Component line counts
425
+
426
+ Components are simpler than sections (no schema), so the threshold is lower.
427
+
428
+ | Lines | Action |
429
+ | ------- | -------------------------------------------------------------------------------------- |
430
+ | < 100 | Fine. |
431
+ | 100–250 | Look for sub-component extraction. |
432
+ | > 250 | **`should`.** A pure presentation partial that's 250+ lines is doing too much — split. |
433
+
434
+ ### Duplication — the rule of three for Liquid
435
+
436
+ Liquid `{% render %}` snippets often carry 8+ arguments. The parameter list itself is maintenance burden the moment you copy it.
437
+
438
+ **`should`** when:
439
+
440
+ - The same `{% render 'card', ... %}` with the same arg list appears 3+ times in one file → extract to a smaller wrapper component or a `{% capture %}` block.
441
+ - The same chunk of markup (5+ lines) appears 3+ times across two or more sections → move to a new `components/{name}/index.liquid` and `{% render %}` it.
442
+ - The same Liquid logic block (`{% assign … %}` followed by computation) appears in 3+ sections → move to a component and `{% render %}` it.
443
+
444
+ ### Block extraction — when an inline branch becomes a block
445
+
446
+ If a section has three or more `{% if section.settings.show_X %}` branches at the top level, that's a sign each `X` wants to be a `{% schema %}` block. Block-driven sections look like this:
447
+
448
+ ```liquid
449
+ {% for block in section.blocks %}
450
+ {% case block.type %}
451
+ {% when 'page_header' %} {%- render 'resource_listing_header' -%}
452
+ {% when 'product_grid' %} {%- render 'product_grid', block: block -%}
453
+ {% when 'cta_banner' %} {%- render 'cta_banner', block: block -%}
454
+ {% endcase %}
455
+ {% endfor %}
456
+ ```
457
+
458
+ Schema side:
459
+
460
+ ```json
461
+ "blocks": [
462
+ { "type": "page_header", "name": "Page Header", "limit": 1, "settings": [...] },
463
+ { "type": "product_grid", "name": "Product Grid", "settings": [...] },
464
+ { "type": "cta_banner", "name": "CTA Banner", "settings": [...] }
465
+ ]
466
+ ```
467
+
468
+ This gives users drag-and-drop reorderability and lets each block's logic and settings live together.
469
+
470
+ ---
471
+
472
+ ## Review workflow — how to actually output findings
473
+
474
+ ### A. Reviewing a GitHub PR (default)
475
+
476
+ Themes live in their own repos. PRs typically arrive via `gh pr view <number>` against `base-theme` or a fork of it.
477
+
478
+ 1. Read the PR diff. For each changed `.liquid` or `.json` file, walk through these check sections in order:
479
+ - Validator-level rules (would `fluid theme lint --json` reject it?)
480
+ - Setting types (every `type:` in the canonical list)
481
+ - Selector singular→list
482
+ - File-size / DRY thresholds
483
+ - Liquid correctness
484
+ - Performance
485
+ - Security
486
+ - Accessibility
487
+ - CSS/JS hygiene
488
+ 2. Group findings by file. Within each file, sort by line number.
489
+ 3. For each finding, prepare a **single inline comment** at the offending line. Format:
490
+
491
+ ````
492
+ [blocker] Invalid setting type `text_area`
493
+
494
+ The schema validator only accepts the canonical type list. Running `fluid theme lint --json`
495
+ locally would fail on this. Use `textarea` (one word):
496
+
497
+ ```json
498
+ { "type": "textarea", "id": "description", "label": "Description" }
499
+ ```
500
+ ````
501
+
502
+ 4. After all inline comments, post **one summary review** (e.g. `gh pr review --comment --body ...`) with the rollup:
503
+
504
+ ```
505
+
506
+ ## Theme review
507
+
508
+ **4 findings** — 1 blocker, 2 should, 1 nit
509
+
510
+ Reproduce locally with `fluid theme lint --json`.
511
+
512
+ | Severity | File | Finding |
513
+ | -------- | ---------------------------------- | --------------------------------------------------- |
514
+ | blocker | sections/featured/index.liquid:42 | Invalid setting type `text_area` |
515
+ | should | sections/featured/index.liquid:118 | Six `product` settings — collapse to `product_list` |
516
+ | should | sections/products/index.liquid:155 | Same `{% render 'product_card', ... %}` repeated 4× |
517
+ | nit | components/button/index.liquid:8 | Missing whitespace control in tight loop |
518
+
519
+ **Suggested fix path:** I can open a follow-up PR with the blocker + the two `should` items. Reply with `open the fix PR` to proceed.
520
+
521
+ ```
522
+
523
+ ### B. Auditing without a PR (local)
524
+
525
+ Same checks, but output goes to the user as a single ranked markdown report. End with the same offer: "I can open a PR with these fixes — say the word."
526
+
527
+ If the user has the CLI installed, also suggest:
528
+
529
+ ```
530
+
531
+ For a fast first pass: `fluid theme lint --json` from the theme repo root.
532
+
533
+ ```
534
+
535
+ ### C. Applying fixes — opening a follow-up PR
536
+
537
+ **Only after explicit approval** (`open the fix PR`, `yes do it`, `apply the fixes`):
538
+
539
+ 1. Branch from the PR's head (not main): `git checkout -b fix/<short-slug>`
540
+ 2. Apply ONE fix at a time, one commit per finding. Commit messages must follow Conventional Commits (this is the fluid-mono style and matches what most theme repos use):
541
+
542
+ ```
543
+
544
+ fix(theme): collapse six product settings into product_list
545
+
546
+ Six identical `type: "product"` settings on the featured-products block —
547
+ replaced with one `type: "product_list"` with `limit: 6` and a single
548
+ `{% for product in section.settings.products %}` loop.
549
+
550
+ Severity: should
551
+
552
+ ```
553
+
554
+ 3. Run `fluid theme lint --json` locally as the final check.
555
+ 4. Push the branch.
556
+ 5. Open the PR with this body (using `gh pr create`):
557
+
558
+ ```
559
+
560
+ ## Theme review fixes
561
+
562
+ Follow-up to #<original-pr>. Each commit addresses one finding from the review.
563
+ Validated locally with `fluid theme lint --json`.
564
+
565
+ | Commit | Severity | Finding |
566
+ | ------ | -------- | ------------------------------------------ |
567
+ | abc123 | blocker | Invalid setting type `text_area` |
568
+ | def456 | should | Collapse `product` × 6 into `product_list` |
569
+ | ... | ... | ... |
570
+
571
+ ```
572
+
573
+ 6. **Never amend or force-push.** Each fix is a reversible commit.
574
+
575
+ ### D. What never to do
576
+
577
+ - **Never** push directly to the PR's branch without approval.
578
+ - **Never** run `fluid theme push` against a production theme. The agent's job is to comment, lint, and propose. The CLI's `push` writes to the live API.
579
+ - **Never** bundle unrelated style/cosmetic changes with a behavioral fix.
580
+ - **Never** mark a `nit` as `blocker` to force engagement.
581
+ - **Never** rewrite a section the user didn't ask you to rewrite. Propose first.
582
+
583
+ ---
584
+
585
+ ## Quick reference — the checklist
586
+
587
+ When reviewing any theme file, run through this in order. Anything unchecked is a finding.
588
+
589
+ **Structure**
590
+
591
+ - [ ] Repo has `assets/`, `config/`, and at least one page-type directory (e.g. `home_page/`, `product/`) — at minimum the ones it needs
592
+ - [ ] `config/settings_schema.json` exists
593
+ - [ ] `layouts/theme.liquid` exists
594
+ - [ ] Sections live under `sections/{name}/index.liquid`; components under `components/{name}/index.liquid`; page templates under `{page_type}/{variant}/index.liquid` (where `variant` is `default` or any user-defined name)
595
+ - [ ] No `templates/` wrapper directory at the theme root (sections/components/page-types sit at root)
596
+ - [ ] No files nest deeper than two directories from theme root (no `sections/{name}/blocks/{...}`, no `assets/icons/social/{...}`)
597
+ - [ ] No sections without `{% schema %}`; no components *with* `{% schema %}`
598
+ - [ ] No `.css`/`.js`/image assets outside `assets/`
599
+ - [ ] No co-located `styles.css` / `style.css` next to a template, section, or component (deprecated — all CSS lives in `assets/`)
600
+ - [ ] Section/component directory names are `snake_case` and globally unique
601
+
602
+ **Layout**
603
+
604
+ - [ ] Every file in `layouts/` emits `{{ content_for_header }}` inside `<head>`
605
+ - [ ] Every file in `layouts/` emits `{{ content_for_layout }}` inside `<body>`
606
+
607
+ **Schema correctness**
608
+
609
+ - [ ] Every `{% schema %}` `type:` value is one of the canonical setting types (`fluid theme lint --json` rejects unknown types)
610
+ - [ ] Every setting has a non-empty, unique `id` — *except* `type: "header"` settings, which carry `content:` instead of `id`
611
+ - [ ] Every block has a `type` and (where required) a `name`
612
+ - [ ] `range` has `min`/`max`/`step`; `select`/`radio` has `options`; `*_list` has `limit`
613
+ - [ ] User-facing settings have a `default:`
614
+ - [ ] Every `{% section 'name' %}` has a matching `sections/name/index.liquid` on disk
615
+ - [ ] `fluid theme lint --json` runs clean
616
+
617
+ **Settings dynamism**
618
+
619
+ - [ ] No user-visible literal text in markup (use `text`/`textarea`/`richtext`)
620
+ - [ ] No hardcoded colors in styles (use `color` / `color_background` or a global token)
621
+ - [ ] No hardcoded image URLs (use `image_picker` or upload to `assets/`)
622
+ - [ ] No external CDN URLs for theme-owned assets
623
+ - [ ] No hardcoded `href` to fixed paths the company might want to change (use `url`)
624
+
625
+ **Global settings (config + layout)**
626
+
627
+ - [ ] `config/settings_schema.json` defines `typography`, `color_schema`, and `spacing` groups (at minimum)
628
+ - [ ] `layouts/theme.liquid` reads those settings and emits CSS variables on `:root`
629
+ - [ ] Sections consume `var(--font-*)`, `var(--color-*)`, `var(--space-*)` rather than re-reading `settings.*` directly
630
+ - [ ] No section-level setting duplicates a global token (per-section overrides are the exception, default `unset`)
631
+ - [ ] Schema is grouped by `name:` (not a flat array)
632
+ - [ ] If dark mode is in schema, it's wired in `theme.liquid` (`@media (prefers-color-scheme: dark)` or `[data-theme="dark"]`)
633
+
634
+ **Blocks-first content model**
635
+
636
+ - [ ] Section *content* (headings, text, images, CTAs, cards, etc.) is modeled as **blocks**, not fixed section settings — even when there's only one today
637
+ - [ ] Section settings hold only true whole-section config (width, background, padding, columns, alignment)
638
+ - [ ] Blocks are **standalone** (`blocks/{name}/index.liquid`); inline blocks only for very small, one-off cases
639
+ - [ ] No content element a merchant would add / remove / reorder is locked into a section setting
640
+
641
+ **Settings ergonomics**
642
+
643
+ - [ ] Section settings panel under ~15 fields, OR repeated variations moved to blocks
644
+ - [ ] No 2+ singular resource pickers playing the same role (collapse to `*_list`)
645
+ - [ ] No `X_1` / `X_2` / `X_3` parallel-named settings (use blocks)
646
+ - [ ] No `*s_list` where the canonical `*_list` exists (e.g. `product_list`, not `products_list`)
647
+
648
+ **Dead code**
649
+
650
+ - [ ] No section files with zero `{% section 'name' %}` references (after confirming editor metadata)
651
+ - [ ] No component files with zero `{% render 'name' %}` references
652
+ - [ ] No `{% schema %}` `blocks:` declaring a `type:` the template never handles
653
+ - [ ] No `assets/` files with zero references in any `.liquid` or `.css` (greppable from the theme root)
654
+
655
+ **Liquid + size**
656
+
657
+ - [ ] Section under 400 lines; component under 250 lines
658
+ - [ ] No identical `{% render %}` snippet appearing 3+ times
659
+ - [ ] No `asset_url` inside a `for` loop
660
+ - [ ] All `for`/`if` in markup context use `{%-` / `-%}` whitespace control
661
+ - [ ] `{% if %}` / `{% endif %}` (and `for`/`case`/`capture`) tags balance
662
+
663
+ **Variable scope**
664
+
665
+ - [ ] Every `{{ section.settings.* }}` / `{{ block.settings.* }}` read has a matching `id` declared in the same file's `{% schema %}`
666
+ - [ ] No resource drops (`product`, `collection`, etc.) read outside the templates where they're in scope, unless explicitly passed
667
+ - [ ] `block` and `forloop.*` references only appear inside their respective loops
668
+ - [ ] Components only read variables passed via `{% render %}` arguments
669
+ - [ ] No typos in setting `id`s (silently render empty)
670
+
671
+ **Navigation**
672
+
673
+ - [ ] No hardcoded `<a href="/...">` rows that visually form a navigation — use `link_list`
674
+ - [ ] No `text` + `url` setting pairs used to fake a menu
675
+
676
+ **Assets**
677
+
678
+ - [ ] No inline `<style>` block over 10 lines (extract to `assets/*.css`)
679
+ - [ ] No inline `<script>` block over 5 lines (extract to `assets/*.js`)
680
+ - [ ] JS tags have `defer` (or a justification for not)
681
+ - [ ] No dead assets under `assets/`
682
+
683
+ **Editor attributes (`section.fluid_attributes` / `block.fluid_attributes`)**
684
+
685
+ - [ ] Every section file emits `{{ section.fluid_attributes }}` on its root wrapper element
686
+ - [ ] Every block iterated via `{% for block in section.blocks %}` carries `{{ block.fluid_attributes }}` on its own root element
687
+ - [ ] No typos (`fluid_attribute`, `fluid_attr`, `fluidAttributes`)
688
+ - [ ] No hand-rolled `data-fluid-section-*` attributes — always go through the helper
689
+ - [ ] When passing through a component, attributes are forwarded as `attr: block.fluid_attributes`
690
+
691
+ **FairShare behavioral attributes (`data-fluid-*`)**
692
+
693
+ - [ ] Every attribute uses kebab-case `data-fluid-*` (no camelCase, no underscores, `data-` prefix required)
694
+ - [ ] `data-fluid-cart` is one of `"open"` / `"close"` / `"toggle"` (lowercase)
695
+ - [ ] Modifier attributes (`quantity`, `subscribe`, `subscription-plan-id`, `bundled-items`, `bundle-selections`, `open-cart-after-add`) are always paired with an add-action attribute
696
+ - [ ] `data-fluid-subscription-plan-id` carries a single integer (never comma-separated)
697
+ - [ ] JSON-valued attributes (`bundled-items`, `bundle-selections`) are valid JSON with numeric IDs and quantities
698
+ - [ ] Attributes live on `<button>` or `<a>` (not `<div>` without semantics)
699
+ - [ ] Exactly one `<script id="fluid-cdn-script">` with `data-fluid-shop` set is loaded by the layout
700
+
701
+ **Security & a11y**
702
+
703
+ - [ ] User content escaped (`| escape`) unless it's a HTML-shaped type (`richtext`/`html`/`html_textarea`)
704
+ - [ ] Images have `alt`; below-the-fold images have `loading="lazy"`
705
+ - [ ] Clickable elements use `<button>` (action) or `<a href>` (nav) — never `<div>`
706
+
707
+ If every box checks, the file is healthy. Each unchecked box is a finding — tag it and report it per [Review workflow](#review-workflow--how-to-actually-output-findings).