You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A static map is the easy half. The interesting form is a function, because the values an analytics layer wants are the ones the component just computed and would otherwise keep to itself — the resolved URL and the derived label:
50
+
51
+
```tsx
52
+
<JLink
53
+
content={cta}
54
+
attributes={({ anchor, state }) => ({
55
+
"data-element-url": anchor.href,
56
+
"data-element-text": state.label,
57
+
"data-element-current": state.isCurrent,
58
+
})}
59
+
/>
60
+
```
61
+
62
+
It receives exactly what `getLinkProps` returns, so the same callback works on both tiers. It is spread last, so it wins over anything else on the element, and it is not called at all when the link is not navigable. `<JImage attributes>` is the same shape, for the same reason.
63
+
64
+
The second is a wrapper that is not a bare `<a>`. A design system's call to action is usually its own component, and wrapping it in an anchor gives you two nested interactive elements. `asChild` hands the link to the element you render instead — Next.js calls the same thing `passHref`:
The child receives `href`, `target`, `rel`, `aria-current` and whatever `attributes` produced, and must forward them to the element it renders. It needs exactly one element child; anything else is an error naming the way out. When the link is not navigable the child is still rendered, simply without the link — `whenUnresolved="none"` is how you drop it entirely.
74
+
39
75
## A target that does not resolve is normal
40
76
41
77
This is the part that surprises people. Publishing a page does **not** publish the pages it links to: `jnt:page` is in `referencedNodeTypesToSkip`. So a perfectly ordinary editorial workflow — build a card, point it at a page that is still a draft, publish the card — leaves you with a reference that resolves to nothing in live.
`anchor` is spreadable onto an `<a>` — every key is a valid anchor attribute, by construction. `state` is not: `navigable`, `isCurrent`, `isAncestor` and `label` are yours to read, never to spread.
111
+
`anchor` is spreadable onto an `<a>` — every key is a valid anchor attribute, by construction. `state` is not: `navigable`, `isCurrent`, `isAncestor`, `label` and `node` are yours to read, never to spread.
112
+
113
+
`state.node` is what the link resolved to — the node target, or the reference read off a content node. It saves the second resolution a fallback usually needs:
114
+
115
+
```tsx
116
+
const { anchor, state } =resolveContentLink(cta, {}, useServerContext()) ?? {};
Reading a reference yourself is the other half of that problem, and it is a JCR concern rather than a link one: an unresolvable reference reaches JavaScript as a plain falsy value, so every view that touches a `weakreference` ends up writing the same try/catch. `readNodeReference` is that try/catch, once, next to `getNodeProps`:
const related =readNodeReference(currentNode, "acme:related");
126
+
// null → the property is unset
127
+
// { uuid } → it is set, and the target is not reachable (unpublished, deleted, forbidden)
128
+
// { uuid, node } → it resolved
129
+
```
130
+
131
+
It never throws. What it cannot tell you is _why_ an unreachable target is unreachable: unpublished, deleted and "you may not see it" arrive identically, and no JCR read separates them.
76
132
77
133
:::info
78
134
`getLinkProps` reads no React context of its own. Inside a view, pass `useServerContext()`. Without it you still get an `href`, but no cache dependency is registered and `isCurrent` is always false — a silent downgrade, not an error.
@@ -90,6 +146,22 @@ Pass the content node and let the library read it:
90
146
91
147
With no children, the label comes from the content: `jcr:title`, then `j:linkTitle`, then the displayable name of the target.
92
148
149
+
### When the link is a mixin, the label is somewhere else
150
+
151
+
That default is right when the link **is** the content — a `jnt:nodeLink` exists to be a link, and its `jcr:title` is the link label. It is wrong as soon as the link is a **mixin on something else**. A CTA mixin sits on a card, a panel or a hero that already has a `jcr:title`, and that title is the heading. Take it as the label and every call to action on the page is named after the section it lives in.
// There is no label property: use the name of the page it points at
160
+
<JLinkcontent={card}labelFrom="target" />
161
+
```
162
+
163
+
`labelProperties` replaces the list that is tried on the content node, in order. `labelFrom="target"` skips the content node altogether — the readable spelling of `labelProperties={[]}` — and takes precedence over `labelProperties` when both are given. An explicit `label`, or children, still wins over either.
164
+
93
165
Because the `j:linkType` convention is a module convention and at least four spellings of it exist in the wild, the discriminator is a parameter:
94
166
95
167
```tsx
@@ -115,6 +187,25 @@ Any string that the library did not build itself goes through a scheme allow-lis
115
187
116
188
This applies to an `href` you pass and to an author-supplied `j:url` alike, and it is applied at render time, so it also covers content stored before anyone thought to validate it. React alone is not enough here: it neutralises `javascript:` by substituting a throwing URL, and it matches no other scheme.
117
189
190
+
A project is often stricter than that. A "partner website" field that must be `https://` and nothing else does not want `tel:` links quietly working. Narrow the list — per call, or once for the whole module:
`setLinkDefaults` is keyed by the module that calls it, the same way `setImageDefaults` is: every JavaScript module in an instance shares one JavaScript context, so a module-level variable would be a policy for the whole server. Call it at the top level of a server file, not inside a render.
204
+
205
+
:::warning
206
+
The option **narrows only**. A scheme that is not on the built-in list is dropped rather than added, because a call site is not the place a project loosens its own URL policy — `javascript:` and `data:` are the reason the list exists. On a development instance the library says so once per scheme; in production the links are simply not navigable.
207
+
:::
208
+
118
209
Query parameters and a fragment are options rather than string surgery, and they land in the right order:
119
210
120
211
```tsx
@@ -187,18 +278,47 @@ A language switcher is the case where the computation is wrong and you know bett
187
278
`target` is validated against the four values `jmix:link` allows (`_blank`, `_parent`, `_self`, `_top`). Anything else omits the attribute rather than emitting `target=""`, which matters because the value often comes straight from content. `rel="noopener noreferrer"` is added whenever `target` resolves to `_blank`; pass `rel` yourself to replace it.
188
279
189
280
:::info
190
-
These are live and preview guarantees. In the page builder, `EditModeFilter` rewrites the anchors it delivers: it turns `/cms/edit/` into `/cms/editframe/` and either deletes `target` or staples `target="_blank"` on with no `rel`. Assert on the delivered DOM, not on what your component returned.
281
+
These are live and preview guarantees. In the page builder, `EditModeFilter` rewrites the anchors it delivers: it either deletes `target` or staples `target="_blank"` on with no `rel`. Assert on the delivered DOM, not on what your component returned.
191
282
:::
192
283
193
284
## `href` is a server-side intermediate
194
285
195
-
The `href` you get back is not the URL the visitor receives. Core finishes it after the render — vanity URLs, SEO rewriting, and the `?jsite=` parameter that live adds to a cross-site link — and it does so by walking the emitted HTML. `URLTraverser` only visits a fixed set of tag/attribute pairs (`a[href]`, `img[src]`, `form[action]`, `link[href]`, and a few more) in an `html` template type.
286
+
The `href` you get back is not the URL the visitor receives. Core finishes it after the render, and it does so by walking the emitted HTML rather than by touching the value you built. Two filters do the work, and each visits a fixed set of tag/attribute pairs in an `html` template type:
|`URLFilter`| Vanity URLs, the SEO server name, and the `?jsite=` parameter of a cross-site link |`a[href]`, `img[src\|srcset\|data-src\|data-srcset]`, `form[action]`, `link[href]`, `source[srcset]`, `embed[src]`, `param[value]`|
291
+
|`EditModeFilter`| In the page builder: `/cms/edit/` → `/cms/editframe/`, and `target` deleted or forced |`a[href]` only |
196
292
197
-
So:
293
+
So the rule is simple, and it is about **where you put the URL**, not about how you built it:
198
294
199
-
- Put the URL anywhere else — a `data-*` attribute, an Island payload, the JSON body of an action — and it stays exactly as you built it. No vanity URL, no `?jsite=`.
295
+
- Emit it as one of those attributes and it is finished for you.
296
+
- Put it anywhere else — a `data-*` attribute of your own, an Island payload, the JSON body of an action, a `<meta>` tag, a JSON-LD block, a CSS `url()` — and it stays exactly as you built it. No vanity URL, no `?jsite=`.
200
297
- Never string-compare an `href`, and never parse it to decide something. Compare nodes, or use `state.isCurrent` and `state.isAncestor`.
201
298
299
+
There is no call that runs the finishing pass for you: it needs the assembled HTML, which does not exist yet while your view runs. What you can do is make the URL correct without it — see below — and reach for `buildNodeUrl(node, { absolute: true })` when the URL leaves the page altogether (`og:url`, an email, JSON-LD).
300
+
301
+
### The edit-mode URL, which used to need a workaround
302
+
303
+
One rewrite used to bite hard enough that projects patched it by hand:
304
+
305
+
```tsx
306
+
// Don't. This is what the library now gets right.
The reason it existed: `/cms/edit/…` does not render a page. On Jahia 8.2.3 it answers `302` to the jContent UI, and it only ever reached the page because `EditModeFilter` substituted the two — for an `a[href]` and nothing else. A URL in an Island payload kept the redirecting form, so the nav rendered by that Island navigated the iframe to a whole second copy of jContent.
311
+
312
+
`buildNodeUrl` now emits `/cms/editframe/…` for edit mode directly, which is what `node.getUrl()` already returned when no `mode`, `language` or `extension` was named. The workaround is no longer needed, and neither is the branch around it:
313
+
314
+
```tsx
315
+
// The URL is correct wherever it goes, including into an Island
316
+
const { anchor, state } =getLinkProps(page, { language }, useServerContext());
Note that this is the URL of the page **inside** the builder's frame. Deep-linking a visitor into the jContent editor is a different URL (`/jahia/jcontent/…`) and not something this API builds.
321
+
202
322
## Links inside Islands
203
323
204
324
The library cannot be imported from a client bundle: the Vite plugin fails the build if you try. An Island therefore receives link _data_, not a link component, and renders the anchor itself:
@@ -229,4 +349,6 @@ If you need a policy on those anchors, it belongs in a render filter — `regist
229
349
-[`JLink`](https://github.com/Jahia/javascript-modules/blob/main/javascript-modules-library/README.md#jlink) — the component
230
350
-[`getLinkProps`](https://github.com/Jahia/javascript-modules/blob/main/javascript-modules-library/README.md#getlinkprops) — the props tier, for Islands and custom markup
231
351
-[`resolveContentLink`](https://github.com/Jahia/javascript-modules/blob/main/javascript-modules-library/README.md#resolvecontentlink) — reading a link off a content node
352
+
-[`setLinkDefaults`](https://github.com/Jahia/javascript-modules/blob/main/javascript-modules-library/README.md#setlinkdefaults) — the module-wide scheme allow-list
353
+
-[`readNodeReference`](https://github.com/Jahia/javascript-modules/blob/main/javascript-modules-library/README.md#readnodereference) — reading a reference property safely
232
354
-[`buildNodeUrl`](https://github.com/Jahia/javascript-modules/blob/main/javascript-modules-library/README.md#buildnodeurl) — the URL tier underneath
0 commit comments