Skip to content

feat(library)!: an open JLink, the mixin label, a narrowable allow-list, and the edit-mode URL - #773

Draft
romain-pm wants to merge 2 commits into
feat/link-apifrom
feat/link-improvements
Draft

feat(library)!: an open JLink, the mixin label, a narrowable allow-list, and the edit-mode URL#773
romain-pm wants to merge 2 commits into
feat/link-apifrom
feat/link-improvements

Conversation

@romain-pm

Copy link
Copy Markdown

Summary

Closes the five gaps that kept the link API off most of a real site. Adopting it on Jahia/jahia.com moved all 24 link sites, but only 5 could use <JLink>; the other 19 dropped to getLinkProps plus a hand-written <a> plus a state.navigable branch. Implements #768, #769, #770, #771 and #772, on top of #751.

<JLink
  content={cta}
  labelProperties={["acme:ctaLabel"]}
  attributes={({ anchor, state }) => ({
    "data-element-url": anchor.href,
    "data-element-text": state.label,
  })}
  asChild
>
  <CTA variant="primary" />
</JLink>

Every part of that call site was previously impossible.

Why

The two blockers were the same shape: the component computed something and would not hand it back. data-element-url and data-element-text are derived from the href and the label <JLink> had just built, and React's typings do not model data-* on a component's props at all — so the only way to write them was to stop using the component. A CTA mixin, meanwhile, sits on a card or a hero whose jcr:title is the heading rather than the link label, which made the derived label wrong on 23 of those call sites.

