Compare commits

...

2 Commits

Author SHA1 Message Date
Nolann Biron d10b9643fa changeset 2026-08-19 18:01:25 +02:00
Nolann Biron c3d8f08a0c Keep block, toolbar and print CSS out of the render-blocking path (RND-12524)
Published pages loaded 8 render-blocking stylesheets (620 KB raw / 71.6 KB
compressed), four of them for features the page did not use: the OpenAPI block
CSS (185 KB), ContentKit (33 KB), the admin toolbar CSS modules, and the PDF
print styles.

Build the OpenAPI and ContentKit stylesheets as standalone assets and link them
from the blocks themselves via React's stylesheet hoisting, so only pages that
render those blocks pay for them. Move the admin toolbar behind a lazy chunk,
inline the print styles in the PDF layout, and fold the two remaining CSS
modules into globals.css so they stop splitting off their own chunks.

Every route is down to 3 stylesheets / 380 KB raw.
2026-08-19 18:01:16 +02:00
25 changed files with 281 additions and 138 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Load the OpenAPI, ContentKit, admin toolbar and PDF print stylesheets only on the pages that need them, cutting published pages from 8 render-blocking stylesheets to 3.
+40 -5
View File
@@ -198,6 +198,7 @@
"@argos-ci/playwright": "^7.2.0",
"@playwright/test": "^1.61.1",
"@scalar/api-client-react": "catalog:",
"@tailwindcss/cli": "4.1.11",
"@tailwindcss/postcss": "^4.1.11",
"@types/js-cookie": "^3.0.6",
"@types/jsontoxml": "^1.0.5",
@@ -1068,6 +1069,32 @@
"@panzoom/panzoom": ["@panzoom/panzoom@4.6.1", "", {}, "sha512-ogf/KhHHjj+DYAvHfaf3TXMQ8OE36pJtKpabLlx1OmpjcgtpCvkUoCiNONA8kuVRPGJdLiqMf0n8LRFXj1OyuA=="],
"@parcel/watcher": ["@parcel/watcher@2.6.0", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.4" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.6.0", "@parcel/watcher-darwin-arm64": "2.6.0", "@parcel/watcher-darwin-x64": "2.6.0", "@parcel/watcher-freebsd-x64": "2.6.0", "@parcel/watcher-linux-arm-glibc": "2.6.0", "@parcel/watcher-linux-arm-musl": "2.6.0", "@parcel/watcher-linux-arm64-glibc": "2.6.0", "@parcel/watcher-linux-arm64-musl": "2.6.0", "@parcel/watcher-linux-x64-glibc": "2.6.0", "@parcel/watcher-linux-x64-musl": "2.6.0", "@parcel/watcher-win32-arm64": "2.6.0", "@parcel/watcher-win32-x64": "2.6.0" } }, "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w=="],
"@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.6.0", "", { "os": "android", "cpu": "arm64" }, "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA=="],
"@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.6.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw=="],
"@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.6.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw=="],
"@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.6.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ=="],
"@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.6.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg=="],
"@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.6.0", "", { "os": "linux", "cpu": "arm" }, "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw=="],
"@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.6.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g=="],
"@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.6.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA=="],
"@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.6.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A=="],
"@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.6.0", "", { "os": "linux", "cpu": "x64" }, "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw=="],
"@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.6.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ=="],
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.6.0", "", { "os": "win32", "cpu": "x64" }, "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A=="],
"@phosphor-icons/core": ["@phosphor-icons/core@2.1.1", "", {}, "sha512-v4ARvrip4qBCImOE5rmPUylOEK4iiED9ZyKjcvzuezqMaiRASCHKcRIuvvxL/twvLpkfnEODCOJp5dM4eZilxQ=="],
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
@@ -1316,6 +1343,8 @@
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
"@tailwindcss/cli": ["@tailwindcss/cli@4.1.11", "", { "dependencies": { "@parcel/watcher": "^2.5.1", "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "enhanced-resolve": "^5.18.1", "mri": "^1.2.0", "picocolors": "^1.1.1", "tailwindcss": "4.1.11" }, "bin": { "tailwindcss": "dist/index.mjs" } }, "sha512-7RAFOrVaXCFz5ooEG36Kbh+sMJiI2j4+Ozp71smgjnLfBRu7DTfoq8DsTvzse2/6nDeo2M3vS/FGaxfDgr3rtQ=="],
"@tailwindcss/container-queries": ["@tailwindcss/container-queries@0.1.1", "", { "peerDependencies": { "tailwindcss": ">=3.2.0" } }, "sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA=="],
"@tailwindcss/node": ["@tailwindcss/node@4.1.11", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.11" } }, "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q=="],
@@ -1948,7 +1977,7 @@
"detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="],
"detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
@@ -2654,6 +2683,8 @@
"next-tick": ["next-tick@1.1.0", "", {}, "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="],
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
"node-fetch": ["node-fetch@2.6.7", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="],
@@ -3460,6 +3491,8 @@
"@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
"@mapbox/node-pre-gyp/detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
"@mapbox/node-pre-gyp/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"@mapbox/node-pre-gyp/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
@@ -3478,6 +3511,8 @@
"@oxc-transform/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
"@parcel/watcher/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
"@playwright/test/playwright": ["playwright@1.61.1", "", { "dependencies": { "playwright-core": "1.61.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ=="],
"@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
@@ -3582,6 +3617,8 @@
"@tailwindcss/node/magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
"@tailwindcss/oxide/detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
"@tailwindcss/oxide/tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" }, "bundled": true }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="],
@@ -3800,6 +3837,8 @@
"jsonwebtoken/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
"lightningcss/detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
"lowlight/highlight.js": ["highlight.js@11.9.0", "", {}, "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw=="],
"mcp-handler/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
@@ -3890,8 +3929,6 @@
"send/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
"sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"side-channel-list/object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"side-channel-map/get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
@@ -3988,8 +4025,6 @@
"@argos-ci/core/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.3", "", { "os": "win32", "cpu": "x64" }, "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA=="],
"@argos-ci/core/sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"@argos-ci/core/sharp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"@aws-crypto/crc32/@aws-sdk/types/@smithy/types": ["@smithy/types@4.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-enhjdwp4D7CXmwLtD6zbcDMbo6/T6WtuuKCY49Xxc6OMOmUWlBEBDREsxxgV2LIdeQPW756+f97GzcgAwp3iLw=="],
+6 -5
View File
@@ -89,37 +89,38 @@
"@argos-ci/playwright": "^7.2.0",
"@playwright/test": "^1.61.1",
"@scalar/api-client-react": "catalog:",
"@tailwindcss/cli": "4.1.11",
"@tailwindcss/postcss": "^4.1.11",
"@types/js-cookie": "^3.0.6",
"@types/jsontoxml": "^1.0.5",
"@types/jsonwebtoken": "^9.0.6",
"@types/mdast": "^4.0.4",
"@types/negotiator": "^0.6.4",
"@types/node": "^20",
"@types/object-hash": "^3.0.6",
"@types/parse-cache-control": "^1.0.4",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"@types/rison": "^0.0.9",
"@types/negotiator": "^0.6.4",
"bun-types": "catalog:",
"deepmerge": "^4.3.1",
"diff": "^8.0.2",
"env-cmd": "^10.1.0",
"jsonwebtoken": "^9.0.2",
"postcss": "^8",
"rss-parser": "^3.13.0",
"stylelint": "^16.16.0",
"stylelint-no-unsupported-browser-features": "^8.1.1",
"tailwindcss": "^4.1.11",
"ts-essentials": "^10.0.1",
"typescript": "catalog:",
"vercel": "50.37.3",
"wrangler": "^4.79.0",
"rss-parser": "^3.13.0"
"wrangler": "^4.79.0"
},
"scripts": {
"generate": "./scripts/generate.sh",
"generate:assets": "bun ./scripts/generate-mermaid-runtime.ts && bun ./scripts/generate-scalar-runtime.ts",
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math && rm -rf ./public/~gitbook/static/mermaid && rm -rf ./public/~gitbook/static/scalar",
"generate:assets": "bun ./scripts/generate-mermaid-runtime.ts && bun ./scripts/generate-scalar-runtime.ts && bun ./scripts/generate-block-css.ts",
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math && rm -rf ./public/~gitbook/static/mermaid && rm -rf ./public/~gitbook/static/scalar && rm -rf ./public/~gitbook/static/css",
"dev": "bun run generate:assets && env-cmd --silent -f ../../.env.local next --webpack",
"build": "bun run generate:assets && next build --webpack",
"build:local": "bun run generate:assets && GITBOOK_URL=http://localhost:3000 next build --webpack",
@@ -0,0 +1,36 @@
import { mkdir, rm } from 'node:fs/promises';
import { createRequire } from 'node:module';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { BLOCK_STYLESHEETS } from '../src/components/DocumentView/blockStylesheets';
const scriptDir = dirname(fileURLToPath(import.meta.url));
const sourceDir = join(scriptDir, '../src');
const outputDir = join(scriptDir, '../public/~gitbook/static/css');
const tailwindCli = join(
dirname(createRequire(import.meta.url).resolve('@tailwindcss/cli/package.json')),
'dist/index.mjs'
);
await rm(outputDir, { force: true, recursive: true });
await mkdir(outputDir, { recursive: true });
for (const [name, source] of Object.entries(BLOCK_STYLESHEETS)) {
const result = Bun.spawnSync(
[
'node',
tailwindCli,
'--input',
join(sourceDir, source),
'--output',
join(outputDir, `${name}.css`),
'--minify',
],
{ stdout: 'pipe', stderr: 'pipe' }
);
if (!result.success) {
throw new Error(`Unable to build ${source}: ${result.stderr.toString()}`);
}
}
@@ -1,4 +1,4 @@
import { AdminToolbarClient } from './AdminToolbarClient';
import { AdminToolbarLazy } from './AdminToolbarLazy';
import { getToolbarChangedPages } from './changedPages';
import type { AdminToolbarContext } from './types';
import type { GitBookSiteContext } from '@/lib/context';
@@ -62,5 +62,5 @@ export async function AdminToolbar(props: AdminToolbarProps) {
changedPages,
};
return <AdminToolbarClient context={minimalContext} />;
return <AdminToolbarLazy context={minimalContext} />;
}
@@ -0,0 +1,17 @@
'use client';
import dynamic from 'next/dynamic';
import type { AdminToolbarClientProps } from './types';
// The toolbar only ever appears after hydration (`useToolbarVisibility` reads cookies and local
// storage), so nothing is lost by deferring it — and it takes its CSS modules out of the route's
// render-blocking stylesheets, where every published page was paying for them.
const AdminToolbarClient = dynamic(
() => import('./AdminToolbarClient').then((mod) => mod.AdminToolbarClient),
{ ssr: false }
);
export function AdminToolbarLazy(props: AdminToolbarClientProps) {
return <AdminToolbarClient {...props} />;
}
@@ -1,6 +1,3 @@
// Only the server entrypoint: re-exporting the client modules would pull their CSS modules back
// into the route's render-blocking stylesheets, defeating the lazy boundary.
export * from './AdminToolbar';
export * from './AdminToolbarClient';
export * from './IframeWrapper';
export * from './Toolbar';
export * from './transitions';
export * from './utils';
@@ -0,0 +1,14 @@
import type { BlockStylesheetName } from './blockStylesheets';
import { getAssetURL } from '@/lib/assets';
import { buildVersion } from '@/lib/build';
export function getBlockStylesheetURL(name: BlockStylesheetName): string {
return getAssetURL(`css/${name}.css?v=${buildVersion()}`);
}
// `href` + `precedence` opt into React's stylesheet hoisting: the tag moves to `<head>`, dedupes
// across every block on the page, and holds paint until it loads, so the block never flashes
// unstyled. `blocks` sorts after Next's own `next` precedence, keeping the previous cascade order.
export function BlockStylesheet(props: { name: BlockStylesheetName }) {
return <link rel="stylesheet" href={getBlockStylesheetURL(props.name)} precedence="blocks" />;
}
@@ -2,9 +2,9 @@ import type { DocumentBlockIntegration, RenderIntegrationUI } from '@gitbook/api
import { ContentKit, ContentKitOutput } from '@gitbook/react-contentkit';
import type { BlockProps } from '../Block';
import { BlockStylesheet } from '../BlockStylesheet';
import { getWebframePageContext, integrationBlockContainsWebframe } from './adaptive';
import { contentKitServerContext } from './contentkit';
import './contentkit.css';
import {
ContentKitWithClientContext,
type WebframeLinkerData,
@@ -105,6 +105,7 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio
return (
<div className={tcls(style)}>
<BlockStylesheet name="contentkit" />
{useClientContext ? (
<ContentKitWithClientContext
{...contentKitProps}
@@ -1,4 +1,5 @@
import type { BlockProps } from '../Block';
import { BlockStylesheet } from '../BlockStylesheet';
import { getOpenAPIBlockClientProps } from './context';
import { OpenAPIBlockLazy } from './OpenAPIBlockLazy';
import { resolveOpenAPIOperationBlock } from '@/lib/openapi/resolveOpenAPIOperationBlock';
@@ -11,9 +12,12 @@ import { tcls } from '@/lib/tailwind';
export async function OpenAPIOperation(props: BlockProps<AnyOpenAPIOperationsBlock>) {
const { style } = props;
return (
<div className={tcls('flex w-full min-w-0', style, 'max-w-full')}>
<OpenAPIOperationBody {...props} />
</div>
<>
<BlockStylesheet name="openapi" />
<div className={tcls('flex w-full min-w-0', style, 'max-w-full')}>
<OpenAPIOperationBody {...props} />
</div>
</>
);
}
@@ -1,4 +1,5 @@
import type { BlockProps } from '../Block';
import { BlockStylesheet } from '../BlockStylesheet';
import { getOpenAPIBlockClientProps } from './context';
import { OpenAPIBlockLazy } from './OpenAPIBlockLazy';
import { resolveOpenAPISchemasBlock } from '@/lib/openapi/resolveOpenAPISchemasBlock';
@@ -11,9 +12,12 @@ import { tcls } from '@/lib/tailwind';
export async function OpenAPISchemas(props: BlockProps<OpenAPISchemasBlock>) {
const { style } = props;
return (
<div className={tcls('flex w-full', style, 'max-w-full')}>
<OpenAPISchemasBody {...props} />
</div>
<>
<BlockStylesheet name="openapi" />
<div className={tcls('flex w-full', style, 'max-w-full')}>
<OpenAPISchemasBody {...props} />
</div>
</>
);
}
@@ -1,4 +1,5 @@
import type { BlockProps } from '../Block';
import { BlockStylesheet } from '../BlockStylesheet';
import { getOpenAPIBlockClientProps } from './context';
import { OpenAPIBlockLazy } from './OpenAPIBlockLazy';
import { resolveOpenAPIWebhookBlock } from '@/lib/openapi/resolveOpenAPIWebhookBlock';
@@ -11,9 +12,12 @@ import { tcls } from '@/lib/tailwind';
export async function OpenAPIWebhook(props: BlockProps<OpenAPIWebhookBlock>) {
const { style } = props;
return (
<div className={tcls('flex w-full min-w-0', style, 'max-w-full')}>
<OpenAPIWebhookBody {...props} />
</div>
<>
<BlockStylesheet name="openapi" />
<div className={tcls('flex w-full min-w-0', style, 'max-w-full')}>
<OpenAPIWebhookBody {...props} />
</div>
</>
);
}
@@ -17,7 +17,6 @@ import { PlainCodeBlock } from '../CodeBlock';
import { DocumentView } from '../DocumentView';
import { Heading } from '../Heading';
import type { OpenAPIBlockClientContextProps } from './OpenAPIBlockClient';
import './style.css';
import { SCALAR_RUNTIME_PATH } from './scalar-runtime-path';
import { DEFAULT_LOCALE, getSpaceLocale } from '@/intl/server';
import { getAssetURL } from '@/lib/assets';
@@ -0,0 +1,9 @@
// Built as standalone assets by `scripts/generate-block-css.ts` rather than imported: an import
// would put them in the route's render-blocking CSS on every page, block or no block.
// Values are the sources to compile, relative to `src/`.
export const BLOCK_STYLESHEETS = {
openapi: 'components/DocumentView/OpenAPI/style.css',
contentkit: 'components/DocumentView/Integration/contentkit.css',
} as const;
export type BlockStylesheetName = keyof typeof BLOCK_STYLESHEETS;
@@ -15,7 +15,6 @@ import {
} from '../primitives/DropdownMenu';
import { SiteAuthLoginDropdownMenuItem } from '../SiteAuth/SiteAuthLoginLink';
import { HeaderLinkMoreDropdown } from './HeaderLinkMoreClient';
import styles from './headerLinks.module.css';
import { isSiteAuthLoginHref } from '@/lib/auth-login-link';
import type { GitBookSiteContext } from '@/lib/context';
import { resolveContentRef } from '@/lib/references';
@@ -34,7 +33,7 @@ export function HeaderLinkMore(props: {
const { label, links, context, socialAccounts } = props;
return (
<div className={`${styles.linkEllipsis} z-20 items-center`}>
<div className="header-links-ellipsis z-20 items-center">
<HeaderLinkMoreDropdown
label={label}
dropdownClassName={tcls(
@@ -1,6 +1,5 @@
import type React from 'react';
import styles from './headerLinks.module.css';
import { type ClassValue, tcls } from '@/lib/tailwind';
interface HeaderLinksProps {
@@ -12,7 +11,7 @@ export async function HeaderLinks({ children, style }: HeaderLinksProps) {
return (
<div
className={tcls(
styles.containerHeaderlinks,
'header-links',
'@4xl:[&>.button+.button]:-ml-2 z-20 ml-auto flex min-w-9 shrink grow @7xl:grow-0 items-center justify-end @4xl:gap-x-6 gap-x-4',
style
)}
@@ -1,80 +0,0 @@
@container header (width < 1280px) {
.containerHeaderlinks {
container-type: inline-size;
container-name: headerlinks;
}
}
.linkEllipsis {
display: none;
& div > a {
display: none;
}
}
@container headerlinks (width < 150px) {
.containerHeaderlinks > :nth-child(n + 1) {
display: none;
}
.containerHeaderlinks > :nth-child(n + 1) ~ .linkEllipsis {
display: flex;
& div > *:nth-child(n + 1) {
display: flex;
}
}
}
@container headerlinks (width < 300px) {
.containerHeaderlinks > :nth-child(n + 2) {
display: none;
}
.containerHeaderlinks > :nth-child(n + 2) ~ .linkEllipsis {
display: flex;
& div > *:nth-child(n + 2) {
display: flex;
}
}
}
@container headerlinks (width < 450px) {
.containerHeaderlinks > :nth-child(n + 3) {
display: none;
}
.containerHeaderlinks > :nth-child(n + 3) ~ .linkEllipsis {
display: flex;
& div > *:nth-child(n + 3) {
display: flex;
}
}
}
@container headerlinks (width < 550px) {
.containerHeaderlinks > :nth-child(n + 4) {
display: none;
}
.containerHeaderlinks > :nth-child(n + 4) ~ .linkEllipsis {
display: flex;
& div > *:nth-child(n + 4) {
display: flex;
}
}
}
@container headerlinks (width < 650px) {
.containerHeaderlinks > :nth-child(n + 5) {
display: none;
}
.containerHeaderlinks > :nth-child(n + 5) ~ .linkEllipsis {
display: flex;
& div > *:nth-child(n + 5) {
display: flex;
}
}
}
@container headerlinks (width < 750px) {
.containerHeaderlinks > :nth-child(n + 6) {
display: none;
}
.containerHeaderlinks > :nth-child(n + 6) ~ .linkEllipsis {
display: flex;
& div > *:nth-child(n + 6) {
display: flex;
}
}
}
@@ -26,7 +26,6 @@ import { sanitizeGitBookAppURL } from '@/lib/app';
import type { GitBookSiteContext, GitBookSpaceContext } from '@/lib/context';
import { getPageDocument } from '@/lib/data';
import { type GitBookLinker, createLinker, linkerWithAbsoluteURLs } from '@/lib/links';
import './pdf.css';
import { resolvePageId } from '@/lib/pages';
import { getLinkerForSiteSpace } from '@/lib/sites';
import { tcls } from '@/lib/tailwind';
@@ -1,5 +1,6 @@
import { CustomizationDefaultThemeMode } from '@gitbook/api';
import { PRINT_STYLES } from './printStyles';
import { CustomizationRootLayout } from '@/components/RootLayout';
import type { GitBookSiteContext, GitBookSpaceContext } from '@/lib/context';
@@ -17,6 +18,9 @@ export async function PDFRootLayout(props: {
context={context}
forcedTheme={CustomizationDefaultThemeMode.Light}
>
<style href="pdf-print" precedence="blocks">
{PRINT_STYLES}
</style>
{children}
</CustomizationRootLayout>
);
@@ -1,3 +1,6 @@
// Inlined rather than imported as CSS: a `.css` import lands in the route's render-blocking
// stylesheets, and webpack was attaching this one to the content pages too.
export const PRINT_STYLES = `
@page {
size: A4;
margin: 60pt 60pt;
@@ -56,3 +59,4 @@
display: block !important;
}
}
`;
@@ -1,6 +1,9 @@
import * as ReactDOM from 'react-dom';
import type { JSONDocument, RevisionPageDocument, SiteInsightsDisplayContext } from '@gitbook/api';
import { DocumentView, DocumentViewSkeleton } from '../DocumentView';
import { getBlockStylesheetURL } from '../DocumentView/BlockStylesheet';
import { CurrentPageProvider } from '../hooks/useCurrentPage';
import { TrackPageViewEvent } from '../Insights';
import { CONTENT_STYLE } from '../layout';
@@ -69,6 +72,14 @@ export async function PageBody(props: {
// to API-reference pages only (matching the `page-api-block` styling), so other pages keep the
// header structure untouched.
const hasAPIBlocks = document ? hasAPIBlock(document) : false;
// The block renders its own stylesheet link, but that only reaches the browser once the block
// streams in — start the request from the shell instead. Top-level blocks only, which is the
// common case; nested ones still get the sheet from the link.
if (hasAPIBlocks) {
ReactDOM.preload(getBlockStylesheetURL('openapi'), { as: 'style' });
}
const wideLayout = wideContent || page.layout.width === 'wide';
const language = await getSpaceLanguage(context);
const updatedAt = page.updatedAt ?? page.createdAt;
@@ -552,3 +552,104 @@ html.dark .highlight-line.diff-deleted .highlight-line-content::before {
.button-group > :is(button, a):not(:first-child):has(~ :is(button, a)) {
@apply rounded-none!;
}
/* Zoomable images */
html:has(.zoom-modal) {
overflow: hidden;
}
.zoom-img {
cursor: zoom-in;
}
.zoom-image-active {
view-transition-name: zoom-image;
}
.zoom-modal img {
view-transition-name: zoom-image;
cursor: zoom-out;
}
/* Header links collapse into the "more" dropdown one by one as the header narrows. Each breakpoint
hides one more link and reveals the ellipsis holding the ones that no longer fit. */
@container header (width < 1280px) {
.header-links {
container-type: inline-size;
container-name: headerlinks;
}
}
.header-links-ellipsis {
display: none;
& div > a {
display: none;
}
}
@container headerlinks (width < 150px) {
.header-links > :nth-child(n + 1) {
display: none;
}
.header-links > :nth-child(n + 1) ~ .header-links-ellipsis {
display: flex;
& div > *:nth-child(n + 1) {
display: flex;
}
}
}
@container headerlinks (width < 300px) {
.header-links > :nth-child(n + 2) {
display: none;
}
.header-links > :nth-child(n + 2) ~ .header-links-ellipsis {
display: flex;
& div > *:nth-child(n + 2) {
display: flex;
}
}
}
@container headerlinks (width < 450px) {
.header-links > :nth-child(n + 3) {
display: none;
}
.header-links > :nth-child(n + 3) ~ .header-links-ellipsis {
display: flex;
& div > *:nth-child(n + 3) {
display: flex;
}
}
}
@container headerlinks (width < 550px) {
.header-links > :nth-child(n + 4) {
display: none;
}
.header-links > :nth-child(n + 4) ~ .header-links-ellipsis {
display: flex;
& div > *:nth-child(n + 4) {
display: flex;
}
}
}
@container headerlinks (width < 650px) {
.header-links > :nth-child(n + 5) {
display: none;
}
.header-links > :nth-child(n + 5) ~ .header-links-ellipsis {
display: flex;
& div > *:nth-child(n + 5) {
display: flex;
}
}
}
@container headerlinks (width < 750px) {
.header-links > :nth-child(n + 6) {
display: none;
}
.header-links > :nth-child(n + 6) ~ .header-links-ellipsis {
display: flex;
& div > *:nth-child(n + 6) {
display: flex;
}
}
}
@@ -1,20 +0,0 @@
html:has(.zoomModal) {
overflow: hidden;
}
.zoomImg {
cursor: zoom-in;
}
.zoomImageActive {
view-transition-name: zoom-image;
}
.zoomModal {
/** Unstyled */
}
.zoomModal img {
view-transition-name: zoom-image;
cursor: zoom-out;
}
@@ -6,7 +6,6 @@ import ReactDOM from 'react-dom';
import { Icon } from '@gitbook/icons';
import styles from './ZoomImage.module.css';
import { tcls } from '@/lib/tailwind';
const PADDING = 32; // Padding around the image in the modal, in pixels
@@ -159,8 +158,8 @@ export function ZoomImage(
}}
className={classNames(
props.className,
zoomable ? styles.zoomImg : null,
active ? styles.zoomImageActive : null
zoomable ? 'zoom-img' : null,
active ? 'zoom-image-active' : null
)}
/>
)}
@@ -200,7 +199,7 @@ function ZoomImageModal(props: {
<div
data-testid="zoom-image-modal"
className={classNames(
styles.zoomModal,
'zoom-modal',
tcls(
'fixed',
'inset-0',
+2 -1
View File
@@ -5,7 +5,8 @@
"outputs": [
"public/~gitbook/static/icons/**/*",
"public/~gitbook/static/math/**/*",
"public/~gitbook/static/embed/**/*"
"public/~gitbook/static/embed/**/*",
"public/~gitbook/static/css/**/*"
],
"dependsOn": ["^generate", "@gitbook/embed#build"]
},