Skip to content

feat(image): support referrerpolicy on rendered images#4056

Open
civing wants to merge 5 commits intomainfrom
feat/image-referrerpolicy
Open

feat(image): support referrerpolicy on rendered images#4056
civing wants to merge 5 commits intomainfrom
feat/image-referrerpolicy

Conversation

@civing
Copy link
Copy Markdown
Contributor

@civing civing commented Apr 30, 2026

Summary

  • Adds an optional referrerpolicy?: ReferrerPolicy field to the shared Image type.
  • Adds a renderImg(image: Image) helper that returns the <img> JSX node, including the spread-cast workaround needed today for referrerpolicy (Stencil's ImgHTMLAttributes typing omits it — see stenciljs/core ImgHTMLAttributes where referrerPolicy is declared on AnchorHTMLAttributes and IframeHTMLAttributes but not ImgHTMLAttributes).
  • Switches limel-list-item, limel-chip, and limel-card to render images via the helper. Removes the previously triplicated inline <img> JSX.
  • Adds two e2e cases on limel-list-item covering the attribute-set and attribute-absent branches.

When the referrerpolicy field is omitted, the attribute is not emitted — every existing consumer is unaffected.

Why

Consumers that build an Image whose src points to a third-party service (an external favicon endpoint, an external avatar service) currently have no way to suppress the Referer header. By default the browser sends the full URL of the page that triggered the image load to the third party — which, for a CRM, leaks customer-relationship metadata (which tenant viewed which record while researching which company) to whoever runs the favicon service.

Setting referrerpolicy="no-referrer" removes the leak with zero functional cost: the image still loads, the third party still receives the request, but no originating-page URL is attached.

Context (downstream consumers)

This change is the upstream prerequisite for the favicon work in lime-crm-components PR #4122, which implements the company-favicons feature requested in hack-tuesday issue #195. Security review of #4122 flagged that only one of four consumer surfaces could currently set the attribute; the other three render through limel-list-item / limel-chip and had no plumbing for it. This PR closes that plumbing gap.

Commit structure

Three atomic commits, each independently buildable:

  1. feat(image): add referrerpolicy to the Image interface — type + api report only.
  2. feat(image): add renderImg helper for shared image rendering — helper file added, unused.
  3. feat(card,chip,list-item): render Image via renderImg helper — wires the three components and adds e2e tests.

Naming

The helper is renderImg (not renderImage) because both limel-list-item and limel-card already have a private method named renderImage — using the same name for the import would force readers to disambiguate every call site. renderImg pairs naturally with the <img> JSX tag and avoids the collision. Easy to rename later if ImgHTMLAttributes upstream gains referrerPolicy and the helper can be deleted.

Test plan

  • npm run build passes on each of the three commits (verified locally).
  • npm run api:update produces no diff beyond the documented referrerpolicy addition.
  • New e2e cases for limel-list-item cover both branches.
  • CI green on the new tip.
  • Visual check: existing images render unchanged when referrerpolicy is unset.
  • Visual check: referrerpolicy="no-referrer" is emitted on the rendered <img> when set.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Images now support configurable referrer policy attribute.
  • Tests

    • Added test coverage to verify referrer policy attribute is properly forwarded to rendered images.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 30, 2026

📝 Walkthrough

Walkthrough

Multiple components (Card, Chip, ListItem) now delegate image rendering to a shared ImageTemplate component instead of managing inline <img> markup. A new ImageTemplate component is introduced to centralize image rendering logic and attribute handling. The Image type definition adds an optional referrerpolicy field. E2E tests verify referrer policy forwarding.

Changes

Cohort / File(s) Summary
Component Image Delegation
src/components/card/card.tsx, src/components/chip/chip.tsx, src/components/list-item/list-item.tsx
Refactored renderImage() methods to use shared ImageTemplate component instead of inline <img> elements with direct attribute binding.
Shared Image Template
src/util/image.template.tsx, src/global/shared-types/image.types.ts
New ImageTemplate functional component and updated Image interface. Template centralizes image markup with src, alt, loading="lazy", and conditional referrerpolicy forwarding via spread-cast workaround.
List-Item Tests
src/components/list-item/list-item.e2e.tsx
Expanded E2E coverage to validate referrerpolicy attribute forwarding in rendered img elements, including cases where the attribute is omitted or explicitly set.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • rasmusflomen
  • LucyChyzhova
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: adding support for the referrerpolicy attribute on rendered images across multiple components.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/image-referrerpolicy

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

Documentation has been published to https://lundalogik.github.io/lime-elements/versions/PR-4056/

@civing civing requested a review from a team as a code owner April 30, 2026 12:18
Add an optional `referrerpolicy?: ReferrerPolicy` field to the shared
`Image` type. Lets consumers express that the rendered `<img>` should
carry a specific referrer-policy attribute — useful when the `src`
points to a third-party service that should not receive the
originating page URL in the `Referer` header (e.g. external favicon
or avatar services).

This commit only widens the type. The components that accept `Image`
do not yet honor the new field; that follows in subsequent commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@civing civing force-pushed the feat/image-referrerpolicy branch from 553ff14 to 789aadc Compare April 30, 2026 12:36
civing and others added 2 commits April 30, 2026 14:39
Add a `renderImg(image: Image)` helper that returns an `<img>` JSX
node with the `Image` fields applied — including the spread-cast
workaround needed today for the `referrerpolicy` attribute (Stencil's
`ImgHTMLAttributes` typing omits it).

Internal helper, not exported from the public API. Wired up to
`limel-list-item`, `limel-chip`, and `limel-card` in the next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switch the three components that accept an `Image`-shaped prop to
delegate their `<img>` rendering to the shared `renderImg` helper.
This both removes the duplicated inline `<img>` JSX and lets the
`referrerpolicy` field added to `Image` flow through to the rendered
element — previously it was on the type but ignored at every render
site.

Adds two e2e cases on `limel-list-item` to verify the attribute is
emitted when set and absent otherwise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@civing civing force-pushed the feat/image-referrerpolicy branch from 0f35118 to 9fd2e0b Compare April 30, 2026 12:39
Comment thread etc/lime-elements.api.md
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second commit seems like it should be a chore, and the third commit a refactor? Otherwise this looks very good. I'll post a full review in a comment.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second commit seems like it should be a chore, and the third commit a refactor? Otherwise this looks very good. I'll post a full review in a comment.

I've noticed that Claude has a different understanding of the feat and fix. It considered only Bug fixes as fix, (and marks "problems that are now fixed" as feat)

@adrianschmidt
Copy link
Copy Markdown
Contributor

Consolidated PR Review -- 6 Dimensions

Generated by a 6-agent review (backward compatibility, code quality, architecture, security, observability, performance) running in parallel against this PR's branch.

PR Summary

This PR adds an optional referrerpolicy?: ReferrerPolicy field to the shared Image type, introduces a renderImg(image: Image) helper centralising <img> JSX (with a documented spread-cast workaround for Stencil's typing gap upstream-tracked at stenciljs/core#6692), and switches limel-card, limel-chip, and limel-list-item to render via the helper. 7 files changed, +66/-9. Two new e2e tests on limel-list-item cover the attribute-set and attribute-absent branches. Unblocks lime-crm-components PR #4122 (company favicons), where security review flagged that 3 of 4 Image consumer surfaces had no plumbing for referrerpolicy.

Merge Readiness -- READY TO MERGE ✅

The PR is strictly better than main along every reviewed dimension: it adds capability without regressing emitted DOM, public types, or runtime cost, and it's the upstream prerequisite for a real information-disclosure fix downstream. No [High] findings from any agent.

  • Blockers: None.
  • Non-blocking but worth addressing: Two [Medium] items in Architecture worth picking up — helper placement (shared-types/ vs. src/util/ or a *.template.tsx colocation), and the silent image.loading drop the helper inherits from the inline JSX. See Top Recommendations.

1. Backward Compatibility -- SAFE ✅

Adding an optional field to a public interface and replacing inline <img> JSX with an internal helper is a clean, additive change. Emitted DOM is identical when referrerpolicy is unset; helper is intentionally not re-exported from src/interface.ts:63, so it stays internal and absent from etc/lime-elements.api.md.

  • [Low] renderImg has no @internal TSDoc tag (src/global/shared-types/image.tsx:13). It's not surfaced today because interface.ts only re-exports image.types, but a future export * over shared-types/ would leak it. Adding @internal would make the contract explicit.
  • [Low] Co-locating image.tsx next to image.types.ts creates two same-base-name files differing only by extension (card.tsx:12, chip.tsx:11, list-item.tsx:11 import via the bare specifier '../../global/shared-types/image'). Resolves unambiguously today, but mildly confusing for maintainers.
  • [Low] Image.loading was unused pre-PR and remains unused (helper hardcodes loading="lazy"). Pre-existing; not a regression. (Also flagged stronger by Architecture.)

Positives: Strictly additive public-type change. card's <div class="image-wrapper"> preserved at card.tsx:245 so existing card.scss:69 selectors keep matching. The "absent" assertion at list-item.e2e.tsx:111 guards the no-emission contract that protects existing consumers. ReferrerPolicy is a global from lib.dom.d.ts (in tsconfig lib), so no downstream typing setup needed. Three atomic commits make any future bisect/revert clean.

2. Code Quality -- GOOD ✅

Solid, focused change. Helper is small but justified by the spread-cast workaround; e2e tests are real (one per branch); JSDoc on the new type field explains why a consumer would want 'no-referrer'.

  • [Low] image.tsx:27 casts to Record<string, string>, which is wider than necessary and discards the ReferrerPolicy union typing. A precise Record<'referrerpolicy', ReferrerPolicy> (or a typed local) would suppress the Stencil typing mismatch without pretending the value is arbitrary.
  • [Low] Truthy check on image.referrerpolicy (image.tsx:18) skips emission for '', which is technically a valid ReferrerPolicy literal meaning "use default". Functionally equivalent to omitting the attribute, so not a bug — but the JSDoc on image.types.ts:31 could note it.
  • [Low] Helper-level test coverage lives in list-item.e2e.tsx only; card and chip got no new tests. A co-located src/global/shared-types/image.spec.tsx (matching the src/util/*.spec.ts convention) would more accurately reflect where the logic lives. Current coverage is adequate.
  • [Low] Helper location: shared-types/ previously held only *.types.ts. Putting a JSX helper there blurs the directory's purpose.
  • [Low] Naming: renderImg resolves the real collision with private renderImage methods in card.tsx:240 and list-item.tsx:259, but reads slightly less consistently than the rest of the codebase. renderImageElement or an alias-at-import would also work.

Positives: Eliminates real triplication (card.tsx, chip.tsx, list-item.tsx previously each had their own <img> JSX). The Stencil-typing escape hatch is isolated to one call site and clearly comment-flagged with the upstream issue link, so it's removable when typing lands. The "absent" e2e assertion is a useful regression guard. API report (etc/lime-elements.api.md:1240) updated, making the type extension a deliberate public-API change rather than drift.

3. Architecture -- ACCEPTABLE ⚠️

The refactor is sound for the three call sites (verified to be the only Image-typed surfaces in this repo). Main concerns are the helper's directory placement and one missed opportunity to fix a pre-existing type/behavior mismatch.

  • [Medium] Helper placement violates the directory's intent. src/global/shared-types/image.tsx is the only .tsx/JSX-emitting file in shared-types/ (every sibling — color.types.ts, icon.types.ts, file.types.ts, etc. — is a pure type module) and the first file there to import @stencil/core, turning a type-only module into a runtime-bearing one. Two existing conventions fit better: src/components/<x>/<x>.template.tsx exporting a FunctionalComponent (see checkbox/checkbox.template.tsx, select/select.template.tsx) or src/util/ (already referenced from interface.ts:68 via image-resize). A FunctionalComponent would also let consumers write <ImageTemplate image={this.image} /> instead of {renderImg(this.image)}, which is more idiomatic Stencil.
  • [Medium] renderImg silently drops image.loading. Image.loading?: 'lazy' | 'eager' is documented on the public type (image.types.ts:22), but image.tsx:26 hardcodes loading="lazy". Pre-existing in the inline JSX, so not a regression — but a centralising PR was the natural place to fix the type/behavior mismatch. Either honour image.loading ?? 'lazy' or remove loading from the interface.
  • [Low] Spread-cast pattern won't scale gracefully — a fifth attribute (crossorigin, decoding, fetchpriority) needing the same workaround would duplicate. A buildOptionalAttrs(image) helper or single accumulating object would be more extensible. Not blocking.
  • [Low] Inconsistent "is this image renderable?" guards: card.tsx:241 uses !this.image?.src, chip.tsx:298 uses !isEmpty(this.image) (lodash), list-item.tsx:260 uses !this.image. Pre-existing; the helper could absorb the guard (return null when !image?.src) and normalize the contract.
  • [Low] Public-vs-internal export depends on interface.ts enumerating files rather than a structural barrier. src/util/ or *.template.tsx would make the boundary structural.

Positives: Correctly identifies all Image-typed surfaces in the repo (other <img> uses in profile-picture.tsx, file-viewer.tsx, email-viewer/* are on non-Image-typed inputs and rightly excluded). Wrapping decisions stay at the call site (card's image-wrapper is a card-specific layout concern). DRYs up triplication without taking over component-level conditional-render decisions.

4. Security -- SAFE ✅

This PR is itself a security improvement: it adds the plumbing for downstream consumers to suppress Referer leakage for <img> elements pointing at third-party services. No new vulnerabilities introduced.

  • [Low] Runtime values of image.referrerpolicy are not validated (the ReferrerPolicy constraint is TS-only). Browsers ignore unknown tokens per spec — no XSS surface — but consumers passing untrusted strings should know an invalid value silently equals "no leak protection."
  • [Low] Only list-item has e2e assertions for both branches; card and chip have none. A future change to renderImg could regress those silently. Defense-in-depth.

Spread-cast attribute-injection check: referrerPolicyAttr is a fresh object literal with one hard-coded key (image.tsx:18-20). JSX spread of a fresh object literal enumerates only own properties, so attacker-controlled image.referrerpolicy cannot inject sibling attributes (onerror, src, etc.) even under hypothetical prototype pollution. The as Record<string, string> cast is type-only. Default-behavior verification: When referrerpolicy is omitted, the helper produces {} and spreads nothing, so the <img> is emitted with exactly the same attributes as before this PR. No change to default leakage potential — matching the PR's stated intent.

Positives: Centralizing forwarding gives one place for future security-relevant attributes (crossorigin, decoding) to land. Conservative emission avoids any change to existing rendered output. Truthy guard correctly handles ''/null/undefined. Unblocks lime-crm-components #4122 to suppress Referer leakage to Google's favicon endpoint — a concrete information-disclosure improvement.

5. Observability -- SAFE ✅

Pure UI plumbing: synchronous JSX-emitting helper with no failure modes, no I/O, no async behavior. Nothing meaningful to log, instrument, or correlate; no observability regression introduced. lime-elements has no project-wide logger/telemetry module the helper could be bypassing by living in shared-types/.

  • [Low] None of the three call sites wire onError/onLoad on the rendered <img> (card.tsx:240-246, chip.tsx:291-300, list-item.tsx:259-265), so a 404, CSP block, or no-referrer rejection still fails silently. Pre-existing — verified the inline JSX had the same shape — and renderImg is now the single right place to add broken-image telemetry if Lime ever wants it.
  • [Low] renderImg doesn't defensively handle image == null. All current call sites guard, but a future caller forgetting the guard would get a TypeError at render time with no helper-level message. A one-line if (!image?.src) return null; would make misuse self-diagnosing.

Positives: New e2e tests at list-item.e2e.tsx:108-131 give a clean regression signal in both directions. Inline comment on the spread-cast at image.tsx:14-17 documents why and links the upstream Stencil issue.

6. Performance -- SAFE ✅

Near-zero-cost refactor. Bundle, tree-shaking, and JSX equivalence all check out. Per-render cost is one extra small object allocation + spread per <img> rendered, dwarfed by the vdom allocation Stencil already does for the same element.

  • [Low] renderImg allocates a {} (or { referrerpolicy }) per call (image.tsx:18-27). Each limel-list-item is its own Stencil component, so a list of N items produces N calls only when those items actually re-render — not a hot inner loop within a single parent render. Not measurable; don't optimize.
  • [Low] The empty-object branch churns a fresh {} each call in the common case. A module-level const EMPTY = {} would reuse the literal, but this is textbook micro-optimization.

Bundle/tree-shaking checks (all clean): import { h } from '@stencil/core' is already in every Stencil component bundle, so zero new runtime dependency. The Image type is type-only and compiles away — no runtime import of image.types.ts is introduced. renderImg is a single named export with no side effects, so tree-shakers strip it for non-importers. JSX output is structurally equivalent to the previous inline JSX (card's <div class="image-wrapper"> is retained at card.tsx:245).

Positives: Centralisation gives one place to tune loading/referrerpolicy in the future. Helper is invoked after the early-return for missing image, so absent images cost nothing extra.


Overall Verdicts

Dimension Verdict
Backward Compatibility SAFE
Code Quality GOOD
Architecture ACCEPTABLE
Security SAFE
Observability SAFE
Performance SAFE
Merge Readiness READY TO MERGE

Top Recommendations

  1. [Medium from Architecture] Move renderImg out of src/global/shared-types/. The directory previously held only *.types.ts files; introducing JSX + a @stencil/core runtime import there is the first crossing of that line. Two existing conventions fit: src/util/render-image.tsx (matches image-resize re-exported via interface.ts:68) or a *.template.tsx colocated near a consumer (matches checkbox/checkbox.template.tsx). The *.template.tsx form would also let it become a FunctionalComponent and be invoked as <ImageTemplate image={this.image} />, which is more idiomatic Stencil.
  2. [Medium from Architecture] Decide what Image.loading means. The field is documented on the public type (image.types.ts:22) but image.tsx:26 hardcodes loading="lazy" and ignores it (the inline JSX did the same — so this PR doesn't regress, but it's the natural place to fix). Either loading={image.loading ?? 'lazy'} in the helper, or remove loading from the public Image interface.
  3. [Low from Code Quality] Tighten the cast at image.tsx:27: replace as Record<string, string> with Record<'referrerpolicy', ReferrerPolicy> (or a small typed local) so the ReferrerPolicy union typing isn't widened to arbitrary strings.
  4. [Low from Backward Compatibility] Add an @internal TSDoc tag on renderImg (image.tsx:13) to make the public-vs-internal contract explicit and tracked under any future strict-trim rollup, since the boundary currently relies only on interface.ts not enumerating the file.
  5. [Low from Code Quality] Mention in the JSDoc on image.types.ts:31 that referrerpolicy: '' is treated the same as omitting the attribute, since the helper's truthy check skips emission for '' (a technically-valid ReferrerPolicy literal). Alternatively, change the helper guard to image.referrerpolicy !== undefined for strict pass-through.
  6. [Low from Observability] Optionally add if (!image?.src) return null; at the top of renderImg so a future caller forgetting the call-site guard gets a self-diagnosing no-op instead of a TypeError. All current callers already guard, so this is purely defensive.
  7. [Low from Security] Consider mirroring the attribute-presence/absence assertions to card and chip e2e tests for defense-in-depth, so a future change to renderImg can't silently regress the no-emission contract on those two consumers.

🤖 Generated by /6-agent-review.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/util/image.template.tsx`:
- Around line 29-33: The ImageTemplate currently hardcodes loading="lazy" which
contradicts the Image contract; update the component to honor the image.loading
value (e.g., use loading={image.loading ?? "lazy"} or remove the loading
attribute if the contract is changed) so consumers’ Image.loading is
respected—modify the JSX in ImageTemplate (the <img ... loading="lazy" ... />
line) to reference image.loading instead of the hardcoded string and keep the
existing referrerPolicyAttr spread.
- Around line 24-26: The referrerPolicyAttr guard treats an explicit
empty-string referrerpolicy as unset because it uses a truthy check; change the
condition that builds referrerPolicyAttr to test image.referrerpolicy !==
undefined (or !== void 0) so that empty string and other falsy but present
values are preserved, i.e., retain the object { referrerpolicy:
image.referrerpolicy } whenever the property exists on image rather than only
when it's truthy.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 75b3766e-f4cd-40e5-bee1-69a8285b3c7d

📥 Commits

Reviewing files that changed from the base of the PR and between 553ff14 and 7d49196.

⛔ Files ignored due to path filters (1)
  • etc/lime-elements.api.md is excluded by !etc/lime-elements.api.md
📒 Files selected for processing (6)
  • src/components/card/card.tsx
  • src/components/chip/chip.tsx
  • src/components/list-item/list-item.e2e.tsx
  • src/components/list-item/list-item.tsx
  • src/global/shared-types/image.types.ts
  • src/util/image.template.tsx

Comment on lines +24 to +26
const referrerPolicyAttr = image.referrerpolicy
? { referrerpolicy: image.referrerpolicy }
: {};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

referrerpolicy guard currently drops explicit empty-string values.

Using a truthy check means referrerpolicy: '' is treated as “unset”. If omission should only happen when the field is absent, check undefined explicitly.

Suggested fix
-    const referrerPolicyAttr = image.referrerpolicy
+    const referrerPolicyAttr = image.referrerpolicy !== undefined
         ? { referrerpolicy: image.referrerpolicy }
         : {};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const referrerPolicyAttr = image.referrerpolicy
? { referrerpolicy: image.referrerpolicy }
: {};
const referrerPolicyAttr = image.referrerpolicy !== undefined
? { referrerpolicy: image.referrerpolicy }
: {};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/image.template.tsx` around lines 24 - 26, The referrerPolicyAttr
guard treats an explicit empty-string referrerpolicy as unset because it uses a
truthy check; change the condition that builds referrerPolicyAttr to test
image.referrerpolicy !== undefined (or !== void 0) so that empty string and
other falsy but present values are preserved, i.e., retain the object {
referrerpolicy: image.referrerpolicy } whenever the property exists on image
rather than only when it's truthy.

Comment on lines +29 to +33
<img
src={image.src}
alt={image.alt}
loading="lazy"
{...(referrerPolicyAttr as Record<string, string>)}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Honor Image.loading in the shared renderer (or remove it from the contract).

Image exposes a loading field, but ImageTemplate hardcodes "lazy". This makes the public type misleading for consumers.

Suggested fix
-            loading="lazy"
+            loading={image.loading ?? 'lazy'}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<img
src={image.src}
alt={image.alt}
loading="lazy"
{...(referrerPolicyAttr as Record<string, string>)}
<img
src={image.src}
alt={image.alt}
loading={image.loading ?? 'lazy'}
{...(referrerPolicyAttr as Record<string, string>)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/image.template.tsx` around lines 29 - 33, The ImageTemplate
currently hardcodes loading="lazy" which contradicts the Image contract; update
the component to honor the image.loading value (e.g., use loading={image.loading
?? "lazy"} or remove the loading attribute if the contract is changed) so
consumers’ Image.loading is respected—modify the JSX in ImageTemplate (the <img
... loading="lazy" ... /> line) to reference image.loading instead of the
hardcoded string and keep the existing referrerPolicyAttr spread.

@Kiarokh
Copy link
Copy Markdown
Contributor

Kiarokh commented Apr 30, 2026

we have a bunch of places we use the <img tag in the Building Blocks too. I think updating them would be a really nice next step. That'll also reduce the concerns (in the favicons PR) that initiated this PR 👍

* when `src` points to a third-party service that should not receive
* the originating page URL in the `Referer` header (e.g. external
* favicon or avatar services). When omitted, the attribute is not
* emitted and the browser's default referrer policy applies.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we always default to 'no-referrer'?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

limel-file-viewer can also display images. perhaps it could use this feature too

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants