A JavaScript module has no link concept, so every module writes <a href={buildNodeUrl(node)}> by hand — and buildNodeUrl throws on a falsy node (urlBuilder.ts:72).
An unresolvable reference is not an edge case. Publishing a page does not publish a jnt:page it links to (referencedNodeTypesToSkip defaults to jnt:page,jmix:autoPublish), so the normal outcome of shipping a link to an unpublished page is a live fragment replaced by <!-- Module error : Error: Expected a node in buildNodeUrl, received undefined--> — HTTP 200, section gone, per visitor. Measured end to end on 8.2.3. Unpublished, deleted and permission-denied all collapse to the same falsy value at the JCR boundary, so a component cannot tell them apart.
The rest is hand-written too: the none | internal | external switch appears in samples/hydrogen, Jahia/luxe-jahia-demo, Jahia/jahia.com and Jahia/se-utils under four incompatible property names, and nothing in the library reads j:target, emits rel on an author-supplied external URL, or checks that a target exists in the language being linked to.
Target: a junior renders a correct, cacheable, accessible link by naming the target.
<JLink node={props["j:linknode"]}>{title}</JLink>
That one line must produce an href built through buildNodeUrl, a cache dependency that still covers the target while it is unresolvable, aria-current="page" when the target is the current page, and — when the reference does not resolve — the children rendered without an anchor. Never a throw, never an <a> with no href.
Architecture (the URL tier already exists):
| Tier |
API |
Owns |
| URL |
buildNodeUrl(node, …) — unchanged |
mode/workspace/language, context path, the nt:file split |
| Props |
getLinkProps(target, …) · resolveContentLink(node) |
resolution, fallback, cache dependency, target/rel, current-page state, scheme allow-list |
| Component |
<JLink node | content | href …> |
the <a>, children, unwrapping when not navigable |
getLinkProps returns { anchor, state }. anchor is DOM-spreadable, every key a valid <a> attribute, exactly as ImageProps is; state (navigable, isCurrent, isAncestor, label) is not, and is never spread.
JLink is server-only. The vite plugin hard-fails any import of the library from a client bundle (vite-plugin/src/index.ts:214-221, :316-321), so an Island receives anchor and renders its own <a {...anchor}> — the shape the image guide already uses for <img {...imageProps} />. A client-rendered anchor loses what core adds after the render (vanity URL, ?jsite=).
Two things the spec deliberately does not own: vanity and SEO rewriting, which URLFilter does once after the render, and rich text, where the sink is dangerouslySetInnerHTML and out of a link component's reach.
API sketch, proposed breakdown and the use-case coverage table are in the comments. This answers the spike #80.
Sub-issues: #752 props tier · #753 component · #754 content-node vocabulary · #755 cache.mainResource · #756 buildNodeUrl gaps · #757 guide + sample fix · #758 tests. First implementation pass in #751.
Grounding: source analysis of the library, the engine and decompiled 8.2.3 core; live experiments on a local 8.2.3 EE (publication of an unpublished reference, dangling reference, permission-denied, vanity URLs, edit-mode anchors); and a link-use-case harvest of Jahia/jahia.com, Jahia/luxe-jahia-demo, samples/hydrogen, Jahia/default, Jahia/dx-base-demo-*, Jahia/industrial, Jahia/se-utils, Jahia/bootstrap3 and Jahia/legacy-default-components. Companion of the image epic #734.
A JavaScript module has no link concept, so every module writes
<a href={buildNodeUrl(node)}>by hand — andbuildNodeUrlthrows on a falsy node (urlBuilder.ts:72).An unresolvable reference is not an edge case. Publishing a page does not publish a
jnt:pageit links to (referencedNodeTypesToSkipdefaults tojnt:page,jmix:autoPublish), so the normal outcome of shipping a link to an unpublished page is a live fragment replaced by<!-- Module error : Error: Expected a node in buildNodeUrl, received undefined-->— HTTP 200, section gone, per visitor. Measured end to end on 8.2.3. Unpublished, deleted and permission-denied all collapse to the same falsy value at the JCR boundary, so a component cannot tell them apart.The rest is hand-written too: the
none | internal | externalswitch appears insamples/hydrogen, Jahia/luxe-jahia-demo, Jahia/jahia.com and Jahia/se-utils under four incompatible property names, and nothing in the library readsj:target, emitsrelon an author-supplied external URL, or checks that a target exists in the language being linked to.Target: a junior renders a correct, cacheable, accessible link by naming the target.
That one line must produce an
hrefbuilt throughbuildNodeUrl, a cache dependency that still covers the target while it is unresolvable,aria-current="page"when the target is the current page, and — when the reference does not resolve — the children rendered without an anchor. Never a throw, never an<a>with nohref.Architecture (the URL tier already exists):
buildNodeUrl(node, …)— unchangednt:filesplitgetLinkProps(target, …)·resolveContentLink(node)target/rel, current-page state, scheme allow-list<JLink node | content | href …><a>, children, unwrapping when not navigablegetLinkPropsreturns{ anchor, state }.anchoris DOM-spreadable, every key a valid<a>attribute, exactly asImagePropsis;state(navigable,isCurrent,isAncestor,label) is not, and is never spread.JLinkis server-only. The vite plugin hard-fails any import of the library from a client bundle (vite-plugin/src/index.ts:214-221,:316-321), so an Island receivesanchorand renders its own<a {...anchor}>— the shape the image guide already uses for<img {...imageProps} />. A client-rendered anchor loses what core adds after the render (vanity URL,?jsite=).Two things the spec deliberately does not own: vanity and SEO rewriting, which
URLFilterdoes once after the render, and rich text, where the sink isdangerouslySetInnerHTMLand out of a link component's reach.API sketch, proposed breakdown and the use-case coverage table are in the comments. This answers the spike #80.
Sub-issues: #752 props tier · #753 component · #754 content-node vocabulary · #755
cache.mainResource· #756buildNodeUrlgaps · #757 guide + sample fix · #758 tests. First implementation pass in #751.Grounding: source analysis of the library, the engine and decompiled 8.2.3 core; live experiments on a local 8.2.3 EE (publication of an unpublished reference, dangling reference, permission-denied, vanity URLs, edit-mode anchors); and a link-use-case harvest of Jahia/jahia.com, Jahia/luxe-jahia-demo,
samples/hydrogen, Jahia/default, Jahia/dx-base-demo-*, Jahia/industrial, Jahia/se-utils, Jahia/bootstrap3 and Jahia/legacy-default-components. Companion of the image epic #734.