From 9281c9d649b4086b3bfe78541d03d5aedc75b4d9 Mon Sep 17 00:00:00 2001 From: CahidArda Date: Thu, 16 Jul 2026 13:01:32 +0300 Subject: [PATCH] fix: auto-repair mis-rendered sidebar on introduction page Mintlify sometimes mis-measures its fixed sidebar while hydrating /introduction (mode: frame) and sets a negative inline top, leaving the sidebar stuck under the header until the user scrolls. Detect that state (negative top while the footer is nowhere near the viewport - negative top is legitimate when clearing the footer) and reproduce the healing scroll: 1px down, restore next frame. --- script.js | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ style.css | 8 ++++- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/script.js b/script.js index 9b6efc63..c14b06b5 100644 --- a/script.js +++ b/script.js @@ -119,6 +119,98 @@ function initialize() { initialize(); +/* On hard loads of /introduction (mode: frame + custom landing content), + Mintlify sometimes mis-measures its fixed sidebar during hydration and sets a + negative inline `top` on #sidebar (e.g. -604px), leaving the sidebar stuck + under the header showing the tail of the nav. Any real scroll makes Mintlify + re-measure and snap it back, so reproduce that: scroll 1px down, restore next + frame. Nudge whenever the page lands on /introduction (Mintlify stamps + data-current-path on after hydration / on SPA navs) and whenever + #sidebar's inline top goes negative — which is never a legitimate state. */ +let sidebarNudging = false; +function nudgeSidebar() { + if (sidebarNudging) return; + sidebarNudging = true; + // double rAF: let any in-flight reflow finish before nudging + requestAnimationFrame(() => { + requestAnimationFrame(() => { + const x = window.scrollX; + const y = window.scrollY; + window.scrollTo(x, y + 1); + requestAnimationFrame(() => { + window.scrollTo(x, y); + window.dispatchEvent(new Event("resize")); + sidebarNudging = false; + }); + }); + }); +} + +function isSidebarBroken() { + const sidebar = document.getElementById("sidebar"); + if (!sidebar) return false; + const top = parseFloat(sidebar.style.top); + if (!(top < 0)) return false; + // Negative top is legitimate only while the footer is in (or near) view: + // Mintlify raises the sidebar to clear it, updating top on every scroll — + // nudging then loops against Mintlify's own updates and shakes the page. + // A negative top with the footer nowhere near the viewport is the + // hydration mis-measure (happens at any restored scroll position). + const footer = document.querySelector("footer"); + if (!footer) return true; + return footer.getBoundingClientRect().top > window.innerHeight + 200; +} + +function watchIntroductionSidebar() { + let watchedSidebar = null; + let autoRepairs = 0; + const styleObserver = new MutationObserver(() => { + // cap observer-driven repairs as a backstop against any feedback loop + if (autoRepairs < 5 && isSidebarBroken()) { + autoRepairs++; + nudgeSidebar(); + } + }); + + const check = () => { + const path = document.documentElement.getAttribute("data-current-path"); + if (path !== "/introduction") return; + // React can recreate #sidebar across SPA navs; re-attach when it changes + const sidebar = document.getElementById("sidebar"); + if (sidebar && sidebar !== watchedSidebar) { + watchedSidebar = sidebar; + styleObserver.disconnect(); + styleObserver.observe(sidebar, { + attributes: true, + attributeFilter: ["style"], + }); + } + if (isSidebarBroken()) nudgeSidebar(); + }; + + let lastPath = null; + new MutationObserver(() => { + const path = document.documentElement.getAttribute("data-current-path"); + if (path === lastPath) return; + lastPath = path; + if (path === "/introduction") { + autoRepairs = 0; + // landing on the page always gets one nudge: it also covers the reflow + // from style.css collapsing the ToC column after the attribute lands + nudgeSidebar(); + check(); + } + }).observe(document.documentElement, { + attributes: true, + attributeFilter: ["data-current-path"], + }); + + window.addEventListener("load", check); + check(); +} + +watchIntroductionSidebar(); + function createCookieConsentBanner() { if (document.getElementById("cookie-consent-banner")) return; diff --git a/style.css b/style.css index 117c840c..bfe49041 100644 --- a/style.css +++ b/style.css @@ -32,7 +32,13 @@ data-current-path hook (only the /introduction page). NOTE: still targets Mintlify-internal ids (#content-area / #content-side-layout); revisit if a Mintlify upgrade renames them. Collapse to zero width, not display:none (which - broke the sticky sidebar, making it render scrolled on load). */ + broke the sticky sidebar, making it render scrolled on load). Even width:0 can + race Mintlify's sidebar measurement on load — script.js nudges a re-measure + (see nudgeSidebar) whenever data-current-path lands on /introduction. + 2026-07: production's renderer no longer has #content-side-layout (rule is + inert there, frame mode fills the width natively now), but local `mint dev` + still renders it; keep until the CLI catches up. The sidebar mis-render still + happens on prod regardless of this rule — see script.js. */ html[data-current-path="/introduction"] #content-side-layout { width: 0 !important; min-width: 0 !important;