From fab927994b1ac91e871df0d2a96c237386e7c163 Mon Sep 17 00:00:00 2001 From: Yigit Gurbulak Date: Fri, 9 Oct 2026 04:20:51 +0300 Subject: [PATCH 1/2] fix(nav): soft-nav cross-route mobile drawer links Closes #272 Co-Authored-By: Claude Opus 5.5 --- components/site/MobileMenu.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/components/site/MobileMenu.tsx b/components/site/MobileMenu.tsx index 2356e05..c6fa247 100644 --- a/components/site/MobileMenu.tsx +++ b/components/site/MobileMenu.tsx @@ -8,6 +8,7 @@ import { useSyncExternalStore, } from "react"; import { createPortal } from "react-dom"; +import { useRouter } from "next/navigation"; import { links } from "@/lib/links"; import { HOME_SECTION_ID, scrollToAnchor } from "@/lib/scroll"; @@ -51,6 +52,7 @@ const getServerSnapshot = () => false; export function MobileMenu({ activeSection, tone, onOpenChange }: Props) { const [open, setOpen] = useState(false); + const router = useRouter(); // Static export: document.body isn't available during prerender. Gate // the portal until after hydration so the first client render matches // the prerendered HTML. @@ -169,12 +171,14 @@ export function MobileMenu({ activeSection, tone, onOpenChange }: Props) { const targetEl = anchor ? document.getElementById(anchor) : null; if (anchor && !targetEl) { - // Drawer opened from a different route (e.g. /blog/*); a full - // reload lets the new page resolve the destination deterministically - // (Next 16 App Router's soft-nav hash behaviour under output: - // "export" isn't documented). Home -> "/", section -> "/#anchor". - // Skip the local restore — the page is about to unload. - window.location.assign(isHome ? "/" : `/#${anchor}`); + // Drawer opened from a different route (e.g. /blog/*), so the + // section isn't in this DOM: soft-nav home and let the router + // scroll to the hash. Home -> "/", section -> "/#anchor". Runs + // after the body unlock above, so the pinned body is released + // before the navigation. Historically a full reload, to dodge the + // `/#id#id` hash-append bug (#116) that Next 16.3.8 fixed + // upstream. Skip the local restore — the destination owns scroll. + router.push(isHome ? "/" : `/#${anchor}`); return; } @@ -217,7 +221,7 @@ export function MobileMenu({ activeSection, tone, onOpenChange }: Props) { focusMovedToSectionRef.current = true; } }; - }, [open]); + }, [open, router]); // Skipped on the initial render (when `open` is already false) via // the wasOpenRef guard. From 55933ead90f265af94479e0f1b19a13cb9222b32 Mon Sep 17 00:00:00 2001 From: Yigit Gurbulak Date: Fri, 9 Oct 2026 05:05:57 +0300 Subject: [PATCH 2/2] docs(nav): correct drawer soft-nav comments Co-Authored-By: Claude Opus 5.5 --- components/site/MobileMenu.tsx | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/components/site/MobileMenu.tsx b/components/site/MobileMenu.tsx index c6fa247..416d3ae 100644 --- a/components/site/MobileMenu.tsx +++ b/components/site/MobileMenu.tsx @@ -77,13 +77,15 @@ export function MobileMenu({ activeSection, tone, onOpenChange }: Props) { } | null>(null); const scrollYRef = useRef(0); // When the drawer closes via an in-page anchor tap, the cleanup must - // scroll to that target instead of restoring the prior scroll - // position — otherwise the body unlock would override the anchor jump. + // scroll (or, off-route, soft-nav) to that target instead of restoring + // the prior scroll position — otherwise the body unlock would override + // the anchor jump. const pendingAnchorRef = useRef(null); - // An anchor-select close moves focus into the target section from the - // Effect-A cleanup, which runs *before* the toggle-refocus effect's - // setup in the same commit. This flag — set there, consumed there — - // stops that effect from yanking focus back to the hamburger. + // An in-page anchor-select close (target in this DOM) moves focus into + // the target section from the scroll-lock effect's cleanup, which runs + // *before* the toggle-refocus effect's setup in the same commit. This + // flag — set there, consumed there — stops that effect from yanking + // focus back to the hamburger. const focusMovedToSectionRef = useRef(false); useEffect(() => { @@ -173,11 +175,13 @@ export function MobileMenu({ activeSection, tone, onOpenChange }: Props) { if (anchor && !targetEl) { // Drawer opened from a different route (e.g. /blog/*), so the // section isn't in this DOM: soft-nav home and let the router - // scroll to the hash. Home -> "/", section -> "/#anchor". Runs - // after the body unlock above, so the pinned body is released - // before the navigation. Historically a full reload, to dodge the - // `/#id#id` hash-append bug (#116) that Next 16.3.8 fixed - // upstream. Skip the local restore — the destination owns scroll. + // scroll to the hash. Home -> "/", section -> "/#anchor". Must + // run after the body unlock above — the router's hash scroll is a + // no-op against a still-pinned body. Historically a full reload, + // because soft-nav hash handling under output: "export" was + // untrusted; its one concrete failure, `/#id#id` (#116), was fixed + // upstream in Next 16.3.8. Skip the local restore and focus move — + // the router owns scroll and the URL. router.push(isHome ? "/" : `/#${anchor}`); return; } @@ -229,8 +233,9 @@ export function MobileMenu({ activeSection, tone, onOpenChange }: Props) { useEffect(() => { if (wasOpenRef.current && !open) { // An anchor-select close already moved focus into the section; - // leave it there. Normal closes (Escape, scrim, toggle, resize) - // leave the flag false and still return focus to the toggle. + // leave it there. Normal closes (Escape, scrim, toggle, resize) and + // an off-route anchor close (soft-navs home) leave the flag false + // and still return focus to the toggle. if (focusMovedToSectionRef.current) { focusMovedToSectionRef.current = false; } else {