@fluid-app/fluid-cli-theme-dev 0.1.26 → 0.1.27

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.
@@ -331,6 +331,7 @@ touches them. Read the matching file when you hit its topic:
331
331
  - **[Editor attributes](references/editor-attributes.md)** — `section.fluid_attributes` / `block.fluid_attributes`.
332
332
  - **[FairShare attributes](references/fairshare-attributes.md)** — `data-fluid-*` cart / add-to-cart behavioral attributes + the CDN script.
333
333
  - **[Performance](references/performance.md)** — `asset_url` in loops, render hygiene.
334
+ - **[Media rendering](references/media-tag.md)** — the `media_tag` filter for responsive, format-negotiated images and video; when raw `<img>`/`<video>` is wrong.
334
335
  - **[Security & accessibility](references/security-accessibility.md)** — escaping user content, alt text, semantic clickables.
335
336
  - **[Dead code](references/dead-code.md)** — unused sections/components/assets, unhandled block types.
336
337
  - **[CSS / JS hygiene](references/css-js-hygiene.md)** — co-located stylesheet deprecation, inline `<style>`/`<script>` thresholds, `defer`.
@@ -619,6 +620,7 @@ When reviewing any theme file, run through this in order. Anything unchecked is
619
620
  - [ ] No user-visible literal text in markup (use `text`/`textarea`/`richtext`)
620
621
  - [ ] No hardcoded colors in styles (use `color` / `color_background` or a global token)
621
622
  - [ ] No hardcoded image URLs (use `image_picker` or upload to `assets/`)
623
+ - [ ] Setting/resource images and videos render through `| media_tag` (not hand-rolled `<img>`/`<video>`)
622
624
  - [ ] No external CDN URLs for theme-owned assets
623
625
  - [ ] No hardcoded `href` to fixed paths the company might want to change (use `url`)
624
626
 
@@ -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
- {%- if block.settings.icon != blank -%}
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>
@@ -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
- {%- if s.banner_image != blank -%}
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
- <img src="{{ product.featured_image }}" />
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
- <img src="{{ product.featured_image }}" alt="{{ product.title | escape }}" />
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
- <img src="..." alt="..." loading="lazy" decoding="async" />
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.26",
3
+ "version": "0.1.27",
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/api-client-core": "0.1.0",
41
+ "@fluid-app/themes-api-client": "0.1.0",
42
42
  "@fluid-app/typescript-config": "0.0.0",
43
- "@fluid-app/theme-schema": "0.1.0",
44
- "@fluid-app/themes-api-client": "0.1.0"
43
+ "@fluid-app/api-client-core": "0.1.0",
44
+ "@fluid-app/theme-schema": "0.1.0"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=18.0.0"