<Link> renders a plain <a> server-side, so it works with JavaScript off, on slow links, and during the brief moment before hydration completes. Once Pylon’s runtime is live, clicks get intercepted and Pylon does a client-side navigation instead of a full page reload.
What it does
| Phase | Behavior |
|---|---|
| SSR / no JS | Renders <a href data-pylon-link>...</a>. Same as a regular anchor. |
| In viewport | IntersectionObserver fires __pylon.prefetch(href) — adds <link rel="prefetch" as="document"> for the URL + <link rel="modulepreload"> for the shared chunk. |
| Hover / touchstart | Same prefetch logic, escalated for users who never saw the link in the viewport (touch devices, fast scrollers). |
| Click | preventDefault(), fetch the new SSR HTML, dynamic-import the new route’s entry chunk (already preloaded), and re-render the React root in place. |
| Modifier keys / target=“_blank” | Falls through to the browser’s default behavior — open in new tab, etc. |
API
className, target, rel, onClick, etc.) passes through.
Examples
Basic
Disable prefetch for off-screen links
A large paginated list with a “next page” link the user might never click:Open in a new tab
Programmatic navigation
<Link> is the prop-based API. For imperative nav (after a form submit, post-login redirect, etc.), call the global runtime:
window.__pylon is defined as soon as hydration completes. Use the ?. form to be safe on the first render.
How nav actually works
- User clicks a
<Link>. The runtime’s delegated handler picks it offa[data-pylon-link]clicks. - Runtime fetches the target URL with the standard
Accept: text/htmlheader. The server SSRs the new page just like a direct navigation would. - Runtime parses the response and extracts the
__PYLON_DATA__JSON tag (component name, layouts, props). - Runtime looks up the new component in the bundle manifest and dynamically imports its entry chunk.
- The entry chunk’s
hydrate(component, Page, Layouts)populates the route cache without re-rendering (the cache is keyed on component path). - Runtime calls
root.render(buildTree(Page, Layouts, newProps)). React’s reconciler diffs against the existing tree — shared layouts get reused, the page subtree swaps. history.pushState, scroll to top, done.
window.location.href = href so the browser handles it the old-fashioned way.
Differences from Next.js’s <Link>
Pylon <Link> | Next.js <Link> | |
|---|---|---|
| Prefetch trigger | IntersectionObserver (256px rootMargin) | IntersectionObserver |
| Hover escalation | Yes (once) | Yes |
| Prefetch payload | HTML + shared chunk | RSC payload + per-route JS |
| Nav model | Re-render in place, layouts preserved | App router preserves layouts the same way |
scroll={false} prop | Not yet | Yes |
Hash-only links (#section) | Falls through to browser | Smooth-scrolls in app router |
scroll props are on the roadmap.