Skip to content

feat(web): implement dynamic Preact re-hydration in ClientLink for SPA navigation - #948

Open
ubay1 wants to merge 2 commits into
nodejs:mainfrom
ubay1:feat/spa-navigation-web
Open

feat(web): implement dynamic Preact re-hydration in ClientLink for SPA navigation#948
ubay1 wants to merge 2 commits into
nodejs:mainfrom
ubay1:feat/spa-navigation-web

Conversation

@ubay1

@ubay1 ubay1 commented Jul 27, 2026

Copy link
Copy Markdown

Summary

This PR introduces the ClientLink component and navigate() utility for the web generator to enable zero-reload Single Page Application (SPA) navigation.
This directly solves the issue where navigating between documentation pages resets the sidebar scroll position back to the top, while also addressing the architectural feedback from nodejs/learn#75 (comment) by avoiding browser storage (localStorage / sessionStorage) entirely.


Problem Statement

When users navigate between generated static documentation pages using standard <a> links:
Sidebar Scroll Reset: The browser performs a full page reload on every click. This destroys and rebuilds the DOM, causing the sidebar (<aside>) scroll position to always jump back to the very top disrupting the user's reading and navigation experience.


Solution: Introducing ClientLink

To solve the sidebar scroll reset cleanly, we introduce the ClientLink component and programmatic navigate() helper 1. Zero-Reload Navigation with In-Memory Scroll State: ClientLink intercepts internal link clicks, fetches target HTML in the background, and swaps the #root container. Before the swap, it saves aside.scrollTop into a module-scoped JavaScript variable and restores it immediately afterward—keeping the sidebar exactly where the user left it without needing localStorage.
2. Smooth Crossfades & History: Uses the View Transitions API (document.startViewTransition()) when available and fully supports browser Back / Forward buttons via popstate.


Usage Guide (How to Use)

1. Using ClientLink in Sidebar / Navigation Components

Import ClientLink and pass it as the as prop to <SideBar> (or use it as a drop-in replacement for standard <a> tags):

import SideBar from '@node-core/ui-components/Containers/Sidebar';
import ClientLink, { navigate } from '@node-core/doc-kit/src/generators/web/ui/components/ClientLink/index.jsx';
export default ({ metadata }) => (
  <SideBar
    pathname={`/learn${metadata.path.replace('/index', '')}`}
    groups={sidebar}
    onSelect={navigate}
    as={ClientLink}
    title="Navigation"
  />
);

Verification & Testing

  • Linting (npm run lint): Passed (0 errors, 0 warnings).

  • Unit Test Suite (npm run test): 100% Passed (448 tests across 137 test suites).

  • Test Coverage (npm run test:coverage): Confirmed no regression in core web generator utilities.

  • Manual Verification: Tested local SPA navigation between pages; confirmed CodeBox copy buttons and CodeTabs work smoothly immediately after page transitions without full reloads.

  • I have read the Contributing Guidelines and made commit messages that follow the guideline.

  • I have run node --run test and all tests passed.

  • I have check code formatting with node --run format & node --run lint.

  • I've covered new added functionality with unit tests if necessary.

ubay1 added 2 commits July 27, 2026 14:31
…A navigation:

Replace hacky legacy DOM query re-initialization functions with native Preact dynamic hydration.
Key changes:
- Dynamically import the target page's entrypoint module script after swapping #root via innerHTML.
- Append a timestamp query parameter to bypass browser ES module evaluation cache, ensuring Preact executes hydrate() on every transition.
- Automatically re-attach all synthetic React/Preact event listeners (CodeBox copy buttons, tabs) without storage dependencies.
- Remove obsolete legacy functions and translate internal comments to English.
- Update web generator documentation (README.md).
@ubay1
ubay1 requested a review from a team as a code owner July 27, 2026 15:03
@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
api-docs-tooling Ready Ready Preview Jul 27, 2026 3:05pm

Request Review

@cursor

cursor Bot commented Jul 27, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Client-side navigation changes how all default sidebar and delegated in-page links behave (history, fetch failures, hydration); mistakes could break interactivity or navigation, though full-page fallback limits blast radius.

Overview
Adds ClientLink and navigate() so the web generator can move between static HTML pages without full reloads, keeping sidebar scroll in module memory instead of storage APIs.

navigate fetches the target page, swaps #root, optionally uses View Transitions, handles popstate and hash links (with exceptions for .picker-header), and falls back to a full navigation on errors. After each swap it re-imports the page entry module with a cache-busting query so Preact can hydrate interactive UI again (e.g. CodeBox, tabs).

The default SideBar now routes version/page selection through navigate and renders links via ClientLink instead of plain <a> + window.location. The web generator README documents usage; local guide markdown files are added to .gitignore.

Reviewed by Cursor Bugbot for commit b091c5f. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

@ubay1 is attempting to deploy a commit to the OpenJS Foundation Team on Vercel.

A member of the Team first needs to authorize it.

@MattIPv4 MattIPv4 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will let others weigh in on whether they want to introduce SPA navigation or not, but I just want to be clear, this is an incredibly complex change to solve the position of a scrollbar, and not at all what I suggested as a solution to the issue:

Yes, I'd encourage you to open a PR in doc-kit instead, to have it scroll to the active item on page load

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b091c5f. Configure here.

const scriptUrl = new URL(
newScript.getAttribute('src'),
window.location.origin
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Script URL resolved against origin

High Severity

reinitPageFeatures builds the entrypoint URL with window.location.origin as the base. Relative script src values such as ../page.js therefore resolve from the domain root instead of the current page path, so the wrong module is requested (or a 404) whenever the site is not hosted at /. Preact never re-hydrates and interactive controls stop working after SPA navigation.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b091c5f. Configure here.

* @param {string} url URL
*/
const redirect = url => (window.location.href = url);
const redirect = url => navigate(url);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Version switch uses SPA navigate

High Severity

redirect now always calls navigate(), including for the version Select onChange. Version values are absolute {baseURL}/latest-{version}/api{path}.html URLs for different builds. Soft-navigating only swaps #root and leaves the previous page’s import map and assets in <head>, so cross-version navigation loads mismatched modules and breaks the page.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b091c5f. Configure here.

// 2. Push browser history (skip for popstate – browser already did it)
if (!isPopState) {
window.history.pushState({}, '', url);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pushState runs outside error handling

Medium Severity

history.pushState runs before the try/catch that falls back to window.location.href. For cross-origin version URLs, pushState throws a SecurityError, so the catch never runs and navigation fails with an unhandled exception instead of a full page load.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b091c5f. Configure here.

@avivkeller avivkeller left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with Matt. While I appreciate the effort here (well, the effort of your agent, I suppose), this is extremely complex and fundamentally changes the way we handle links

@ubay1

ubay1 commented Jul 27, 2026

Copy link
Copy Markdown
Author

I agree with Matt. While I appreciate the effort here (well, the effort of your agent, I suppose), this is extremely complex and fundamentally changes the way we handle links

Yes it's very complicated. I just had the idea and my agent helped me turn it into code. At least for me, these docs are now much easier to use.

@MattIPv4

Copy link
Copy Markdown
Member

If you want to solve the problem, that the sidebar scroll position resets, we don't need to rearchitect the entire project... as I suggested originally, a very simple solution would be for the sidebar to scroll to whichever item is active when the page loads.

@MattIPv4

Copy link
Copy Markdown
Member

I just had the idea and my agent helped me turn it into code.

Please disclose AI usage in your PRs: https://ai-coding-assistants-policy.openjsf.org/

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.

3 participants