@fluid-app/fluid-cli-theme-dev 0.1.19 → 0.1.21

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,160 @@
1
+ # Editor selector attributes — fluid_attributes
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 `fluid_attributes` emits
8
+ - The two drops
9
+ - Correct usage
10
+ - Findings to surface
11
+ - Examples from the reference theme
12
+ - Quick audit
13
+
14
+
15
+ Sections and blocks have a Liquid drop called `fluid_attributes` that emits the `data-fluid-section-*` attributes the visual editor uses to find, select, click-to-edit, and drag-to-reorder the element. **Without these attributes on the right element, the editor cannot interact with the content** — click handlers don't bind, inspector panels don't open, drag-and-drop is dead.
16
+
17
+ > **Note:** Despite the `data-fluid-*` prefix, these are _editor_ attributes — not the same as the runtime [FairShare behavioral attributes](#fairshare-behavioral-attributes--data-fluid-) below (which wire up cart, checkout, enrollment behavior). Keep them straight: `section.fluid_attributes` / `block.fluid_attributes` are Liquid drops the _theme engine_ emits in editor mode; FairShare attributes are _hand-written_ HTML attributes that the FairShare runtime SDK reads.
18
+
19
+ These attributes are only populated in editor preview mode; in production rendering, the drops emit an empty string. So a missing call has zero visible effect at runtime — the bug only surfaces in the editor. That's why reviewers must catch it.
20
+
21
+ ### What `fluid_attributes` emits
22
+
23
+ In editor mode, `{{ block.fluid_attributes }}` expands to the full set of `data-fluid-*` attributes the editor needs to find and edit the block — block id, parent section type, section id, block type, and a serialized settings payload — rendered as one attribute string on the element:
24
+
25
+ ```html
26
+ <div
27
+ data-fluid-section-block-id="abc123"
28
+ data-fluid-parent-section-type="hero"
29
+ data-fluid-section-id="45678"
30
+ data-fluid-section-block-type="heading"
31
+ data-fluid-block-attribute='{"color":"#000000",...}'
32
+ ></div>
33
+ ```
34
+
35
+ ### The two drops
36
+
37
+ | Drop | Where it's available | Where to emit it |
38
+ | -------------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------- |
39
+ | `{{ section.fluid_attributes }}` | Inside a section file (`sections/{name}/index.liquid`) | On the **root** wrapper element of the section |
40
+ | `{{ block.fluid_attributes }}` | Inside a `{% for block in section.blocks %}` loop, or when a block context is passed to a render | On the **root** wrapper element of _each_ block |
41
+
42
+ **These are the only two.** There is no `component.fluid_attributes`, no dropzone drop, no slot drop, no inline-editing helper. Components are pure partials — they don't have editor presence on their own; the section/block that _renders_ them carries the attributes.
43
+
44
+ ### Correct usage
45
+
46
+ ```liquid
47
+ {%- comment -%} Section root: section's attributes on the outer wrapper {%- endcomment -%}
48
+ <section class="featured-products" {{ section.fluid_attributes }}>
49
+ {%- for block in section.blocks -%}
50
+ {%- case block.type -%}
51
+ {%- when 'heading' -%}
52
+ <h2 class="featured-products__heading" {{ block.fluid_attributes }}>
53
+ {{ block.settings.text | escape }}
54
+ </h2>
55
+
56
+ {%- when 'product_card' -%}
57
+ <article class="featured-products__card" {{ block.fluid_attributes }}>
58
+ {% render 'product_card', product: block.settings.product %}
59
+ </article>
60
+
61
+ {%- when 'cta' -%}
62
+ {%- comment -%} Passing into a component? Forward the attrs through a named arg. {%- endcomment -%}
63
+ {% render 'button',
64
+ text: block.settings.label,
65
+ href: block.settings.link,
66
+ variant: block.settings.variant,
67
+ attr: block.fluid_attributes %}
68
+ {%- endcase -%}
69
+ {%- endfor -%}
70
+ </section>
71
+ ```
72
+
73
+ When a block's content is fully delegated to a component, the standard pattern is to forward the attributes as a named argument (`attr: block.fluid_attributes`) and have the component apply them on its root element. See `sections/hero_section/index.liquid` in the reference theme and the `button` component for examples.
74
+
75
+ ### Findings to surface
76
+
77
+ | You see | Severity | Fix |
78
+ | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
79
+ | Section file has no `{{ section.fluid_attributes }}` anywhere | `blocker` | Add it to the outer wrapper element of the section. Editor cannot select the section without it. |
80
+ | Section uses `{% for block in section.blocks %}` and the block's root element has no `{{ block.fluid_attributes }}` | `blocker` | Each block's outer element must carry its own `block.fluid_attributes`. Without it the editor can't click-to-edit individual blocks. |
81
+ | Typo: `{{ section.fluid_attribute }}` (singular) | `blocker` | The drop name is `fluid_attributes` (plural). The singular form renders empty. |
82
+ | Typo: `{{ block.fluid_attr }}` / `{{ section.fluidAttributes }}` / `{{ section.fluid_attribute_set }}` | `blocker` | Only `fluid_attributes` exists. |
83
+ | Attributes applied on a child element instead of the root | `blocker` | Move to the outermost element of the section/block. The editor walks ancestors looking for the closest match; placing it on a child means the editor picks up _that_ element as the section bounds, not the real one. |
84
+ | Same `{{ section.fluid_attributes }}` applied to multiple elements | `should` | Apply it once — to the root. Duplicates render the same `data-*` attributes twice in the DOM. |
85
+ | Hardcoded `data-fluid-*` attributes (`data-section-id="{{ section.id }}"`) instead of the helper | `blocker` | Replace with `{{ section.fluid_attributes }}` / `{{ block.fluid_attributes }}`. The helper emits a full attribute set; hand-rolling drops most of them. |
86
+ | Block-aware component (renders inside `{% for block %}`) has no `attr` parameter to receive `block.fluid_attributes` | `should` | Add an `attr` argument to the component's signature and apply it on the component's root element. |
87
+ | Conditional render without protecting against `nil`: `<div {{ section.fluid_attributes }}>` where `section` may be undefined | `should` (rarely `blocker`) | Wrap: `{% if section %}{{ section.fluid_attributes }}{% endif %}`. Same for `block`. Only relevant where the value can legitimately be nil. |
88
+
89
+ ### Examples from the reference theme
90
+
91
+ Section root with attributes — `sections/main_product/index.liquid:49-51` in the reference fluid theme:
92
+
93
+ ```liquid
94
+ <div
95
+ class="MainProductBlock MainProductBlock--breadcrumb MainProductBlock--{{ block.id }}"
96
+ {{ block.fluid_attributes }}
97
+ >
98
+ ```
99
+
100
+ Block forwarding through to a component as `attr:` — `sections/hero_section/index.liquid:140-149` in the reference fluid theme:
101
+
102
+ ```liquid
103
+ {%- if block.type == 'button' -%}
104
+ {%- render 'button',
105
+ text: block.settings.text,
106
+ href: block.settings.link,
107
+ variant: block.settings.variant | default: 'primary',
108
+ size: block.settings.size | default: 'md',
109
+ attr: block.fluid_attributes
110
+ -%}
111
+ ```
112
+
113
+ Multiple blocks each carrying their own attrs — `sections/main_category/index.liquid:109,123,158` in the reference fluid theme:
114
+
115
+ ```liquid
116
+ <div class="categories-page-header-copy" {% if header_block %}{{ header_block.fluid_attributes }}{% endif %}>
117
+ <h2>{{ page_title }}</h2>
118
+ </div>
119
+
120
+ <div class="categories-page-view-all" {% if view_all_block %}{{ view_all_block.fluid_attributes }}{% endif %}>
121
+ {% render 'button', ... %}
122
+ </div>
123
+
124
+ <div
125
+ class="splide carousel"
126
+ data-fluid-resource-slider
127
+ {% if grid_block %}{{ grid_block.fluid_attributes }}{% endif %}
128
+ >
129
+ ```
130
+
131
+ ### Quick audit
132
+
133
+ From the theme repo root:
134
+
135
+ ```bash
136
+ # Sections missing section.fluid_attributes entirely
137
+ for f in sections/*/index.liquid; do
138
+ grep -q 'section\.fluid_attributes' "$f" || echo "MISSING section.fluid_attributes $f"
139
+ done
140
+
141
+ # Sections that iterate blocks but never emit block.fluid_attributes
142
+ for f in sections/*/index.liquid; do
143
+ iterates=$(grep -q 'for\s\+block\s\+in\s\+section\.blocks' "$f" && echo yes)
144
+ emits=$(grep -q 'block\.fluid_attributes' "$f" && echo yes)
145
+ if [ "$iterates" = "yes" ] && [ "$emits" != "yes" ]; then
146
+ echo "MISSING block.fluid_attributes $f"
147
+ fi
148
+ done
149
+
150
+ # Common typos
151
+ grep -rE 'fluid_attribute[^s]|fluidAttributes|fluid_attribute_set' sections/ components/
152
+
153
+ # Hardcoded data-fluid-* attributes (likely should use the helper)
154
+ grep -rE 'data-fluid-section-id|data-fluid-section-block-id|data-fluid-parent-section-type' sections/ components/ \
155
+ | grep -v 'fluid_attributes'
156
+ ```
157
+
158
+ The last grep is the one to actually open a PR comment on — any hit is a hand-rolled subset of what the helper would emit.
159
+
160
+ ---
@@ -0,0 +1,111 @@
1
+ # Worked examples — full section reviews
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
+ - Summary review output
8
+ - Inline comment (line 9)
9
+ - Inline comment (lines 5-8)
10
+
11
+
12
+ **File:** `sections/featured_products/index.liquid`
13
+
14
+ ```liquid
15
+ {% schema %}
16
+ {
17
+ "name": "Featured Products",
18
+ "settings": [
19
+ { "type": "text", "id": "heading", "label": "Heading" },
20
+ { "type": "product", "id": "p1", "label": "Product 1" },
21
+ { "type": "product", "id": "p2", "label": "Product 2" },
22
+ { "type": "product", "id": "p3", "label": "Product 3" },
23
+ { "type": "product", "id": "p4", "label": "Product 4" },
24
+ { "type": "text_area", "id": "desc", "label": "Description" },
25
+ { "type": "image", "id": "bg", "label": "Background" }
26
+ ]
27
+ }
28
+ {% endschema %}
29
+
30
+ <section>
31
+ <h2>{{ section.settings.heading }}</h2>
32
+
33
+ {% for product in products %}
34
+ <img src="{{ 'placeholder.png' | asset_url }}" />
35
+ <a href="{{ product.url }}">{{ product.title }}</a>
36
+ {% endfor %}
37
+
38
+ {% if section.settings.p1 != blank %}{% render 'product_card', product: section.settings.p1 %}{% endif %}
39
+ {% if section.settings.p2 != blank %}{% render 'product_card', product: section.settings.p2 %}{% endif %}
40
+ {% if section.settings.p3 != blank %}{% render 'product_card', product: section.settings.p3 %}{% endif %}
41
+ {% if section.settings.p4 != blank %}{% render 'product_card', product: section.settings.p4 %}{% endif %}
42
+ </section>
43
+ ````
44
+
45
+ ### Summary review output
46
+
47
+ ```
48
+ ## Theme review — featured_products.liquid
49
+
50
+ **8 findings** — 3 blocker, 4 should, 1 nit
51
+
52
+ Reproduce locally with `fluid theme lint --json`.
53
+
54
+ | Severity | Line | Finding |
55
+ |---|---|---|
56
+ | blocker | 9 | Invalid type `text_area` — use `textarea`. Validator rejects. |
57
+ | blocker | 16 | Missing `{{ section.fluid_attributes }}` on the section root — editor cannot select or click-to-edit this section. Replace `<section>` with `<section {{ section.fluid_attributes }}>`. |
58
+ | blocker | 18 | `{% for product in products %}` — `products` is undefined in this scope. It is not a global drop, and the schema declares no `products` setting. Loop silently iterates `nil`. Collapse to the `product_list` fix below (`section.settings.products`) or remove. |
59
+ | should | 10 | Type `image` is valid but `image_picker` is clearer for new code |
60
+ | should | 5-8 | Four `product` settings, same role — collapse to one `product_list` with `limit: 4` |
61
+ | should | 19 | `asset_url` resolved inside loop — hoist above |
62
+ | should | 16-21 | Missing whitespace control on `{% for %}` / `{% endfor %}` |
63
+ | nit | 17 | `<h2>{{ section.settings.heading }}</h2>` — add `| default: 'Featured'` |
64
+ ```
65
+
66
+ ### Inline comment (line 9)
67
+
68
+ ````
69
+ [blocker] Invalid setting type `text_area`
70
+
71
+ The schema validator only accepts the canonical type list. `fluid theme lint --json`
72
+ fails on this. The correct type is `textarea`:
73
+
74
+ ```json
75
+ { "type": "textarea", "id": "desc", "label": "Description" }
76
+ ```
77
+ ````
78
+
79
+ ### Inline comment (lines 5-8)
80
+
81
+ ````
82
+
83
+ [should] Four `product` settings — collapse to `product_list`
84
+
85
+ Four singular product pickers playing the same role. Replace with one list:
86
+
87
+ ```json
88
+ {
89
+ "type": "product_list",
90
+ "id": "products",
91
+ "label": "Products",
92
+ "limit": 4
93
+ }
94
+ ```
95
+
96
+ Render block becomes a single loop:
97
+
98
+ ```liquid
99
+ {%- for product in section.settings.products -%}
100
+ {% render 'product_card', product: product %}
101
+ {%- endfor -%}
102
+ ```
103
+
104
+ This drops 4 conditionals + 4 settings into 1 + 1 and removes the per-slot upper
105
+ bound the manual setup encoded by accident. A `product_list` does **not** give
106
+ drag-and-drop reorder — that's a blocks feature; model items as blocks if the
107
+ merchant needs to reorder them.
108
+
109
+ ````
110
+
111
+ ---
@@ -0,0 +1,185 @@
1
+ # FairShare behavioral attributes — data-fluid-*
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
+ - Naming convention
8
+ - Loading the runtime
9
+ - The attributes
10
+ - Correct usage examples
11
+ - Findings to surface
12
+ - Quick audit
13
+ - Checklist for FairShare-attributed elements
14
+
15
+
16
+ These are the **runtime behavioral attributes** consumed by the FairShare web-widget SDK (loaded from `https://assets.fluid.app/scripts/fluid-sdk/latest/web-widgets/index.js`). Theme authors hand-write them on buttons, links, and elements to wire up commerce behavior — add to cart, open cart drawer, add enrollment packs, etc.
17
+
18
+ **This is different from `section.fluid_attributes` above.** The editor attributes are emitted by Liquid drops in editor mode. These are static HTML attributes you write into the template that the runtime SDK scans for on `DOMContentLoaded`.
19
+
20
+ ### Naming convention
21
+
22
+ All FairShare runtime attributes are `data-fluid-*` (kebab-case, `data-` prefix). The SDK is case-sensitive — `data-fluid-add-to-cart` works, `data-fluid-addToCart` and `data-fluid_add_to_cart` do not. The runtime auto-scans the DOM; no init call is required.
23
+
24
+ ### Loading the runtime
25
+
26
+ ```html
27
+ <script
28
+ id="fluid-cdn-script"
29
+ src="https://assets.fluid.app/scripts/fluid-sdk/latest/web-widgets/index.js"
30
+ data-fluid-shop="{{ shop.handle }}"
31
+ defer
32
+ ></script>
33
+ ```
34
+
35
+ `data-fluid-shop` is required. Optional script-level attributes: `data-fluid-api-base-url`, `data-fluid-country` (2-letter ISO), `data-fluid-language` (2–3 letter), `data-fluid-rep-id`, `data-share-guid`, `data-debug`.
36
+
37
+ ### The attributes
38
+
39
+ | Attribute | Value shape | Goes on | Notes |
40
+ | --------------------------------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
41
+ | `data-fluid-cart` | `"open"` / `"close"` / `"toggle"` | button/link | Lowercase only. Strict equality check in the SDK. |
42
+ | `data-fluid-add-to-cart` | comma-separated variant IDs (integers) | button/link | Companion for `data-fluid-quantity`, `data-fluid-subscribe`, `data-fluid-subscription-plan-id`, `data-fluid-bundled-items`, `data-fluid-open-cart-after-add`. |
43
+ | `data-fluid-quantity` | positive integer | button/link **with** `data-fluid-add-to-cart` or `data-fluid-add-enrollment-pack` | Defaults to 1. |
44
+ | `data-fluid-subscribe` | `"true"` / `"false"` or comma-separated per item | button/link **with** `data-fluid-add-to-cart` | Per-item shape mirrors the variant ID list. |
45
+ | `data-fluid-subscription-plan-id` | **single** integer | button/link **with** `data-fluid-add-to-cart` | **Only one** — comma-separated values are not supported. |
46
+ | `data-fluid-bundled-items` | JSON string `[{"variant_id": int, "quantity": int}, ...]` | button/link **with** `data-fluid-add-to-cart` | Invalid JSON is logged and silently dropped. |
47
+ | `data-fluid-open-cart-after-add` | `"true"` / `"false"` | button/link **with** add-to-cart or enrollment-pack | Defaults to `"true"`. Set `"false"` to keep shopping. |
48
+ | `data-fluid-add-enrollment-pack` | enrollment pack ID (integer) | button/link | Can combine with `data-fluid-add-to-cart` for pack + items in one click. |
49
+ | `data-fluid-bundle-selections` | JSON string `[{"variant_id": int, "bundled_items": [...]}]` | button/link **with** `data-fluid-add-enrollment-pack` | For packs whose products take variant/bundle config. |
50
+ | `data-fluid-add-combined` | `"{enrollmentPackId}+{variantId1},{variantId2},..."` | button/link | Alternative to setting both `data-fluid-add-to-cart` and `data-fluid-add-enrollment-pack`. |
51
+
52
+ ### Correct usage examples
53
+
54
+ ```html
55
+ {%- comment -%} Open cart drawer {%- endcomment -%}
56
+ <button data-fluid-cart="open">Cart ({{ cart.item_count }})</button>
57
+
58
+ {%- comment -%} Add one item {%- endcomment -%}
59
+ <button data-fluid-add-to-cart="{{ product.first_variant.id }}">
60
+ Add to cart
61
+ </button>
62
+
63
+ {%- comment -%} Add with quantity + subscription {%- endcomment -%}
64
+ <button
65
+ data-fluid-add-to-cart="{{ variant.id }}"
66
+ data-fluid-quantity="2"
67
+ data-fluid-subscribe="true"
68
+ data-fluid-subscription-plan-id="{{ section.settings.default_plan_id }}"
69
+ >
70
+ Subscribe &amp; save
71
+ </button>
72
+
73
+ {%- comment -%} Add a bundle product with its bundled items {%- endcomment -%}
74
+ <button
75
+ data-fluid-add-to-cart="{{ bundle_variant.id }}"
76
+ data-fluid-bundled-items='[
77
+ {%- for item in selected_items -%}
78
+ { "variant_id": {{ item.variant_id }}, "quantity": {{ item.qty }} }{%- unless forloop.last -%},{%- endunless -%}
79
+ {%- endfor -%}
80
+ ]'
81
+ data-fluid-open-cart-after-add="false"
82
+ >
83
+ Add bundle
84
+ </button>
85
+
86
+ {%- comment -%} Add an enrollment pack {%- endcomment -%}
87
+ <button data-fluid-add-enrollment-pack="{{ section.settings.starter_pack.id }}">
88
+ Join with starter pack
89
+ </button>
90
+ ```
91
+
92
+ ### Findings to surface
93
+
94
+ The reviewer should flag these on any element that uses `data-fluid-*`:
95
+
96
+ #### Wrong attribute names (typos / wrong casing)
97
+
98
+ **`blocker`** — the SDK only recognizes the exact kebab-case forms listed above.
99
+
100
+ | You see | Severity | Fix |
101
+ | ----------------------------------------------------------- | --------- | ------------------------------------------------ |
102
+ | `data-fluid-addToCart="..."` | `blocker` | `data-fluid-add-to-cart="..."` |
103
+ | `data-fluid_add_to_cart="..."` (underscores) | `blocker` | Kebab-case only. |
104
+ | `datafluid-add-to-cart="..."` (missing hyphen after `data`) | `blocker` | Must be `data-fluid-*`. |
105
+ | `fluid-add-to-cart="..."` (missing `data-` prefix) | `blocker` | Add `data-`. |
106
+ | `data-fluid-cart="Open"` / `"OPEN"` | `blocker` | Lowercase: `"open"` / `"close"` / `"toggle"`. |
107
+ | `data-fluid-cart="show"` / `"display"` | `blocker` | Only `open` / `close` / `toggle` are recognized. |
108
+
109
+ #### Missing required companions / dangling modifiers
110
+
111
+ **`blocker`** — modifier attributes are inert without their owner.
112
+
113
+ | You see | Severity | Fix |
114
+ | -------------------------------------------------------------------------------------------------------------- | --------- | ------------------------------------------------------- |
115
+ | `data-fluid-quantity` on an element with neither `data-fluid-add-to-cart` nor `data-fluid-add-enrollment-pack` | `blocker` | Either pair it with one of the add-actions or remove. |
116
+ | `data-fluid-subscribe` without `data-fluid-add-to-cart` | `blocker` | Subscribe is meaningful only in an add-to-cart context. |
117
+ | `data-fluid-subscription-plan-id` without `data-fluid-add-to-cart` | `blocker` | Same. |
118
+ | `data-fluid-bundled-items` without `data-fluid-add-to-cart` | `blocker` | Bundled items modify the add-to-cart action. |
119
+ | `data-fluid-bundle-selections` without `data-fluid-add-enrollment-pack` | `blocker` | Only meaningful when adding a pack. |
120
+ | `data-fluid-open-cart-after-add` on an element with no add-action | `should` | Inert — remove or pair with an add-action. |
121
+
122
+ #### Wrong value shapes
123
+
124
+ | You see | Severity | Fix |
125
+ | ------------------------------------------------------------------------------------------ | --------- | ------------------------------------------------------------------------------------------------------------- |
126
+ | `data-fluid-subscription-plan-id="4,5"` (comma-separated) | `blocker` | Single integer only. If the items need different plans, that's not supported — drop down to single-item rows. |
127
+ | `data-fluid-quantity="2.5"` / `"0"` / `"-1"` | `blocker` | Positive integer. |
128
+ | `data-fluid-subscribe="yes"` / `"1"` / `"on"` | `blocker` | String `"true"` or `"false"` only. |
129
+ | `data-fluid-bundled-items='[{"variant_id":"39325", "quantity":1}]'` (variant_id as string) | `blocker` | `variant_id` and `quantity` must be JSON numbers. |
130
+ | `data-fluid-bundled-items='{...}'` (object, not array) | `blocker` | Must be a JSON array. |
131
+ | Single-quote-wrapped JSON without escaping inner double quotes | `blocker` | Use `'[...]'` outer + `"..."` inner per the examples. Mismatched quoting breaks parsing silently. |
132
+ | `data-fluid-country="USA"` (3-letter) on the script tag | `blocker` | 2-letter ISO. `"US"`, not `"USA"`. |
133
+ | `data-fluid-language="english"` on the script tag | `blocker` | 2–3 letter code: `"en"`, `"es"`, `"fil"`. |
134
+
135
+ #### Wrong element type / placement
136
+
137
+ | You see | Severity | Fix |
138
+ | ----------------------------------------------------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
139
+ | `data-fluid-add-to-cart` on a `<div>` with no click semantics | `should` (`blocker` for accessibility-critical pages) | Use `<button type="button">` or `<a href="...">`. |
140
+ | `data-fluid-add-to-cart` on a parent that wraps multiple buttons | `blocker` | Move to the specific clickable child — the SDK click handler fires once per click, on the element with the attribute. |
141
+ | `data-fluid-shop` missing from the `<script id="fluid-cdn-script">` tag | `blocker` | Required for SDK init. Without it, none of the `data-fluid-*` attributes do anything. |
142
+ | Multiple `<script>` tags with `id="fluid-cdn-script"` | `blocker` | The SDK uses this ID for self-discovery; duplicates pick a random one. |
143
+
144
+ #### Liquid-side mistakes
145
+
146
+ | You see | Severity | Fix |
147
+ | ---------------------------------------------------------------------------------------------- | ---------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------- |
148
+ | `data-fluid-add-to-cart="{{ variant }}"` (drop, not `.id`) | `blocker` | `{{ variant.id }}`. |
149
+ | `data-fluid-quantity="{{ section.settings.default_qty | default: 1 }}"` rendering to empty | `should` | Always provide a `default:` filter for settings that are required by the SDK to be integers. |
150
+ | `data-fluid-bundled-items` JSON built with un-escaped Liquid (commas/quotes from user content) | `blocker` | Render through ` | json` or hand-craft only with sanitized numeric IDs. User-controlled strings inside JSON attributes are an XSS path. |
151
+
152
+ ### Quick audit
153
+
154
+ From the theme repo root:
155
+
156
+ ```bash
157
+ # Wrong attribute prefixes
158
+ grep -rE 'datafluid-|data-fluid_|[^a-z-]fluid-add-to-cart|[^a-z-]fluid-cart=' --include='*.liquid' . 2>/dev/null
159
+
160
+ # Common camelCase typos
161
+ grep -rE 'data-fluid-(addToCart|openCartAfterAdd|subscriptionPlanId|bundledItems)' --include='*.liquid' . 2>/dev/null
162
+
163
+ # Modifier attributes without an owner action in the same element
164
+ # (heuristic — manual review still needed)
165
+ grep -rln 'data-fluid-quantity' --include='*.liquid' . 2>/dev/null | while read -r f; do
166
+ if ! grep -q 'data-fluid-add-to-cart\|data-fluid-add-enrollment-pack' "$f"; then
167
+ echo "POSSIBLE_ORPHAN $f"
168
+ fi
169
+ done
170
+
171
+ # Wrong cart action values
172
+ grep -rE 'data-fluid-cart="(?!(open|close|toggle)")' --include='*.liquid' . 2>/dev/null
173
+ ```
174
+
175
+ ### Checklist for FairShare-attributed elements
176
+
177
+ - [ ] Attribute name is exact kebab-case with `data-fluid-` prefix
178
+ - [ ] `data-fluid-cart` value is `open` / `close` / `toggle` (lowercase)
179
+ - [ ] Modifier attributes (`quantity`, `subscribe`, `subscription-plan-id`, `bundled-items`, `open-cart-after-add`) are paired with an add-action attribute
180
+ - [ ] `data-fluid-subscription-plan-id` carries a single integer
181
+ - [ ] `data-fluid-bundled-items` / `data-fluid-bundle-selections` are valid JSON arrays with numeric IDs and quantities
182
+ - [ ] Attributes live on a `<button>` or `<a>` (or have appropriate ARIA + click semantics)
183
+ - [ ] Script tag has `id="fluid-cdn-script"` and `data-fluid-shop` set, and there is exactly one
184
+
185
+ ---