Skip to content

Improve the developer experience when rendering links #749

Description

@romain-pm

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    EpicdocumentationImprovements or additions to documentationfeatA new feature

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions