From c2879fb640d05a826b33e11966e56ef958047c2e Mon Sep 17 00:00:00 2001 From: Koichi ITO Date: Wed, 29 Jul 2026 17:12:24 +0900 Subject: [PATCH] Fix flash of unstyled content when loading the docs site ## Motivation and Context Opening https://ruby.sdk.modelcontextprotocol.io/ briefly showed a black half-circle at the bottom of the screen. The theme preference script in `head_custom.html` called `window.jtd.setTheme` on every page load. That helper rewrites the `href` of the render-blocking color stylesheet link, which detaches the loaded stylesheet and refetches it, so the page paints unstyled until the replacement arrives. During that window the search magnifier SVG, sized only by CSS, rendered at its fallback size and appeared as a large black half-circle near the bottom of the viewport. The site sets no `color_scheme`, so the default stylesheet is identical to the light one and most visitors paid the flash for a no-op swap. This change replaces the `jtd.setTheme` call with a local swapper that: - Leaves the stylesheet untouched when the resolved theme already matches (treating `default` as `light`). - Swaps non-destructively otherwise: the new link is inserted next to the current one and the old link is removed only after the new stylesheet has loaded. On error the new link is dropped and the page keeps its current styles. - Sets `blocking="render"` on the new link so supporting browsers hold first paint until the dark stylesheet arrives. It also pre-applies the dark background and text colors in `custom.scss`, keyed on the `data-theme` attribute that is set synchronously in the head, to soften the light-colored first paint for dark-preference visitors in browsers without `blocking="render"` support. ## How Has This Been Tested? Mirrored the live site locally, patched the inline script with the new code, and drove it in Chrome: - Unpatched baseline reproduced the old behavior: the link `href` was rewritten and `just-the-docs-light.css` was fetched in addition to `just-the-docs-default.css`. - Patched light and system-light loads keep the original link untouched with no extra stylesheet request. - Patched dark loads settle on a single `just-the-docs-dark.css` link, with `jtd.getTheme()` still reporting the right theme. - Rapid toggling keeps at most two color stylesheet links and settles on exactly one matching the final preference. - With `just-the-docs-dark.css` returning 404, the failed link is dropped and the page stays styled. ## Breaking Changes None. --- docs/_includes/head_custom.html | 79 +++++++++++++++++++++++++++++++-- docs/_sass/custom/custom.scss | 10 +++++ 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/docs/_includes/head_custom.html b/docs/_includes/head_custom.html index 701d0ae6..4a919ce1 100644 --- a/docs/_includes/head_custom.html +++ b/docs/_includes/head_custom.html @@ -40,6 +40,81 @@ return mediaQuery.matches ? "dark" : "light"; } + const colorSchemePattern = /just-the-docs-(default|light|dark)\.css(\?.*)?$/; + let pendingColorLink = null; + + function findColorStylesheet() { + const links = document.querySelectorAll('link[rel="stylesheet"]'); + for (let index = 0; index < links.length; index += 1) { + if (colorSchemePattern.test(links[index].getAttribute("href") || "")) { + return links[index]; + } + } + + return null; + } + + function stylesheetTheme(link) { + const match = colorSchemePattern.exec(link.getAttribute("href") || ""); + const scheme = match ? match[1] : "default"; + return scheme === "default" ? "light" : scheme; + } + + /* + * Swap the color stylesheet without going through jtd.setTheme, which + * rewrites the href of the render-blocking link and lets the page paint + * unstyled until the replacement CSS arrives. The site has no color_scheme, + * so the default stylesheet equals the light one and needs no swap at all; + * for dark, keep the old sheet applied until the new one has loaded. + */ + function setColorStylesheet(theme) { + const currentLink = findColorStylesheet(); + if (!currentLink) { + return; + } + + if (pendingColorLink) { + if (pendingColorLink.parentNode) { + pendingColorLink.parentNode.removeChild(pendingColorLink); + } + pendingColorLink = null; + } + + if (stylesheetTheme(currentLink) === theme) { + return; + } + + const newLink = document.createElement("link"); + newLink.setAttribute("rel", "stylesheet"); + newLink.setAttribute("blocking", "render"); + newLink.setAttribute( + "href", + currentLink.getAttribute("href").replace(colorSchemePattern, "just-the-docs-" + theme + ".css") + ); + + newLink.addEventListener("load", function() { + if (pendingColorLink !== newLink) { + return; + } + pendingColorLink = null; + if (currentLink.parentNode) { + currentLink.parentNode.removeChild(currentLink); + } + }); + + newLink.addEventListener("error", function() { + if (pendingColorLink === newLink) { + pendingColorLink = null; + } + if (newLink.parentNode) { + newLink.parentNode.removeChild(newLink); + } + }); + + currentLink.parentNode.insertBefore(newLink, currentLink.nextSibling); + pendingColorLink = newLink; + } + function nextPreference(preference) { const index = validPreferences.indexOf(preference); if (index === -1 || index === validPreferences.length - 1) { @@ -62,9 +137,7 @@ const safePreference = isValidPreference(preference) ? preference : "system"; const theme = resolvedTheme(safePreference); - if (window.jtd && typeof window.jtd.setTheme === "function") { - window.jtd.setTheme(theme); - } + setColorStylesheet(theme); document.documentElement.setAttribute("data-theme-preference", safePreference); document.documentElement.setAttribute("data-theme", theme); setThemeToggleLabel(safePreference); diff --git a/docs/_sass/custom/custom.scss b/docs/_sass/custom/custom.scss index 5149a2a3..498de1b3 100644 --- a/docs/_sass/custom/custom.scss +++ b/docs/_sass/custom/custom.scss @@ -62,6 +62,16 @@ color-scheme: dark; } +/* + * Applied before the dark stylesheet finishes loading, so browsers that + * do not support blocking="render" avoid a light-colored first paint. + * Values match $grey-dk-300 and $grey-lt-300 from the dark color scheme. + */ +:root[data-theme="dark"] body { + background-color: #27262b; + color: #e6e1e8; +} + :root[data-theme="light"] { color-scheme: light; }