Skip to content

feat(library): an image API for JavaScript modules - #746

Draft
romain-pm wants to merge 9 commits into
mainfrom
feat/image-api
Draft

feat(library): an image API for JavaScript modules#746
romain-pm wants to merge 9 commits into
mainfrom
feat/image-api

Conversation

@romain-pm

@romain-pm romain-pm commented Aug 22, 2026

Copy link
Copy Markdown

Summary

Gives the library an image concept, so a module author renders a responsive, accessible, cacheable content image by declaring intent instead of deriving it. First implementation pass on #734.

<JImage node={cover} alt={title} width={400} className={classes.cover} />

That one line now produces a src sized for the slot, srcSet candidates, a derived sizes, intrinsic width/height, loading="lazy", and a registered cache dependency.

Why

Nothing in the platform mentioned srcset, sizes, resizing or Media Optimization, and both the blog tutorial and the hydrogen sample rendered the full-resolution original with alt="" — the pattern every module copies. So each module re-derived the same logic (Jahia/luxe-jahia-demo: ~250 lines), including two facts documented nowhere: a size hint reaches an external DAM through getUrl(List) args but the default provider through ?w= query parameters, and a plain local instance honours neither, returning identical bytes for every candidate.

Changes

  • buildImageUrl(node, { width, height }) — picks and reports the channel: provider (a DAM decorator signs a transformed URL), thumbnail (a pre-generated size, the only resize a plain instance performs), query (?w=, honoured by Media Optimization in the Cloud), original (vectors, and no-op resizes). Clamps to the intrinsic size; never upscales.
  • getImageProps(node, { alt, layout, width }) — derives both srcSet and sizes from layout intent (constrained | fixed | full-width) plus the slot width. widths/sizes stay as escape hatches. Returns plain serializable data, so it is also what crosses an Island; alt is required, with alt="" as the deliberate decorative declaration. Registers the cache dependency.
  • <JImage> — an unstyled <img>, lazy only once the intrinsic dimensions are known, priority for the LCP image, fallback for a missing node. Server-only.
  • Typings.java-ts-bind now includes JCRStoreProvider and whitelists getProvider/isDefault, so provider routing needs no cast.
  • buildNodeUrl's args JSDoc — says that the default provider discards them, instead of documenting a silent no-op as a feature. This JSDoc ships in the published .d.ts.
  • Docs — a rendering-images guide (docs/2-guides/8-images), and the tutorial and hydrogen sample switched to JImage.
  • Tests — vitest, which the monorepo did not have, pinning channel routing, clamping, de-duplication, comma encoding and the layout derivation.

Semantics worth flagging, each learned from a real bug in the userland copy: candidate de-duplication keeps the smallest width when a provider collapses several requests onto one rendition (over-claiming makes browsers paint an upscaled file); the original joins the ladder only when it is within 2× of the largest requested width (never send an 8000px master to a 640px card); and commas are percent-encoded inside srcSet only, because core's rewriter splits on every comma (Jahia/jahia#23).

Validation

  • tsc --noEmit clean, eslint clean, prettier --check clean.
  • yarn workspace @jahia/javascript-modules-library test — 21 passing.
  • Not yet exercised on a running instance: the component and the cache dependency need a deployed module. The natural next step is Adopt the platform image API and delete commons/image luxe-jahia-demo#456, which adopts this API and deletes its userland copy — that is also the real test of whether the API is complete.

Decided, and what is left

  • Named JImageImage would collide with the design-system Image most modules already have, forcing an alias at every import site.
  • The candidate ladder is documented, because the API reads as second-guessing the width you just gave it until you know that the slot and the files are different widths. The guide now names both, says which layouts consult the ladder (constrained below its maximum, full-width always; fixed never), and gives the reason for each limit: candidates stop at 2× the slot (a 3× file roughly doubles the bytes for a difference few people see) and start at 320.
  • Still to do: delete src/jcr-provider-augmentation.d.ts once target/types is regenerated. That regeneration is a Maven step which downloads the OpenJDK 17 source tree — java-ts-bind reads Java sources, not bytecode, so it needs the JDK's own sources for the java.* types that appear in Jahia signatures. Nothing this PR can run; the file is not shipped, and is a typed declaration merge rather than a cast.

Fixes nothing on its own — part of #734, and each commit references the sub-issue it implements.

JCRNodeWrapper.getProvider() and JCRStoreProvider.isDefault() exist in core
but never reached the published typings: JCRStoreProvider sat in the
java-ts-bind blacklist and neither method was whitelisted. Modules that need
to know where an asset lives — a local file or an external DAM mount, which
decides how a resize reaches it — had to cast their way there.

Refs #739
…provider

