Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docusaurus.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ const config: Config = {
locales: ['en'],
},

clientModules: [
require.resolve('./src/clientModules/scrollToAnchor.ts'),
],

presets: [
[
'classic',
Expand Down
51 changes: 51 additions & 0 deletions src/clientModules/scrollToAnchor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type {ClientModule} from '@docusaurus/types';

/**
* Docusaurus's built-in scroll handler skips hash-based scrolling on initial
* page load (when previousLocation is null). It assumes the browser handles it
* natively, but React hydration can reset the scroll position before the
* browser finishes its native anchor scroll. This client module fills that gap
* by explicitly scrolling to the hash target after hydration settles.
*
* @see https://linear.app/roocode/issue/CLO-805
*/
const scrollToAnchorModule: ClientModule = {
onRouteDidUpdate({location, previousLocation}) {
// Docusaurus core already handles hash scrolling for subsequent
// navigations. This module only covers the initial page load case.
if (previousLocation != null) {
return;
}

const {hash} = location;
if (!hash) {
return;
}

const id = decodeURIComponent(hash.substring(1));
if (!id) {
return;
}
Comment on lines +25 to +28
Copy link
Contributor Author

Choose a reason for hiding this comment

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

decodeURIComponent throws a URIError when the hash contains a malformed percent-encoded sequence (e.g. #%ZZ). If someone shares or bookmarks such a URL, the unhandled exception would crash this callback and could prevent other client module hooks from running. Wrapping it in a try-catch keeps things defensive.

Suggested change
const id = decodeURIComponent(hash.substring(1));
if (!id) {
return;
}
let id: string;
try {
id = decodeURIComponent(hash.substring(1));
} catch {
return;
}
if (!id) {
return;
}

Fix it with Roo Code or mention @roomote and request a fix.


// Wait for the next animation frame so that React hydration has settled
// and the DOM reflects the final rendered state.
requestAnimationFrame(() => {
const element = document.getElementById(id);
if (element) {
// scrollIntoView respects the scroll-margin-top set by
// Docusaurus's anchorTargetStickyNavbar class on headings.
element.scrollIntoView();
return;
}

// If the element wasn't found on the first frame (e.g. lazy-loaded
// content), retry once after a short delay.
setTimeout(() => {
const retryElement = document.getElementById(id);
retryElement?.scrollIntoView();
}, 150);
});
},
};

export default scrollToAnchorModule;