@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.
- package/.turbo/turbo-build.log +2 -2
- package/.turbo/turbo-typecheck.log +1 -1
- package/package.json +5 -5
- package/skills/themes-review/SKILL.md +707 -0
- package/skills/themes-review/references/blocks-vs-sections.md +131 -0
- package/skills/themes-review/references/css-js-hygiene.md +153 -0
- package/skills/themes-review/references/dead-code.md +161 -0
- package/skills/themes-review/references/dynamism.md +88 -0
- package/skills/themes-review/references/editor-attributes.md +160 -0
- package/skills/themes-review/references/examples.md +111 -0
- package/skills/themes-review/references/fairshare-attributes.md +185 -0
- package/skills/themes-review/references/global-settings.md +221 -0
- package/skills/themes-review/references/liquid-correctness.md +176 -0
- package/skills/themes-review/references/navigation.md +88 -0
- package/skills/themes-review/references/performance.md +85 -0
- package/skills/themes-review/references/security-accessibility.md +70 -0
- package/skills/themes-review/references/setting-types.md +118 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Blocks vs. sections
|
|
2
|
+
|
|
3
|
+
> Part of the `themes-review` skill. See [`../SKILL.md`](../SKILL.md) for the review workflow, severity ladder, and validator rules.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
|
|
7
|
+
- The rule — blocks first
|
|
8
|
+
- Standalone blocks over inline blocks
|
|
9
|
+
- Threshold
|
|
10
|
+
- Bad — settings-panel wall of fields
|
|
11
|
+
- Good — block per feature
|
|
12
|
+
- Benefits the reviewer should cite when suggesting the fix
|
|
13
|
+
- When sections-with-many-fields _is_ the right answer
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
Fluid themes are **blocks-first**: a section is a layout shell, and its *content* lives in blocks. A section that bundles every piece of content into one giant `settings: [...]` array becomes a wall of fields the merchant can't navigate, can't reorder, and can't extend without a developer.
|
|
17
|
+
|
|
18
|
+
### The rule — blocks first
|
|
19
|
+
|
|
20
|
+
- **Block settings** hold **content** — anything a merchant might add, remove, reorder, or repeat: headings, paragraphs, images, buttons/CTAs, cards, FAQs, features, logos. Model content as blocks **even when there is only one today.** "There's just one heading" is not a reason to make it a section setting — a merchant who later wants a second heading, wants to drop the image, or wants the CTA above the text should do it in the editor, not in a PR.
|
|
21
|
+
- **Section settings** hold only **true whole-section configuration**: width, background, padding, column count, vertical alignment — values that are singular for the whole section by nature and that a merchant would never duplicate or reorder.
|
|
22
|
+
|
|
23
|
+
That split is the whole point: the **section** carries only a handful of settings (its whole-section config), while every **element-specific** setting lives on the block it belongs to. The section panel stays short and scannable, and each setting sits next to the element it controls.
|
|
24
|
+
|
|
25
|
+
**Litmus test:** _"Could a merchant reasonably want two of these, none of these, or these in a different order?"_ If yes → block. If it is genuinely one-per-section plumbing → section setting.
|
|
26
|
+
|
|
27
|
+
`setting_a_1` / `setting_a_2` / `setting_a_3` (or `show_card_1` / `show_card_2`) is the obvious failure mode — but the blocks-first bar is higher: reach for blocks _before_ the duplication ever appears.
|
|
28
|
+
|
|
29
|
+
### Standalone blocks over inline blocks
|
|
30
|
+
|
|
31
|
+
Once content is a block, prefer a **standalone block** — `blocks/{name}/index.liquid`, referenced from the section's `"blocks"` array via `@theme` or a named-block reference (`{ "type": "<name>" }`) — over an **inline block** defined inside the section's `{% schema %}`.
|
|
32
|
+
|
|
33
|
+
- **Default to standalone.** It is reusable across sections, testable on its own, keeps the section schema small, and is how Fluid themes are meant to be composed.
|
|
34
|
+
- **Inline blocks only when the block is very small** (a couple of settings, a few lines of markup) **and** clearly specific to one section. If there's any chance of reuse, go standalone.
|
|
35
|
+
- Extracting an inline block into `blocks/` is always a valid, encouraged fix; never flag the reverse.
|
|
36
|
+
|
|
37
|
+
| You see | Severity | Fix |
|
|
38
|
+
| ---------------------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------ |
|
|
39
|
+
| Section content (a heading / text / image / CTA a merchant would add, remove, or reorder) modeled as fixed section settings | `should` | Model it as a block. |
|
|
40
|
+
| A non-trivial inline block (many settings or substantial markup) defined inside a section schema | `should` | Extract to a standalone `blocks/{name}/index.liquid` and reference it. |
|
|
41
|
+
| The same inline block duplicated across 2+ sections | `should` | Extract to one standalone `blocks/{name}/`. |
|
|
42
|
+
| A tiny, genuinely one-off inline block | `nit` | Acceptable; suggest standalone only if reuse is likely. |
|
|
43
|
+
|
|
44
|
+
### Threshold
|
|
45
|
+
|
|
46
|
+
**`should`** when:
|
|
47
|
+
|
|
48
|
+
- A section has **more than ~15 top-level settings**, _and_ any of them look like variations of the same thing
|
|
49
|
+
- A section has 2+ pairs of settings of the form `X_1` / `X_2`
|
|
50
|
+
- A section has a fixed N copies of "card", "feature", "tile", "testimonial", "FAQ", "logo" — anything that semantically wants to be a list
|
|
51
|
+
|
|
52
|
+
### Bad — settings-panel wall of fields
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{% schema %}
|
|
56
|
+
{
|
|
57
|
+
"name": "Three Features",
|
|
58
|
+
"settings": [
|
|
59
|
+
{ "type": "image_picker", "id": "feat_1_icon", "label": "Feature 1 icon" },
|
|
60
|
+
{ "type": "text", "id": "feat_1_title", "label": "Feature 1 title" },
|
|
61
|
+
{ "type": "textarea", "id": "feat_1_body", "label": "Feature 1 body" },
|
|
62
|
+
{ "type": "image_picker", "id": "feat_2_icon", "label": "Feature 2 icon" },
|
|
63
|
+
{ "type": "text", "id": "feat_2_title", "label": "Feature 2 title" },
|
|
64
|
+
{ "type": "textarea", "id": "feat_2_body", "label": "Feature 2 body" },
|
|
65
|
+
{ "type": "image_picker", "id": "feat_3_icon", "label": "Feature 3 icon" },
|
|
66
|
+
{ "type": "text", "id": "feat_3_title", "label": "Feature 3 title" },
|
|
67
|
+
{ "type": "textarea", "id": "feat_3_body", "label": "Feature 3 body" }
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
{% endschema %}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Good — block per feature
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{% schema %}
|
|
77
|
+
{
|
|
78
|
+
"name": "Features",
|
|
79
|
+
"settings": [
|
|
80
|
+
{ "type": "text", "id": "heading", "label": "Section heading", "default": "Why us" },
|
|
81
|
+
{ "type": "select", "id": "columns", "label": "Columns",
|
|
82
|
+
"options": [{ "value": "2", "label": "2" }, { "value": "3", "label": "3" }, { "value": "4", "label": "4" }],
|
|
83
|
+
"default": "3" }
|
|
84
|
+
],
|
|
85
|
+
"blocks": [
|
|
86
|
+
{
|
|
87
|
+
"type": "feature",
|
|
88
|
+
"name": "Feature",
|
|
89
|
+
"settings": [
|
|
90
|
+
{ "type": "image_picker", "id": "icon", "label": "Icon" },
|
|
91
|
+
{ "type": "text", "id": "title", "label": "Title" },
|
|
92
|
+
{ "type": "textarea", "id": "body", "label": "Description" }
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
{% endschema %}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Rendering:
|
|
101
|
+
|
|
102
|
+
```liquid
|
|
103
|
+
<div class="features features--cols-{{ section.settings.columns }}">
|
|
104
|
+
{%- for block in section.blocks -%}
|
|
105
|
+
<article class="feature" {{ block.fluid_attributes }}>
|
|
106
|
+
{%- if block.settings.icon != blank -%}
|
|
107
|
+
<img src="{{ block.settings.icon }}" alt="{{ block.settings.title | escape }}" />
|
|
108
|
+
{%- endif -%}
|
|
109
|
+
<h3>{{ block.settings.title }}</h3>
|
|
110
|
+
<p>{{ block.settings.body }}</p>
|
|
111
|
+
</article>
|
|
112
|
+
{%- endfor -%}
|
|
113
|
+
</div>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Prefer this as a standalone block.** Define `feature` at `blocks/feature/index.liquid` and reference it from the section's `"blocks"` array (`{ "type": "feature" }`) instead of inlining it — see [Standalone blocks over inline blocks](#standalone-blocks-over-inline-blocks). (A stricter blocks-first read would make `heading` a block too; it's kept as a section setting here only for brevity.)
|
|
117
|
+
|
|
118
|
+
### Benefits the reviewer should cite when suggesting the fix
|
|
119
|
+
|
|
120
|
+
- **Editor UX:** Each block shows up as a collapsible item with its own fields — far easier to scan than a 27-field wall.
|
|
121
|
+
- **Drag-and-drop reorder for free.**
|
|
122
|
+
- **Add/remove without code changes:** users can add a 4th feature without a developer.
|
|
123
|
+
- **Per-block dropzone hooks:** `{{ block.fluid_attributes }}` gives the editor click-to-edit on each one.
|
|
124
|
+
- **`limit:` on the block** (optional) enforces a max count without baking it into Liquid.
|
|
125
|
+
- **Reusable across sections** when defined standalone (`blocks/{name}/index.liquid`) instead of inline.
|
|
126
|
+
|
|
127
|
+
### When sections-with-many-fields _is_ the right answer
|
|
128
|
+
|
|
129
|
+
Some sections genuinely have many settings that _all_ apply to the whole section — e.g. a configuration-heavy hero with background, overlay, video poster, mobile alt, autoplay, mute, etc. That's fine: those are whole-section **config**, not content. The test isn't field count — it's whether each field is one-per-section plumbing (section setting) or a piece of content a merchant would add / remove / reorder (block).
|
|
130
|
+
|
|
131
|
+
---
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# CSS / JS asset hygiene
|
|
2
|
+
|
|
3
|
+
> Part of the `themes-review` skill. See [`../SKILL.md`](../SKILL.md) for the review workflow, severity ladder, and validator rules.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
|
|
7
|
+
- 1. Naming and scoping — everything lives under `assets/`
|
|
8
|
+
- 1a. Deprecated — co-located `styles.css` / `style.css`
|
|
9
|
+
- 2. Tailwind class duplication
|
|
10
|
+
- 3. Dead code
|
|
11
|
+
- 4. Inline `<style>` blocks — extract to assets
|
|
12
|
+
- 5. Inline `<script>` blocks — extract to assets
|
|
13
|
+
- 6. Hardcoded URLs — use `asset_url`
|
|
14
|
+
- 7. Hardcoded copy in markup
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### 1. Naming and scoping — everything lives under `assets/`
|
|
18
|
+
|
|
19
|
+
**All stylesheets live under `assets/`.** Co-located `styles.css` / `style.css` files inside `sections/{name}/`, `components/{name}/`, etc. **are deprecated** and should be migrated.
|
|
20
|
+
|
|
21
|
+
- Section CSS: `assets/{section_name}.css`
|
|
22
|
+
- Component CSS: `assets/{component_name}.css`
|
|
23
|
+
- Global / theme-wide CSS: `assets/theme.css` (or named after intent — `assets/typography.css`, `assets/utilities.css`)
|
|
24
|
+
- Custom stylesheets: `assets/{name}.css`
|
|
25
|
+
- Scope class names by section: `.cart-page__line-item`, not `.line-item`
|
|
26
|
+
|
|
27
|
+
Referenced via `asset_url`:
|
|
28
|
+
|
|
29
|
+
```liquid
|
|
30
|
+
{{ 'featured_products.css' | asset_url | stylesheet_tag }}
|
|
31
|
+
````
|
|
32
|
+
|
|
33
|
+
### 1a. Deprecated — co-located `styles.css` / `style.css`
|
|
34
|
+
|
|
35
|
+
The earlier theme convention placed a `styles.css` next to each template, section, or component (e.g. `sections/featured/styles.css`). **This is being phased out** in favor of `assets/`-only. The reference `base-theme` still contains `styles.css` files inside section / page-variant directories — these are being migrated.
|
|
36
|
+
|
|
37
|
+
**`should`** when a PR adds or modifies a co-located stylesheet. **`blocker`** for any new file added under this pattern (new code must follow the assets-only convention).
|
|
38
|
+
|
|
39
|
+
| You see | Severity | Fix |
|
|
40
|
+
| ------------------------------------------------------------------------------------------------------------------ | --------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
41
|
+
| New file `sections/{name}/styles.css` | `blocker` | Place it under `assets/{name}.css` instead and reference via `{{ '{name}.css' \| asset_url \| stylesheet_tag }}`. |
|
|
42
|
+
| New file `components/{name}/style.css` | `blocker` | Same — `assets/{name}.css` and `asset_url`. |
|
|
43
|
+
| New file under a page-variant directory like `{type}/{variant}/styles.css` | `blocker` | Move to `assets/{name}.css`. |
|
|
44
|
+
| Existing co-located stylesheet edited in the PR | `should` | Suggest migrating to `assets/` as part of the same PR if the diff is small, or a follow-up if not. |
|
|
45
|
+
| Global / custom stylesheets anywhere outside `assets/` (other than the engine-recognized root `global_styles.css`) | `blocker` | Move to `assets/`. |
|
|
46
|
+
| Hardcoded path like `<link rel="stylesheet" href="/sections/{name}/styles.css">` (bypassing `asset_url`) | `blocker` | Move the file and switch to `asset_url`. |
|
|
47
|
+
|
|
48
|
+
Migration recipe (suggest in the comment when applicable):
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# 1. Move the file
|
|
52
|
+
git mv sections/featured/styles.css assets/featured.css
|
|
53
|
+
|
|
54
|
+
# 2. Update the reference in the section template
|
|
55
|
+
# Find: {{ 'sections/featured/styles.css' | asset_url | stylesheet_tag }}
|
|
56
|
+
# Replace:{{ 'featured.css' | asset_url | stylesheet_tag }}
|
|
57
|
+
|
|
58
|
+
# 3. Validate
|
|
59
|
+
fluid theme lint --json
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 2. Tailwind class duplication
|
|
63
|
+
|
|
64
|
+
Long, identical `class="…"` strings repeated across cards → extract via `{% capture %}` or a component. **`should`** when 4+ identical class strings appear.
|
|
65
|
+
|
|
66
|
+
### 3. Dead code
|
|
67
|
+
|
|
68
|
+
See [Dead code — unused blocks, components, sections, assets](#dead-code--unused-blocks-components-sections-assets) for the unified audit. Assets are one category among four.
|
|
69
|
+
|
|
70
|
+
### 4. Inline `<style>` blocks — extract to assets
|
|
71
|
+
|
|
72
|
+
**`should`** when any inline `<style>` block exceeds **10 lines**, or when the same inline rules repeat across multiple sections. The engine deduplicates external stylesheets across the page, so external is almost always better.
|
|
73
|
+
|
|
74
|
+
```liquid
|
|
75
|
+
{%- comment -%} Bad: 40 lines of CSS shipped per section render {%- endcomment -%}
|
|
76
|
+
<style>
|
|
77
|
+
.featured-products { padding: 64px 0; }
|
|
78
|
+
.featured-products__heading { font-size: 48px; }
|
|
79
|
+
.featured-products__grid { display: grid; gap: 24px; ... }
|
|
80
|
+
/* ... 30 more lines ... */
|
|
81
|
+
</style>
|
|
82
|
+
|
|
83
|
+
{%- comment -%} Good {%- endcomment -%}
|
|
84
|
+
{{ 'featured_products.css' | asset_url | stylesheet_tag }}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Exception:** A small block of _dynamic_ CSS that depends on schema settings is fine inline — the dynamism is the whole point. Even then, extract the static base to a stylesheet and leave only the per-section overrides inline:
|
|
88
|
+
|
|
89
|
+
```liquid
|
|
90
|
+
{{ 'featured_products.css' | asset_url | stylesheet_tag }}
|
|
91
|
+
<style>
|
|
92
|
+
.featured-products[data-section-id="{{ section.id }}"] {
|
|
93
|
+
--section-bg: {{ section.settings.bg_color }};
|
|
94
|
+
--section-pad: {{ section.settings.section_pad }};
|
|
95
|
+
}
|
|
96
|
+
</style>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 5. Inline `<script>` blocks — extract to assets
|
|
100
|
+
|
|
101
|
+
Same rule, lower threshold. **`should`** when any inline `<script>` block exceeds **5 lines**. Inline JS bypasses caching, defeats `defer`, and runs render-blocking unless explicitly deferred via `setTimeout`.
|
|
102
|
+
|
|
103
|
+
```liquid
|
|
104
|
+
{%- comment -%} Bad {%- endcomment -%}
|
|
105
|
+
<script>
|
|
106
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
107
|
+
const slider = document.querySelector('.featured-slider');
|
|
108
|
+
new Splide(slider, { type: 'loop', perPage: 3 }).mount();
|
|
109
|
+
// ...
|
|
110
|
+
});
|
|
111
|
+
</script>
|
|
112
|
+
|
|
113
|
+
{%- comment -%} Good {%- endcomment -%}
|
|
114
|
+
<script src="{{ 'featured_slider.js' | asset_url }}" defer></script>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Exception:** Tiny dynamic config bridges — passing schema settings into a global config object — are acceptable inline if under 5 lines. Keep the runtime in an asset:
|
|
118
|
+
|
|
119
|
+
```liquid
|
|
120
|
+
<script>
|
|
121
|
+
window.fluidFeaturedConfig = {{ section.settings | json }};
|
|
122
|
+
</script>
|
|
123
|
+
<script src="{{ 'featured_slider.js' | asset_url }}" defer></script>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 6. Hardcoded URLs — use `asset_url`
|
|
127
|
+
|
|
128
|
+
Any reference to an external CDN URL, hardcoded path, or `https://cdn.example.com/...` for assets the theme owns is a finding. The asset belongs in `assets/` and the URL should resolve through `| asset_url`.
|
|
129
|
+
|
|
130
|
+
| You see | Severity | Fix |
|
|
131
|
+
| -------------------------------------------------------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
132
|
+
| `src="https://cdn.example.com/img/banner.jpg"` (external CDN, theme-owned asset) | `blocker` | Download the file into `assets/banner.jpg` and use `{{ 'banner.jpg' \| asset_url }}`. External CDNs can disappear or change CORS. |
|
|
133
|
+
| `src="/assets/banner.jpg"` (hardcoded path) | `blocker` | Use `{{ 'banner.jpg' \| asset_url }}`. The engine versions/CDN-fronts the URL. |
|
|
134
|
+
| `href="https://fonts.googleapis.com/css?family=..."` | `should` | Either upload the font files to `assets/` and `@font-face` them via stylesheet, or use `font_picker`. |
|
|
135
|
+
| `<link rel="stylesheet" href="/some-static.css">` | `blocker` | `{{ 'some-static.css' \| asset_url \| stylesheet_tag }}`. |
|
|
136
|
+
| `<script src="https://unpkg.com/some-lib"></script>` (third-party library) | `should` | Vendor it into `assets/` and reference via `asset_url`. unpkg/jsDelivr can go down and you have no control over what loads. |
|
|
137
|
+
| Image URL hardcoded in CSS (`background-image: url(/assets/x.png)`) | `should` | Move the rule into Liquid where `asset_url` is available, or pre-process the CSS, or use a CSS variable injected via inline `<style>` block. |
|
|
138
|
+
| Hardcoded route paths (`href="/products"`, `href="/cart"`) | `nit` | Acceptable when they're well-known engine routes. Surface only if the route ever changes per company. |
|
|
139
|
+
|
|
140
|
+
**Why these matter:**
|
|
141
|
+
|
|
142
|
+
- `asset_url` returns a fingerprinted CDN URL — cache-busted on deploy, CDN-fronted globally.
|
|
143
|
+
- External CDNs introduce a dependency on a server you don't control.
|
|
144
|
+
- Hardcoded paths break when the theme is renamed or moved.
|
|
145
|
+
|
|
146
|
+
### 7. Hardcoded copy in markup
|
|
147
|
+
|
|
148
|
+
Already covered as **`should`** under [Dynamism](#dynamism--settings-over-hardcoded-values), but it's worth restating here because reviewers often miss it during a CSS/JS pass: literal English (or any language) hardcoded in markup is the same shape of problem as a hardcoded URL — it locks the theme to one company / one locale / one campaign.
|
|
149
|
+
|
|
150
|
+
- User-visible string → `text` / `textarea` / `richtext` setting
|
|
151
|
+
- Repeated brand-agnostic UI string (Add to cart, Read more) → `locales/en.json` and `{{ 'add_to_cart' | t }}`
|
|
152
|
+
|
|
153
|
+
---
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Dead code — unused blocks, components, sections, assets
|
|
2
|
+
|
|
3
|
+
> Part of the `themes-review` skill. See [`../SKILL.md`](../SKILL.md) for the review workflow, severity ladder, and validator rules.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
|
|
7
|
+
- What to check
|
|
8
|
+
- Sections — unused
|
|
9
|
+
- Components — unused
|
|
10
|
+
- Block types — declared but never rendered
|
|
11
|
+
- Assets — unused
|
|
12
|
+
- Output format for removal recommendations
|
|
13
|
+
- When dead-code findings turn into fixes
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
A theme accumulates dead code faster than almost any other codebase: schemas evolve, blocks get renamed, components get inlined, CSS files outlive their consumers. Every dead artifact is **download weight, cognitive load, and a maintenance trap** (someone will eventually edit the dead one wondering why their changes don't show up).
|
|
17
|
+
|
|
18
|
+
The reviewer's posture: **suggest removal, default to `should`, never delete from a PR without explicit approval.** Most "unused" detections have a small false-positive rate (dynamic strings, future-use), so the recommendation is always "consider removing" with the audit command attached.
|
|
19
|
+
|
|
20
|
+
### What to check
|
|
21
|
+
|
|
22
|
+
There are four categories. Each has the same recipe: enumerate what's defined, enumerate what's referenced, take the set difference.
|
|
23
|
+
|
|
24
|
+
| Category | Defined | Referenced via | Severity |
|
|
25
|
+
| --------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------- | --------------------------------- |
|
|
26
|
+
| **Sections** | `sections/{name}/index.liquid` | `{% section 'name' %}` in any `.liquid` | `should` |
|
|
27
|
+
| **Components** | `components/{name}/index.liquid` | `{% render 'name' %}` / `{% include 'name' %}` | `should` |
|
|
28
|
+
| **Block types** | `{% schema %}` `blocks: [{ "type": "X", ... }]` arrays | `{% case block.type %}` / `{% if block.type == 'X' %}` in the same section file | `should` |
|
|
29
|
+
| **Assets** | files in `assets/` (flat) | `{{ 'name' \| asset_url }}` in any `.liquid` | `nit` to flag, `should` to remove |
|
|
30
|
+
|
|
31
|
+
### Sections — unused
|
|
32
|
+
|
|
33
|
+
A section file with no `{% section 'name' %}` consumer is dormant. Either it's been deprecated and forgotten, or it was never wired into any page template.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# From the theme repo root
|
|
37
|
+
for d in sections/*/; do
|
|
38
|
+
name=$(basename "$d")
|
|
39
|
+
count=$(grep -rE "\\{%\\s*section\\s+['\"]${name}['\"]" sections/ components/ layouts/ */default/ 2>/dev/null | wc -l)
|
|
40
|
+
[ "$count" -eq 0 ] && echo "UNUSED $d"
|
|
41
|
+
done
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**False-positive guard:** some sections are added dynamically by users through the editor — these don't appear in any `{% section %}` literal but are still live. Before recommending removal, check `config/sections/*.json` (per-section metadata) — if metadata exists, the section is registered with the editor and probably in use. Drop the severity to `nit` and frame it as "appears unused in templates — confirm before removing".
|
|
45
|
+
|
|
46
|
+
### Components — unused
|
|
47
|
+
|
|
48
|
+
A component partial that nothing renders is dead weight. Lower false-positive rate than sections (no editor-driven instantiation).
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
for d in components/*/; do
|
|
52
|
+
name=$(basename "$d")
|
|
53
|
+
count=$(grep -rE "\\{%\\s*(render|include)\\s+['\"]${name}['\"]" sections/ components/ layouts/ */default/ 2>/dev/null | wc -l)
|
|
54
|
+
[ "$count" -eq 0 ] && echo "UNUSED $d"
|
|
55
|
+
done
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Watch for dynamic render:** `{% render snippet_name %}` where `snippet_name` is a variable. Grep for the literal first; if zero hits, also grep for the bare name as a string anywhere:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
grep -rE "['\"]${name}['\"]" sections/ components/ layouts/ */default/ 2>/dev/null | grep -v "components/${name}/"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
If that's also zero, it's safe to recommend removal.
|
|
65
|
+
|
|
66
|
+
### Block types — declared but never rendered
|
|
67
|
+
|
|
68
|
+
The most insidious form of dead code: a section's `{% schema %}` declares a block type, the editor exposes it to users, users add it, and the template renders **nothing** because the `{% case block.type %}` switch has no matching `when`.
|
|
69
|
+
|
|
70
|
+
Inside each section file:
|
|
71
|
+
|
|
72
|
+
```liquid
|
|
73
|
+
{% schema %}
|
|
74
|
+
{
|
|
75
|
+
"blocks": [
|
|
76
|
+
{ "type": "page_header", "name": "Page Header" },
|
|
77
|
+
{ "type": "product_grid", "name": "Product Grid" },
|
|
78
|
+
{ "type": "cta_banner", "name": "CTA Banner" } ← declared
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
{% endschema %}
|
|
82
|
+
|
|
83
|
+
{% for block in section.blocks %}
|
|
84
|
+
{% case block.type %}
|
|
85
|
+
{% when 'page_header' %} ...
|
|
86
|
+
{% when 'product_grid' %} ...
|
|
87
|
+
{%- comment -%} cta_banner has no `when` branch — silently rendered as nothing {%- endcomment -%}
|
|
88
|
+
{% endcase %}
|
|
89
|
+
{% endfor %}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**`should`** when the schema declares a block type that the template never handles. **`blocker`** if users have already populated this block in production (visible as orphan data in `settings_data.json` / per-section metadata) — they're staring at an empty section right now.
|
|
93
|
+
|
|
94
|
+
Quick audit per section:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
for f in sections/*/index.liquid; do
|
|
98
|
+
# block types declared in schema (very rough — schema is JSON inside Liquid)
|
|
99
|
+
declared=$(awk '/{% schema %}/,/{% endschema %}/' "$f" | grep -oE '"type":\s*"[^"]+"' | grep -v '"type": *"[a-z_]+"$' | sed 's/.*"type":\s*"\([^"]*\)".*/\1/' | sort -u)
|
|
100
|
+
# block types referenced in template
|
|
101
|
+
referenced=$(grep -oE "block\.type\s*==\s*['\"][^'\"]+['\"]|when\s+['\"][^'\"]+['\"]" "$f" | grep -oE "['\"][^'\"]+['\"]" | tr -d '"'"'" | sort -u)
|
|
102
|
+
for d in $declared; do
|
|
103
|
+
echo "$referenced" | grep -qx "$d" || echo "UNHANDLED $f block type: $d"
|
|
104
|
+
done
|
|
105
|
+
done
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
(The above is a heuristic — verify each hit manually before recommending. Block schema lives inside JSON-in-Liquid, which is hard to parse with shell tools.)
|
|
109
|
+
|
|
110
|
+
### Assets — unused
|
|
111
|
+
|
|
112
|
+
Files in `assets/` that no Liquid reference. Lowest severity because removal is cheapest and risk lowest.
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
for f in assets/*; do
|
|
116
|
+
name=$(basename "$f")
|
|
117
|
+
count=$(grep -rE "['\"]${name}['\"]" --include='*.liquid' --include='*.css' . 2>/dev/null | wc -l)
|
|
118
|
+
echo "$count $name"
|
|
119
|
+
done | sort -n | awk '$1 == 0 { print "UNUSED " $2 }'
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**False positives to filter:**
|
|
123
|
+
|
|
124
|
+
- Asset referenced via concatenation: `{% assign css = section.id | append: '.css' %}` — rare, but real.
|
|
125
|
+
- Asset referenced from another asset: a CSS file `@import`'ing another CSS file in `assets/`. Re-grep `assets/` itself before recommending removal:
|
|
126
|
+
```bash
|
|
127
|
+
grep -rl "$name" assets/
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Output format for removal recommendations
|
|
131
|
+
|
|
132
|
+
Always frame as "consider removing" with the audit command. Never propose deletion without leaving the human a way to verify:
|
|
133
|
+
|
|
134
|
+
````
|
|
135
|
+
[should] `components/old_card/index.liquid` appears unused
|
|
136
|
+
|
|
137
|
+
Audit:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
grep -rE "\{%\s*(render|include)\s+['\"]old_card['\"]" sections/ components/ layouts/ */default/ 2>/dev/null
|
|
141
|
+
# 0 results
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
If the grep stays at 0 after this PR, consider removing the file. If it's
|
|
145
|
+
referenced dynamically (variable name into `render`), keep it and add a
|
|
146
|
+
comment at the top noting that.
|
|
147
|
+
|
|
148
|
+
````
|
|
149
|
+
|
|
150
|
+
### When dead-code findings turn into fixes
|
|
151
|
+
|
|
152
|
+
If the user says `open the fix PR`:
|
|
153
|
+
|
|
154
|
+
1. **Remove sections, components, and assets** in separate commits, one per artifact, so reverting any one is a one-commit revert.
|
|
155
|
+
2. **For block types**, the fix is to either:
|
|
156
|
+
- Add the missing `when` branch in the template (preferred — the block was meant to render something), or
|
|
157
|
+
- Remove the declaration from the `{% schema %}` `blocks:` array (only if confirmed no users have populated it).
|
|
158
|
+
3. **Run `fluid theme lint --json` after each removal** — the validator catches `{% section 'name' %}` references that became dangling.
|
|
159
|
+
4. **Never bulk-delete** across categories in one commit. Each removal is its own decision.
|
|
160
|
+
|
|
161
|
+
---
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Dynamism — settings over hardcoded values
|
|
2
|
+
|
|
3
|
+
> Part of the `themes-review` skill. See [`../SKILL.md`](../SKILL.md) for the review workflow, severity ladder, and validator rules.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
|
|
7
|
+
- What "hardcoded" looks like
|
|
8
|
+
- What dynamic looks like
|
|
9
|
+
- Findings to surface
|
|
10
|
+
- When _not_ to make something a setting
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
The point of a theme is that the _company_ can edit it without touching code. Anything user-visible that's hardcoded into Liquid is a missed setting. The reviewer should be relentless here: every literal string, color, image URL, link, count, or toggle in markup is a candidate.
|
|
14
|
+
|
|
15
|
+
**The principle:** if a company employee, designer, or marketer might ever want to change it, it belongs in `{% schema %}`.
|
|
16
|
+
|
|
17
|
+
### What "hardcoded" looks like
|
|
18
|
+
|
|
19
|
+
```liquid
|
|
20
|
+
{%- comment -%} Bad: every value here is locked in code {%- endcomment -%}
|
|
21
|
+
<section style="background:#0a0a0a;padding:64px 0;">
|
|
22
|
+
<h2 style="font-size:48px;color:white;">Our Bestsellers</h2>
|
|
23
|
+
<p>Browse this season's most popular picks.</p>
|
|
24
|
+
<img src="https://cdn.example.com/static/banner.jpg" alt="Banner" />
|
|
25
|
+
<a href="/shop?sort=popular" class="btn">Shop now</a>
|
|
26
|
+
</section>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Every one of those values — background, padding, heading text, body text, image, button text, button link — is a hardcoded decision the company can never override without a code change.
|
|
30
|
+
|
|
31
|
+
### What dynamic looks like
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{% schema %}
|
|
35
|
+
{
|
|
36
|
+
"name": "Bestsellers Banner",
|
|
37
|
+
"settings": [
|
|
38
|
+
{ "type": "text", "id": "heading", "label": "Heading", "default": "Our Bestsellers" },
|
|
39
|
+
{ "type": "textarea", "id": "body", "label": "Body text", "default": "Browse this season's most popular picks." },
|
|
40
|
+
{ "type": "image_picker", "id": "banner_image", "label": "Banner image" },
|
|
41
|
+
{ "type": "color_background", "id": "bg_color", "label": "Background", "default": "#0a0a0a" },
|
|
42
|
+
{ "type": "color", "id": "text_color", "label": "Text color", "default": "#ffffff" },
|
|
43
|
+
{ "type": "padding", "id": "section_pad", "label": "Section padding" },
|
|
44
|
+
{ "type": "text", "id": "cta_label", "label": "Button label", "default": "Shop now" },
|
|
45
|
+
{ "type": "url", "id": "cta_url", "label": "Button link", "default": "/shop?sort=popular" },
|
|
46
|
+
{ "type": "checkbox", "id": "show_cta", "label": "Show button", "default": true }
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
{% endschema %}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```liquid
|
|
53
|
+
{%- assign s = section.settings -%}
|
|
54
|
+
<section style="background:{{ s.bg_color }};padding:{{ s.section_pad }};">
|
|
55
|
+
<h2 style="color:{{ s.text_color }};">{{ s.heading | default: 'Our Bestsellers' }}</h2>
|
|
56
|
+
<p>{{ s.body }}</p>
|
|
57
|
+
{%- if s.banner_image != blank -%}
|
|
58
|
+
<img src="{{ s.banner_image }}" alt="{{ s.heading | escape }}" />
|
|
59
|
+
{%- endif -%}
|
|
60
|
+
{%- if s.show_cta -%}
|
|
61
|
+
<a href="{{ s.cta_url }}" class="btn">{{ s.cta_label }}</a>
|
|
62
|
+
{%- endif -%}
|
|
63
|
+
</section>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Findings to surface
|
|
67
|
+
|
|
68
|
+
| You see | Severity | Why |
|
|
69
|
+
| ---------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
70
|
+
| User-visible literal text in markup (heading, paragraph, button label) | `should` | Should be a `text` / `textarea` / `richtext` setting. |
|
|
71
|
+
| Hardcoded colors (`#0a0a0a`, `rgb(...)`, `red`) in style/attribute | `should` | Should be a `color` or `color_background` setting. |
|
|
72
|
+
| Hardcoded image URL (external CDN, hardcoded path) | `blocker` if external CDN, `should` if internal | External CDNs can break, get blocked, or change. Upload to `assets/` or expose as `image_picker`. |
|
|
73
|
+
| Hardcoded `href` to a fixed page or `?sort=popular`-style URL | `should` | Use a `url` setting. |
|
|
74
|
+
| Hardcoded counts (`limit: 6`, `limit: 12`) in `for` loops | `nit` → `should` | Expose as a `range` setting if the company might want to change density. |
|
|
75
|
+
| Boolean branches with no user toggle (`{% if true %}`) | `should` | Either delete or expose as a `checkbox`. |
|
|
76
|
+
| Tailwind class strings that bake in color (`bg-black text-white`) | `should` | Use CSS variables driven by `color` settings. |
|
|
77
|
+
|
|
78
|
+
### When _not_ to make something a setting
|
|
79
|
+
|
|
80
|
+
Adding settings has a real cost — they clutter the editor and shift the burden to non-technical users. Skip settings for:
|
|
81
|
+
|
|
82
|
+
- Internal layout primitives (grid gaps, micro-margins) the design system already controls
|
|
83
|
+
- Engineering plumbing (data attributes, schema markup hooks)
|
|
84
|
+
- Values that _must_ stay in lockstep with code (icon class names, route slugs the theme relies on)
|
|
85
|
+
|
|
86
|
+
The test: _would I expect a non-developer to want to change this?_ No → leave it hardcoded. Yes → make it a setting.
|
|
87
|
+
|
|
88
|
+
---
|