Fix anchor links not scrolling to target on client-side navigation (RND-11844)#4396
Open
zenoachtig wants to merge 8 commits into
Open
Fix anchor links not scrolling to target on client-side navigation (RND-11844)#4396zenoachtig wants to merge 8 commits into
zenoachtig wants to merge 8 commits into
Conversation
…ND-11844) Cross-page anchor links (e.g. a homepage card linking to `/page#heading`) landed at the top of the destination page on soft navigation, while the same URL loaded directly scrolled to the heading correctly. `useScrollToHash` (the per-page safety net in PageClientLayout that scrolls once the page blocks are rendered) only depended on the navigation hash. The hash is set at click time while the previous page is still mounted, so the effect fired before the target element existed. Because sibling pages share the same `[pagePath]` route, PageClientLayout is reused rather than remounted, so the effect never re-ran once the destination content committed to the DOM. Add `pathname` as a dependency so the scroll is re-attempted when the destination page commits and the target heading is present. The `if (hash)` guard is unchanged, so hash-less navigations still scroll to top via ScrollPage and back/forward restoration is unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TpGe6QkAF4Y1HDGUD4vbke
🦋 Changeset detectedLatest commit: d414f09 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Contributor
|
The latest updates on your projects. Learn more about Argos notifications ↗︎
|
…hors land on target (RND-11844) The first attempt (re-running useScrollToHash on `pathname`) had no effect: the root-layout `ScrollPage` hook already called `scrollToHash` on every hash+pathname change, so the scroll was firing — but Next's own default post-navigation scroll (App Router hash handling is unreliable, see useHash / vercel/next.js#49465) runs after it and scrolls to top, overriding us. Same-page anchors are unaffected because Link.tsx handles those manually with preventDefault (no NextLink scroll), and direct URL loads work via the browser's native hash scroll — which matches the reported behavior. Set `scroll={false}` on the internal NextLink so Next stops managing scroll on client-side navigations; GitBook's ScrollPage then owns it (scroll to the hash when present, else to the top). Revert the redundant `pathname` dependency added in the previous commit. Not verified in a browser: this sandbox's network policy blocks the preview deployment and the GitBook API, so it needs a browser check on a site with cross-page anchor links (e.g. n8n) plus a regression pass on ordinary navigation scroll-to-top and back/forward restoration. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TpGe6QkAF4Y1HDGUD4vbke
RND-11844) Browser diagnostics on the reported n8n case showed the real cause: the scroll hooks fire when the hash is set (on link click, still on the previous page), so `document.getElementById(hash)` misses; by the time the destination heading is in the DOM, `scrollToHash` is never called again, so the page stays at the top. Next's own scroll was never involved — an earlier `scroll={false}` attempt changed nothing and is reverted here. Make `scrollToHash` retry across animation frames (bounded to ~1s) until the target element exists, then scroll to it once. A single in-flight retry is tracked and cancelled if a newer scroll is requested. Hash-less navigation is unaffected (ScrollPage scrolls to top directly, without calling scrollToHash). Still needs a browser check: rerunning the console diagnostic should now show `scrollIntoView` firing on the attempt where the element resolves. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TpGe6QkAF4Y1HDGUD4vbke
zenoachtig
force-pushed
the
claude/tender-allen-f9l6hh
branch
from
July 14, 2026 13:36
9b8c6a7 to
c2d1804
Compare
…target (RND-11844) Browser diagnostics on the reported n8n case showed two cooperating causes, which is why the earlier single-sided attempts failed: 1. The scroll hooks fire when the hash is set (on link click, still on the previous page), so the target heading isn't in the DOM yet and scrollToHash missed and never retried. Fixed in the previous commit by retrying scrollToHash across frames. 2. With the retry in place, our scrollIntoView *does* fire once the heading commits — but Next's own post-navigation scroll (its hash scrollIntoView plus a scroll-to-top, see useHash / vercel/next.js#49465) runs after us and wins, snapping back to the top. Set `scroll={false}` on the internal NextLink so Next stops managing scroll on client-side navigation and GitBook's ScrollPage/useScrollToHash own it exclusively: retry to the hash when present, scroll to top otherwise. Neither change works alone — the retry needs Next to stop overriding it, and scroll={false} needs the retry to find the late-committing element. Still needs a browser check: rerunning the console diagnostic should now show our scrollIntoView fire without a following scroll-to-top, landing on the heading. Because scroll={false} routes all navigation scroll through ScrollPage, also regression-check plain page-to-page nav (top), same-page anchors, and back/forward. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TpGe6QkAF4Y1HDGUD4vbke
Browser diagnostics on the reported n8n case identified the actual blocker. Our scroll
now fires once the target heading commits, but Next's scroll restoration fires a
`window.scrollTo(0, 0)` *after* it during the client navigation, so a one-shot
scrollIntoView loses — whoever scrolls last wins, and Next scrolls last. This is why
none of retry-only / `scroll={false}` / their combination worked: they all scrolled once.
Rework `scrollToHash` to (a) retry across frames until the element exists, then (b)
re-assert the scroll for a short window (~0.3s) so it survives the late reset, bailing
immediately if the user scrolls (wheel / touchmove / keydown) so it never hijacks intent.
Use `behavior: 'instant'` — a smooth animation is trivially interrupted by the reset, and
re-asserting an already-reached instant scroll is a no-op. `scroll={false}` on the Link is
reverted: it neither stopped the reset nor was needed once we out-last it, and it churned
unrelated navigation-scroll behavior.
Still needs a browser check: the console diagnostic should now show scrollY settling on
the heading with no trailing scroll-to-top winning. Regression pass: same-page anchors,
plain page-to-page nav (top), back/forward.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpGe6QkAF4Y1HDGUD4vbke
Collapse scrollToHash to one requestAnimationFrame loop: scroll to the target whenever it's present, retrying ~1s for it to commit and holding ~0.3s afterward so we scroll after Next's late scroll-to-top. Drops the separate wheel/touchmove/keydown abort machinery — the hold window is short enough that a user scrolling within it is a rare, brief edge, not worth the ceremony. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TpGe6QkAF4Y1HDGUD4vbke
zenoachtig
marked this pull request as ready for review
July 14, 2026 16:23
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7d40ef2060
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
zenoachtig
force-pushed
the
claude/tender-allen-f9l6hh
branch
from
July 14, 2026 16:52
7d40ef2 to
749b977
Compare
) A cross-page anchor link navigated to the right page but stayed at the top. GitBook's `scrollToHash` scrolled once, but a soft navigation delivers the destination asynchronously — content keeps mounting and reflowing for a few hundred ms after the URL changes — so the single scroll landed before the target reached its final position, and #4089 routed cross-section navigations through it. It also scrolled to the top on a transiently-empty hash context. - scrollToHash re-scrolls to the target on each DOM change until the DOM goes quiet, mirroring the fragment scrolling the browser does for free during a full page load. It bails if the visitor scrolls, so we never fight them. - useScrollPage decides the hash from the context value for same-page anchors (authoritative, set on click) and from `window.location.hash` for cross-page navigations (where the context hash is transiently empty during the remount). Next keeps handling the baseline scroll (top on plain/query navigations, the top fallback for missing anchors); GitBook only re-asserts the hash scroll it would otherwise land too early on. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
zenoachtig
force-pushed
the
claude/tender-allen-f9l6hh
branch
from
July 14, 2026 17:36
749b977 to
be0d5d2
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Linear: RND-11844 — "Anchor links on docs site fail to scroll to target" (reported by n8n, https://docs.n8n.io/).
Overview
/connect/connect-to-n8n-mcp-server#connecting-codex-cli-to-n8n-mcp-server) navigated to the right page but stayed at the top instead of scrolling to the heading.#anchorclicks, already worked — only cross-page anchor navigation was broken.Root cause
GitBook manages scroll-to-hash itself (
ScrollPage/useScrollToHash→scrollToHash), because App Router's native hash handling is unreliable (useHash.tsx, vercel/next.js#49465). Confirmed with in-browser instrumentation:scrollToHashscrolled only once, so it landed before the target reached its final position. #4089 then routed cross-section navigations through this same one-shot path, turning "slightly off" into "stuck at the top".ScrollPagealso scrolled to the top on a transiently-empty hash context (theuseHashvalue is briefly empty while a section remounts).The fix
scrollToHashnow re-scrolls to the target on every DOM change until the DOM goes quiet (~400ms) — the same fragment scrolling the browser does for free during a full page load, which soft navigation loses. This handles the target mounting late and content reflowing above it. It bails the moment the visitor scrolls, so we never fight them.useScrollPagedecides the hash from the authoritative source per case: the contexthashfor same-page anchors (set on click, before Next commits the URL), andwindow.location.hashfor cross-page navigations (where the context hash is transiently empty during the remount).Next keeps handling the baseline scroll — top on plain and query-only navigations, and the top fallback for a missing/stale anchor — so GitBook only re-asserts the hash scroll it would otherwise land too early on. (An earlier revision disabled Next's scroll with
scroll={false}; that was dropped, since the re-assertion wins the target position without it and disabling Next regressed query-only navigations and the missing-anchor fallback.)Verification
Verified in the local dev server and on the Vercel preview against the reported n8n case, plus regressions:
CI notes
docs.cherry-ai.com › Homevisual failure is a cookie-consent-dialog timeout, unrelated to this change — worth confirming it also fails onmain.— Authored by Claude