@fluid-app/fluid-cli-theme-dev 0.1.26 → 0.1.28
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/dist/skills/template-stylesheet-to-asset-migration/SKILL.md +164 -16
- package/dist/skills/themes-review/SKILL.md +7 -4
- package/dist/skills/themes-review/references/blocks-vs-sections.md +1 -3
- package/dist/skills/themes-review/references/css-js-hygiene.md +2 -1
- package/dist/skills/themes-review/references/dynamism.md +2 -3
- package/dist/skills/themes-review/references/media-tag.md +67 -0
- package/dist/skills/themes-review/references/security-accessibility.md +9 -7
- package/dist/skills/themes-review/references/setting-types.md +2 -0
- package/package.json +3 -3
|
@@ -1,33 +1,45 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: template-stylesheet-to-asset-migration
|
|
3
3
|
description: |
|
|
4
|
-
Migrates
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
4
|
+
Migrates deprecated stylesheet files in a local Fluid theme repo to theme-level assets.
|
|
5
|
+
Covers two related shapes: (1) per-template `<themeable_type>/<name>/styles.css` files
|
|
6
|
+
→ `assets/template-*.css` referenced from each template's `index.liquid`; (2) the
|
|
7
|
+
theme-root `styles.css` and `global_styles.css` (theme-level `custom_stylesheet` /
|
|
8
|
+
`global_stylesheet` columns) → `assets/styles.css` and `assets/global_styles.css`
|
|
9
|
+
referenced from the theme's `layouts/theme.liquid` inside `<head>`. Use when
|
|
10
|
+
`fluid theme push` warned about skipped stylesheets, when a push failed with 422
|
|
11
|
+
because the backing company has `STYLESHEET_STRICT_INPUT` on, or when proactively
|
|
12
|
+
bringing an older repo onto the new asset-based shape. The skill walks the repo
|
|
13
|
+
one file at a time — moves CSS bytes, chooses inline vs external emission based
|
|
14
|
+
on size, updates the appropriate Liquid file, and lints. Never pushes without
|
|
15
|
+
explicit approval.
|
|
13
16
|
---
|
|
14
17
|
|
|
15
|
-
#
|
|
18
|
+
# Stylesheet → Theme-Level Asset Migration
|
|
19
|
+
|
|
20
|
+
Two shapes ship in the same repo migration: **per-template** stylesheets (the older, larger surface — dozens of files) and **theme-level** stylesheets (two files at the theme root). Both funnel through the same `STYLESHEET_STRICT_INPUT` company feature flag on the backend; once it flips on, every deprecated stylesheet payload is rejected with 422 and every deprecated stylesheet column is nilled from API responses.
|
|
21
|
+
|
|
22
|
+
Work Part 1 first (bulk of files), then Part 2 (the two theme-root files).
|
|
16
23
|
|
|
17
24
|
## Why this migration exists
|
|
18
25
|
|
|
19
|
-
Fluid used to inject
|
|
26
|
+
Fluid used to inject deprecated stylesheet columns into the rendered HTML as inline `<style>` blocks on every request. That bloated HTML, defeated browser and CDN caching, and made stylesheets impossible to author independently of Liquid. The new model:
|
|
20
27
|
|
|
21
|
-
- Each
|
|
22
|
-
- The template's `index.liquid` **explicitly references** the asset via a
|
|
28
|
+
- Each stylesheet lives in a **theme-level `FileResource`** — a real asset under `assets/` in the theme repo, cacheable, editable independently.
|
|
29
|
+
- The referencing Liquid file (each template's `index.liquid` for per-template, `layouts/theme.liquid` for theme-level) **explicitly references** the asset via a Shopify-compat filter:
|
|
23
30
|
|
|
24
31
|
```liquid
|
|
25
|
-
{{ 'template-<themeable_type>-<name>.css' | asset_url | stylesheet_tag }}
|
|
32
|
+
{{ 'template-<themeable_type>-<name>.css' | asset_url | stylesheet_tag }} {%- comment -%} per-template external <link> {%- endcomment -%}
|
|
33
|
+
{{ 'styles.css' | inline_asset_content }} {%- comment -%} theme-level inline <style> {%- endcomment -%}
|
|
26
34
|
```
|
|
27
35
|
|
|
28
|
-
- Once a template has been migrated (backend marker `stylesheet_tag_injected: true`), the CLI **skips** any local `<themeable_type>/<name>/styles.css` on push and prints a warning
|
|
36
|
+
- Once a per-template stylesheet has been migrated (backend marker `stylesheet_tag_injected: true`), the CLI **skips** any local `<themeable_type>/<name>/styles.css` on push and prints a warning. Under the `STYLESHEET_STRICT_INPUT` company feature flag, the API rejects both deprecated shapes with 422 for any caller that still sends them.
|
|
37
|
+
|
|
38
|
+
The migration is one-way. Once the flag flips on for a company, legacy `styles.css` pushes just fail — the repo needs to be on the new shape first.
|
|
39
|
+
|
|
40
|
+
# Part 1 — Per-template stylesheets
|
|
29
41
|
|
|
30
|
-
|
|
42
|
+
Applies to every `<themeable_type>/<name>/styles.css` under the theme root except mysite.
|
|
31
43
|
|
|
32
44
|
## When to use this skill
|
|
33
45
|
|
|
@@ -186,3 +198,139 @@ Every migrated template's `index.liquid` starts with a `{{ 'template-*.css' | ..
|
|
|
186
198
|
- **Refactoring the CSS while migrating.** Two changes at once — hard to review, easy to break. Do the byte-for-byte move first, refactor later in its own commit.
|
|
187
199
|
- **Skipping the lint step.** The validator catches missing assets, dangling Liquid expressions, and other easy misses.
|
|
188
200
|
- **Pushing from within this skill.** Not this skill's job. Hand the reshaped repo back to the user; they decide when to push.
|
|
201
|
+
|
|
202
|
+
# Part 2 — Theme-level stylesheets (`styles.css` + `global_styles.css` at the theme root)
|
|
203
|
+
|
|
204
|
+
## What this shape is
|
|
205
|
+
|
|
206
|
+
Two files at the theme root — **NOT** inside a `<themeable_type>/<name>/` folder:
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
theme_root/
|
|
210
|
+
├── styles.css ← theme-level custom_stylesheet column
|
|
211
|
+
├── global_styles.css ← theme-level global_stylesheet column
|
|
212
|
+
├── variables.json
|
|
213
|
+
├── layouts/theme.liquid ← the layout template
|
|
214
|
+
├── assets/
|
|
215
|
+
└── …
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Both files are the legacy on-disk shape of two DB columns: `application_themes.custom_stylesheet` and `application_themes.global_stylesheet`. The storefront renderer used to inline them into `<head>` from those columns; the new shape is theme-level FRs the layout references directly.
|
|
219
|
+
|
|
220
|
+
## Skip conditions
|
|
221
|
+
|
|
222
|
+
- Repo has no `styles.css` and no `global_styles.css` at the theme root → nothing to do.
|
|
223
|
+
- Repo's `layouts/theme.liquid` already includes both `{{ 'styles.css' | inline_asset_content }}` and `{{ 'global_styles.css' | inline_asset_content }}` **AND** the two files no longer exist at the theme root → already migrated.
|
|
224
|
+
- Root theme (no company_id) — theme-level migration is out of scope for root themes; only company-scoped themes go through this shape.
|
|
225
|
+
|
|
226
|
+
## Emission choice — always inline
|
|
227
|
+
|
|
228
|
+
Both filters exist for theme-level too (`asset_url | stylesheet_tag` for external, `inline_asset_content` for inline). **The theme-level migration always uses inline** — matches the pre-migration output byte-for-byte (`PageBuilder` used to emit one inline `<style>` block for these). No size threshold; no external link. Storefront behavior is preserved exactly.
|
|
229
|
+
|
|
230
|
+
## Workflow
|
|
231
|
+
|
|
232
|
+
Work the two files together — they migrate as a pair because they share one target Liquid file (`layouts/theme.liquid`).
|
|
233
|
+
|
|
234
|
+
1. **Read** the three inputs:
|
|
235
|
+
- `styles.css` at theme root (may or may not exist).
|
|
236
|
+
- `global_styles.css` at theme root (may or may not exist).
|
|
237
|
+
- `layouts/theme.liquid` — the layout to edit.
|
|
238
|
+
2. **Move** each present file to `assets/` with the same filename (no rename):
|
|
239
|
+
- `styles.css` → `assets/styles.css`
|
|
240
|
+
- `global_styles.css` → `assets/global_styles.css`
|
|
241
|
+
3. **Edit** `layouts/theme.liquid` — insert both tags immediately before `</head>` (see "Placement" below).
|
|
242
|
+
4. **Delete** the theme-root originals (already moved).
|
|
243
|
+
5. **Validate** with `fluid theme lint --json` — parse, fix, re-run until clean.
|
|
244
|
+
6. **Preview** — if `fluid theme dev` is running, reload the storefront and confirm the CSS still renders. Byte-for-byte identical to pre-migration.
|
|
245
|
+
|
|
246
|
+
## Filename convention
|
|
247
|
+
|
|
248
|
+
Fixed. Do **not** rename these to `template-*.css` — they are theme-level, not per-template. The backend expects the exact filenames `styles.css` and `global_styles.css` on the theme's FR list. Existing pre-migration FRs at these filenames (author-uploaded, prior migrator runs) are prepended on backfill, not overwritten.
|
|
249
|
+
|
|
250
|
+
## Placement in `layouts/theme.liquid`
|
|
251
|
+
|
|
252
|
+
Insert both tags immediately before `</head>`, `global_styles.css` first (foundation) then `styles.css` (override). The `<head>` region of `theme.liquid` typically has other `{% style %}` literals for CSS variables — put the two `inline_asset_content` tags AFTER those so the theme-level custom CSS wins the cascade for equal specificity.
|
|
253
|
+
|
|
254
|
+
```liquid
|
|
255
|
+
{% style %}
|
|
256
|
+
:root {
|
|
257
|
+
/* CSS variables from settings */
|
|
258
|
+
}
|
|
259
|
+
{% endstyle %}
|
|
260
|
+
|
|
261
|
+
{{ 'global_styles.css' | inline_asset_content }}
|
|
262
|
+
{{ 'styles.css' | inline_asset_content }}
|
|
263
|
+
</head>
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Worked example
|
|
267
|
+
|
|
268
|
+
Starting state — theme root:
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
theme_root/
|
|
272
|
+
├── styles.css ← 800 bytes of custom overrides
|
|
273
|
+
├── global_styles.css ← 51,000 bytes of foundation CSS
|
|
274
|
+
├── variables.json
|
|
275
|
+
├── layouts/theme.liquid
|
|
276
|
+
├── assets/
|
|
277
|
+
└── …
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**Step 1 — move both:**
|
|
281
|
+
|
|
282
|
+
```
|
|
283
|
+
mv styles.css assets/styles.css
|
|
284
|
+
mv global_styles.css assets/global_styles.css
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Step 2 — edit `layouts/theme.liquid`, insert before `</head>`:**
|
|
288
|
+
|
|
289
|
+
```liquid
|
|
290
|
+
{% style %}
|
|
291
|
+
:root { /* ... */ }
|
|
292
|
+
{% endstyle %}
|
|
293
|
+
|
|
294
|
+
{{ 'global_styles.css' | inline_asset_content }}
|
|
295
|
+
{{ 'styles.css' | inline_asset_content }}
|
|
296
|
+
</head>
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**Step 3 — lint:**
|
|
300
|
+
|
|
301
|
+
```
|
|
302
|
+
fluid theme lint --json
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
**Step 4 — dev-server sanity check:**
|
|
306
|
+
|
|
307
|
+
Confirm the storefront renders with the same CSS. Both `<style>` blocks appear in `<head>` in the DOM inspector.
|
|
308
|
+
|
|
309
|
+
## What good looks like after Part 2
|
|
310
|
+
|
|
311
|
+
- `styles.css` and `global_styles.css` no longer exist at the theme root — only in `assets/`.
|
|
312
|
+
- `layouts/theme.liquid` has both `{{ '…' | inline_asset_content }}` tags before `</head>`.
|
|
313
|
+
- `fluid theme lint --json` is clean.
|
|
314
|
+
- `fluid theme push` reports no 422 and no "Skipped … styles.css" warning.
|
|
315
|
+
|
|
316
|
+
## Post-migration repo shape
|
|
317
|
+
|
|
318
|
+
```
|
|
319
|
+
theme_root/
|
|
320
|
+
├── variables.json
|
|
321
|
+
├── layouts/theme.liquid ← references both theme-level FRs via inline_asset_content
|
|
322
|
+
├── assets/
|
|
323
|
+
│ ├── styles.css ← was theme-root styles.css (custom_stylesheet)
|
|
324
|
+
│ ├── global_styles.css ← was theme-root global_styles.css (global_stylesheet)
|
|
325
|
+
│ ├── template-*.css ← Part 1 outputs (one per migrated template)
|
|
326
|
+
│ └── …
|
|
327
|
+
├── components/, sections/ ← out of scope for Part 2 (theme-level). Any co-located `styles.css` / `style.css` files here remain deprecated — see Part 1 above and `themes-review/references/css-js-hygiene.md`. Migrate them separately.
|
|
328
|
+
├── <themeable_type>/<name>/ ← index.liquid + variables.json only (no styles.css after Part 1)
|
|
329
|
+
└── …
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## Anti-patterns specific to Part 2
|
|
333
|
+
|
|
334
|
+
- **Choosing external `<link>` for theme-level.** Storefront behavior is preserved by staying inline. External `<link>` would add an extra HTTP round trip for the foundation CSS every page.
|
|
335
|
+
- **Renaming `styles.css` / `global_styles.css`.** The backend FR lookup keys on these exact filenames. Rename breaks the storefront's `inline_asset_content` resolution.
|
|
336
|
+
- **Placing the tags outside `<head>` or after `</head>`.** The `<style>` block emitted by `inline_asset_content` MUST land inside `<head>` for the browser to treat it as page-level CSS.
|
|
@@ -42,6 +42,9 @@ A typical theme repo looks like this (verified against the `base-theme` starter
|
|
|
42
42
|
```
|
|
43
43
|
my-theme/
|
|
44
44
|
├── assets/ ← CSS, JS, images, video — flat at root
|
|
45
|
+
│ ├── styles.css ← theme-level custom_stylesheet (was theme-root styles.css)
|
|
46
|
+
│ ├── global_styles.css ← theme-level global_stylesheet (was theme-root global_styles.css)
|
|
47
|
+
│ └── … ← all other CSS/JS/images
|
|
45
48
|
├── components/{name}/ ← shared partials, at root (no templates/ wrapper)
|
|
46
49
|
│ ├── pagination/index.liquid
|
|
47
50
|
│ ├── cart_template/index.liquid
|
|
@@ -76,7 +79,6 @@ my-theme/
|
|
|
76
79
|
│ └── ...
|
|
77
80
|
├── styleguide/index.liquid ← optional dev-only preview
|
|
78
81
|
├── cover.png ← theme thumbnail for the editor picker
|
|
79
|
-
├── global_styles.css ← optional theme-wide CSS at root
|
|
80
82
|
├── .fluid-theme.json ← CLI metadata (themeId, company, checksums)
|
|
81
83
|
├── .fluidignore ← like .gitignore
|
|
82
84
|
└── README.md
|
|
@@ -117,8 +119,7 @@ Every artifact has a fixed depth and shape. **No deeper nesting is allowed under
|
|
|
117
119
|
| Layouts | `layouts/{name}.liquid` (flat file, no wrapping dir) | `layouts/theme.liquid` | Multiple layouts possible; each is a flat `.liquid` file. |
|
|
118
120
|
| Global config | `config/settings_schema.json`, `config/settings_data.json` (flat) | — | — |
|
|
119
121
|
| Locales | `locales/{lang}.json` (flat) | `locales/en.json`, `locales/de.json` | — |
|
|
120
|
-
| Assets | `assets/{file}` (flat) | `assets/
|
|
121
|
-
| Root-level CSS | `global_styles.css` at the theme root | — | Optional theme-wide stylesheet. |
|
|
122
|
+
| Assets | `assets/{file}` (flat) | `assets/styles.css`, `assets/global_styles.css`, `assets/logo.svg` | No sub-folders. Theme-level stylesheets live here too: `styles.css` (custom_stylesheet) and `global_styles.css` (global_stylesheet). Referenced from `layouts/theme.liquid` via `\| inline_asset_content` before `</head>`. |
|
|
122
123
|
| Theme thumbnail | `cover.png` at the theme root | — | — |
|
|
123
124
|
| Styleguide | `styleguide/index.liquid` | — | Optional dev-only preview. |
|
|
124
125
|
|
|
@@ -188,7 +189,7 @@ A layout missing either one is broken. Missing `content_for_header` breaks edito
|
|
|
188
189
|
| Component file with a `{% schema %}` block | `blocker` | Components don't have schemas. Move to `sections/`, or strip the schema. |
|
|
189
190
|
| Section file without `{% schema %}` | `blocker` | A section without a schema is just markup — make it a component (move to `components/{name}/index.liquid`). |
|
|
190
191
|
| Page template at `{type}/index.liquid` (missing variant dir) | `blocker` | Wrap in a variant directory: `{type}/default/index.liquid`. |
|
|
191
|
-
| Asset (`.css`, `.js`, image) outside `assets
|
|
192
|
+
| Asset (`.css`, `.js`, image) outside `assets/`, or `styles.css` / `global_styles.css` at the theme root, or `cover.png` outside root | `blocker` | Move to `assets/` and reference via `\| asset_url` (or `\| inline_asset_content` for the theme-level `styles.css` / `global_styles.css`). |
|
|
192
193
|
| Sub-folder inside `assets/` (e.g. `assets/icons/`) | `should` | Flatten: rename file to `assets/icon-{whatever}.svg`. |
|
|
193
194
|
| Sub-folder inside `sections/{name}/` or `components/{name}/` (e.g. `sections/hero/blocks/`) | `blocker` | Sections and components don't nest. A reusable block belongs in the top-level `blocks/{name}/` directory, not under a section. |
|
|
194
195
|
| Co-located `styles.css` / `style.css` next to a section, component, or page template variant | `blocker` for new files, `should` for existing | **Co-located stylesheets are deprecated** — all CSS lives under `assets/`. See [CSS hygiene §1a](references/css-js-hygiene.md). |
|
|
@@ -331,6 +332,7 @@ touches them. Read the matching file when you hit its topic:
|
|
|
331
332
|
- **[Editor attributes](references/editor-attributes.md)** — `section.fluid_attributes` / `block.fluid_attributes`.
|
|
332
333
|
- **[FairShare attributes](references/fairshare-attributes.md)** — `data-fluid-*` cart / add-to-cart behavioral attributes + the CDN script.
|
|
333
334
|
- **[Performance](references/performance.md)** — `asset_url` in loops, render hygiene.
|
|
335
|
+
- **[Media rendering](references/media-tag.md)** — the `media_tag` filter for responsive, format-negotiated images and video; when raw `<img>`/`<video>` is wrong.
|
|
334
336
|
- **[Security & accessibility](references/security-accessibility.md)** — escaping user content, alt text, semantic clickables.
|
|
335
337
|
- **[Dead code](references/dead-code.md)** — unused sections/components/assets, unhandled block types.
|
|
336
338
|
- **[CSS / JS hygiene](references/css-js-hygiene.md)** — co-located stylesheet deprecation, inline `<style>`/`<script>` thresholds, `defer`.
|
|
@@ -619,6 +621,7 @@ When reviewing any theme file, run through this in order. Anything unchecked is
|
|
|
619
621
|
- [ ] No user-visible literal text in markup (use `text`/`textarea`/`richtext`)
|
|
620
622
|
- [ ] No hardcoded colors in styles (use `color` / `color_background` or a global token)
|
|
621
623
|
- [ ] No hardcoded image URLs (use `image_picker` or upload to `assets/`)
|
|
624
|
+
- [ ] Setting/resource images and videos render through `| media_tag` (not hand-rolled `<img>`/`<video>`)
|
|
622
625
|
- [ ] No external CDN URLs for theme-owned assets
|
|
623
626
|
- [ ] No hardcoded `href` to fixed paths the company might want to change (use `url`)
|
|
624
627
|
|
|
@@ -103,9 +103,7 @@ Rendering:
|
|
|
103
103
|
<div class="features features--cols-{{ section.settings.columns }}">
|
|
104
104
|
{%- for block in section.blocks -%}
|
|
105
105
|
<article class="feature" {{ block.fluid_attributes }}>
|
|
106
|
-
{
|
|
107
|
-
<img src="{{ block.settings.icon }}" alt="{{ block.settings.title | escape }}" />
|
|
108
|
-
{%- endif -%}
|
|
106
|
+
{{ block.settings.icon | media_tag: alt: block.settings.title }}
|
|
109
107
|
<h3>{{ block.settings.title }}</h3>
|
|
110
108
|
<p>{{ block.settings.body }}</p>
|
|
111
109
|
</article>
|
|
@@ -42,7 +42,8 @@ The earlier theme convention placed a `styles.css` next to each template, sectio
|
|
|
42
42
|
| New file `components/{name}/style.css` | `blocker` | Same — `assets/{name}.css` and `asset_url`. |
|
|
43
43
|
| New file under a page-variant directory like `{type}/{variant}/styles.css` | `blocker` | Move to `assets/{name}.css`. |
|
|
44
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
|
-
|
|
|
45
|
+
| Theme-level `styles.css` or `global_styles.css` at the theme root (pre-migration shape) | `blocker` | Move to `assets/styles.css` / `assets/global_styles.css` and reference from `layouts/theme.liquid` via `\| inline_asset_content` before `</head>`. See the CLI skill `packages/cli/theme-dev/skills/template-stylesheet-to-asset-migration/SKILL.md` Part 2. |
|
|
46
|
+
| Global / custom stylesheets anywhere else outside `assets/` | `blocker` | Move to `assets/`. |
|
|
46
47
|
| 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
|
|
|
48
49
|
Migration recipe (suggest in the comment when applicable):
|
|
@@ -54,9 +54,7 @@ Every one of those values — background, padding, heading text, body text, imag
|
|
|
54
54
|
<section style="background:{{ s.bg_color }};padding:{{ s.section_pad }};">
|
|
55
55
|
<h2 style="color:{{ s.text_color }};">{{ s.heading | default: 'Our Bestsellers' }}</h2>
|
|
56
56
|
<p>{{ s.body }}</p>
|
|
57
|
-
{
|
|
58
|
-
<img src="{{ s.banner_image }}" alt="{{ s.heading | escape }}" />
|
|
59
|
-
{%- endif -%}
|
|
57
|
+
{{ s.banner_image | media_tag: alt: s.heading }}
|
|
60
58
|
{%- if s.show_cta -%}
|
|
61
59
|
<a href="{{ s.cta_url }}" class="btn">{{ s.cta_label }}</a>
|
|
62
60
|
{%- endif -%}
|
|
@@ -70,6 +68,7 @@ Every one of those values — background, padding, heading text, body text, imag
|
|
|
70
68
|
| User-visible literal text in markup (heading, paragraph, button label) | `should` | Should be a `text` / `textarea` / `richtext` setting. |
|
|
71
69
|
| Hardcoded colors (`#0a0a0a`, `rgb(...)`, `red`) in style/attribute | `should` | Should be a `color` or `color_background` setting. |
|
|
72
70
|
| 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`. |
|
|
71
|
+
| Setting/resource image rendered with a raw `<img>` instead of `\| media_tag` | `should` | Misses responsive `srcset` + format negotiation. See [Media rendering](media-tag.md). |
|
|
73
72
|
| Hardcoded `href` to a fixed page or `?sort=popular`-style URL | `should` | Use a `url` setting. |
|
|
74
73
|
| 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
74
|
| Boolean branches with no user toggle (`{% if true %}`) | `should` | Either delete or expose as a `checkbox`. |
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Media rendering — `media_tag`
|
|
2
|
+
|
|
3
|
+
> Part of the `themes-review` skill. See [`../SKILL.md`](../SKILL.md) for the review workflow, severity ladder, and validator rules.
|
|
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.
|
|
6
|
+
|
|
7
|
+
Docs: <https://docs.fluid.app/docs/themes/media-tag>
|
|
8
|
+
|
|
9
|
+
## Contents
|
|
10
|
+
|
|
11
|
+
- 1. The rule
|
|
12
|
+
- 2. Options
|
|
13
|
+
- 3. Video
|
|
14
|
+
- 4. What it does *not* replace
|
|
15
|
+
|
|
16
|
+
### 1. The rule
|
|
17
|
+
|
|
18
|
+
**`should`.** A raw `<img src="{{ ... }}">` or `<video>` that renders an `image` / `image_picker` / `video_picker` / `media_picker` setting, 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
|
+
|
|
20
|
+
```liquid
|
|
21
|
+
{%- comment -%} Bad: fixed original, manual (easy-to-forget) attributes {%- endcomment -%}
|
|
22
|
+
<img src="{{ section.settings.banner_image }}" alt="{{ section.settings.heading | escape }}" loading="lazy" decoding="async" />
|
|
23
|
+
|
|
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.heading }}
|
|
26
|
+
```
|
|
27
|
+
|
|
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.
|
|
29
|
+
|
|
30
|
+
### 2. Options
|
|
31
|
+
|
|
32
|
+
Pass options as filter arguments. Anything not in this table becomes a plain HTML attribute (`class`, `id`, `data-*`).
|
|
33
|
+
|
|
34
|
+
| Option | Default | Purpose |
|
|
35
|
+
| --------- | ------------------- | -------------------------------------------------------------------------- |
|
|
36
|
+
| `sizes` | `100vw` | Browser sizing hint — set it to the real rendered width. |
|
|
37
|
+
| `widths` | `400,800,1200,1600` | `srcset` breakpoints. |
|
|
38
|
+
| `width` | — | Fixed display width; emits a 2× retina `srcset`. |
|
|
39
|
+
| `height` | — | Layout stability (avoids CLS). |
|
|
40
|
+
| `quality` | `80` | 1–100. |
|
|
41
|
+
| `format` | `auto` | WebP/AVIF negotiation. |
|
|
42
|
+
| `crop` | — | Focus point. |
|
|
43
|
+
| `loading` | `lazy` | Use `eager` for the hero / above-the-fold image — `lazy` there delays LCP. |
|
|
44
|
+
| `alt` | image's alt text | Set it for `*_picker` settings; `alt: ''` for decorative. |
|
|
45
|
+
|
|
46
|
+
```liquid
|
|
47
|
+
{%- comment -%} Fixed-width logo, retina srcset {%- endcomment -%}
|
|
48
|
+
{{ section.settings.logo | media_tag: width: 240, alt: 'Logo' }}
|
|
49
|
+
|
|
50
|
+
{%- comment -%} Hero — eager so it doesn't delay LCP {%- endcomment -%}
|
|
51
|
+
{{ section.settings.hero | media_tag: sizes: '100vw', loading: 'eager', alt: section.settings.heading }}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 3. Video
|
|
55
|
+
|
|
56
|
+
A video URL (`.mp4` / `.mov` / `.webm` / `.m4v`) or video object renders a `<video>`. Options: `autoplay`, `loop`, `muted`, `controls`, `poster`, `format` (default `mp4`), `quality`.
|
|
57
|
+
|
|
58
|
+
```liquid
|
|
59
|
+
{{ section.settings.bg_video | media_tag: autoplay: true, loop: true, muted: true }}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 4. What it does *not* replace
|
|
63
|
+
|
|
64
|
+
- **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` settings carry none, so pass one (or `alt: ''` for decorative). The alt rule in [security & accessibility](security-accessibility.md) still applies.
|
|
66
|
+
|
|
67
|
+
---
|
|
@@ -41,15 +41,17 @@
|
|
|
41
41
|
|
|
42
42
|
### 1. Missing `alt` on images
|
|
43
43
|
|
|
44
|
+
Render the image with `| media_tag` (see [Media rendering](media-tag.md)) and pass `alt`:
|
|
45
|
+
|
|
44
46
|
```liquid
|
|
45
|
-
{%- comment -%} Bad {%- endcomment -%}
|
|
46
|
-
|
|
47
|
+
{%- comment -%} Bad: image_picker carries no stored alt, so this ships alt-less {%- endcomment -%}
|
|
48
|
+
{{ section.settings.image | media_tag }}
|
|
47
49
|
|
|
48
50
|
{%- comment -%} Good {%- endcomment -%}
|
|
49
|
-
|
|
51
|
+
{{ section.settings.image | media_tag: alt: section.settings.heading }}
|
|
50
52
|
```
|
|
51
53
|
|
|
52
|
-
Decorative images: `alt
|
|
54
|
+
`media_tag` defaults `alt` from a resource image (`product.featured_image`), but `*_picker` settings carry none — pass one explicitly. Decorative images: `alt: ''`.
|
|
53
55
|
|
|
54
56
|
### 2. Heading hierarchy
|
|
55
57
|
|
|
@@ -61,10 +63,10 @@ A clickable `<div>` is `blocker`. Use `<button>` for actions, `<a href>` for nav
|
|
|
61
63
|
|
|
62
64
|
### 4. `decoding="async"` and `loading="lazy"`
|
|
63
65
|
|
|
66
|
+
`| 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
|
+
|
|
64
68
|
```liquid
|
|
65
|
-
|
|
69
|
+
{{ section.settings.hero | media_tag: loading: 'eager', alt: section.settings.heading }}
|
|
66
70
|
```
|
|
67
71
|
|
|
68
|
-
Don't set `loading="lazy"` on hero/above-the-fold images — it delays LCP.
|
|
69
|
-
|
|
70
72
|
---
|
|
@@ -41,6 +41,8 @@ These are the canonical setting types. **Any `type:` value not in this list is r
|
|
|
41
41
|
|
|
42
42
|
### Visual & media
|
|
43
43
|
|
|
44
|
+
Render `image` / `image_picker` / `video_picker` / `media_picker` values with the `media_tag` filter, not a hand-rolled `<img>`/`<video>` — see [Media rendering](media-tag.md).
|
|
45
|
+
|
|
44
46
|
| `type` | Notes |
|
|
45
47
|
| ------------------ | ----------------------------------------------------------------- |
|
|
46
48
|
| `color` | Single color picker. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fluid-app/fluid-cli-theme-dev",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.28",
|
|
4
4
|
"description": "Fluid CLI plugin for theme developer workflows — dev server, push, pull, init",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
"jest": "^29.7.0",
|
|
39
39
|
"tsdown": "^0.21.0",
|
|
40
40
|
"typescript": "^5",
|
|
41
|
+
"@fluid-app/themes-api-client": "0.1.0",
|
|
41
42
|
"@fluid-app/api-client-core": "0.1.0",
|
|
42
43
|
"@fluid-app/typescript-config": "0.0.0",
|
|
43
|
-
"@fluid-app/theme-schema": "0.1.0"
|
|
44
|
-
"@fluid-app/themes-api-client": "0.1.0"
|
|
44
|
+
"@fluid-app/theme-schema": "0.1.0"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": ">=18.0.0"
|