The option was documented as if it worked everywhere, and this JSDoc ships in
the published .d.ts. Core's JCRNodeWrapperImpl.getUrl(List) discards its
parameters, so on a local /files asset the args produce the plain URL — which
reads as a broken API rather than a provider-specific channel. Also points at
the thumbnails Jahia pre-generates, the only variants a plain instance serves.

Refs #736
Rendering a content image well needs a file sized for its slot, reserved
space, a cache dependency and alternative text. None of that existed here, so
every module derived it again — and the samples showed the full-resolution
original with an empty alt.

Three tiers, so each layer can be used on its own:

- buildImageUrl picks the channel that actually carries a size for the node at
  hand: an external provider's decorator (a DAM signs a transformed URL), a
  pre-generated thumbnail (the only resize a plain instance performs), or the
  `?w=` query parameters that Media Optimization honours in the Cloud. The
  chosen channel is returned, so "nothing resizes on my machine" becomes a
  fact the API states rather than a mystery.
- getImageProps derives both `srcSet` and `sizes` from layout intent
  (constrained | fixed | full-width) plus the slot width, instead of asking
  every call site to hand-write candidate widths and a sizes expression. It
  returns plain serializable data, which is also what an Island needs, and
  requires `alt` — decorative images declare `alt=""` deliberately.
- Image renders it: an unstyled <img>, lazy only once the intrinsic dimensions
  are known, eager and high-priority under `priority`.

Refs #735, #743, #744, #738
The monorepo had no unit-test runner. The image logic is pure computation with
edge cases worth holding still: channel routing, clamping to the original,
no-op detection, candidate de-duplication keeping the smallest width when a
provider collapses several onto one rendition, comma encoding for Jahia's
srcset rewriter, and the layout-to-sizes derivation.

Refs #745
…teach

No platform surface mentioned srcset, sizes, resizing or Media Optimization,
while the blog tutorial and the hydrogen sample both rendered the
full-resolution original with alt="" — the pattern every module author copies.

The guide covers layout intent, why sizes exists, priority for the LCP image,
required alternative text, images across an Island boundary, and a table of
which channel resizes where, including the one that surprises everyone: a
plain instance serves identical bytes for every ?w= candidate.

Refs #737
@romain-pm romain-pm added the feat A new feature label Aug 22, 2026
@github-actions

Copy link
Copy Markdown

📝 Documentation Guidelines

Thank you for contributing to our documentation! To ensure your contributions meet our standards, please review these resources:

This comment is posted automatically when changes are detected in the docs/ folder.

@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown

🦜 Chachalog

javascript-modules minor
  • Added an image API for rendering content images: a JImage component, plus getImageProps and buildImageUrl for cases that need the data or just a URL. (feat(library): an image API for JavaScript modules #746)

    Declare how the image sits in the page — <JImage node={cover} alt={title} width={400} /> — and the library sizes the file to the slot, offers the browser alternatives for high-density and narrow screens, reserves the space so the layout does not shift while it loads, and refreshes cached pages when an editor replaces the picture. Alternative text is now required, so a missing one is caught while you write the view rather than by an accessibility audit later. A new Rendering Images guide explains which setups actually resize images, and which serve the original.

Create a new entry online or run npx chachalog@0.5.4 prompt to create a new entry locally.

@pkg-pr-new

pkg-pr-new Bot commented Aug 22, 2026

Copy link
Copy Markdown

Open in StackBlitz

yarn add https://pkg.pr.new/@jahia/create-module@746.tgz
yarn add https://pkg.pr.new/@jahia/javascript-modules-library@746.tgz
yarn add https://pkg.pr.new/@jahia/vite-plugin@746.tgz

commit: 8d77711

`Image` collides with the design-system `Image` that most modules already
have, forcing an alias at every import site. `JImage` follows JCRNodeWrapper's
J prefix and reads as "the Jahia one".

Refs #743
The guide asked developers to declare a slot width without saying why a list
of other widths then appears in the markup — the obvious reading being that
the library second-guesses the size they just gave it.

Names the two kinds of width, says which layouts consult the ladder and which
do not, and states the two limits and their reasons: candidates stop at twice
the slot width, and start at 320.

Refs #737, #744
…he inert resize

A module could not tell a developer's instance from a production one, so the
library could not say anything only a developer wants to hear. The engine
already knows — SettingsBean.isDevelopmentMode() — it just was not on the
`server` bridge.

Puts it on ConfigHelper (`server.config.isDevelopmentMode()`) and uses it for
the worst image trap: on an instance without Media Optimization, `?w=`
candidates come back byte-identical, and until now nothing said so — the
markup looked right and the bytes never shrank. getImageProps now warns once,
naming an example image, pointing at the guide.

Once per engine lifetime, not per image: what it reports is a property of the
instance, so a second line would repeat the first. Development mode only, and
silent about anything it cannot read — a diagnostic that breaks a render is
worse than no diagnostic.

Refs #748, #737
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant