diff --git a/src/plugins/preload-css.js b/src/plugins/preload-css.js index 2a8f909aa..68e7060ff 100644 --- a/src/plugins/preload-css.js +++ b/src/plugins/preload-css.js @@ -4,6 +4,51 @@ const fs = require("fs") const path = require("path") +/** + * Split minified CSS into top-level rule blocks, handling nested braces + * (e.g. @media queries containing inner rules). + */ +function splitCssBlocks(css) { + const blocks = [] + let i = 0 + while (i < css.length) { + const braceStart = css.indexOf("{", i) + if (braceStart === -1) { + if (i < css.length) blocks.push(css.slice(i)) + break + } + let depth = 0 + let j = braceStart + while (j < css.length) { + if (css[j] === "{") depth++ + else if (css[j] === "}") { + depth-- + if (depth === 0) { + blocks.push(css.slice(i, j + 1)) + i = j + 1 + break + } + } + j++ + } + if (j >= css.length) { + blocks.push(css.slice(i)) + break + } + } + return blocks +} + +function isDocSearchBlock(block) { + if (!block.includes(".DocSearch") && !block.includes("--docsearch-")) + return false + // Keep all :root blocks — CSS variable definitions are needed for initial layout + if (block.includes(":root")) return false + // Keep DocSearch button styles in main CSS — the button is always visible + if (block.includes(".DocSearch-Button")) return false + return true +} + module.exports = function embedCssPlugin() { return { name: "embed-css", @@ -16,7 +61,34 @@ module.exports = function embedCssPlugin() { ) if (!cssFile) return - const cssContent = fs.readFileSync(path.join(cssDir, cssFile), "utf8") + const cssPath = path.join(cssDir, cssFile) + const fullCss = fs.readFileSync(cssPath, "utf8") + + // --- Phase 1: Extract DocSearch CSS into a separate file --- + const blocks = splitCssBlocks(fullCss) + const mainBlocks = [] + const docSearchBlocks = [] + + for (const block of blocks) { + if (isDocSearchBlock(block)) { + docSearchBlocks.push(block) + } else { + mainBlocks.push(block) + } + } + + const mainCss = mainBlocks.join("") + const docSearchCss = docSearchBlocks.join("") + + // Extract the hash from the original filename (styles.HASH.css) + const hash = cssFile.replace("styles.", "").replace(".css", "") + const docSearchFile = `docsearch.${hash}.css` + + fs.writeFileSync(cssPath, mainCss) + fs.writeFileSync(path.join(cssDir, docSearchFile), docSearchCss) + + // --- Phase 2: Embed main CSS inline + lazy-load DocSearch CSS --- + const lazyLoadScript = `` function walk(dir) { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { @@ -34,9 +106,10 @@ module.exports = function embedCssPlugin() { if (!html.includes(cssFile)) return + // Replace the stylesheet link with inline styles + DocSearch lazy loader html = html.replace( /]*\bhref="[^"]*styles\.[^"]*\.css"[^>]*>/i, - ``, + `${lazyLoadScript}`, ) fs.writeFileSync(filePath, html) } diff --git a/vercel.json b/vercel.json index dc7d3aa89..cce01d1bd 100644 --- a/vercel.json +++ b/vercel.json @@ -1235,7 +1235,7 @@ ], "headers": [ { - "source": "/docs/(assets)/(.*)", + "source": "/docs/assets/(.*)", "headers": [ { "key": "Vercel-CDN-Cache-Control",