@fluid-app/fluid-cli-theme-dev 0.1.63 → 0.1.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/dist-cli-preview/.vite/manifest.json +225 -225
- package/dist/dist-cli-preview/account-screens.js +1 -1
- package/dist/dist-cli-preview/chunks/{Breadcrumb-CZ-skiUy.js → Breadcrumb-BQKo8hPc.js} +1 -1
- package/dist/dist-cli-preview/chunks/{Combobox-BGiCYa56.js → Combobox-cf11L_T-.js} +4 -4
- package/dist/dist-cli-preview/chunks/{ContactsScreen-vsNgy_gn.js → ContactsScreen-gIiy4u7c.js} +1160 -1155
- package/dist/dist-cli-preview/chunks/{MemberManagementProviders-DMwkh3JW.js → MemberManagementProviders-C1bwc3tB.js} +258 -224
- package/dist/dist-cli-preview/chunks/{MessagingScreen-DzJOwPGH.js → MessagingScreen-B8ZxAE0L.js} +3904 -3927
- package/dist/dist-cli-preview/chunks/{MobileActionSheet-DqVPgFKI.js → MobileActionSheet-DMeTsLlK.js} +100 -100
- package/dist/dist-cli-preview/chunks/{MySiteScreen-BSmWlCKA.js → MySiteScreen-Dtw1rsF9.js} +69 -69
- package/dist/dist-cli-preview/chunks/OrdersScreen-C-b6i5jB.js +9 -0
- package/dist/dist-cli-preview/chunks/{OrdersScreen-CVEl9FnN.js → OrdersScreen-D9HfcTaa.js} +247 -249
- package/dist/dist-cli-preview/chunks/{ProfileScreen-BC5gyJDY.js → ProfileScreen-DwXozaLs.js} +1051 -965
- package/dist/dist-cli-preview/chunks/{SubscriptionsScreen-CFM4Pufg.js → SubscriptionsScreen-CIdYNpWT.js} +3 -3
- package/dist/dist-cli-preview/chunks/{account-screen-runtime-DxhJm6Qd.js → account-screen-runtime-DYMunv0A.js} +2482 -2407
- package/dist/dist-cli-preview/chunks/ar-DaN3xEy4.js +351 -0
- package/dist/dist-cli-preview/chunks/{arrow-left-UFKI-PQf.js → arrow-left-Bd7dJtIV.js} +1 -1
- package/dist/dist-cli-preview/chunks/bg-D3Np0de3.js +351 -0
- package/dist/dist-cli-preview/chunks/bn-BDSehnZ3.js +351 -0
- package/dist/dist-cli-preview/chunks/{cli-preview-DunPfnjU.js → cli-preview-BT_ARKjk.js} +13 -13
- package/dist/dist-cli-preview/chunks/{client.gen-CSTBgfxj.js → client.gen-Ck6Pm2jV.js} +246 -236
- package/dist/dist-cli-preview/chunks/contacts-ChSWj70z.js +4 -0
- package/dist/dist-cli-preview/chunks/{countries-api-adapter-B3usO25w.js → countries-api-adapter-lxSE748x.js} +2 -2
- package/dist/dist-cli-preview/chunks/{create-card-entry-container-DHgsbSyr.js → create-card-entry-container-D6kSC2Aw.js} +20726 -20509
- package/dist/dist-cli-preview/chunks/cs-CrmATj4L.js +351 -0
- package/dist/dist-cli-preview/chunks/da-CmJdErjd.js +351 -0
- package/dist/dist-cli-preview/chunks/de-C0KTNBZq.js +351 -0
- package/dist/dist-cli-preview/chunks/el-BOQs49GU.js +351 -0
- package/dist/dist-cli-preview/chunks/es-Dt3VSQPu.js +351 -0
- package/dist/dist-cli-preview/chunks/fi-B8UZ7eJL.js +351 -0
- package/dist/dist-cli-preview/chunks/fr-BomEiNL0.js +351 -0
- package/dist/dist-cli-preview/chunks/he-CfmnXjsr.js +351 -0
- package/dist/dist-cli-preview/chunks/{hey-api-client-DSvKWGPq.js → hey-api-client-DGRBsZ7Y.js} +3 -3
- package/dist/dist-cli-preview/chunks/hi-CTk5zGoJ.js +351 -0
- package/dist/dist-cli-preview/chunks/hr-DaNToqEm.js +351 -0
- package/dist/dist-cli-preview/chunks/hu-lcAuIwez.js +351 -0
- package/dist/dist-cli-preview/chunks/id-CEThcKZG.js +351 -0
- package/dist/dist-cli-preview/chunks/it-BCRCoDOg.js +351 -0
- package/dist/dist-cli-preview/chunks/ja-BIkFdIfq.js +351 -0
- package/dist/dist-cli-preview/chunks/ko-CD3bk6gv.js +351 -0
- package/dist/dist-cli-preview/chunks/{log-out-CBUq5Hd-.js → log-out-CSPZjXTK.js} +1 -1
- package/dist/dist-cli-preview/chunks/{map-pin-DHfjSyoR.js → map-pin-B2ScoijR.js} +1 -1
- package/dist/dist-cli-preview/chunks/messaging-Bq5UPkvG.js +4 -0
- package/dist/dist-cli-preview/chunks/{mount-account-screen-pc1VISwF.js → mount-account-screen-C_CdoGir.js} +2453 -2392
- package/dist/dist-cli-preview/chunks/ms-DI4mGa5X.js +351 -0
- package/dist/dist-cli-preview/chunks/mysite-DcpuKsuW.js +4 -0
- package/dist/dist-cli-preview/chunks/nl-B-XAD3Ra.js +351 -0
- package/dist/dist-cli-preview/chunks/no-C_NmYGbe.js +351 -0
- package/dist/dist-cli-preview/chunks/orders-B_57xKkq.js +4 -0
- package/dist/dist-cli-preview/chunks/pl-mQRzJZEW.js +351 -0
- package/dist/dist-cli-preview/chunks/{plus-C-lo0pFM.js → plus-CocoE5gC.js} +1 -1
- package/dist/dist-cli-preview/chunks/profile-BfM2KxKe.js +4 -0
- package/dist/dist-cli-preview/chunks/pt-BYMWPjog.js +351 -0
- package/dist/dist-cli-preview/chunks/ro-B98EuFQI.js +351 -0
- package/dist/dist-cli-preview/chunks/ru-DBO9IasC.js +351 -0
- package/dist/dist-cli-preview/chunks/schemas-D94nZk5x.js +5374 -0
- package/dist/dist-cli-preview/chunks/{screen-route-slug-utils-B254xI_J.js → screen-route-slug-utils-BDjSpDoe.js} +1 -1
- package/dist/dist-cli-preview/chunks/{sheet-haptics-context-D2WUiXJm.js → sheet-haptics-context-iOjKcNDj.js} +1 -1
- package/dist/dist-cli-preview/chunks/{sidebar-CMSWZCPS.js → sidebar-Ckzom4Xc.js} +12 -12
- package/dist/dist-cli-preview/chunks/sk-hRvNJfiN.js +351 -0
- package/dist/dist-cli-preview/chunks/{store-api-context-BIdILNWL.js → store-api-context-B96Ip2FB.js} +1 -1
- package/dist/dist-cli-preview/chunks/subscriptions-8VtIA4w9.js +4 -0
- package/dist/dist-cli-preview/chunks/sv-noYtTQbO.js +351 -0
- package/dist/dist-cli-preview/chunks/th-CJCAKsR-.js +351 -0
- package/dist/dist-cli-preview/chunks/tl-Bx8pJ7p7.js +351 -0
- package/dist/dist-cli-preview/chunks/tr-C4tCNiV7.js +351 -0
- package/dist/dist-cli-preview/chunks/{trash-2-Bq4IqH0j.js → trash-2-wVweQZMw.js} +4 -4
- package/dist/dist-cli-preview/chunks/uk-xFjXQDP3.js +351 -0
- package/dist/dist-cli-preview/chunks/{use-optional-store-Csor1Z3z.js → use-optional-store-lZUz976C.js} +2 -2
- package/dist/dist-cli-preview/chunks/use-store-DVmgsDXy.js +103 -0
- package/dist/dist-cli-preview/chunks/{useInfiniteQuery-BpFtQ7DH.js → useInfiniteQuery-Dtzov9uh.js} +1 -1
- package/dist/dist-cli-preview/chunks/{user-BGrQJ1GJ.js → user-B81IrSgp.js} +1 -1
- package/dist/dist-cli-preview/chunks/{users-DBL0isYy.js → users-BXTjoCUz.js} +1 -1
- package/dist/dist-cli-preview/chunks/vi-BAvhRTgb.js +351 -0
- package/dist/dist-cli-preview/chunks/zh_CN-Cyq9zeex.js +351 -0
- package/dist/dist-cli-preview/chunks/zh_TW-DFM5MhMA.js +351 -0
- package/dist/index.mjs +346 -32
- package/dist/index.mjs.map +1 -1
- package/dist/skills/themes-cart-feedback/SKILL.md +9 -14
- package/dist/skills/themes-review/SKILL.md +40 -34
- package/dist/skills/themes-review/references/blocks-vs-sections.md +2 -2
- package/dist/skills/themes-review/references/dead-code.md +1 -1
- package/dist/skills/themes-review/references/dynamism.md +9 -3
- package/dist/skills/themes-review/references/editor-attributes.md +25 -26
- package/dist/skills/themes-review/references/examples.md +3 -4
- package/dist/skills/themes-review/references/fairshare-attributes.md +5 -5
- package/dist/skills/themes-review/references/global-settings.md +56 -67
- package/dist/skills/themes-review/references/liquid-correctness.md +10 -15
- package/dist/skills/themes-review/references/media-tag.md +8 -7
- package/dist/skills/themes-review/references/navigation.md +18 -13
- package/dist/skills/themes-review/references/security-accessibility.md +11 -6
- package/dist/skills/themes-review/references/setting-types.md +136 -77
- package/dist/skills/themes-settings-schema/SKILL.md +15 -2
- package/package.json +2 -2
- package/dist/dist-cli-preview/chunks/OrdersScreen-d4fJE_dm.js +0 -9
- package/dist/dist-cli-preview/chunks/ar-B7NQmkZ1.js +0 -341
- package/dist/dist-cli-preview/chunks/bg-CxfL8rZN.js +0 -341
- package/dist/dist-cli-preview/chunks/bn-CdhHCK--.js +0 -341
- package/dist/dist-cli-preview/chunks/contacts-CpYlEgAq.js +0 -4
- package/dist/dist-cli-preview/chunks/cs-DTe-xGeI.js +0 -341
- package/dist/dist-cli-preview/chunks/da-B8Cl_vkl.js +0 -341
- package/dist/dist-cli-preview/chunks/de-D8cYy8G-.js +0 -341
- package/dist/dist-cli-preview/chunks/el-Chjin2JW.js +0 -341
- package/dist/dist-cli-preview/chunks/es-mp8RwFN4.js +0 -341
- package/dist/dist-cli-preview/chunks/fi-91gcpxoy.js +0 -341
- package/dist/dist-cli-preview/chunks/fr-ekvUCKTH.js +0 -341
- package/dist/dist-cli-preview/chunks/he-B7QiuquT.js +0 -341
- package/dist/dist-cli-preview/chunks/hi-DNNhi7ud.js +0 -341
- package/dist/dist-cli-preview/chunks/hr-CAwxMKX_.js +0 -341
- package/dist/dist-cli-preview/chunks/hu-D_oQwH4Q.js +0 -341
- package/dist/dist-cli-preview/chunks/id-oeAszBoo.js +0 -341
- package/dist/dist-cli-preview/chunks/isAfter-BUGgByEf.js +0 -1414
- package/dist/dist-cli-preview/chunks/it-Dme6GXSG.js +0 -341
- package/dist/dist-cli-preview/chunks/ja-CRnvQNCa.js +0 -341
- package/dist/dist-cli-preview/chunks/ko-8mk8A3_I.js +0 -341
- package/dist/dist-cli-preview/chunks/messaging-DkyH4CPP.js +0 -4
- package/dist/dist-cli-preview/chunks/ms-Bs0GvdOw.js +0 -341
- package/dist/dist-cli-preview/chunks/mysite-wvfITV7T.js +0 -4
- package/dist/dist-cli-preview/chunks/nl-tsI6lLxl.js +0 -341
- package/dist/dist-cli-preview/chunks/no-DMot6qX0.js +0 -341
- package/dist/dist-cli-preview/chunks/orders-c13r499A.js +0 -4
- package/dist/dist-cli-preview/chunks/pl-DmTxONNg.js +0 -341
- package/dist/dist-cli-preview/chunks/profile-CoWo9Sxe.js +0 -4
- package/dist/dist-cli-preview/chunks/pt-DgmwnAV-.js +0 -341
- package/dist/dist-cli-preview/chunks/ro-B4M17oRc.js +0 -341
- package/dist/dist-cli-preview/chunks/ru-BS2gZnbs.js +0 -341
- package/dist/dist-cli-preview/chunks/schemas-9_ZwC5nC.js +0 -3977
- package/dist/dist-cli-preview/chunks/sk-BdDjjHfn.js +0 -341
- package/dist/dist-cli-preview/chunks/subscriptions-BHALQrcU.js +0 -4
- package/dist/dist-cli-preview/chunks/sv-CW7FL5HA.js +0 -341
- package/dist/dist-cli-preview/chunks/th-BYBMKmSh.js +0 -341
- package/dist/dist-cli-preview/chunks/tl-CDigaLsK.js +0 -341
- package/dist/dist-cli-preview/chunks/tr-PxiurNCr.js +0 -341
- package/dist/dist-cli-preview/chunks/uk-B4aYnJ8t.js +0 -341
- package/dist/dist-cli-preview/chunks/vi-BoKSdzH3.js +0 -341
- package/dist/dist-cli-preview/chunks/zh_CN-DrCrXsZC.js +0 -341
- package/dist/dist-cli-preview/chunks/zh_TW-JmyC1W5K.js +0 -341
|
@@ -18,79 +18,60 @@ The most-overlooked layer of a theme is **`config/settings_schema.json` + `layou
|
|
|
18
18
|
|
|
19
19
|
### The pattern
|
|
20
20
|
|
|
21
|
-
1. **`config/settings_schema.json`** declares theme-wide settings, grouped by `name` (
|
|
22
|
-
2. **`layouts/theme.liquid`** reads `{{ settings.* }}` and emits **CSS custom properties on `:root`** inside a `<style>` block.
|
|
23
|
-
3. **Sections and components** consume those custom properties via `var(--
|
|
21
|
+
1. **`config/settings_schema.json`** declares theme-wide settings, grouped by `name`. The editor gives four group names special meaning — `typography` (text-preset cards), `color_schema`, `padding`, and `corner_radius` (the presets offered by section and block controls). Other groups (`shadows`, `cards`, `appearance`, …) render as plain lists.
|
|
22
|
+
2. **`layouts/theme.liquid`** reads `{{ settings.* }}` and emits **CSS custom properties on `:root`** inside a `{% style %}` or `<style>` block, one `--<setting_id>` per setting. The editor only offers a setting as a preset when such a declaration exists, and it writes `--<setting_id>` itself when it manages a variable.
|
|
23
|
+
3. **Sections and components** consume those custom properties via `var(--font_family_body)`, `var(--color_primary)`, etc. — never re-reading the underlying `settings.*` value.
|
|
24
|
+
|
|
25
|
+
The `themes-settings-schema` skill (installed as a sibling: [`../../themes-settings-schema/SKILL.md`](../../themes-settings-schema/SKILL.md)) has the full typography-preset contract.
|
|
24
26
|
|
|
25
27
|
### What `config/settings_schema.json` looks like
|
|
26
28
|
|
|
27
29
|
```json
|
|
28
30
|
[
|
|
29
|
-
{
|
|
30
|
-
"name": "theme_info",
|
|
31
|
-
"theme_name": "My Theme",
|
|
32
|
-
"theme_version": "1.0.0",
|
|
33
|
-
"theme_author": "..."
|
|
34
|
-
},
|
|
35
31
|
{
|
|
36
32
|
"name": "typography",
|
|
37
33
|
"settings": [
|
|
38
|
-
{ "type": "header", "content": "Body" },
|
|
39
34
|
{
|
|
40
35
|
"type": "font_picker",
|
|
41
36
|
"id": "font_family_body",
|
|
42
|
-
"label": "
|
|
37
|
+
"label": "Body font",
|
|
38
|
+
"default": "Inter"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"type": "font_picker",
|
|
42
|
+
"id": "font_family_heading",
|
|
43
|
+
"label": "Heading font",
|
|
43
44
|
"default": "Inter"
|
|
44
45
|
},
|
|
45
46
|
{
|
|
46
47
|
"type": "range",
|
|
47
48
|
"id": "font_weight_body",
|
|
48
|
-
"label": "
|
|
49
|
+
"label": "Body weight",
|
|
49
50
|
"min": 100,
|
|
50
51
|
"max": 900,
|
|
51
52
|
"step": 100,
|
|
52
53
|
"default": 400
|
|
53
54
|
},
|
|
54
|
-
{ "type": "header", "content": "Headings" },
|
|
55
|
-
{
|
|
56
|
-
"type": "font_picker",
|
|
57
|
-
"id": "font_family_heading",
|
|
58
|
-
"label": "Font family",
|
|
59
|
-
"default": "Inter"
|
|
60
|
-
},
|
|
61
55
|
{
|
|
62
56
|
"type": "range",
|
|
63
57
|
"id": "font_size_h1",
|
|
64
|
-
"label": "H1
|
|
58
|
+
"label": "H1",
|
|
65
59
|
"min": 24,
|
|
66
60
|
"max": 96,
|
|
67
61
|
"step": 1,
|
|
68
62
|
"default": 48,
|
|
69
|
-
"unit": "px"
|
|
63
|
+
"unit": "px",
|
|
64
|
+
"role": "heading",
|
|
65
|
+
"font_family_ref": "font_family_heading"
|
|
70
66
|
}
|
|
71
67
|
]
|
|
72
68
|
},
|
|
73
69
|
{
|
|
74
70
|
"name": "color_schema",
|
|
75
71
|
"settings": [
|
|
76
|
-
{
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
"label": "Primary",
|
|
80
|
-
"default": "#0a0a0a"
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
"type": "color",
|
|
84
|
-
"id": "color_secondary",
|
|
85
|
-
"label": "Secondary",
|
|
86
|
-
"default": "#ffffff"
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
"type": "color",
|
|
90
|
-
"id": "color_text",
|
|
91
|
-
"label": "Body text",
|
|
92
|
-
"default": "#111111"
|
|
93
|
-
}
|
|
72
|
+
{ "type": "color", "id": "color_primary", "label": "Primary", "default": "#0a0a0a" },
|
|
73
|
+
{ "type": "color", "id": "color_secondary", "label": "Secondary", "default": "#ffffff" },
|
|
74
|
+
{ "type": "color", "id": "color_text", "label": "Body text", "default": "#111111" }
|
|
94
75
|
]
|
|
95
76
|
},
|
|
96
77
|
{
|
|
@@ -99,7 +80,7 @@ The most-overlooked layer of a theme is **`config/settings_schema.json` + `layou
|
|
|
99
80
|
{
|
|
100
81
|
"type": "checkbox",
|
|
101
82
|
"id": "enable_dark_mode",
|
|
102
|
-
"label": "Enable dark mode
|
|
83
|
+
"label": "Enable dark mode",
|
|
103
84
|
"default": false
|
|
104
85
|
}
|
|
105
86
|
]
|
|
@@ -107,53 +88,59 @@ The most-overlooked layer of a theme is **`config/settings_schema.json` + `layou
|
|
|
107
88
|
]
|
|
108
89
|
```
|
|
109
90
|
|
|
110
|
-
Note: the **outer array is grouped**, not flat. Each object with a `name:` becomes a settings group in the
|
|
91
|
+
Note: the **outer array is grouped**, not flat. Each object with a `name:` becomes a settings group in the theme panel. The theme panel drops any setting without an `id`, `type`, and `label` — so `{"type": "header"}` entries render nothing there; use separate groups instead. Fluid does not read a Shopify-style `theme_info` group.
|
|
111
92
|
|
|
112
93
|
### What `layouts/theme.liquid` does with them
|
|
113
94
|
|
|
114
95
|
```liquid
|
|
115
96
|
<head>
|
|
116
97
|
...
|
|
117
|
-
{
|
|
118
|
-
|
|
98
|
+
{%- comment -%} Load the default fonts yourself; fonts a merchant adds in the Theme panel are linked into this <head> by the editor {%- endcomment -%}
|
|
99
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
100
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
101
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
|
119
102
|
|
|
120
|
-
|
|
103
|
+
{% style %}
|
|
121
104
|
:root {
|
|
122
105
|
/* Typography */
|
|
123
|
-
--
|
|
124
|
-
--
|
|
125
|
-
--
|
|
126
|
-
--
|
|
106
|
+
--font_family_body: {{ settings.font_family_body | default: "Inter" }}, system-ui, sans-serif;
|
|
107
|
+
--font_family_heading: {{ settings.font_family_heading | default: "Inter" }}, system-ui, sans-serif;
|
|
108
|
+
--font_weight_body: {{ settings.font_weight_body | default: 400 }};
|
|
109
|
+
--font_size_h1: {{ settings.font_size_h1 | default: 48 }}px;
|
|
127
110
|
|
|
128
111
|
/* Color */
|
|
129
|
-
--
|
|
130
|
-
--
|
|
131
|
-
--
|
|
112
|
+
--color_primary: {{ settings.color_primary }};
|
|
113
|
+
--color_secondary: {{ settings.color_secondary }};
|
|
114
|
+
--color_text: {{ settings.color_text }};
|
|
132
115
|
}
|
|
133
116
|
|
|
134
117
|
{%- if settings.enable_dark_mode -%}
|
|
135
118
|
@media (prefers-color-scheme: dark) {
|
|
136
119
|
:root {
|
|
137
|
-
--
|
|
138
|
-
--
|
|
139
|
-
--
|
|
120
|
+
--color_primary: #ffffff;
|
|
121
|
+
--color_secondary: #0a0a0a;
|
|
122
|
+
--color_text: #fafafa;
|
|
140
123
|
}
|
|
141
124
|
}
|
|
142
125
|
{%- endif -%}
|
|
143
|
-
|
|
126
|
+
{% endstyle %}
|
|
144
127
|
</head>
|
|
145
128
|
```
|
|
146
129
|
|
|
130
|
+
A `font_picker` value is a plain family-name string. Do not use Shopify's font filters: `font_family` does not exist in Fluid (unknown filters are silently ignored), and Fluid's `font_face` expects a font object with `family` and `src`, so `{{ settings.font_family_body | font_face }}` emits an empty `@font-face`. A global `color` setting is a color value: it renders as the saved color and also exposes `.rgb`, `.rgba`, `.hue`, `.saturation`, `.lightness`.
|
|
131
|
+
|
|
147
132
|
And sections then look like:
|
|
148
133
|
|
|
149
134
|
```css
|
|
150
135
|
.featured-products h2 {
|
|
151
|
-
font-family: var(--
|
|
152
|
-
font-size: var(--
|
|
153
|
-
color: var(--
|
|
136
|
+
font-family: var(--font_size_h1_font_family, var(--font_family_heading));
|
|
137
|
+
font-size: var(--font_size_h1);
|
|
138
|
+
color: var(--color_text);
|
|
154
139
|
}
|
|
155
140
|
```
|
|
156
141
|
|
|
142
|
+
Read a text preset's own variable (`--<preset_id>_font_family`, written by the editor once a merchant applies the preset) before the font variable, so repointing the preset in the editor moves the theme's own headings too.
|
|
143
|
+
|
|
157
144
|
### What belongs at the global level
|
|
158
145
|
|
|
159
146
|
Anything that should stay consistent across the whole theme:
|
|
@@ -162,13 +149,12 @@ Anything that should stay consistent across the whole theme:
|
|
|
162
149
|
| --------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
163
150
|
| `typography` | Body + heading font families, font weights, font size scale (`xs`–`9xl`), heading sizes (h1–h6), line height |
|
|
164
151
|
| `color_schema` | Brand palette (primary, secondary, accent, text, surface, border), plus dark-mode variants |
|
|
165
|
-
| `
|
|
152
|
+
| `padding` | Spacing scale used by section/block `padding` controls (preset group) |
|
|
166
153
|
| `layout` | Max container width, gutter, grid breakpoints |
|
|
167
|
-
| `
|
|
154
|
+
| `corner_radius` | Radius scale used by `corner_radius` controls (preset group) |
|
|
168
155
|
| `shadows` | Shadow scale (sm, md, lg) |
|
|
169
156
|
| `cards` | Card-wide defaults (radius, shadow, padding) that per-card-type groups inherit |
|
|
170
157
|
| `appearance` | Dark mode toggle, motion-reduce, animation preferences |
|
|
171
|
-
| `theme_info` | Theme name, version, author, docs URL — **metadata only, no `settings:`** |
|
|
172
158
|
|
|
173
159
|
### What does NOT belong at the global level
|
|
174
160
|
|
|
@@ -183,13 +169,13 @@ The test: _would changing this break the visual coherence of the rest of the the
|
|
|
183
169
|
|
|
184
170
|
| You see | Severity | Fix |
|
|
185
171
|
| ------------------------------------------------------------------------------------------------------------------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
186
|
-
| Section adds its own `font_family` / `font_size` / `color_*` setting when the global already exists | `should` | Use `var(--
|
|
172
|
+
| Section adds its own `font_family` / `font_size` / `color_*` setting when the global already exists | `should` | Use `var(--font_family_body)`, `var(--color_primary)`, etc. Per-section overrides should be the _exception_, configured via section setting `color_override` that defaults to `unset`. |
|
|
187
173
|
| Theme has 0 global typography settings but every section hardcodes fonts in inline styles | `blocker` | Add a `typography` group to `config/settings_schema.json` and a `:root` `<style>` block in `layouts/theme.liquid`. |
|
|
188
174
|
| Theme has 0 global color settings but uses hardcoded hex everywhere | `blocker` | Add a `color_schema` group. |
|
|
189
|
-
| `layouts/theme.liquid` reads `settings.*` but emits no `<style>`
|
|
190
|
-
| `config/settings_schema.json` is a **flat array of settings** (no `name:` groups) | `
|
|
191
|
-
| `
|
|
192
|
-
|
|
|
175
|
+
| `layouts/theme.liquid` reads `settings.*` but emits no `{% style %}` / `<style>` CSS variables | `should` | Wire the settings into `--var` declarations on `:root`. Without this layer, sections have no way to consume globals. |
|
|
176
|
+
| `config/settings_schema.json` is a **flat array of settings** (no `name:` groups) | `blocker` | The theme panel drops every entry without a group `name`, so a flat array renders an empty Theme panel. Group the settings by `name:` (typography, color_schema, …). |
|
|
177
|
+
| Theme panel groups named `spacing`, `border_radius`, `Typography`, or `colors` | `should` | The editor builds presets only from `typography`, `color_schema`, `padding`, and `corner_radius` (exact, case-sensitive). Rename so section controls offer the presets. |
|
|
178
|
+
| `font_family_body` and `font_family_heading` settings exist but every section uses a hardcoded `font-family: 'Inter'` | `blocker` | Rewire sections to `var(--font_family_heading)` / `var(--font_family_body)`. Globals that nothing consumes are dead weight. |
|
|
193
179
|
| Dark-mode toggle exists in schema but no `@media (prefers-color-scheme: dark)` / `[data-theme="dark"]` block in `theme.liquid` | `blocker` | Setting is a lie — wire it. |
|
|
194
180
|
|
|
195
181
|
### Dark-mode patterns — two acceptable shapes
|
|
@@ -209,7 +195,10 @@ jq -r '.[].name' config/settings_schema.json
|
|
|
209
195
|
grep -E 'settings\.' layouts/theme.liquid | head
|
|
210
196
|
|
|
211
197
|
# 3. Does the layout emit CSS variables?
|
|
212
|
-
grep -E '^\s*--[a-
|
|
198
|
+
grep -E '^\s*--[a-z_-]+:' layouts/theme.liquid | head
|
|
199
|
+
|
|
200
|
+
# 3b. Shopify font filters that don't work in Fluid
|
|
201
|
+
grep -nE '\| *(font_face|font_family)\b' layouts/theme.liquid
|
|
213
202
|
|
|
214
203
|
# 4. Do sections actually USE the variables (vs. hardcoded)?
|
|
215
204
|
grep -rE 'font-family:\s*[A-Za-z]' --include='*.liquid' --include='*.css' . 2>/dev/null # hardcoded fonts
|
|
@@ -23,7 +23,7 @@ The variables a template can read fall into three buckets:
|
|
|
23
23
|
|
|
24
24
|
| Bucket | Available in… | Examples |
|
|
25
25
|
| ---------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
26
|
-
| **Always available globals** | every template
|
|
26
|
+
| **Always available globals** | every storefront template | `company`, `request`, `params`, `localization`, `country`, `affiliate`, `username`, `routes`, `settings` (global), and the on-demand collections `products`, `collections`, `categories`, `posts`, `enrollment_packs`. No `cart` (only in `cart_page`), and no Shopify `shop`, `customer`, or `linklists` |
|
|
27
27
|
| **Resource context** | the matching page template, plus any section it renders | `product` (only on `product/{variant}/index.liquid` and sections rendered from it); `collection`, `category`, `post`, etc. |
|
|
28
28
|
| **Section / block scope** | inside that section only | `section`, `section.settings`, `section.blocks`, `block`, `block.settings` |
|
|
29
29
|
| **Component args** | inside a component only when explicitly passed | only the keys passed to `{% render 'name', key: value %}` |
|
|
@@ -34,7 +34,7 @@ Things that go wrong:
|
|
|
34
34
|
| -------------------------------------------------------------------------------------------------------------------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
|
|
35
35
|
| `{{ product.title }}` inside a section that's rendered on the homepage (`home_page/default/index.liquid`) without an explicit `product:` arg | `blocker` | `product` is only in scope on product page templates. On `home_page` it's `nil`, silently rendering empty. |
|
|
36
36
|
| `{{ collection.products }}` inside `sections/main_navbar/index.liquid` | `blocker` | `collection` is not in scope in a navbar section. Pass the resource explicitly via a block setting (`collection` type) and read `block.settings.collection.products`. |
|
|
37
|
-
| `{{ cart.item_count }}`
|
|
37
|
+
| `{{ cart.item_count }}` (or any `cart.*`) outside a `cart_page` template | `blocker` | `cart` exists only in `cart_page` templates; elsewhere it renders empty. Use the SDK's `<span id="fluid-cart-count">` for a live count. |
|
|
38
38
|
| `{{ section.settings.thing }}` where the schema does not declare `thing` | `blocker` | Typo or stale reference. Validator does not catch this — it only checks the schema, not the template. Grep the schema for the `id` and fix. |
|
|
39
39
|
| `{{ block.settings.thing }}` where the block's `settings:` array does not declare `thing` | `blocker` | Same as above for blocks. |
|
|
40
40
|
| `{{ block.settings.x }}` inside a section that has `blocks` declared but NOT inside a `{% for block in section.blocks %}` loop | `blocker` | `block` is only in scope inside the iteration; outside it's `nil`. |
|
|
@@ -81,19 +81,21 @@ Liquid is forgiving. `nil.foo` does not raise — it renders empty. That's worse
|
|
|
81
81
|
<h2>{{ section.settings.heading | default: 'Featured products' }}</h2>
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
**`blocker`** when
|
|
84
|
+
**`blocker`** when markup around a resource picker is unguarded and the page can render with a blank picker. An unset resolved setting is an empty object — the chain renders empty rather than raising, leaving an empty `<span>` or a broken link behind. Guard with `!= blank` (a bare `{% if p %}` is always true for an empty object):
|
|
85
85
|
|
|
86
86
|
```liquid
|
|
87
|
-
{%- comment -%} Bad:
|
|
88
|
-
<span>{{ section.settings.product.
|
|
87
|
+
{%- comment -%} Bad: renders an empty <span> when no product is picked {%- endcomment -%}
|
|
88
|
+
<span>{{ section.settings.product.selected_or_first_available_variant.price }}</span>
|
|
89
89
|
|
|
90
90
|
{%- comment -%} Good {%- endcomment -%}
|
|
91
91
|
{%- assign p = section.settings.product -%}
|
|
92
|
-
{%- if p != blank
|
|
93
|
-
<span>{{ p.
|
|
92
|
+
{%- if p != blank -%}
|
|
93
|
+
<span>{{ p.selected_or_first_available_variant.price }}</span>
|
|
94
94
|
{%- endif -%}
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
+
Prices on products and variants are already formatted for the visitor's currency. Fluid has no `money` filter — an unknown filter is silently ignored.
|
|
98
|
+
|
|
97
99
|
### 2. Whitespace control
|
|
98
100
|
|
|
99
101
|
Liquid keeps every whitespace character outside tags. In tight loops, this inflates HTML by KBs. **`should`** when a `for`/`if` block in markup context lacks `-`:
|
|
@@ -116,14 +118,7 @@ Rule of thumb: control tags (`{%- ... -%}`) get dashes; output tags (`{{ ... }}`
|
|
|
116
118
|
|
|
117
119
|
### 3. Unclosed tags / mismatched conditionals
|
|
118
120
|
|
|
119
|
-
**`blocker`.**
|
|
120
|
-
|
|
121
|
-
```bash
|
|
122
|
-
grep -cE '{%-?\s*(if|for|case|capture|comment|unless)\b' file.liquid
|
|
123
|
-
grep -cE '{%-?\s*end(if|for|case|capture|comment|unless)\b' file.liquid
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
The two counts must match.
|
|
121
|
+
**`blocker`.** An unclosed `{% if %}` (or `for`, `case`, `capture`, `unless`, …) is a Liquid syntax error. The upload still succeeds, but the storefront can no longer render that file correctly. `fluid theme lint --json` reports each unbalanced block tag with its line — run it rather than counting tags by hand.
|
|
127
122
|
|
|
128
123
|
### 4. Filter ordering
|
|
129
124
|
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
> Part of the `themes-review` skill. See [`../SKILL.md`](../SKILL.md) for the review workflow, severity ladder, and validator rules.
|
|
4
4
|
|
|
5
|
-
Render every setting- or resource-backed image and video with the `media_tag` filter. It turns an image or video into a complete, responsive HTML element — `srcset`, WebP/AVIF negotiation, `loading`, `decoding="async"`, and `alt` — so a hand-rolled `<img>` / `<video>` in a section is almost always the wrong call.
|
|
5
|
+
Render every setting- or resource-backed image and video with the `media_tag` filter — except a Fluid media item from a `media_picker`, which renders as `<fluid-media-widget>` (see the rule below). It turns an image or video into a complete, responsive HTML element — `srcset`, WebP/AVIF negotiation, `loading`, `decoding="async"`, and `alt` — so a hand-rolled `<img>` / `<video>` in a section is almost always the wrong call.
|
|
6
6
|
|
|
7
|
-
Docs: <https://docs.fluid.app/
|
|
7
|
+
Docs: <https://docs.fluid.app/themes/media-tag>
|
|
8
8
|
|
|
9
9
|
## Contents
|
|
10
10
|
|
|
@@ -15,14 +15,14 @@ Docs: <https://docs.fluid.app/docs/themes/media-tag>
|
|
|
15
15
|
|
|
16
16
|
### 1. The rule
|
|
17
17
|
|
|
18
|
-
**`should`.** A raw `<img src="{{ ... }}">` or `<video>` that renders an `image` / `image_picker` / `video_picker`
|
|
18
|
+
**`should`.** A raw `<img src="{{ ... }}">` or `<video>` that renders an `image` / `image_picker` / `video_picker` setting, a plain-file `media_picker` value, or a resource image (`product.image`, `collection.image`, …), should be `| media_tag`. Hand-rolled markup ships one fixed-size original — no responsive `srcset`, no format negotiation — which bloats the download and hurts LCP, and forces you to remember `loading` / `decoding` / `alt` every time.
|
|
19
19
|
|
|
20
20
|
```liquid
|
|
21
21
|
{%- comment -%} Bad: fixed original, manual (easy-to-forget) attributes {%- endcomment -%}
|
|
22
22
|
<img src="{{ section.settings.banner_image }}" alt="{{ section.settings.heading | escape }}" loading="lazy" decoding="async" />
|
|
23
23
|
|
|
24
24
|
{%- comment -%} Good: responsive, format-negotiated, accessible — one filter {%- endcomment -%}
|
|
25
|
-
{{ section.settings.banner_image | media_tag: sizes: '(max-width: 768px) 100vw, 1200px', alt: section.settings.
|
|
25
|
+
{{ section.settings.banner_image | media_tag: sizes: '(max-width: 768px) 100vw, 1200px', alt: section.settings.banner_image_alt }}
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
`media_tag` escapes its own output, so do **not** wrap the setting in `| escape` yourself. It renders nothing on blank input, so the `{%- if image != blank -%}` guard is optional — keep it only when surrounding markup must be suppressed too.
|
|
@@ -41,14 +41,14 @@ Pass options as filter arguments. Anything not in this table becomes a plain HTM
|
|
|
41
41
|
| `format` | `auto` | WebP/AVIF negotiation. |
|
|
42
42
|
| `crop` | — | Focus point. |
|
|
43
43
|
| `loading` | `lazy` | Use `eager` for the hero / above-the-fold image — `lazy` there delays LCP. |
|
|
44
|
-
| `alt` | image's alt text |
|
|
44
|
+
| `alt` | image's alt text | For `*_picker` settings pass the `<id>_alt` companion; `alt: ''` for decorative. |
|
|
45
45
|
|
|
46
46
|
```liquid
|
|
47
47
|
{%- comment -%} Fixed-width logo, retina srcset {%- endcomment -%}
|
|
48
48
|
{{ section.settings.logo | media_tag: width: 240, alt: 'Logo' }}
|
|
49
49
|
|
|
50
50
|
{%- comment -%} Hero — eager so it doesn't delay LCP {%- endcomment -%}
|
|
51
|
-
{{ section.settings.hero | media_tag: sizes: '100vw', loading: 'eager', alt: section.settings.
|
|
51
|
+
{{ section.settings.hero | media_tag: sizes: '100vw', loading: 'eager', alt: section.settings.hero_alt }}
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
### 3. Video
|
|
@@ -61,7 +61,8 @@ A video URL (`.mp4` / `.mov` / `.webm` / `.m4v`) or video object renders a `<vid
|
|
|
61
61
|
|
|
62
62
|
### 4. What it does *not* replace
|
|
63
63
|
|
|
64
|
+
- **Fluid media items.** A `media_picker` value with a positive `fluid_media_id` is a Fluid media item. Render it as `<fluid-media-widget media-id="{{ media.fluid_media_id }}" embed-type="{{ media.embed_type | default: 'default' }}">`, passing the saved `embed_settings` through as widget attributes (`autoOpen` → `auto-open`, `width`/`height` → `px` lengths when not responsive, `hideCta` → a disabled `cta-options-manual-override`; see [Setting types](setting-types.md#visual--media)) — `media_tag` would emit a bare `<img>`/`<video>` from its `url` and drop the embed (popover, inline shopping, CTAs). Only a plain file (`fluid_media_id` `0` or absent) goes through `media_tag`. Compare with `> 0`: Liquid treats `0` as true. Flag a `media_picker` rendered only through `media_tag` as a `blocker`.
|
|
64
65
|
- **Static theme assets** (`'logo.svg' | asset_url`) are not ImageKit media — `media_tag` passes non-ImageKit URLs through as a plain tag, so `asset_url` + a plain `<img>` stays correct for files shipped in `assets/`.
|
|
65
|
-
- **`alt` judgement** is still yours — `media_tag` defaults `alt` from the image, but `*_picker`
|
|
66
|
+
- **`alt` judgement** is still yours — `media_tag` defaults `alt` from the image, but `*_picker` values carry none. Pass the editor-managed `<id>_alt` setting (for `banner_image`, `section.settings.banner_image_alt`), or `alt: ''` for decorative. The alt rule in [security & accessibility](security-accessibility.md) still applies.
|
|
66
67
|
|
|
67
68
|
---
|
|
@@ -14,7 +14,13 @@ Prefer a `link_list` setting — **the engine's menu selector** — for navigati
|
|
|
14
14
|
|
|
15
15
|
The findings below are maintainability recommendations. Explain the benefit of managed menus, respect the user's chosen implementation, and verify destinations, dropdowns, accessibility, and desktop/mobile rendering. Hardcoded navigation is supported; it is not a validation blocker.
|
|
16
16
|
|
|
17
|
-
`link_list` is one of the canonical setting types (a single-resource picker). It
|
|
17
|
+
`link_list` is one of the canonical setting types (a single-resource picker). It saves a menu's slug, and Fluid resolves it before the template reads it:
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
{ title, handle, menu_items: [ { title, url, sub_menu_items: [ { title, url, sub_menu_items }, … ] }, … ] }
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This is **not** Shopify's shape: there is no `menu.links`, `link.links`, `link.active`, or `linklists` global. Those render empty.
|
|
18
24
|
|
|
19
25
|
### Bad — hardcoded nav
|
|
20
26
|
|
|
@@ -57,33 +63,32 @@ Or this — better, but still wrong: company-level text/URL settings paired into
|
|
|
57
63
|
|
|
58
64
|
```liquid
|
|
59
65
|
{%- assign menu = section.settings.menu -%}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
</nav>
|
|
66
|
+
{%- if menu != blank -%}
|
|
67
|
+
<nav class="main-nav" aria-label="{{ menu.title | escape }}">
|
|
68
|
+
{%- for item in menu.menu_items -%}
|
|
69
|
+
<a href="{{ item.url }}" class="main-nav__link">{{ item.title | escape }}</a>
|
|
70
|
+
{%- endfor -%}
|
|
71
|
+
</nav>
|
|
72
|
+
{%- endif -%}
|
|
68
73
|
```
|
|
69
74
|
|
|
70
|
-
The company now picks an existing menu in admin (the editor surfaces all menus the store has configured), and adds/reorders/renames items without ever touching code. Multi-level dropdowns work via `
|
|
75
|
+
The company now picks an existing menu in admin (the editor surfaces all menus the store has configured), and adds/reorders/renames items without ever touching code. Multi-level dropdowns work via `item.sub_menu_items`. There is no `active` flag on menu items. An unset `link_list` is an empty object, so guard with `!= blank`.
|
|
71
76
|
|
|
72
77
|
### Findings to surface
|
|
73
78
|
|
|
74
79
|
| You see | Severity | Fix |
|
|
75
80
|
| ----------------------------------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
76
|
-
| 2+ hardcoded `<a href="/...">` tags that visually form a navigation (header, footer columns, mobile drawer, social row) | `should` | Replace with a `link_list` setting and a `{% for
|
|
81
|
+
| 2+ hardcoded `<a href="/...">` tags that visually form a navigation (header, footer columns, mobile drawer, social row) | `should` | Replace with a `link_list` setting and a `{% for item in menu.menu_items %}` loop. |
|
|
82
|
+
| `menu.links`, `link.links`, `link.active`, or `linklists` (Shopify names) | `blocker` | Renders nothing in Fluid. Use `menu.menu_items` and `item.sub_menu_items`; menu items have no `active` flag. |
|
|
77
83
|
| Multiple parallel `text` + `url` settings used to fake a menu (`nav_label_1` / `nav_url_1`, `nav_label_2` / `nav_url_2`, ...) | `should` | Collapse to one `link_list`. Each menu item gains drag-reorder, depth, and active-state tracking. |
|
|
78
84
|
| Section that lists footer columns by hardcoding the same column twice | `should` | One block per column with a `link_list` inside the block. Users add/remove/rename columns. |
|
|
79
|
-
| Iterating `linklists` (the global) without exposing a `link_list` picker | `nit` | Acceptable for "site-wide menu" cases, but giving the user a `link_list` setting is more flexible — the section can be reused for different menus. |
|
|
80
85
|
| Breadcrumbs, social-link rows, related-links rails — anything that's a list of `{ label, url }` pairs | `should` | All belong in a `link_list`. Don't reinvent. |
|
|
81
86
|
|
|
82
87
|
### When to keep nav items singular
|
|
83
88
|
|
|
84
89
|
The `link_list` heuristic does not apply to:
|
|
85
90
|
|
|
86
|
-
- **Single CTAs** in marketing sections — these are real "hero button" / "secondary button" roles. A standalone `
|
|
91
|
+
- **Single CTAs** in marketing sections — these are real "hero button" / "secondary button" roles. A standalone `plaintext` + `url` pair is fine.
|
|
87
92
|
- **Brand logo links** — the logo's `href` is almost always "go home"; a fixed `/` is acceptable, or expose a single `url` setting.
|
|
88
93
|
- **Legal/footer links that are _required by policy_** (Terms, Privacy) and not the company's call to omit — but even these are usually better as a `link_list` so they can be reordered or extended.
|
|
89
94
|
|
|
@@ -15,12 +15,17 @@
|
|
|
15
15
|
|
|
16
16
|
### 1. Unescaped user content
|
|
17
17
|
|
|
18
|
-
**`blocker`.**
|
|
18
|
+
**`blocker`.** Escape plain strings: resource fields such as `product.title`, comments, `plaintext` settings, and global `settings.<id>` values of type `text` (the Theme panel renders `text` as a plain input).
|
|
19
19
|
|
|
20
20
|
```liquid
|
|
21
21
|
<h2>{{ product.title | escape }}</h2>
|
|
22
|
+
<input placeholder="{{ section.settings.email_placeholder | escape }}"> {%- comment -%} plaintext setting {%- endcomment -%}
|
|
22
23
|
```
|
|
23
24
|
|
|
25
|
+
Do **not** escape the HTML-shaped **section and block** setting types: `text`, `textarea`, `richtext` / `rich_text`, and `html` / `html_textarea`. In section and block schemas, `text` and `textarea` are rich-text editors — they save HTML such as `Free delivery on <strong>every</strong> order` — so `| escape` shows merchants `<strong>`. Render them raw, in element content only. A section or block value that must go into an attribute, a CSS value, a URL, or a script needs a `plaintext` setting instead; flag `text`/`textarea` in those positions as a `blocker`.
|
|
26
|
+
|
|
27
|
+
Global settings from `config/settings_schema.json` are different: the Theme panel renders `text` as a plain input and `textarea` as rich text, and offers no `plaintext`. Escape a global `text` value (`{{ settings.announcement | escape }}`); render a global `textarea` raw.
|
|
28
|
+
|
|
24
29
|
### 2. Raw output of URLs in `href` / `src`
|
|
25
30
|
|
|
26
31
|
**`should`.** Escape:
|
|
@@ -44,14 +49,14 @@
|
|
|
44
49
|
Render the image with `| media_tag` (see [Media rendering](media-tag.md)) and pass `alt`:
|
|
45
50
|
|
|
46
51
|
```liquid
|
|
47
|
-
{%- comment -%} Bad:
|
|
52
|
+
{%- comment -%} Bad: the picker value is a bare URL, so this ships alt-less {%- endcomment -%}
|
|
48
53
|
{{ section.settings.image | media_tag }}
|
|
49
54
|
|
|
50
|
-
{%- comment -%} Good {%- endcomment -%}
|
|
51
|
-
{{ section.settings.image | media_tag: alt: section.settings.
|
|
55
|
+
{%- comment -%} Good: the editor stores alt text in the `<id>_alt` companion setting {%- endcomment -%}
|
|
56
|
+
{{ section.settings.image | media_tag: alt: section.settings.image_alt }}
|
|
52
57
|
```
|
|
53
58
|
|
|
54
|
-
`
|
|
59
|
+
Every `image_picker`, `video_picker`, and `media_picker` gets an editor-managed `<id>_alt` setting that merchants fill in next to the picker; don't declare it, read it. Don't reuse a heading as alt text — a `text` heading is HTML. `media_tag` takes `alt` from an image value that carries one (an entry in `product.images`, for example); picker values carry none. Decorative images: `alt: ''`.
|
|
55
60
|
|
|
56
61
|
### 2. Heading hierarchy
|
|
57
62
|
|
|
@@ -66,7 +71,7 @@ A clickable `<div>` is `blocker`. Use `<button>` for actions, `<a href>` for nav
|
|
|
66
71
|
`| media_tag` applies `decoding="async"` and `loading="lazy"` for you — don't hand-roll them. For hero/above-the-fold images pass `loading: 'eager'`, since lazy loading there delays LCP:
|
|
67
72
|
|
|
68
73
|
```liquid
|
|
69
|
-
{{ section.settings.hero | media_tag: loading: 'eager', alt: section.settings.
|
|
74
|
+
{{ section.settings.hero | media_tag: loading: 'eager', alt: section.settings.hero_alt }}
|
|
70
75
|
```
|
|
71
76
|
|
|
72
77
|
---
|