Changes

  • attributes (#768) — a record, or a function of the resolved link. It receives exactly what getLinkProps returns, so the same callback works on both tiers, and it is the same shape <JImage attributes> settled on. Spread last; not called when the link is not navigable.
  • asChild (#768) — the child element becomes the link, for a design-system call to action that is not a bare <a>. Next.js met the same need and called it passHref. Exactly one element child; anything else is an error naming the way out.
  • Derived omit (#768) — the anchor attributes the component accepts are now Omit<AnchorHTMLAttributes, keyof JLinkOwnProps> rather than a hand-written list, so a prop added later cannot silently swallow an attribute. Same fix shape as #762 on the image side.
  • state.node and readNodeReference (#769) — the link hands back what it resolved to, and the safe reference read behind it is exported next to getNodeProps, because reading a weakreference without letting a dangling one break the render is a JCR concern, not a link one. It separates the two cases a falsy value merges: null for unset, { uuid } with no node for set-but-unreachable.
  • labelProperties / labelFrom (#770) — where the label of a link mixin actually lives. labelFrom="target" is the readable spelling of labelProperties={[]} and takes precedence over it.
  • allowedSchemes and setLinkDefaults (#771) — per call and module-wide, bundleKey-scoped exactly like setImageDefaults. Narrowing only: a scheme outside the built-in list is dropped rather than added, with a development-mode warning so the links do not silently disappear.
  • /cms/editframe/ (#772) — the manual branch of buildNodeUrl now emits the servlet path that renders a page. See below.
  • Guide — a section on attributes and asChild, one on the mixin label, the allow-list narrowing, state.node / readNodeReference, and a table of exactly which tag/attribute pairs core finishes a URL in.

The empirical question #772 asked

Is the /cms/edit//cms/editframe/ replace still needed on 8.2.3? Yes — but it is a library bug, not core cargo, so it disappears rather than needing a new API. Measured on a local jahia/jahia-ee:8.2.3 with the luxe site:

URL Result
/cms/edit/default/en/sites/luxe/home.html 302/jahia/jcontent/luxe/en/pages/home
/cms/editframe/default/en/sites/luxe/home.html 200, the page

In the rendered edit-mode page, every <a href> carried /cms/editframe/… while the language-switcher Island's JSON payload carried /cms/edit/… — the exact defect the issue describes, reproduced. The cause: EditModeFilter substitutes the two, and its traverser visits a[href] and nothing else, so the same URL in an Island payload keeps the redirecting form and navigates the builder's iframe into a second copy of jContent. node.getUrl() already returned the editframe path; only the manual branch of buildNodeUrl — the one any of mode, language or extension selects — hardcoded /cms/edit/.

So the remedy is one string, not a new call: emit /cms/editframe/. Jahia/jahia.com's 15-year-old .replace("/cms/edit/", "/cms/editframe/") can be deleted, and the URL is now correct wherever it is put. This also ticks the mode: "edit" box of #756.

The rest of #772 does shrink, and what remains is documentation rather than API. There is no "final URL" call to add: the finishing pass is URLFilter walking assembled HTML, which does not exist while a view runs. What a developer needs is to know where it applies, so the guide now states it exactly — a[href], img[src|srcset|data-src|data-srcset], form[action], link[href], source[srcset], embed[src], param[value], in an html template type — and that a data-* attribute of your own, an Island payload, an action's JSON, a <meta> tag and JSON-LD get nothing. The vanity-URL and ?jsite= gap in an Island payload is real and stays open.

Validation

All run in ~/dev/jsm-wt/768 with the real exit code captured, after the final source edit.

  • npx tsc --noEmit -p tsconfig.json — 0. npx tsc --noEmit -p tsconfig.spec.json — 0.
  • npx vitest run — 0, 257 tests / 7 files (up from 214). New coverage for the record and function forms of attributes, asChild in all four states, the derived omit, state.node, readNodeReference, the mixin label precedence, the allow-list narrowing including the dev warning and the production silence, and the /cms/editframe/ path.
  • Compile-time checks that the three-way target union still rejects two targets, no target, an href with no accessible name, and a discriminator or label option on the node shape.
  • npx eslint javascript-modules-library samples/hydrogen — 0. npx prettier --check on every changed file — clean.
  • yarn build after rm -rf dist — 0, publint "All good!". npx tsc --noEmit in samples/hydrogen against the fresh dist — 0. Emitted declarations read back to confirm the new surface.
  • The 8.2.3 measurements above are the only thing exercised on a running instance. The dev-mode warning is proven by unit tests that stub server.config.isDevelopmentMode().

Decided

  • attributes receives LinkProps, not AnchorProps. #768 sketched ({ href, label, isCurrent }), but two of the five attributes jahia.com needs come from anchor and two from state. Passing the whole thing keeps one shape across both tiers instead of inventing a third.
  • state.node, not state.target. target is already the anchor attribute, and the two are never the same thing.
  • readNodeReference lives in utils/jcr/. #769 asked whether it belongs next to getNodeProps; it does. resolveContentLink is now a caller rather than the owner.
  • allowedSchemes intersects rather than replaces. A call site is not where a project loosens its URL policy — javascript: and data: are why the list exists. Silently dropping a request would look like missing content, so development mode says so once per scheme.
  • asChild still renders its child when the link is not navigable, without the link. A call to action that lost its target is a call to action with no href, not a hole in the page; whenUnresolved="none" is how you drop it.

Not done

  • The downstream adoptions are not updated. Jahia/jahia.com can now delete the /cms/edit/ replace, the hand-written readReference in NavBar.tsx, and the getLinkProps + <a> + branch shape at ~19 sites; Jahia/luxe-jahia-demo can drop its own.
  • Vanity URLs and ?jsite= still do not reach an Island payload. Documented, not solved.
  • samples/hydrogen demonstrates none of the new options: its call to action is a plain <a> and needed no change.

Based on #751, which is based on #766.

@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.

@pkg-pr-new

pkg-pr-new Bot commented Aug 23, 2026

Copy link
Copy Markdown

Open in StackBlitz

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

commit: effccbb

…is correct

`attributes` takes a record or a function of the resolved link, so a value
derived from the URL and the label the component computed is reachable at
last; `asChild` hands the link to a wrapper that is not a bare `<a>`; and the
anchor attributes the component accepts are derived from its own props rather
than hand-listed.

`labelProperties` and `labelFrom` say where the label of a link mixin lives,
`state.node` returns what the link resolved to, `readNodeReference` exports the
safe reference read behind it as a JCR concern, and `allowedSchemes` /
`setLinkDefaults` narrow the scheme allow-list per call or per module.

`buildNodeUrl` emits `/cms/editframe/` for edit mode: `/cms/edit/` redirects to
the jContent UI on 8.2.3 and only ever reached a page because EditModeFilter
substitutes the two for an `a[href]`.
@romain-pm
romain-pm force-pushed the feat/link-improvements branch from 10b025d to effccbb Compare August 23, 2026 22:22
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.

1 participant