Compare commits

..

12 Commits

63 changed files with 1595 additions and 2885 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Show the theme toggle in the footer whenever the outline column that hosts the other toggle isn't pinned open, so it stays reachable on laptop-sized screens in wide layouts and while the AI chat is open.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Upgrade react-hotkeys-hook to v5 and use its native `useKey` option so keyboard shortcuts match the produced key on non-QWERTY layouts.
-5
View File
@@ -1,5 +0,0 @@
---
"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.
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix the search field losing focus if it was focused just before the page finished hydrating.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Self-host the default fonts and inline only the families a site uses, instead of shipping the `@font-face` rules for all 23 supported fonts on every page.
@@ -21,59 +21,59 @@ outputs:
description: 'Deployment URL'
value: ${{ steps.deploy_middleware.outputs.deployment-url }}
runs:
using: 'composite'
steps:
- id: wrangler_status
name: Check wrangler deployment status
uses: cloudflare/wrangler-action@v3.14.0
with:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.43.0'
environment: ${{ inputs.environment }}
command: deployments status --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
using: 'composite'
steps:
- id: wrangler_status
name: Check wrangler deployment status
uses: cloudflare/wrangler-action@v3.14.0
with:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.122.0'
environment: ${{ inputs.environment }}
command: deployments status --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
# This step is used to get the version ID that is currently deployed to Cloudflare.
- id: extract_current_version
- id: extract_current_version
name: Extract current version
shell: bash
run: |
version_id=$(echo "${{ steps.wrangler_status.outputs.command-output }}" | grep -A 3 "(100%)" | grep -oP '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
echo "version_id=$version_id" >> $GITHUB_OUTPUT
- id: deploy_server
name: Deploy server to Cloudflare at 0%
uses: cloudflare/wrangler-action@v3.14.0
with:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.43.0'
environment: ${{ inputs.environment }}
command: versions deploy ${{ steps.extract_current_version.outputs.version_id }}@100% ${{ inputs.serverVersionId }}@0% -y --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
- id: deploy_server
name: Deploy server to Cloudflare at 0%
uses: cloudflare/wrangler-action@v3.14.0
with:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.122.0'
environment: ${{ inputs.environment }}
command: versions deploy ${{ steps.extract_current_version.outputs.version_id }}@100% ${{ inputs.serverVersionId }}@0% -y --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
# Since we use version overrides headers, we can directly deploy the middleware to 100%.
- id: deploy_middleware
name: Deploy middleware to Cloudflare at 100%
uses: cloudflare/wrangler-action@v3.14.0
with:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.43.0'
environment: ${{ inputs.environment }}
command: versions deploy ${{ inputs.middlewareVersionId }}@100% -y --config ./packages/gitbook/openNext/customWorkers/middlewareWrangler.jsonc
# Since we use version overrides headers, we can directly deploy the middleware to 100%.
- id: deploy_middleware
name: Deploy middleware to Cloudflare at 100%
uses: cloudflare/wrangler-action@v3.14.0
with:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.122.0'
environment: ${{ inputs.environment }}
command: versions deploy ${{ inputs.middlewareVersionId }}@100% -y --config ./packages/gitbook/openNext/customWorkers/middlewareWrangler.jsonc
- name: Deploy server to Cloudflare at 100%
uses: cloudflare/wrangler-action@v3.14.0
with:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.43.0'
environment: ${{ inputs.environment }}
command: versions deploy ${{ inputs.serverVersionId }}@100% -y --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
- name: Deploy server to Cloudflare at 100%
uses: cloudflare/wrangler-action@v3.14.0
with:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.122.0'
environment: ${{ inputs.environment }}
command: versions deploy ${{ inputs.serverVersionId }}@100% -y --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
- name: Outputs
shell: bash
@@ -67,19 +67,32 @@ runs:
run: bun run turbo build:cloudflare
env:
GITBOOK_RUNTIME: cloudflare
VERCEL_TARGET_ENV: ${{ inputs.environment }}
GITBOOK_BLOCK_SEARCH_INDEXATION: ${{ inputs.environment == 'preview' && 'true' || '' }}
GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE: ${{ inputs.environment == 'preview' && 'true' || '' }}
shell: bash
- id: extract_deployment_id
name: Extract Next deployment ID
shell: bash
run: |
deployment_id=$(bun -e '
const buildOutput = await Bun.file("packages/gitbook/.open-next/server-functions/default/packages/gitbook/.next/required-server-files.json").json();
const deploymentId = buildOutput.config?.deploymentId;
if (!deploymentId) throw new Error("Next build did not emit a deployment ID");
console.log(deploymentId);
')
echo "deployment_id=$deployment_id" >> $GITHUB_OUTPUT
- name: Upload the DO worker
uses: cloudflare/wrangler-action@v3.14.0
with:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.43.0'
wranglerVersion: '4.122.0'
environment: ${{ inputs.environment }}
command: deploy --config ./packages/gitbook/openNext/customWorkers/doWrangler.jsonc
command: ${{ format('deploy --var OPEN_NEXT_BUILD_ID:{0} --config ./packages/gitbook/openNext/customWorkers/doWrangler.jsonc', steps.extract_deployment_id.outputs.deployment_id) }}
- id: upload_server
name: Upload server to Cloudflare
@@ -88,7 +101,7 @@ runs:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.43.0'
wranglerVersion: '4.122.0'
environment: ${{ inputs.environment }}
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
@@ -111,7 +124,7 @@ runs:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.43.0'
wranglerVersion: '4.122.0'
environment: ${{ inputs.environment }}
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook/openNext/customWorkers/middlewareWrangler.jsonc
+88 -57
View File
@@ -113,7 +113,7 @@
"version": "0.27.2",
"dependencies": {
"@base-ui/react": "catalog:",
"@cloudflare/workers-types": "^4.20251011.0",
"@cloudflare/workers-types": "^5.20260716.1",
"@gitbook/api": "catalog:",
"@gitbook/browser-types": "workspace:*",
"@gitbook/cache-tags": "workspace:*",
@@ -171,7 +171,7 @@
"quick-lru": "^7.0.1",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-hotkeys-hook": "^5.3.3",
"react-hotkeys-hook": "^4.4.1",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"rehype-stringify": "^10.0.1",
@@ -198,7 +198,6 @@
"@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",
@@ -224,7 +223,7 @@
"ts-essentials": "^10.0.1",
"typescript": "catalog:",
"vercel": "50.37.3",
"wrangler": "^4.79.0",
"wrangler": "^4.122.0",
},
},
"packages/icons": {
@@ -581,21 +580,21 @@
"@chevrotain/utils": ["@chevrotain/utils@11.1.1", "", {}, "sha512-71eTYMzYXYSFPrbg/ZwftSaSDld7UYlS8OQa3lNnn9jzNtpFbaReRRyghzqS7rI3CDaorqpPJJcXGHK+FE1TVQ=="],
"@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.2", "", {}, "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ=="],
"@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="],
"@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.0", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-8ovsRpwzPoEqPUzoErAYVv8l3FMZNeBVQfJTvtzP4AgLSRGZISRfuChFxHWUQd3n6cnrwkuTGxT+2cGo8EsyYg=="],
"@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="],
"@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260401.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZSmceM70jH6k+/62VkEcmMNzrpr4kSctkX5Lsgqv38KktfhPY/hsh75y1lRoPWS3H3kgMa4p2pUSlidZR1u2hw=="],
"@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260811.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-i5jqz+ywtOefr0AJbiAc8qxBLfSim/B0WJG7aW3B+pWnoVfMJdUQvi+BWcFKZJ0MoCci3KadTx6g31VfuEEqpQ=="],
"@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260401.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7UKWF+IUZ3NXMVPsDg8Cjg0r58b+uYlfvs5Yt8bvtU+geCtW4P2MxRHmRSEo8SryckXOJjb/b8tcncgCykFu8g=="],
"@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260811.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NoOUM/nvaDdm2Onlnz33FikWjtatzulNtvwvy4xs0IrHaTCHwC0c8NwIt6s+AI13FkDs02/vm2I3GTPLCT9+hQ=="],
"@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260401.1", "", { "os": "linux", "cpu": "x64" }, "sha512-MDWUH/0bvL/l9aauN8zEddyYOXId1OueqrUCXXENNJ95R/lSmF6OgGVuXaYhoIhxQkNiEJ/0NOlnVYj9mJq4dw=="],
"@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260811.1", "", { "os": "linux", "cpu": "x64" }, "sha512-sdYq2jL1AD1supa3fsi5O4zTB28wSjvTHj7Migh6/ts8EROPdvrSwv+rdGHhv8HJNAz/wbIAY3wZsi1Rw4uUIg=="],
"@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260401.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-UgkzpMzVWM/bwbo3vjCTg2aoKfGcUhiEoQoDdo6RGWvbHRJyLVZ4VQCG9ZcISiztkiS2ICCoYOtPy6M/lV6Gcw=="],
"@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260811.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-RIRv4shbu1kg05sD+DHTpSFCNnb5Dl2SkPDMUykqZa508tkPqe7VVw7gO0Q5msTBGyL0FfFrLuRxwwfA8u5Sow=="],
"@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260401.1", "", { "os": "win32", "cpu": "x64" }, "sha512-HBLzcQF5iF4Qv20tQ++pG7xs3OsCnaIbc+GAi6fmhUKZhvmzvml/jwrQzLJ+MPm0cQo41K5OO/U3T4S8tvJetQ=="],
"@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260811.1", "", { "os": "win32", "cpu": "x64" }, "sha512-g6VquwjASlYAibcNW/0E6Zszht4qLkmnXOGwIjjRHl2A0Qz48kVeMcGvyH6eA0G9U3OzZojjYFpP+YeyQmmdjw=="],
"@cloudflare/workers-types": ["@cloudflare/workers-types@4.20251011.0", "", {}, "sha512-gQpih+pbq3sP4uXltUeCSbPgZxTNp2gQd8639SaIbQMwgA6oJNHLhIART1fWy6DQACngiRzDVULA2x0ohmkGTQ=="],
"@cloudflare/workers-types": ["@cloudflare/workers-types@5.20260716.1", "", {}, "sha512-LqQPmGAvdpQxzZGAMlDI6fnCsTlr8nRQibWsREaERqD0PucwFl25aXpUskSob70uSY2n6K1sp6Te9xkmzSFgaw=="],
"@codemirror/autocomplete": ["@codemirror/autocomplete@6.18.4", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-sFAphGQIqyQZfP2ZBsSHV7xQvo9Py0rV0dW7W3IMRdS+zDuNb2l3no78CvUaWKGfzFjI4FTrLdUSj86IGb2hRA=="],
@@ -1069,32 +1068,6 @@
"@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=="],
@@ -1343,8 +1316,6 @@
"@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=="],
@@ -1977,7 +1948,7 @@
"detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
@@ -2641,7 +2612,7 @@
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
"miniflare": ["miniflare@4.20260401.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.24.4", "workerd": "1.20260401.1", "ws": "8.18.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-lngHPzZFN9sxYG/mhzvnWiBMNVAN5MsO/7g32ttJ07rymtiK/ZBalODTKb8Od+BQdlU5DOR4CjVt9NydjnUyYg=="],
"miniflare": ["miniflare@5.20260811.0-alpha", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.35.2", "undici": "7.29.0", "workerd": "1.20260811.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" } }, "sha512-sypXsD5fjY88fZNedPqnwrwR1dwfnfbfW7MfvMyIfPJdtRiCCOpUnjWGeFVYYZ+0fQVICye6Juu+vZgzTEx8XA=="],
"minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
@@ -2683,8 +2654,6 @@
"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=="],
@@ -2893,7 +2862,7 @@
"react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
"react-hotkeys-hook": ["react-hotkeys-hook@5.3.3", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-aswgyWUnE25hmhzHTfKDmKzsaSE5DJ4LKaU/o6rQSXkDd/1Bh9TfAFQbHkf6fLy11HvlYkp+cDDarGdhmCDhoQ=="],
"react-hotkeys-hook": ["react-hotkeys-hook@4.5.1", "", { "peerDependencies": { "react": ">=16.8.1", "react-dom": ">=16.8.1" } }, "sha512-scAEJOh3Irm0g95NIn6+tQVf/OICCjsQsC9NBHfQws/Vxw4sfq1tDQut5fhTEvPraXhu/sHxRd9lOtxzyYuNAg=="],
"read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="],
@@ -3291,9 +3260,9 @@
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"workerd": ["workerd@1.20260401.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260401.1", "@cloudflare/workerd-darwin-arm64": "1.20260401.1", "@cloudflare/workerd-linux-64": "1.20260401.1", "@cloudflare/workerd-linux-arm64": "1.20260401.1", "@cloudflare/workerd-windows-64": "1.20260401.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-mUYCd+ohaWJWF5nhDzxugWaAD/DM8Dw0ze3B7bu8JaA7S70+XQJXcvcvwE8C4qGcxSdCyqjsrFzqxKubECDwzg=="],
"workerd": ["workerd@1.20260811.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260811.1", "@cloudflare/workerd-darwin-arm64": "1.20260811.1", "@cloudflare/workerd-linux-64": "1.20260811.1", "@cloudflare/workerd-linux-arm64": "1.20260811.1", "@cloudflare/workerd-windows-64": "1.20260811.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-kh+FFm55JQ4ssxhHZV9VPdMQq3D1nHxNJgwxMtWGD4dGppJvLySdguTRDKgeNTvgq6heSz+6TTXyPSDGj8Yllw=="],
"wrangler": ["wrangler@4.80.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.2", "@cloudflare/unenv-preset": "2.16.0", "blake3-wasm": "2.1.5", "esbuild": "0.27.3", "miniflare": "4.20260401.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260401.1" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260401.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-2ZKF7uPeOZy65BGk3YfvqBCPo/xH1MrAlMmH9mVP+tCNBrTUMnwOHSj1HrZHgR8LttkAqhko0fGz+I4ax1rzyQ=="],
"wrangler": ["wrangler@4.122.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "5.20260811.0-alpha", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260811.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260811.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js", "cf-wrangler": "bin/cf-wrangler.js" } }, "sha512-qkskzgQ76Y1qvVe5JARgvc3RISq6BC2rPoxQhFoKH1dKIwQc3GDFttQ/7m2OfeQ+tmQRzynv2dy/DXnxFCj2Lw=="],
"wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="],
@@ -3303,7 +3272,7 @@
"write-file-atomic": ["write-file-atomic@5.0.1", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw=="],
"ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
"ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
"xdg-app-paths": ["xdg-app-paths@5.1.0", "", { "dependencies": { "xdg-portable": "^7.0.0" } }, "sha512-RAQ3WkPf4KTU1A8RtFx3gWywzVKe00tfOPFfl2NDGqbIFENQO4kqAJp7mhQjNj/33W5x5hiWWUdyfPq/5SU3QA=="],
@@ -3491,8 +3460,6 @@
"@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=="],
@@ -3511,8 +3478,6 @@
"@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=="],
@@ -3617,8 +3582,6 @@
"@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=="],
@@ -3837,8 +3800,6 @@
"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=="],
@@ -3869,7 +3830,9 @@
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"miniflare/undici": ["undici@7.24.4", "", {}, "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w=="],
"miniflare/sharp": ["sharp@0.35.2", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.4" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.2", "@img/sharp-darwin-x64": "0.35.2", "@img/sharp-freebsd-wasm32": "0.35.2", "@img/sharp-libvips-darwin-arm64": "1.3.1", "@img/sharp-libvips-darwin-x64": "1.3.1", "@img/sharp-libvips-linux-arm": "1.3.1", "@img/sharp-libvips-linux-arm64": "1.3.1", "@img/sharp-libvips-linux-ppc64": "1.3.1", "@img/sharp-libvips-linux-riscv64": "1.3.1", "@img/sharp-libvips-linux-s390x": "1.3.1", "@img/sharp-libvips-linux-x64": "1.3.1", "@img/sharp-libvips-linuxmusl-arm64": "1.3.1", "@img/sharp-libvips-linuxmusl-x64": "1.3.1", "@img/sharp-linux-arm": "0.35.2", "@img/sharp-linux-arm64": "0.35.2", "@img/sharp-linux-ppc64": "0.35.2", "@img/sharp-linux-riscv64": "0.35.2", "@img/sharp-linux-s390x": "0.35.2", "@img/sharp-linux-x64": "0.35.2", "@img/sharp-linuxmusl-arm64": "0.35.2", "@img/sharp-linuxmusl-x64": "0.35.2", "@img/sharp-webcontainers-wasm32": "0.35.2", "@img/sharp-win32-arm64": "0.35.2", "@img/sharp-win32-ia32": "0.35.2", "@img/sharp-win32-x64": "0.35.2" } }, "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w=="],
"miniflare/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="],
"mlly/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
@@ -3929,6 +3892,8 @@
"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=="],
@@ -4025,6 +3990,8 @@
"@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=="],
@@ -4343,6 +4310,62 @@
"micro/raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
"miniflare/sharp/@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
"miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.1" }, "os": "darwin", "cpu": "arm64" }, "sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg=="],
"miniflare/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.1" }, "os": "darwin", "cpu": "x64" }, "sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw=="],
"miniflare/sharp/@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.2", "", { "dependencies": { "@img/sharp-wasm32": "0.35.2" }, "os": "freebsd" }, "sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw=="],
"miniflare/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g=="],
"miniflare/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ=="],
"miniflare/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.1", "", { "os": "linux", "cpu": "arm" }, "sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg=="],
"miniflare/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw=="],
"miniflare/sharp/@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng=="],
"miniflare/sharp/@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.1", "", { "os": "linux", "cpu": "none" }, "sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw=="],
"miniflare/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew=="],
"miniflare/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A=="],
"miniflare/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw=="],
"miniflare/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg=="],
"miniflare/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.1" }, "os": "linux", "cpu": "arm" }, "sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A=="],
"miniflare/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.1" }, "os": "linux", "cpu": "arm64" }, "sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA=="],
"miniflare/sharp/@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.1" }, "os": "linux", "cpu": "ppc64" }, "sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg=="],
"miniflare/sharp/@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.1" }, "os": "linux", "cpu": "none" }, "sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA=="],
"miniflare/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.1" }, "os": "linux", "cpu": "s390x" }, "sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA=="],
"miniflare/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.1" }, "os": "linux", "cpu": "x64" }, "sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA=="],
"miniflare/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.1" }, "os": "linux", "cpu": "arm64" }, "sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg=="],
"miniflare/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.1" }, "os": "linux", "cpu": "x64" }, "sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg=="],
"miniflare/sharp/@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.2", "", { "dependencies": { "@img/sharp-wasm32": "0.35.2" }, "cpu": "none" }, "sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g=="],
"miniflare/sharp/@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ=="],
"miniflare/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog=="],
"miniflare/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.2", "", { "os": "win32", "cpu": "x64" }, "sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ=="],
"miniflare/sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"miniflare/sharp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"next/postcss/nanoid": ["nanoid@3.3.7", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g=="],
"next/postcss/picocolors": ["picocolors@1.1.0", "", {}, "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="],
@@ -4475,6 +4498,10 @@
"micro/raw-body/http-errors/toidentifier": ["toidentifier@1.0.0", "", {}, "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="],
"miniflare/sharp/@img/sharp-freebsd-wasm32/@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.2", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw=="],
"miniflare/sharp/@img/sharp-webcontainers-wasm32/@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.2", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw=="],
"rimraf/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
"rimraf/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
@@ -4509,6 +4536,10 @@
"@zenuml/core/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"miniflare/sharp/@img/sharp-freebsd-wasm32/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="],
"miniflare/sharp/@img/sharp-webcontainers-wasm32/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="],
"rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"@opennextjs/aws/express/body-parser/raw-body/http-errors/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
+1 -5
View File
@@ -21,11 +21,7 @@ const allowedDevOrigins =
]
: undefined;
// We don't use the deployment ID yet on 2c, we need to remove it because of https://github.com/opennextjs/opennextjs-aws/issues/1136
let deploymentId =
process.env.GITBOOK_RUNTIME === 'cloudflare'
? undefined
: process.env.GITBOOK_HEAD_SHA || process.env.GITHUB_SHA || Date.now().toString(); // Needed because we use a custom deployment method i.e. https://vercel.com/docs/skew-protection#custom-deployment-id
let deploymentId = process.env.GITBOOK_HEAD_SHA || process.env.GITHUB_SHA || Date.now().toString(); // Needed because we use a custom deployment method i.e. https://vercel.com/docs/skew-protection#custom-deployment-id
const { VERCEL_TARGET_ENV } = process.env;
+4 -4
View File
@@ -8,8 +8,8 @@ export default {
proxyExternalRequest: 'fetch',
queue: () => import('./openNext/queue/middleware').then((m) => m.default),
incrementalCache: () =>
import('./openNext/incrementalCache/server').then((m) => m.default),
tagCache: () => import('./openNext/tagCache/middleware').then((m) => m.default),
import('./openNext/incrementalCache/cacheWorkerClient').then((m) => m.default),
tagCache: () => import('./openNext/tagCache/cacheWorkerClient').then((m) => m.default),
},
},
middleware: {
@@ -20,8 +20,8 @@ export default {
proxyExternalRequest: 'fetch',
queue: () => import('./openNext/queue/middleware').then((m) => m.default),
incrementalCache: () =>
import('./openNext/incrementalCache/middleware').then((m) => m.default),
tagCache: () => import('./openNext/tagCache/middleware').then((m) => m.default),
import('./openNext/incrementalCache/cacheWorkerClient').then((m) => m.default),
tagCache: () => import('./openNext/tagCache/cacheWorkerClient').then((m) => m.default),
},
},
dangerous: {
@@ -33,6 +33,10 @@
"binding": "WORKER_SELF_REFERENCE",
"service": "gitbook-open-v2-server-dev",
},
{
"binding": "NEXT_INC_CACHE_WORKER",
"service": "gitbook-open-v2-do-dev",
},
],
"durable_objects": {
"bindings": [
@@ -70,6 +74,10 @@
"binding": "WORKER_SELF_REFERENCE",
"service": "gitbook-open-v2-server-preview",
},
{
"binding": "NEXT_INC_CACHE_WORKER",
"service": "gitbook-open-v2-do-preview",
},
],
"durable_objects": {
"bindings": [
@@ -106,6 +114,10 @@
"binding": "WORKER_SELF_REFERENCE",
"service": "gitbook-open-v2-server-staging",
},
{
"binding": "NEXT_INC_CACHE_WORKER",
"service": "gitbook-open-v2-do-staging",
},
],
"durable_objects": {
"bindings": [
@@ -150,6 +162,10 @@
"binding": "WORKER_SELF_REFERENCE",
"service": "gitbook-open-v2-server-production",
},
{
"binding": "NEXT_INC_CACHE_WORKER",
"service": "gitbook-open-v2-do-production",
},
],
"durable_objects": {
"bindings": [
@@ -1,38 +0,0 @@
// This worker only purposes it to host the different DO that we will need in the other workers.
import { DurableObject } from 'cloudflare:workers';
// `use cache` could cause multiple writes to the same key to happen concurrently, there is a limit of 1 write per key/second
// so we need to buffer writes to the R2 bucket to avoid hitting this limit.
export class R2WriteBuffer extends DurableObject {
writePromise;
async write(cacheKey, value) {
// We are already writing to this key
if (this.writePromise) {
return;
}
this.writePromise = this.env.NEXT_INC_CACHE_R2_BUCKET.put(cacheKey, value);
this.ctx.waitUntil(
this.writePromise.finally(() => {
this.writePromise = undefined;
})
);
}
}
export { DOQueueHandler } from '../../.open-next/.build/durable-objects/queue.js';
export { DOShardedTagCache } from '../../.open-next/.build/durable-objects/sharded-tag-cache.js';
export default {
async fetch() {
// This worker does not handle any requests, it only provides Durable Objects
return new Response('This worker is not meant to handle requests directly', {
status: 400,
headers: {
'Content-Type': 'text/plain',
},
});
},
};
@@ -0,0 +1,144 @@
import { beforeEach, describe, expect, it, mock } from 'bun:test';
const runWithCloudflareRequestContext = mock(
async <T>(_: Request, __: unknown, ___: unknown, operation: () => Promise<T>) => operation()
);
const get = mock();
const getTagsFromValue = mock();
const hasBeenRevalidated = mock();
mock.module('cloudflare:workers', () => ({
DurableObject: class {},
WorkerEntrypoint: class {},
}));
mock.module('../../.open-next/cloudflare/init.js', () => ({ runWithCloudflareRequestContext }));
mock.module('../incrementalCache/incrementalCache', () => ({
GitbookIncrementalCache: class {
get = get;
},
}));
mock.module('@opennextjs/aws/utils/cache.js', () => ({ getTagsFromValue }));
mock.module('../tagCache/middleware', () => ({
default: { hasBeenRevalidated },
}));
const { default: IncrementalCacheWorker } = await import('./do');
const CACHE_CONTROL = 'public, s-maxage=3600, stale-while-revalidate=86400';
const NO_STORE_CACHE_CONTROL = 'private, no-store, max-age=0, must-revalidate';
const cacheValue = {
type: 'page' as const,
html: '<p>cached</p>',
json: {},
revalidate: 60,
};
describe('IncrementalCacheWorker fetch', () => {
const selfFetch = mock();
beforeEach(() => {
selfFetch.mockReset();
get.mockReset();
getTagsFromValue.mockReset();
hasBeenRevalidated.mockReset();
getTagsFromValue.mockReturnValue(['space:1']);
hasBeenRevalidated.mockResolvedValue(false);
});
const fetch = (request: Request) =>
IncrementalCacheWorker.prototype.fetch.call(
{
env: { WORKER_SELF_REFERENCE: { fetch: selfFetch } },
ctx: {},
},
request
);
it('forwards cache reads to the internal endpoint and restores cache metadata', async () => {
selfFetch.mockResolvedValue(
Response.json(
{ value: cacheValue, lastModified: 123 },
{
headers: {
'x-gitbook-cache-control': CACHE_CONTROL,
'x-gitbook-cache-tag': 'incremental-cache:entry,space:1',
},
}
)
);
const response = await fetch(
new Request('https://incremental-cache.internal/?key=entry&cacheType=cache')
);
expect(response.headers.get('cache-control')).toBe(CACHE_CONTROL);
expect(response.headers.get('cache-tag')).toBe('incremental-cache:entry,space:1');
const forwardedRequest = selfFetch.mock.calls[0]?.[0] as Request;
const forwardedURL = new URL(forwardedRequest.url);
expect(forwardedURL.pathname).toBe('/internal');
expect(forwardedURL.searchParams.get('key')).toBe('entry');
expect(forwardedURL.searchParams.get('cacheType')).toBe('cache');
});
it('reads and annotates a cache hit only on the internal endpoint', async () => {
get.mockResolvedValue({ value: cacheValue, lastModified: Date.now() });
const response = await fetch(
new Request('https://incremental-cache.internal/internal?key=entry&cacheType=cache')
);
expect(get).toHaveBeenCalledWith('entry', 'cache');
expect(selfFetch).not.toHaveBeenCalled();
expect(response.headers.get('cache-control')).toBe(CACHE_CONTROL);
expect(response.headers.get('x-gitbook-cache-control')).toBe(CACHE_CONTROL);
expect(response.headers.get('cache-tag')).toBe('incremental-cache:entry,space:1');
expect(response.headers.get('x-gitbook-cache-tag')).toBe('incremental-cache:entry,space:1');
});
it('keeps cache misses out of the worker cache', async () => {
get.mockResolvedValue(null);
const response = await fetch(
new Request('https://incremental-cache.internal/internal?key=missing')
);
expect(response.headers.get('cache-control')).toBe(NO_STORE_CACHE_CONTROL);
expect(response.headers.get('x-gitbook-cache-control')).toBe(NO_STORE_CACHE_CONTROL);
expect(await response.json()).toBeNull();
});
it('keeps revalidated and stale entries out of the worker cache', async () => {
get.mockResolvedValueOnce({ value: cacheValue, lastModified: Date.now() });
hasBeenRevalidated.mockResolvedValueOnce(true);
const revalidatedResponse = await fetch(
new Request('https://incremental-cache.internal/internal?key=revalidated')
);
get.mockResolvedValueOnce({
value: { ...cacheValue, revalidate: 0 },
lastModified: Date.now() - 1_000,
});
const staleResponse = await fetch(
new Request('https://incremental-cache.internal/internal?key=stale')
);
expect(revalidatedResponse.headers.get('cache-control')).toBe(NO_STORE_CACHE_CONTROL);
expect(revalidatedResponse.headers.get('x-gitbook-cache-revalidated')).toBe('true');
expect(staleResponse.headers.get('cache-control')).toBe(NO_STORE_CACHE_CONTROL);
expect(staleResponse.headers.get('x-gitbook-cache-revalidated')).toBe('true');
});
it('rejects invalid internal requests and does not forward non-GET requests', async () => {
const invalidResponse = await fetch(
new Request('https://incremental-cache.internal/internal?key=entry&cacheType=invalid')
);
const methodResponse = await fetch(
new Request('https://incremental-cache.internal/?key=entry', { method: 'POST' })
);
expect(invalidResponse.status).toBe(400);
expect(methodResponse.status).toBe(405);
expect(selfFetch).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,199 @@
import { DurableObject, WorkerEntrypoint } from 'cloudflare:workers';
import type {
CacheEntryType,
CacheValue,
NextModeTagCacheWriteInput,
} from '@opennextjs/aws/types/overrides.js';
import { getTagsFromValue } from '@opennextjs/aws/utils/cache.js';
import { softTagFilter } from '@opennextjs/cloudflare/overrides/tag-cache/tag-cache-filter';
// @ts-ignore Generated by the Cloudflare build.
import { runWithCloudflareRequestContext } from '../../.open-next/cloudflare/init.js';
import { GitbookIncrementalCache } from '../incrementalCache/incrementalCache';
import tagCache from '../tagCache/middleware';
type CacheWorkerEnv = {
NEXT_INC_CACHE_R2_BUCKET: {
put(key: string, value: string): Promise<unknown>;
};
WORKER_SELF_REFERENCE: {
fetch(request: Request): Promise<Response>;
};
};
//@ts-ignore - Just to avoid tag cache crashing
globalThis.openNextConfig = {
dangerous: {
enableCacheInterception: true,
},
};
const cacheEntryTypes = new Set<CacheEntryType>(['cache', 'fetch', 'composable']);
const isCacheEntryType = (value: string | null): value is CacheEntryType =>
value !== null && cacheEntryTypes.has(value as CacheEntryType);
const NO_STORE_CACHE_CONTROL = 'private, no-store, max-age=0, must-revalidate';
const INTERNAL_PATH = '/internal';
const CACHE_CONTROL_HEADER = 'x-gitbook-cache-control';
const CACHE_TAG_HEADER = 'x-gitbook-cache-tag';
const getCacheHeaders = (cacheControl: string, cacheTag?: string): HeadersInit => ({
'Cache-Control': cacheControl,
[CACHE_CONTROL_HEADER]: cacheControl,
...(cacheTag
? {
'Cache-Tag': cacheTag,
[CACHE_TAG_HEADER]: cacheTag,
}
: {}),
});
const nullCacheResponse = (hasBeenRevalidated = false): Response =>
Response.json(null, {
headers: {
...getCacheHeaders(NO_STORE_CACHE_CONTROL),
...(hasBeenRevalidated ? { 'x-gitbook-cache-revalidated': 'true' } : {}),
},
});
const restoreCacheHeaders = (response: Response): Response => {
const headers = new Headers(response.headers);
const cacheControl = headers.get(CACHE_CONTROL_HEADER);
const cacheTag = headers.get(CACHE_TAG_HEADER);
if (cacheControl) {
headers.set('Cache-Control', cacheControl);
}
if (cacheTag) {
headers.set('Cache-Tag', cacheTag);
}
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
});
};
const isTimeStale = (value: CacheValue<CacheEntryType>, lastModified?: number): boolean => {
const revalidate = value.revalidate;
if (typeof revalidate !== 'number') {
return false;
}
return Date.now() >= (lastModified ?? Date.now()) + revalidate * 1000;
};
const getTagName = (tag: NextModeTagCacheWriteInput): string =>
typeof tag === 'string' ? tag : tag.tag;
// `use cache` can write the same key concurrently, but R2 accepts one write per key per second.
export class R2WriteBuffer extends DurableObject<CacheWorkerEnv> {
private writePromise: Promise<unknown> | undefined;
async write(cacheKey: string, value: string): Promise<void> {
if (this.writePromise) {
return;
}
const writePromise = this.env.NEXT_INC_CACHE_R2_BUCKET.put(cacheKey, value);
this.writePromise = writePromise;
this.ctx.waitUntil(
writePromise.finally(() => {
this.writePromise = undefined;
})
);
}
}
// @ts-ignore Generated by the Cloudflare build.
export { DOQueueHandler } from '../../.open-next/.build/durable-objects/queue.js';
// @ts-ignore Generated by the Cloudflare build.
export { DOShardedTagCache } from '../../.open-next/.build/durable-objects/sharded-tag-cache.js';
export default class IncrementalCacheWorker extends WorkerEntrypoint<CacheWorkerEnv> {
async fetch(request: Request): Promise<Response> {
if (request.method !== 'GET') {
return new Response('Method not allowed', { status: 405 });
}
const url = new URL(request.url);
if (url.pathname !== INTERNAL_PATH) {
url.pathname = INTERNAL_PATH;
const response = await this.env.WORKER_SELF_REFERENCE.fetch(new Request(url, request));
return restoreCacheHeaders(response);
}
const key = url.searchParams.get('key');
const cacheType = url.searchParams.get('cacheType');
if (!key || (cacheType !== null && !isCacheEntryType(cacheType))) {
return new Response('Invalid cache request', { status: 400 });
}
return runWithCloudflareRequestContext(request, this.env, this.ctx, async () => {
const value = await new GitbookIncrementalCache().get(key, cacheType ?? undefined);
if (!value?.value) {
return nullCacheResponse();
}
const tags = getTagsFromValue(value?.value as CacheValue<'cache'> | undefined);
if (await tagCache.hasBeenRevalidated(tags, value.lastModified)) {
return nullCacheResponse(true);
}
if (isTimeStale(value.value, value.lastModified)) {
return nullCacheResponse(true);
}
return Response.json(value, {
headers: getCacheHeaders(
// 1 hour cache, with a 1 day stale-while-revalidate.
'public, s-maxage=3600, stale-while-revalidate=86400',
[`incremental-cache:${key}`, ...tags].join(',')
),
});
});
}
async set<CacheType extends CacheEntryType>(
key: string,
value: CacheValue<CacheType>,
cacheType?: CacheType
): Promise<void> {
await this.#runRpcOperation(async () => {
await new GitbookIncrementalCache().set(key, value, cacheType);
});
}
async delete(key: string): Promise<void> {
await this.#runRpcOperation(async () => {
await new GitbookIncrementalCache().delete(key);
});
}
async writeTags(tags: NextModeTagCacheWriteInput[]): Promise<void> {
const tagsToWrite = tags.filter(softTagFilter);
if (tagsToWrite.length === 0) {
return;
}
await this.#runRpcOperation(async () => {
await tagCache.writeTags(tagsToWrite);
await this.ctx.cache?.purge({ tags: tagsToWrite.map(getTagName) });
});
}
async #runRpcOperation(operation: () => Promise<void>): Promise<void> {
await runWithCloudflareRequestContext(
new Request('https://incremental-cache.internal'),
this.env,
this.ctx,
async () => {
await operation();
return new Response(null, { status: 204 });
}
);
}
}
@@ -1,7 +1,7 @@
{
"main": "do.js",
"main": "do.ts",
"name": "gitbook-open-v2-do",
"compatibility_date": "2025-04-14",
"compatibility_date": "2026-06-14",
"keep_names": false,
"compatibility_flags": [
"nodejs_compat",
@@ -11,10 +11,17 @@
"observability": {
"enabled": false,
},
"cache": {
"enabled": true,
},
"placement": {
"region": "gcp:us-central1",
},
"env": {
"preview": {
"dev": {
"vars": {
"STAGE": "preview",
"STAGE": "dev",
"OPEN_NEXT_BUILD_ID": "local",
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true",
},
"r2_buckets": [
@@ -26,7 +33,7 @@
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "gitbook-open-v2-preview",
"service": "gitbook-open-v2-do-dev",
},
],
"durable_objects": {
@@ -52,6 +59,56 @@
},
],
},
"preview": {
"vars": {
"STAGE": "preview",
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true",
},
"r2_buckets": [
{
"binding": "NEXT_INC_CACHE_R2_BUCKET",
"bucket_name": "gitbook-open-v2-cache-preview",
},
],
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "gitbook-open-v2-do-preview",
},
],
"durable_objects": {
"bindings": [
{
"name": "NEXT_CACHE_DO_QUEUE",
"class_name": "DOQueueHandler",
},
{
"name": "NEXT_TAG_CACHE_DO_SHARDED",
"class_name": "DOShardedTagCache",
},
{
"name": "WRITE_BUFFER",
"class_name": "R2WriteBuffer",
},
],
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache", "R2WriteBuffer"],
},
],
"observability": {
"traces": {
"enabled": true,
"head_sampling_rate": 1,
},
"logs": {
"enabled": true,
"head_sampling_rate": 1,
},
},
},
"staging": {
"vars": {
"STAGE": "staging",
@@ -71,7 +128,7 @@
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "gitbook-open-v2-staging",
"service": "gitbook-open-v2-do-staging",
},
],
"durable_objects": {
@@ -120,7 +177,7 @@
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "gitbook-open-v2-production",
"service": "gitbook-open-v2-do-production",
},
],
"durable_objects": {
@@ -23,9 +23,6 @@
"vars": {
"STAGE": "dev",
"NEXT_PRIVATE_DEBUG_CACHE": "true",
// When deployed locally, we don't have access to the tag cache here,
// we should just bypass the cache to go to the server directly
"SHOULD_BYPASS_CACHE": "true",
"OPEN_NEXT_REQUEST_ID_HEADER": "true",
"GITBOOK_URL": "http://localhost:8771",
},
@@ -44,6 +41,10 @@
"binding": "DEFAULT_WORKER",
"service": "gitbook-open-v2-server-dev",
},
{
"binding": "NEXT_INC_CACHE_WORKER",
"service": "gitbook-open-v2-do-dev",
},
],
},
"preview": {
@@ -67,6 +68,10 @@
"binding": "DEFAULT_WORKER",
"service": "gitbook-open-v2-server-preview",
},
{
"binding": "NEXT_INC_CACHE_WORKER",
"service": "gitbook-open-v2-do-preview",
},
],
"durable_objects": {
"bindings": [
@@ -87,6 +92,16 @@
},
],
},
"observability": {
"traces": {
"enabled": true,
"head_sampling_rate": 1,
},
"logs": {
"enabled": true,
"head_sampling_rate": 1,
},
},
},
"staging": {
"vars": {
@@ -119,6 +134,10 @@
"binding": "DEFAULT_WORKER",
"service": "gitbook-open-v2-server-staging",
},
{
"binding": "NEXT_INC_CACHE_WORKER",
"service": "gitbook-open-v2-do-staging",
},
],
"tail_consumers": [
{
@@ -188,6 +207,10 @@
"binding": "DEFAULT_WORKER",
"service": "gitbook-open-v2-server-production",
},
{
"binding": "NEXT_INC_CACHE_WORKER",
"service": "gitbook-open-v2-do-production",
},
],
"tail_consumers": [
{
@@ -0,0 +1,84 @@
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
const getCloudflareContext = mock();
mock.module('@opennextjs/cloudflare', () => ({ getCloudflareContext }));
const { GitbookIncrementalCache } = await import('./cacheWorkerClient');
const cacheValue = {
type: 'page' as const,
html: '<p>cached</p>',
json: {},
};
describe('GitbookIncrementalCache cache worker client', () => {
const fetch = mock();
const set = mock();
const remove = mock();
const originalConsoleError = console.error;
beforeEach(() => {
fetch.mockReset();
set.mockReset();
remove.mockReset();
getCloudflareContext.mockReturnValue({
env: {
NEXT_INC_CACHE_WORKER: { fetch, set, delete: remove },
},
});
console.error = mock();
});
afterEach(() => {
console.error = originalConsoleError;
});
it('gets cache entries through the service binding', async () => {
fetch.mockResolvedValue(
Response.json({
value: cacheValue,
lastModified: 123,
})
);
const result = await new GitbookIncrementalCache().get('key with / characters', 'cache');
expect(result).toEqual({ value: cacheValue, lastModified: 123 });
const request = fetch.mock.calls[0]?.[0] as Request;
const url = new URL(request.url);
expect(url.searchParams.get('key')).toBe('key with / characters');
expect(url.searchParams.get('cacheType')).toBe('cache');
});
it('returns null for cache misses and failed reads', async () => {
fetch.mockResolvedValue(Response.json(null));
expect(await new GitbookIncrementalCache().get('missing')).toBeNull();
fetch.mockResolvedValueOnce(new Response(null, { status: 503 }));
expect(await new GitbookIncrementalCache().get('unavailable-response')).toBeNull();
fetch.mockRejectedValueOnce(new Error('service unavailable'));
expect(await new GitbookIncrementalCache().get('unavailable')).toBeNull();
});
it('uses RPC for writes and deletes', async () => {
set.mockResolvedValue(undefined);
remove.mockResolvedValue(undefined);
const cache = new GitbookIncrementalCache();
await cache.set('entry', cacheValue, 'cache');
await cache.delete('entry');
expect(set).toHaveBeenCalledWith('entry', cacheValue, 'cache');
expect(remove).toHaveBeenCalledWith('entry');
});
it('contains mutation failures', async () => {
set.mockRejectedValue(new Error('service unavailable'));
remove.mockRejectedValue(new Error('service unavailable'));
const cache = new GitbookIncrementalCache();
await expect(cache.set('entry', cacheValue, 'cache')).resolves.toBeUndefined();
await expect(cache.delete('entry')).resolves.toBeUndefined();
});
});
@@ -0,0 +1,78 @@
import type {
CacheEntryType,
CacheValue,
IncrementalCache,
WithLastModified,
} from '@opennextjs/aws/types/overrides.js';
import { getCloudflareContext } from '@opennextjs/cloudflare';
export const BINDING_NAME = 'NEXT_INC_CACHE_WORKER';
type CacheWorker = {
fetch(request: Request): Promise<Response>;
set<CacheType extends CacheEntryType>(
key: string,
value: CacheValue<CacheType>,
cacheType?: CacheType
): Promise<void>;
delete(key: string): Promise<void>;
};
export class GitbookIncrementalCache implements IncrementalCache {
name = 'GitbookIncrementalCache';
async get<CacheType extends CacheEntryType = 'cache'>(
key: string,
cacheType?: CacheType
): Promise<WithLastModified<CacheValue<CacheType>> | null> {
try {
const url = new URL('https://incremental-cache.internal');
url.searchParams.set('key', key);
if (cacheType) {
url.searchParams.set('cacheType', cacheType);
}
const response = await this.getWorker().fetch(new Request(url));
if (!response.ok) {
console.error('Failed to get from cache worker', response.status);
return null;
}
return (await response.json()) as WithLastModified<CacheValue<CacheType>> | null;
} catch (error) {
console.error('Failed to get from cache worker', error);
return null;
}
}
async set<CacheType extends CacheEntryType = 'cache'>(
key: string,
value: CacheValue<CacheType>,
cacheType?: CacheType
): Promise<void> {
try {
await this.getWorker().set(key, value, cacheType);
} catch (error) {
console.error('Failed to set to cache worker', error);
}
}
async delete(key: string): Promise<void> {
try {
await this.getWorker().delete(key);
} catch (error) {
console.error('Failed to delete from cache worker', error);
}
}
private getWorker(): CacheWorker {
const env = getCloudflareContext().env as Record<string, unknown>;
const worker = env[BINDING_NAME] as CacheWorker | undefined;
if (!worker) {
throw new Error(`Missing ${BINDING_NAME} service binding`);
}
return worker;
}
}
export default new GitbookIncrementalCache();
@@ -0,0 +1,51 @@
import { afterEach, describe, expect, it } from 'bun:test';
import { createHash } from 'node:crypto';
import { DEFAULT_PREFIX, GitbookIncrementalCache } from './incrementalCache';
const environmentKeys = ['OPEN_NEXT_BUILD_ID', 'DEPLOYMENT_ID', 'NEXT_BUILD_ID'] as const;
const originalEnvironment = Object.fromEntries(
environmentKeys.map((key) => [key, process.env[key]])
);
const hash = (key: string) => createHash('sha256').update(key).digest('hex');
afterEach(() => {
for (const key of environmentKeys) {
const value = originalEnvironment[key];
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
});
describe('GitbookIncrementalCache cache keys', () => {
it('uses the deployment-aware OpenNext build ID', () => {
process.env.OPEN_NEXT_BUILD_ID = 'deployment-id';
process.env.NEXT_BUILD_ID = 'legacy-build-id';
expect(new GitbookIncrementalCache().getR2Key('entry')).toBe(
`${DEFAULT_PREFIX}/deployment-id/${hash('entry')}.cache`
);
});
it('uses the Cloudflare deployment ID when the worker build ID is unavailable', () => {
delete process.env.OPEN_NEXT_BUILD_ID;
process.env.DEPLOYMENT_ID = 'runtime-deployment-id';
expect(new GitbookIncrementalCache().getR2Key('entry')).toBe(
`${DEFAULT_PREFIX}/runtime-deployment-id/${hash('entry')}.cache`
);
});
it('normalizes composable cache keys before applying the deployment namespace', () => {
process.env.OPEN_NEXT_BUILD_ID = 'deployment-id';
const key = JSON.stringify(['next-build-id', 'cache-key']);
expect(new GitbookIncrementalCache().getR2Key(key, 'composable')).toBe(
`${DEFAULT_PREFIX}/dataCache/${hash(JSON.stringify(['cache-key']))}.composable`
);
});
});
@@ -128,7 +128,8 @@ export class GitbookIncrementalCache implements IncrementalCache {
}
const hash = createHash('sha256').update(key).digest('hex');
return `${DEFAULT_PREFIX}/${cacheType === 'cache' ? process.env?.NEXT_BUILD_ID : 'dataCache'}/${hash}.${cacheType}`.replace(
const buildId = process.env.OPEN_NEXT_BUILD_ID ?? process.env.DEPLOYMENT_ID;
return `${DEFAULT_PREFIX}/${cacheType === 'cache' ? buildId : 'dataCache'}/${hash}.${cacheType}`.replace(
/\/+/g,
'/'
);
@@ -1,13 +1,14 @@
import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache';
// import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache';
import { GitbookIncrementalCache } from './cacheWorkerClient';
import { GitbookIncrementalCache } from './incrementalCache';
// export default withRegionalCache(new GitbookIncrementalCache(), {
// mode: 'long-lived',
// // We can do it because we use our own logic to invalidate the cache
// bypassTagCacheOnCacheHit: true,
// //TODO: bump it again once I figured out the race condition
// defaultLongLivedTtlSec: 5 * 60, // 5 minutes
// // We don't want to update the cache entry on every cache hit
// shouldLazilyUpdateOnCacheHit: false,
// });
export default withRegionalCache(new GitbookIncrementalCache(), {
mode: 'long-lived',
// We can do it because we use our own logic to invalidate the cache
bypassTagCacheOnCacheHit: true,
//TODO: bump it again once I figured out the race condition
defaultLongLivedTtlSec: 5 * 60, // 5 minutes
// We don't want to update the cache entry on every cache hit
shouldLazilyUpdateOnCacheHit: false,
});
export default new GitbookIncrementalCache();
@@ -1,16 +1,17 @@
import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache';
// import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache';
import { GitbookIncrementalCache } from './cacheWorkerClient';
import { GitbookIncrementalCache } from './incrementalCache';
// // We cannot have regional cache only in the middleware, otherwise it will override things on cache miss
// // and cause race conditions. This will be fixed in a future release of OpenNext
// export default withRegionalCache(new GitbookIncrementalCache(), {
// mode: 'long-lived',
// // Because of a race condition, the middleware may have populated the cache entry before `cache.match` had time to run on the server.
// // TODO: We should bypass the incremental cache entirely when the interceptor has caught the request. Should be done in OpenNext.
// bypassTagCacheOnCacheHit: false,
// //TODO: remove, reducing cache ttl of regional cache to help debugging
// defaultLongLivedTtlSec: 5 * 60 /* 5 minutes */,
// // We don't want to update the cache entry on every cache hit
// shouldLazilyUpdateOnCacheHit: false,
// });
// We cannot have regional cache only in the middleware, otherwise it will override things on cache miss
// and cause race conditions. This will be fixed in a future release of OpenNext
export default withRegionalCache(new GitbookIncrementalCache(), {
mode: 'long-lived',
// Because of a race condition, the middleware may have populated the cache entry before `cache.match` had time to run on the server.
// TODO: We should bypass the incremental cache entirely when the interceptor has caught the request. Should be done in OpenNext.
bypassTagCacheOnCacheHit: false,
//TODO: remove, reducing cache ttl of regional cache to help debugging
defaultLongLivedTtlSec: 5 * 60 /* 5 minutes */,
// We don't want to update the cache entry on every cache hit
shouldLazilyUpdateOnCacheHit: false,
});
export default new GitbookIncrementalCache();
@@ -0,0 +1,44 @@
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
const getCloudflareContext = mock();
mock.module('@opennextjs/cloudflare', () => ({ getCloudflareContext }));
const { default: tagCache } = await import('./cacheWorkerClient');
describe('GitbookTagCache cache worker client', () => {
const writeTags = mock();
beforeEach(() => {
writeTags.mockReset();
getCloudflareContext.mockReturnValue({
env: {
NEXT_INC_CACHE_WORKER: { writeTags },
},
});
});
afterEach(() => {
getCloudflareContext.mockReset();
});
it('writes hard tags through the cache worker service binding', async () => {
writeTags.mockResolvedValue(undefined);
await tagCache.writeTags([
'content',
{ tag: 'with-duration', stale: 100, expire: 200 },
'_N_T_/soft-tag',
]);
expect(writeTags).toHaveBeenCalledWith([
'content',
{ tag: 'with-duration', stale: 100, expire: 200 },
]);
});
it('does not call the worker when all tags are soft tags', async () => {
await tagCache.writeTags(['_N_T_/soft-tag']);
expect(writeTags).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,39 @@
import type { NextModeTagCache, NextModeTagCacheWriteInput } from '@opennextjs/aws/types/overrides';
import { getCloudflareContext } from '@opennextjs/cloudflare';
import { softTagFilter } from '@opennextjs/cloudflare/overrides/tag-cache/tag-cache-filter';
const BINDING_NAME = 'NEXT_INC_CACHE_WORKER';
type CacheWorker = {
writeTags(tags: NextModeTagCacheWriteInput[]): Promise<void>;
};
const getWorker = (): CacheWorker => {
const env = getCloudflareContext().env as Record<string, unknown>;
const worker = env[BINDING_NAME] as CacheWorker | undefined;
if (!worker) {
throw new Error(`Missing ${BINDING_NAME} service binding`);
}
return worker;
};
export default {
name: 'GitbookTagCache',
mode: 'nextMode',
// Do nothing.
getLastRevalidated: async () => {
return 0;
},
// Return false, everything handled at the incremental cache level in the do worker.
hasBeenRevalidated: async () => {
return false;
},
writeTags: async (tags: NextModeTagCacheWriteInput[]) => {
const tagsToWrite = tags.filter(softTagFilter);
if (tagsToWrite.length === 0) {
return;
}
await getWorker().writeTags(tagsToWrite);
},
} satisfies NextModeTagCache;
@@ -1,4 +1,7 @@
import type { NextModeTagCache } from '@opennextjs/aws/types/overrides.js';
import type {
NextModeTagCache,
NextModeTagCacheWriteInput,
} from '@opennextjs/aws/types/overrides.js';
import doShardedTagCache from '@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache';
import { softTagFilter } from '@opennextjs/cloudflare/overrides/tag-cache/tag-cache-filter';
@@ -48,7 +51,7 @@ export default {
return false; // In case of error, return false
}
},
writeTags: async (tags: string[]) => {
writeTags: async (tags: NextModeTagCacheWriteInput[]) => {
const tagsToWrite = tags.filter(softTagFilter);
if (tagsToWrite.length === 0) {
const logger = getLogger().subLogger('gitbookTagCache');
+8 -9
View File
@@ -4,7 +4,7 @@
"private": true,
"dependencies": {
"@base-ui/react": "catalog:",
"@cloudflare/workers-types": "^4.20251011.0",
"@cloudflare/workers-types": "^5.20260716.1",
"@gitbook/api": "catalog:",
"@gitbook/browser-types": "workspace:*",
"@gitbook/cache-tags": "workspace:*",
@@ -62,7 +62,7 @@
"quick-lru": "^7.0.1",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-hotkeys-hook": "^5.3.3",
"react-hotkeys-hook": "^4.4.1",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"rehype-stringify": "^10.0.1",
@@ -89,39 +89,37 @@
"@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"
"wrangler": "^4.122.0",
"rss-parser": "^3.13.0"
},
"scripts": {
"generate": "./scripts/generate.sh",
"generate:assets": "bun ./scripts/generate-mermaid-runtime.ts && bun ./scripts/generate-scalar-runtime.ts && bun ./scripts/generate-block-css.ts && bun ./scripts/download-fonts.ts",
"generate:fonts": "bun ./scripts/generate-font-faces.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 && rm -rf ./public/~gitbook/static/fonts",
"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",
"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",
@@ -133,6 +131,7 @@
"dev:cf:middleware": "wrangler dev --port 8771 --inspector-port 9230 --env dev --config ./openNext/customWorkers/middlewareWrangler.jsonc",
"dev:cf:server": "wrangler dev --port 8772 --env dev --config ./openNext/customWorkers/defaultWrangler.jsonc",
"profile:cf:memory": "bun run build:cloudflare && bun ./scripts/profile-opennext-memory.ts",
"dev:cf:cache": "wrangler dev --env dev --config ./openNext/customWorkers/doWrangler.jsonc",
"e2e": "playwright test e2e/internal.spec.ts e2e/cookie-banner.spec.ts e2e/pdf.spec.ts e2e/select.spec.ts --project=chromium",
"e2e-customers": "playwright test e2e/customers.spec.ts --project=chromium",
"e2e-style-perf": "playwright test e2e/style-perf.spec.ts --project=chromium --reporter=list",
@@ -1,73 +0,0 @@
import { copyFile, mkdir, readdir, rm, writeFile } from 'node:fs/promises';
import { dirname, join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
import sourcesData from '../src/fonts/generated/sources.json' with { type: 'json' };
import type { FontSourcesData } from '../src/fonts/types';
const CONCURRENCY = 8;
const ATTEMPTS = 3;
const scriptDir = dirname(fileURLToPath(import.meta.url));
const fontsDir = join(scriptDir, '../src/fonts');
const outputDir = join(scriptDir, '../public/~gitbook/static/fonts');
const { google, local } = sourcesData as FontSourcesData;
const sources = new Map<string, string>(Object.entries(local));
for (const [googleId, { prefix, files }] of Object.entries(google)) {
for (const file of files) {
sources.set(`${googleId}/${file}`, `${prefix}/${file}`);
}
}
// Faces move between releases; stale files would otherwise pile up in the deployed assets.
await mkdir(outputDir, { recursive: true });
for (const entry of await readdir(outputDir, { recursive: true, withFileTypes: true })) {
if (entry.isFile()) {
const file = relative(outputDir, join(entry.parentPath, entry.name));
if (!sources.has(file)) {
await rm(join(outputDir, file));
}
}
}
const pending = [...sources].filter(([file]) => !Bun.file(join(outputDir, file)).size);
if (pending.length > 0) {
console.log(`Downloading ${pending.length} font files…`);
}
const queue = pending.values();
await Promise.all(Array.from({ length: CONCURRENCY }, () => worker()));
async function worker() {
for (const [file, source] of queue) {
const target = join(outputDir, file);
await mkdir(dirname(target), { recursive: true });
if (source.startsWith('./')) {
await copyFile(join(fontsDir, source), target);
continue;
}
await writeFile(target, await fetchWithRetries(source));
}
}
// Google Fonts intermittently refuses connections when a build asks for hundreds of files at once,
// and a single miss fails the whole build.
async function fetchWithRetries(url: string): Promise<Buffer> {
for (let attempt = 1; ; attempt++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`${response.status} ${response.statusText}`);
}
return Buffer.from(await response.arrayBuffer());
} catch (error) {
if (attempt >= ATTEMPTS) {
throw new Error(`Unable to download ${url}: ${error}`);
}
await new Promise((resolve) => setTimeout(resolve, 500 * 2 ** attempt));
}
}
}
@@ -1,36 +0,0 @@
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,186 +0,0 @@
import { createHash } from 'node:crypto';
import { readFile, writeFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import { basename, dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { ABC_FAVORIT, FONT_DEFINITIONS, type FontDefinition } from '../src/fonts/definitions';
import type {
FontFacesData,
FontFallbackFaceData,
FontSourcesData,
FontVariantData,
} from '../src/fonts/types';
// Google Fonts picks the file format from the user agent — the same modern Chrome `next/font` sends,
// so we keep getting compact woff2 (and vector rather than bitmap emoji).
const USER_AGENT =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36';
// The precalculated metrics `next/font/google` uses, so the fallback faces stay identical.
const { calculateSizeAdjustValues } = createRequire(import.meta.url)(
'next/dist/server/font-utils'
) as {
calculateSizeAdjustValues: (family: string) => {
ascent: string;
descent: string;
lineGap: string;
fallbackFont: string;
sizeAdjust: string;
};
};
type ResolvedFace = {
weight: string;
style: string;
file: string;
source: string;
unicodeRange: string;
};
const scriptDir = dirname(fileURLToPath(import.meta.url));
const faces: FontFacesData = {};
const sources: FontSourcesData = { google: {}, local: {} };
for (const [name, definition] of Object.entries(FONT_DEFINITIONS)) {
const resolved = definition.googleId
? await getGoogleFaces(definition)
: await getABCFavoritFaces();
if (resolved.length === 0) {
throw new Error(`No font faces resolved for ${name}`);
}
recordSources(definition, resolved);
const subsets = [...new Set(resolved.map((face) => face.unicodeRange))];
const variants = new Map<string, FontVariantData>();
for (const face of resolved) {
const key = `${face.weight}|${face.style}`;
let variant = variants.get(key);
if (!variant) {
variant = { weight: face.weight, style: face.style, files: [] };
variants.set(key, variant);
}
variant.files[subsets.indexOf(face.unicodeRange)] = face.file;
}
faces[name] = {
family: definition.family,
variable: definition.variable,
fontFamilyValue: [
`"${definition.family}"`,
...(definition.adjustFallback ? [`"${definition.family} Fallback"`] : []),
...definition.fallback,
].join(','),
subsets,
variants: [...variants.values()],
fallbackFace: definition.adjustFallback ? getFallbackFace(name, definition.family) : null,
...(definition.googleId ? {} : { ascentOverride: ABC_FAVORIT.ascentOverride }),
};
}
const generatedDir = join(scriptDir, '../src/fonts/generated');
await writeFile(join(generatedDir, 'faces.json'), `${JSON.stringify(faces, null, 4)}\n`);
await writeFile(join(generatedDir, 'sources.json'), `${JSON.stringify(sources, null, 4)}\n`);
/** Google serves every file of a family from one versioned directory, so only the names differ. */
function recordSources(definition: FontDefinition, resolved: ResolvedFace[]) {
if (!definition.googleId) {
for (const face of resolved) {
sources.local[face.file] = face.source;
}
return;
}
const prefixes = new Set(
resolved.map((face) => face.source.slice(0, face.source.lastIndexOf('/')))
);
if (prefixes.size !== 1) {
throw new Error(`${definition.family} spans several Google Fonts directories`);
}
sources.google[definition.googleId] = {
prefix: [...prefixes][0] as string,
files: [...new Set(resolved.map((face) => basename(face.file)))],
};
}
async function getGoogleFaces(definition: FontDefinition): Promise<ResolvedFace[]> {
const { family, googleId, weights } = definition;
const url = `https://fonts.googleapis.com/css2?family=${family.replaceAll(' ', '+')}:wght@${weights.join(';')}&display=swap`;
const response = await fetch(url, { headers: { 'User-Agent': USER_AGENT } });
if (!response.ok) {
throw new Error(`Unable to fetch ${family} from Google Fonts: ${response.status} (${url})`);
}
const resolved = [...(await response.text()).matchAll(/@font-face\s*\{([^}]*)\}/g)].map(
(match) => {
const block = match[1] ?? '';
const source = read(block, 'src')?.match(/url\((https:[^)]+\.woff2)\)/)?.[1];
const weight = read(block, 'font-weight');
const unicodeRange = read(block, 'unicode-range');
if (!source || !weight || !unicodeRange) {
throw new Error(`Unexpected @font-face for ${family}: ${block}`);
}
return {
weight,
style: read(block, 'font-style') ?? 'normal',
// Google's filenames are content-addressed, so the asset can stay immutable.
file: `${googleId}/${basename(new URL(source).pathname)}`,
source,
unicodeRange: unicodeRange.toLowerCase(),
};
}
);
const missing = weights.filter((weight) => !resolved.some((face) => face.weight === weight));
if (missing.length > 0) {
throw new Error(`Google Fonts returned no ${missing.join('/')} weight for ${family}`);
}
return resolved;
}
async function getABCFavoritFaces(): Promise<ResolvedFace[]> {
return Promise.all(
ABC_FAVORIT.sources.map(async (source) => {
const path = join(scriptDir, '../src/fonts/ABCFavorit', source.file);
const digest = createHash('sha256')
.update(await readFile(path))
.digest('hex');
return {
weight: source.weight,
style: source.style,
file: `abcfavorit/${digest.slice(0, 16)}.woff2`,
source: `./ABCFavorit/${source.file}`,
unicodeRange: '',
};
})
);
}
function getFallbackFace(name: string, family: string): FontFallbackFaceData {
if (name === 'ABCFavorit') {
return { family: `${family} Fallback`, local: 'Arial', ...ABC_FAVORIT.fallbackMetrics };
}
const metrics = calculateSizeAdjustValues(family);
return {
family: `${family} Fallback`,
local: metrics.fallbackFont,
ascentOverride: `${metrics.ascent}%`,
descentOverride: `${metrics.descent}%`,
lineGapOverride: `${metrics.lineGap}%`,
sizeAdjust: `${metrics.sizeAdjust}%`,
};
}
function read(block: string, property: string): string | undefined {
return block.match(new RegExp(`${property}\\s*:\\s*([^;]+);`))?.[1]?.trim();
}
@@ -0,0 +1,205 @@
/**
* Usage:
* bun packages/gitbook/scripts/test-cache-revalidation.ts benchmark
* bun packages/gitbook/scripts/test-cache-revalidation.ts revalidate
*/
import { createHmac } from 'node:crypto';
// Edit this section for the local site under test. `bun dev` exposes published
// sites at /url/<published-site-host>/<path>.
const PAGE_URLS = [
'http://localhost:3000/url/example.gitbook.io/docs',
'http://localhost:3000/url/example.gitbook.io/docs/getting-started',
];
// The revalidation route must use the same /url/<published-site-host>/ prefix.
// Leave REVALIDATE_SECRET empty when the deployment does not set GITBOOK_SECRET.
const REVALIDATE_URL = 'http://localhost:3000/url/example.gitbook.io/~gitbook/revalidate';
const REVALIDATE_SECRET = '';
const REVALIDATE_BODY = '{"tags":["space:YOUR_SPACE_ID"]}';
// Next sets x-nextjs-cache to HIT or MISS. Change this when testing through a
// proxy that exposes the result under a different header.
const CACHE_STATUS_HEADER = 'x-nextjs-cache';
const BENCHMARK_RUNS = 3;
const WARMUP_REQUESTS = 2;
const WAIT_AFTER_REVALIDATION_MS = 1_000;
type RequestResult = {
cacheStatus: string | null;
status: number;
totalMs: number;
ttfbMs: number;
url: string;
};
function printTableHeader() {
console.log(
`${'request'.padEnd(13)} ${'HTTP'.padEnd(4)} ${CACHE_STATUS_HEADER.padEnd(16)} ${'TTFB / total'.padEnd(22)} URL`
);
}
function printResult(label: string, result: RequestResult) {
const cacheStatus = result.cacheStatus ?? '<missing>';
const timings = `${result.ttfbMs.toFixed(1)} / ${result.totalMs.toFixed(1)} ms`;
console.log(
`${label.padEnd(13)} ${String(result.status).padEnd(4)} ${cacheStatus.padEnd(16)} ${timings.padEnd(22)} ${result.url}`
);
}
async function requestPage(url: string): Promise<RequestResult> {
const start = performance.now();
const response = await fetch(url);
const ttfbMs = performance.now() - start;
// Consume the response body so totalMs measures the entire transfer.
await response.arrayBuffer();
return {
cacheStatus: response.headers.get(CACHE_STATUS_HEADER),
status: response.status,
totalMs: performance.now() - start,
ttfbMs,
url: response.url,
};
}
function isSuccessful(result: RequestResult) {
return result.status >= 200 && result.status < 400;
}
async function benchmark(): Promise<number> {
let failures = 0;
printTableHeader();
for (let run = 1; run <= BENCHMARK_RUNS; run += 1) {
for (const url of PAGE_URLS) {
try {
const result = await requestPage(url);
printResult(`benchmark-${run}`, result);
if (!isSuccessful(result)) {
failures += 1;
}
} catch (error) {
console.error(`benchmark-${run} ERR ${url}: ${String(error)}`);
failures += 1;
}
}
}
return failures;
}
function validateRevalidationConfig() {
if (
REVALIDATE_URL.includes('example.gitbook.io') ||
REVALIDATE_BODY.includes('YOUR_') ||
PAGE_URLS.some((url) => url.includes('example.gitbook.io'))
) {
throw new Error(
'Set PAGE_URLS, REVALIDATE_URL, and REVALIDATE_BODY before revalidate mode.'
);
}
}
function createSignature() {
if (!REVALIDATE_SECRET) {
return undefined;
}
return createHmac('sha256', REVALIDATE_SECRET).update(REVALIDATE_BODY).digest('hex');
}
async function revalidate(): Promise<number> {
validateRevalidationConfig();
let failures = 0;
console.log(`Warming each page (${WARMUP_REQUESTS} request(s) each)...`);
printTableHeader();
for (let run = 1; run <= WARMUP_REQUESTS; run += 1) {
for (const url of PAGE_URLS) {
try {
const result = await requestPage(url);
printResult(`warmup-${run}`, result);
if (!isSuccessful(result)) {
failures += 1;
}
} catch (error) {
console.error(`warmup-${run} ERR ${url}: ${String(error)}`);
failures += 1;
}
}
}
const headers = new Headers({ 'content-type': 'application/json' });
const signature = createSignature();
if (signature) {
headers.set('x-gitbook-signature', signature);
}
console.log(`Revalidating ${REVALIDATE_BODY}...`);
const revalidationResponse = await fetch(REVALIDATE_URL, {
body: REVALIDATE_BODY,
headers,
method: 'POST',
});
const revalidationResponseBody = await revalidationResponse.text();
if (!revalidationResponse.ok) {
throw new Error(
`Revalidation failed with ${revalidationResponse.status}: ${revalidationResponseBody}`
);
}
console.log(revalidationResponseBody);
await Bun.sleep(WAIT_AFTER_REVALIDATION_MS);
console.log('Checking that the next response is a MISS...');
printTableHeader();
for (const url of PAGE_URLS) {
try {
const result = await requestPage(url);
printResult('revalidated', result);
if (!isSuccessful(result) || result.cacheStatus?.toUpperCase() !== 'MISS') {
failures += 1;
}
} catch (error) {
console.error(`revalidated ERR ${url}: ${String(error)}`);
failures += 1;
}
}
return failures;
}
async function main() {
const mode = process.argv[2] ?? 'benchmark';
let failures: number;
try {
switch (mode) {
case 'benchmark':
failures = await benchmark();
break;
case 'revalidate':
failures = await revalidate();
break;
default:
throw new Error(`Usage: bun ${process.argv[1]} {benchmark|revalidate}`);
}
} catch (error) {
console.error(String(error));
return 1;
}
if (failures > 0) {
console.error(`${failures} request(s) failed.`);
return 1;
}
return 0;
}
void main().then((exitCode) => {
process.exitCode = exitCode;
});
@@ -63,9 +63,7 @@ export function AIChat() {
() => {
chatController.close();
},
{
useKey: true,
}
[]
);
// Track the view of the AI chat
@@ -77,9 +77,9 @@ export function AIChatInput(props: {
},
{
enableOnFormTags: true,
// Match the produced key so Dvorak ⌘-C (physical "I" key) copies
// Match the logical character so Dvorak ⌘-C (physical "I" key) copies
// instead of focusing the Assistant input. RND-11340.
useKey: true,
ignoreEventWhen: (e) => e.key.toLowerCase() !== 'i',
}
);
@@ -1,4 +1,4 @@
import { AdminToolbarLazy } from './AdminToolbarLazy';
import { AdminToolbarClient } from './AdminToolbarClient';
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 <AdminToolbarLazy context={minimalContext} />;
return <AdminToolbarClient context={minimalContext} />;
}
@@ -1,17 +0,0 @@
'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,3 +1,6 @@
// 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';
@@ -1,14 +0,0 @@
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,7 +105,6 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio
return (
<div className={tcls(style)}>
<BlockStylesheet name="contentkit" />
{useClientContext ? (
<ContentKitWithClientContext
{...contentKitProps}
@@ -1,5 +1,4 @@
import type { BlockProps } from '../Block';
import { BlockStylesheet } from '../BlockStylesheet';
import { getOpenAPIBlockClientProps } from './context';
import { OpenAPIBlockLazy } from './OpenAPIBlockLazy';
import { resolveOpenAPIOperationBlock } from '@/lib/openapi/resolveOpenAPIOperationBlock';
@@ -12,12 +11,9 @@ import { tcls } from '@/lib/tailwind';
export async function OpenAPIOperation(props: BlockProps<AnyOpenAPIOperationsBlock>) {
const { style } = props;
return (
<>
<BlockStylesheet name="openapi" />
<div className={tcls('flex w-full min-w-0', style, 'max-w-full')}>
<OpenAPIOperationBody {...props} />
</div>
</>
<div className={tcls('flex w-full min-w-0', style, 'max-w-full')}>
<OpenAPIOperationBody {...props} />
</div>
);
}
@@ -1,5 +1,4 @@
import type { BlockProps } from '../Block';
import { BlockStylesheet } from '../BlockStylesheet';
import { getOpenAPIBlockClientProps } from './context';
import { OpenAPIBlockLazy } from './OpenAPIBlockLazy';
import { resolveOpenAPISchemasBlock } from '@/lib/openapi/resolveOpenAPISchemasBlock';
@@ -12,12 +11,9 @@ import { tcls } from '@/lib/tailwind';
export async function OpenAPISchemas(props: BlockProps<OpenAPISchemasBlock>) {
const { style } = props;
return (
<>
<BlockStylesheet name="openapi" />
<div className={tcls('flex w-full', style, 'max-w-full')}>
<OpenAPISchemasBody {...props} />
</div>
</>
<div className={tcls('flex w-full', style, 'max-w-full')}>
<OpenAPISchemasBody {...props} />
</div>
);
}
@@ -1,5 +1,4 @@
import type { BlockProps } from '../Block';
import { BlockStylesheet } from '../BlockStylesheet';
import { getOpenAPIBlockClientProps } from './context';
import { OpenAPIBlockLazy } from './OpenAPIBlockLazy';
import { resolveOpenAPIWebhookBlock } from '@/lib/openapi/resolveOpenAPIWebhookBlock';
@@ -12,12 +11,9 @@ import { tcls } from '@/lib/tailwind';
export async function OpenAPIWebhook(props: BlockProps<OpenAPIWebhookBlock>) {
const { style } = props;
return (
<>
<BlockStylesheet name="openapi" />
<div className={tcls('flex w-full min-w-0', style, 'max-w-full')}>
<OpenAPIWebhookBody {...props} />
</div>
</>
<div className={tcls('flex w-full min-w-0', style, 'max-w-full')}>
<OpenAPIWebhookBody {...props} />
</div>
);
}
@@ -17,6 +17,7 @@ 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';
@@ -1,9 +0,0 @@
// 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;
@@ -30,11 +30,8 @@ export function Footer(props: { context: GitBookSiteContext }) {
data-gb-site-footer
className={tcls(
'border-tint-subtle border-t',
// If the footer only contains a mode toggle, we only show it where the outline
// column — which carries its own toggle — isn't pinned open.
mobileOnly
? 'layout-default:xl:chat-closed:hidden layout-default:3xl:hidden layout-wide:3xl:chat-closed:hidden page-api-block:page-has-outline:min-[96rem]:hidden'
: null
// If the footer only contains a mode toggle, we only show it on smaller screens
mobileOnly ? 'xl:hidden' : null
)}
>
<div className="lg:chat-open:pr-(--ai-chat-width) transition-[padding] duration-300 motion-reduce:transition-none">
@@ -99,9 +96,7 @@ export function Footer(props: { context: GitBookSiteContext }) {
{
// Theme Toggle
customization.themes.toggeable ? (
// Hidden where the outline column is pinned open (see PageAside),
// since that column carries its own toggle.
<div className="page-api-block:page-has-outline:min-[96rem]:hidden -col-start-2 row-start-1 flex items-start @xs:justify-end layout-default:xl:chat-closed:hidden layout-default:3xl:hidden layout-wide:3xl:chat-closed:hidden">
<div className="-col-start-2 row-start-1 flex items-start @xs:justify-end xl:hidden">
<React.Suspense fallback={null}>
<ThemeToggler />
</React.Suspense>
@@ -15,6 +15,7 @@ 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';
@@ -33,7 +34,7 @@ export function HeaderLinkMore(props: {
const { label, links, context, socialAccounts } = props;
return (
<div className="header-links-ellipsis z-20 items-center">
<div className={`${styles.linkEllipsis} z-20 items-center`}>
<HeaderLinkMoreDropdown
label={label}
dropdownClassName={tcls(
@@ -1,5 +1,6 @@
import type React from 'react';
import styles from './headerLinks.module.css';
import { type ClassValue, tcls } from '@/lib/tailwind';
interface HeaderLinksProps {
@@ -11,7 +12,7 @@ export async function HeaderLinks({ children, style }: HeaderLinksProps) {
return (
<div
className={tcls(
'header-links',
styles.containerHeaderlinks,
'@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
)}
@@ -0,0 +1,80 @@
@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,6 +26,7 @@ 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,6 +1,5 @@
import { CustomizationDefaultThemeMode } from '@gitbook/api';
import { PRINT_STYLES } from './printStyles';
import { CustomizationRootLayout } from '@/components/RootLayout';
import type { GitBookSiteContext, GitBookSpaceContext } from '@/lib/context';
@@ -18,9 +17,6 @@ export async function PDFRootLayout(props: {
context={context}
forcedTheme={CustomizationDefaultThemeMode.Light}
>
<style href="pdf-print" precedence="blocks">
{PRINT_STYLES}
</style>
{children}
</CustomizationRootLayout>
);
@@ -1,6 +1,3 @@
// 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;
@@ -59,4 +56,3 @@ export const PRINT_STYLES = `
display: block !important;
}
}
`;
@@ -1,9 +1,6 @@
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';
@@ -72,14 +69,6 @@ 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;
@@ -26,12 +26,8 @@ import { AnnouncementDismissedScript } from '../Announcement';
import { SelectStateScript } from '../Select';
import { OperatingSystemClassScript } from './OperatingSystemClassScript';
import { RootLayoutClientContexts } from './RootLayoutClientContexts';
import {
DEFAULT_MONOSPACE_FONT,
type FontData,
generateEmojiFontFacesCSS,
getFontData,
} from '@/fonts';
import { type FontData, getFontData } from '@/fonts';
import { fontNotoColorEmoji, fonts } from '@/fonts/default';
import './globals.css';
import { getContentLocale, getSpaceLanguage } from '@/intl/server';
import { getAssetURL } from '@/lib/assets';
@@ -95,10 +91,12 @@ export async function CustomizationRootLayout(props: {
const fontData = getFontData(customization.styling.font, 'content');
// Temporarily add a if here while the cache is being warmed up.
// We can remove the condition after 14-07-2025.
const monospaceFontData = getFontData(
customization.styling.monospaceFont ?? DEFAULT_MONOSPACE_FONT,
'mono'
);
const monospaceFontData = customization.styling.monospaceFont
? getFontData(customization.styling.monospaceFont, 'mono')
: {
type: 'default' as const,
variable: fonts.IBMPlexMono.variable,
};
// Preconnect and preload custom fonts if needed
preloadFont(fontData);
@@ -129,8 +127,10 @@ export async function CustomizationRootLayout(props: {
sidebarStyles.list && `sidebar-list-${sidebarStyles.list}`,
'links' in customization.styling && `links-${customization.styling.links}`,
'depth' in customization.styling && `depth-${customization.styling.depth}`,
typeof customization.styling.font === 'string'
? `font-${customization.styling.font}`
fontNotoColorEmoji.variable,
monospaceFontData.type === 'default' ? monospaceFontData.variable : null,
fontData.type === 'default'
? [fontData.variable, `font-${customization.styling.font}`]
: null,
// Set the dark/light class statically to avoid flashing and make it work when JS is disabled
@@ -150,10 +150,11 @@ export async function CustomizationRootLayout(props: {
{/* Apply the visitor's content selection to <html> before first paint (no flash) */}
<SelectStateScript />
{/* Only the picked families, so the head never carries every font we support */}
<style>{generateEmojiFontFacesCSS()}</style>
<style>{fontData.fontFaceRules}</style>
<style>{monospaceFontData.fontFaceRules}</style>
{/* Inject custom font @font-face rules */}
{fontData.type === 'custom' ? <style>{fontData.fontFaceRules}</style> : null}
{monospaceFontData.type === 'custom' ? (
<style>{monospaceFontData.fontFaceRules}</style>
) : null}
{/* Inject a script to detect if the announcmeent banner has been dismissed */}
{'announcement' in customization && customization.announcement?.enabled ? (
@@ -552,104 +552,3 @@ 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;
}
}
}
@@ -75,9 +75,10 @@ export function SearchContainer({
},
{
enableOnFormTags: true,
// Match the produced key, not the physical key position, so non-QWERTY
// layouts don't trigger the shortcut by position. RND-11340.
useKey: true,
// Match the logical character typed, not the physical key position, so
// non-QWERTY layouts don't trigger the shortcut by position (e.g. on
// Dvorak the physical "K"/"I" keys produce other characters). RND-11340.
ignoreEventWhen: (e) => e.key.toLowerCase() !== 'k',
}
);
@@ -93,9 +94,9 @@ export function SearchContainer({
},
{
enableOnFormTags: true,
// Match the produced key so Dvorak ⌘-C (physical "I" key) copies
// Match the logical character so Dvorak ⌘-C (physical "I" key) copies
// instead of opening the Assistant. RND-11340.
useKey: true,
ignoreEventWhen: (e) => e.key.toLowerCase() !== 'i',
}
);
@@ -46,22 +46,11 @@ export const SearchInput = React.forwardRef<HTMLDivElement, SearchInputProps>(
} = props;
const inputRef = useRef<HTMLInputElement>(null);
const isFrame = mode === 'frame';
const isInitialMountRef = useRef(true);
const language = useLanguage();
useEffect(() => {
const isInitialMount = isInitialMountRef.current;
isInitialMountRef.current = false;
if (!isOpen) {
// A user can focus the SSR'd input natively before hydration attaches
// React's focus handler; syncing to "open" instead of blurring avoids
// stealing that focus back on mount.
if (isInitialMount && document.activeElement === inputRef.current) {
onFocus?.();
return;
}
inputRef.current?.blur();
return;
}
@@ -80,7 +69,7 @@ export const SearchInput = React.forwardRef<HTMLDivElement, SearchInputProps>(
const timeout = window.setTimeout(focusInput, 150);
return () => window.clearTimeout(timeout);
}, [isFrame, isOpen, value.length, onFocus]);
}, [isFrame, isOpen, value.length]);
return (
<div
@@ -0,0 +1,20 @@
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,6 +6,7 @@ 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
@@ -158,8 +159,8 @@ export function ZoomImage(
}}
className={classNames(
props.className,
zoomable ? 'zoom-img' : null,
active ? 'zoom-image-active' : null
zoomable ? styles.zoomImg : null,
active ? styles.zoomImageActive : null
)}
/>
)}
@@ -199,7 +200,7 @@ function ZoomImageModal(props: {
<div
data-testid="zoom-image-modal"
className={classNames(
'zoom-modal',
styles.zoomModal,
tcls(
'fixed',
'inset-0',
+285 -71
View File
@@ -1,83 +1,297 @@
import { CustomizationDefaultMonospaceFont } from '@gitbook/api';
import {
DM_Mono,
Fira_Code,
Fira_Sans_Extra_Condensed,
IBM_Plex_Mono,
IBM_Plex_Serif,
Inconsolata,
Inter,
JetBrains_Mono,
Lato,
Merriweather,
Noto_Color_Emoji,
Noto_Sans,
Open_Sans,
Overpass,
Poppins,
Raleway,
Roboto,
Roboto_Mono,
Roboto_Slab,
Source_Code_Pro,
Source_Sans_3,
Space_Mono,
Ubuntu,
} from 'next/font/google';
import localFont from 'next/font/local';
import { EMOJI_FONT, type FontName } from './definitions';
import faces from './generated/faces.json';
import type { FontFacesData, FontFamilyData } from './types';
import { getAssetURL } from '@/lib/assets';
import { CustomizationDefaultFont, CustomizationDefaultMonospaceFont } from '@gitbook/api';
const fontFaces = faces as FontFacesData;
export const fontNotoColorEmoji = Noto_Color_Emoji({
variable: '--font-noto-color-emoji',
weight: ['400'],
preload: false,
display: 'swap',
});
// The rules only vary by font, and every page renders a few dozen of them.
const cache = new Map<FontName, string>();
/*
Fonts are downloaded and loaded by next/font.
/** Used until the cache has warmed up for sites saved before the setting existed. */
export const DEFAULT_MONOSPACE_FONT = CustomizationDefaultMonospaceFont.IBMPlexMono;
We can't use "preload: true" as otherwise Next will preload all the fonts on the page
while spaces only use one font at a time.
*/
// Emitted per picked family and inlined in the head, so a page never carries the other 20 families
// the way a shared `next/font` stylesheet did.
export function generateDefaultFontFacesCSS(font: FontName): string {
const cached = cache.get(font);
if (cached !== undefined) {
return cached;
}
const inter = Inter({
weight: ['400', '500', '600', '700'],
variable: '--font-content',
preload: false,
display: 'swap',
fallback: ['system-ui', 'arial'],
});
const family = fontFaces[font];
if (!family) {
throw new Error(`Missing generated font faces for ${font}`);
}
const firaSans = Fira_Sans_Extra_Condensed({
weight: ['400', '500', '600', '700'],
variable: '--font-content',
preload: false,
display: 'swap',
fallback: ['system-ui', 'arial'],
});
const css = [
...family.variants.flatMap((variant) =>
variant.files.map((file, subset) =>
generateFace(family, variant.weight, variant.style, file, family.subsets[subset])
)
),
generateFallbackFace(family),
`:root { ${family.variable}: ${family.fontFamilyValue}; }`,
]
.filter(Boolean)
.join('\n');
const ibmPlexSerif = IBM_Plex_Serif({
weight: ['400', '500', '600', '700'],
variable: '--font-content',
preload: false,
display: 'swap',
fallback: ['serif'],
});
cache.set(font, css);
return css;
}
const lato = Lato({
weight: ['400', '700', '900'],
variable: '--font-content',
preload: false,
display: 'swap',
fallback: ['system-ui', 'arial'],
});
export function generateEmojiFontFacesCSS(): string {
return generateDefaultFontFacesCSS(EMOJI_FONT);
}
const merriweather = Merriweather({
weight: ['400', '700', '900'],
variable: '--font-content',
preload: false,
display: 'swap',
fallback: ['serif'],
});
function generateFace(
family: FontFamilyData,
weight: string,
style: string,
file: string,
unicodeRange: string | undefined
): string {
return `@font-face {
font-family: "${family.family}";
font-style: ${style};
font-weight: ${weight};
font-display: swap;${family.ascentOverride ? `\n ascent-override: ${family.ascentOverride};` : ''}
src: url(${getAssetURL(`fonts/${file}`)}) format("woff2");${
unicodeRange ? `\n unicode-range: ${unicodeRange};` : ''
}
}`;
}
const notoSans = Noto_Sans({
weight: ['400', '500', '600', '700'],
variable: '--font-content',
preload: false,
display: 'swap',
fallback: ['system-ui', 'arial'],
});
// A `local()` face with the real font's metrics: the page keeps the system font's glyphs but the
// picked font's proportions, so swapping it in barely shifts the layout.
function generateFallbackFace(family: FontFamilyData): string {
const fallback = family.fallbackFace;
if (!fallback) {
return '';
}
const openSans = Open_Sans({
weight: ['400', '500', '600', '700'],
variable: '--font-content',
preload: false,
display: 'swap',
fallback: ['system-ui', 'arial'],
});
return `@font-face {
font-family: "${fallback.family}";
src: local("${fallback.local}");
ascent-override: ${fallback.ascentOverride};
descent-override: ${fallback.descentOverride};
line-gap-override: ${fallback.lineGapOverride};
size-adjust: ${fallback.sizeAdjust};
}`;
}
const overpass = Overpass({
weight: ['400', '500', '600', '700'],
variable: '--font-content',
preload: false,
display: 'swap',
fallback: ['system-ui', 'arial'],
});
const poppins = Poppins({
weight: ['400', '500', '600', '700'],
variable: '--font-content',
preload: false,
display: 'swap',
fallback: ['system-ui', 'arial'],
});
const raleway = Raleway({
weight: ['400', '500', '600', '700'],
variable: '--font-content',
preload: false,
display: 'swap',
fallback: ['system-ui', 'arial'],
});
const roboto = Roboto({
weight: ['400', '500', '600', '700'],
variable: '--font-content',
preload: false,
display: 'swap',
fallback: ['system-ui', 'arial'],
});
const robotoSlab = Roboto_Slab({
weight: ['400', '500', '600', '700'],
variable: '--font-content',
preload: false,
display: 'swap',
fallback: ['system-ui', 'arial'],
});
const sourceSansPro = Source_Sans_3({
weight: ['400', '500', '600', '700'],
variable: '--font-content',
preload: false,
display: 'swap',
fallback: ['system-ui', 'arial'],
});
const ubuntu = Ubuntu({
weight: ['400', '500', '700'],
variable: '--font-content',
preload: false,
display: 'swap',
fallback: ['system-ui', 'arial'],
});
const abcFavorit = localFont({
variable: '--font-content',
preload: false,
display: 'swap',
fallback: ['system-ui', 'arial'],
src: [
{
path: './ABCFavorit/ABCFavorit-Variable.woff2',
weight: '400 700',
style: 'normal',
},
{
path: './ABCFavorit/ABCFavorit-BoldItalic.woff2',
weight: '700',
style: 'italic',
},
{
path: './ABCFavorit/ABCFavorit-MediumItalic.woff2',
weight: '500',
style: 'italic',
},
{
path: './ABCFavorit/ABCFavorit-RegularItalic.woff2',
weight: '400',
style: 'italic',
},
],
declarations: [{ prop: 'ascent-override', value: '100%' }],
});
const ibmPlexMono = IBM_Plex_Mono({
weight: ['400', '500', '600', '700'],
variable: '--font-mono',
style: 'normal',
display: 'swap',
preload: false,
fallback: ['monospace'],
adjustFontFallback: false,
});
const dmMono = DM_Mono({
weight: ['400', '500'],
variable: '--font-mono',
style: 'normal',
display: 'swap',
preload: false,
fallback: ['monospace'],
adjustFontFallback: false,
});
const firaCode = Fira_Code({
weight: ['400', '500', '600', '700'],
variable: '--font-mono',
style: 'normal',
display: 'swap',
preload: false,
fallback: ['monospace'],
adjustFontFallback: false,
});
const inconsolata = Inconsolata({
weight: ['400', '500', '600', '700'],
variable: '--font-mono',
style: 'normal',
display: 'swap',
preload: false,
fallback: ['monospace'],
adjustFontFallback: false,
});
const jetBrainsMono = JetBrains_Mono({
weight: ['400', '500', '600', '700'],
variable: '--font-mono',
style: 'normal',
display: 'swap',
preload: false,
fallback: ['monospace'],
adjustFontFallback: false,
});
const robotoMono = Roboto_Mono({
weight: ['400', '500', '600', '700'],
variable: '--font-mono',
style: 'normal',
display: 'swap',
preload: false,
fallback: ['monospace'],
adjustFontFallback: false,
});
const sourceCodePro = Source_Code_Pro({
weight: ['400', '500', '600', '700'],
variable: '--font-mono',
style: 'normal',
display: 'swap',
preload: false,
fallback: ['monospace'],
adjustFontFallback: false,
});
const spaceMono = Space_Mono({
weight: ['400', '700'],
variable: '--font-mono',
style: 'normal',
display: 'swap',
preload: false,
fallback: ['monospace'],
adjustFontFallback: false,
});
/**
* Font definitions.
*/
export const fonts: {
[fontName in CustomizationDefaultFont | CustomizationDefaultMonospaceFont]: {
variable: string;
};
} = {
[CustomizationDefaultFont.Inter]: inter,
[CustomizationDefaultFont.FiraSans]: firaSans,
[CustomizationDefaultFont.IBMPlexSerif]: ibmPlexSerif,
[CustomizationDefaultFont.Lato]: lato,
[CustomizationDefaultFont.Merriweather]: merriweather,
[CustomizationDefaultFont.NotoSans]: notoSans,
[CustomizationDefaultFont.OpenSans]: openSans,
[CustomizationDefaultFont.Overpass]: overpass,
[CustomizationDefaultFont.Poppins]: poppins,
[CustomizationDefaultFont.Raleway]: raleway,
[CustomizationDefaultFont.Roboto]: roboto,
[CustomizationDefaultFont.RobotoSlab]: robotoSlab,
[CustomizationDefaultFont.SourceSansPro]: sourceSansPro,
[CustomizationDefaultFont.Ubuntu]: ubuntu,
[CustomizationDefaultFont.ABCFavorit]: abcFavorit,
[CustomizationDefaultMonospaceFont.IBMPlexMono]: ibmPlexMono,
[CustomizationDefaultMonospaceFont.DMMono]: dmMono,
[CustomizationDefaultMonospaceFont.FiraCode]: firaCode,
[CustomizationDefaultMonospaceFont.Inconsolata]: inconsolata,
[CustomizationDefaultMonospaceFont.JetBrainsMono]: jetBrainsMono,
[CustomizationDefaultMonospaceFont.RobotoMono]: robotoMono,
[CustomizationDefaultMonospaceFont.SourceCodePro]: sourceCodePro,
[CustomizationDefaultMonospaceFont.SpaceMono]: spaceMono,
};
-178
View File
@@ -1,178 +0,0 @@
import { CustomizationDefaultFont, CustomizationDefaultMonospaceFont } from '@gitbook/api';
/** The emoji font is always loaded, alongside whichever content and monospace fonts a site picked. */
export const EMOJI_FONT = 'NotoColorEmoji';
export type FontName =
| CustomizationDefaultFont
| CustomizationDefaultMonospaceFont
| typeof EMOJI_FONT;
export interface FontDefinition {
/** `font-family` to declare the faces under. */
family: string;
/** Family id on Google Fonts, or `null` for the fonts we ship ourselves. */
googleId: string | null;
weights: string[];
/** CSS variable the family is exposed as. */
variable: '--font-content' | '--font-mono' | '--font-noto-color-emoji';
/** Families appended after the (metric-adjusted) fallback. */
fallback: string[];
/** Declare a `<family> Fallback` face with metric overrides, so the swap shifts layout as
* little as possible. Monospace fonts opt out, as they did under `next/font`. */
adjustFallback: boolean;
}
const content = (
family: string,
googleId: string,
weights: string[],
fallback: string[] = ['system-ui', 'arial']
): FontDefinition => ({
family,
googleId,
weights,
variable: '--font-content',
fallback,
adjustFallback: true,
});
const mono = (family: string, googleId: string, weights: string[]): FontDefinition => ({
family,
googleId,
weights,
variable: '--font-mono',
fallback: ['monospace'],
adjustFallback: false,
});
export const FONT_DEFINITIONS: Record<FontName, FontDefinition> = {
[CustomizationDefaultFont.Inter]: content('Inter', 'inter', ['400', '500', '600', '700']),
[CustomizationDefaultFont.FiraSans]: content(
'Fira Sans Extra Condensed',
'fira-sans-extra-condensed',
['400', '500', '600', '700']
),
[CustomizationDefaultFont.IBMPlexSerif]: content(
'IBM Plex Serif',
'ibm-plex-serif',
['400', '500', '600', '700'],
['serif']
),
[CustomizationDefaultFont.Lato]: content('Lato', 'lato', ['400', '700', '900']),
[CustomizationDefaultFont.Merriweather]: content(
'Merriweather',
'merriweather',
['400', '700', '900'],
['serif']
),
[CustomizationDefaultFont.NotoSans]: content('Noto Sans', 'noto-sans', [
'400',
'500',
'600',
'700',
]),
[CustomizationDefaultFont.OpenSans]: content('Open Sans', 'open-sans', [
'400',
'500',
'600',
'700',
]),
[CustomizationDefaultFont.Overpass]: content('Overpass', 'overpass', [
'400',
'500',
'600',
'700',
]),
[CustomizationDefaultFont.Poppins]: content('Poppins', 'poppins', ['400', '500', '600', '700']),
[CustomizationDefaultFont.Raleway]: content('Raleway', 'raleway', ['400', '500', '600', '700']),
[CustomizationDefaultFont.Roboto]: content('Roboto', 'roboto', ['400', '500', '600', '700']),
[CustomizationDefaultFont.RobotoSlab]: content('Roboto Slab', 'roboto-slab', [
'400',
'500',
'600',
'700',
]),
[CustomizationDefaultFont.SourceSansPro]: content('Source Sans 3', 'source-sans-3', [
'400',
'500',
'600',
'700',
]),
[CustomizationDefaultFont.Ubuntu]: content('Ubuntu', 'ubuntu', ['400', '500', '700']),
[CustomizationDefaultFont.ABCFavorit]: {
family: 'abcFavorit',
googleId: null,
weights: [],
variable: '--font-content',
fallback: ['system-ui', 'arial'],
adjustFallback: true,
},
[CustomizationDefaultMonospaceFont.IBMPlexMono]: mono('IBM Plex Mono', 'ibm-plex-mono', [
'400',
'500',
'600',
'700',
]),
[CustomizationDefaultMonospaceFont.DMMono]: mono('DM Mono', 'dm-mono', ['400', '500']),
[CustomizationDefaultMonospaceFont.FiraCode]: mono('Fira Code', 'fira-code', [
'400',
'500',
'600',
'700',
]),
[CustomizationDefaultMonospaceFont.Inconsolata]: mono('Inconsolata', 'inconsolata', [
'400',
'500',
'600',
'700',
]),
[CustomizationDefaultMonospaceFont.JetBrainsMono]: mono('JetBrains Mono', 'jetbrains-mono', [
'400',
'500',
'600',
'700',
]),
[CustomizationDefaultMonospaceFont.RobotoMono]: mono('Roboto Mono', 'roboto-mono', [
'400',
'500',
'600',
'700',
]),
[CustomizationDefaultMonospaceFont.SourceCodePro]: mono('Source Code Pro', 'source-code-pro', [
'400',
'500',
'600',
'700',
]),
[CustomizationDefaultMonospaceFont.SpaceMono]: mono('Space Mono', 'space-mono', ['400', '700']),
[EMOJI_FONT]: {
family: 'Noto Color Emoji',
googleId: 'noto-color-emoji',
weights: ['400'],
variable: '--font-noto-color-emoji',
fallback: [],
adjustFallback: true,
},
};
// `abcFavorit` is licensed, so it ships in the repo instead of coming from Google Fonts. The
// fallback metrics are the ones `next/font/local` measured from these exact files with fontkit.
export const ABC_FAVORIT = {
sources: [
{ file: 'ABCFavorit-Variable.woff2', weight: '400 700', style: 'normal' },
{ file: 'ABCFavorit-BoldItalic.woff2', weight: '700', style: 'italic' },
{ file: 'ABCFavorit-MediumItalic.woff2', weight: '500', style: 'italic' },
{ file: 'ABCFavorit-RegularItalic.woff2', weight: '400', style: 'italic' },
],
/** The design sits low in its em box; without this the fallback swap jumps vertically. */
ascentOverride: '100%',
fallbackMetrics: {
ascentOverride: '90.97%',
descentOverride: '37.34%',
lineGapOverride: '0.00%',
sizeAdjust: '104.43%',
},
} as const;
File diff suppressed because it is too large Load Diff
@@ -1,341 +0,0 @@
{
"google": {
"inter": {
"prefix": "https://fonts.gstatic.com/s/inter/v20",
"files": [
"UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7W0Q5n-wU.woff2",
"UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7W0Q5n-wU.woff2",
"UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7W0Q5n-wU.woff2",
"UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7W0Q5n-wU.woff2",
"UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7W0Q5n-wU.woff2",
"UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7W0Q5n-wU.woff2",
"UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7W0Q5nw.woff2"
]
},
"fira-sans-extra-condensed": {
"prefix": "https://fonts.gstatic.com/s/firasansextracondensed/v11",
"files": [
"NaPKcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda1fKuulWcrE5Hcg.woff2",
"NaPKcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda1fuuulWcrE5Hcg.woff2",
"NaPKcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda1fOuulWcrE5Hcg.woff2",
"NaPKcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda1fyuulWcrE5Hcg.woff2",
"NaPKcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda1fCuulWcrE5Hcg.woff2",
"NaPKcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda1fGuulWcrE5Hcg.woff2",
"NaPKcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda1f-uulWcrE4.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3QyNr3W-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3QyNr3y-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3QyNr3S-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3QyNr3u-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3QyNr3e-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3QyNr3a-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3QyNr3i-oWR9e2U.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3SCKr3W-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3SCKr3y-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3SCKr3S-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3SCKr3u-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3SCKr3e-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3SCKr3a-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3SCKr3i-oWR9e2U.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3USLr3W-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3USLr3y-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3USLr3S-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3USLr3u-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3USLr3e-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3USLr3a-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3USLr3i-oWR9e2U.woff2"
]
},
"ibm-plex-serif": {
"prefix": "https://fonts.gstatic.com/s/ibmplexserif/v20",
"files": [
"jizDREVNn1dOx-zrZ2X3pZvkTiUS2zcZiVbJsNo.woff2",
"jizDREVNn1dOx-zrZ2X3pZvkTiUb2zcZiVbJsNo.woff2",
"jizDREVNn1dOx-zrZ2X3pZvkTiUQ2zcZiVbJsNo.woff2",
"jizDREVNn1dOx-zrZ2X3pZvkTiUR2zcZiVbJsNo.woff2",
"jizDREVNn1dOx-zrZ2X3pZvkTiUf2zcZiVbJ.woff2",
"jizAREVNn1dOx-zrZ2X3pZvkTi3s-CI5q1vjitOh3oc.woff2",
"jizAREVNn1dOx-zrZ2X3pZvkTi3s-CIwq1vjitOh3oc.woff2",
"jizAREVNn1dOx-zrZ2X3pZvkTi3s-CI7q1vjitOh3oc.woff2",
"jizAREVNn1dOx-zrZ2X3pZvkTi3s-CI6q1vjitOh3oc.woff2",
"jizAREVNn1dOx-zrZ2X3pZvkTi3s-CI0q1vjitOh.woff2",
"jizAREVNn1dOx-zrZ2X3pZvkTi3A_yI5q1vjitOh3oc.woff2",
"jizAREVNn1dOx-zrZ2X3pZvkTi3A_yIwq1vjitOh3oc.woff2",
"jizAREVNn1dOx-zrZ2X3pZvkTi3A_yI7q1vjitOh3oc.woff2",
"jizAREVNn1dOx-zrZ2X3pZvkTi3A_yI6q1vjitOh3oc.woff2",
"jizAREVNn1dOx-zrZ2X3pZvkTi3A_yI0q1vjitOh.woff2",
"jizAREVNn1dOx-zrZ2X3pZvkTi2k_iI5q1vjitOh3oc.woff2",
"jizAREVNn1dOx-zrZ2X3pZvkTi2k_iIwq1vjitOh3oc.woff2",
"jizAREVNn1dOx-zrZ2X3pZvkTi2k_iI7q1vjitOh3oc.woff2",
"jizAREVNn1dOx-zrZ2X3pZvkTi2k_iI6q1vjitOh3oc.woff2",
"jizAREVNn1dOx-zrZ2X3pZvkTi2k_iI0q1vjitOh.woff2"
]
},
"lato": {
"prefix": "https://fonts.gstatic.com/s/lato/v25",
"files": [
"S6uyw4BMUTPHjxAwXiWtFCfQ7A.woff2",
"S6uyw4BMUTPHjx4wXiWtFCc.woff2",
"S6u9w4BMUTPHh6UVSwaPGQ3q5d0N7w.woff2",
"S6u9w4BMUTPHh6UVSwiPGQ3q5d0.woff2",
"S6u9w4BMUTPHh50XSwaPGQ3q5d0N7w.woff2",
"S6u9w4BMUTPHh50XSwiPGQ3q5d0.woff2"
]
},
"merriweather": {
"prefix": "https://fonts.gstatic.com/s/merriweather/v33",
"files": [
"u-4e0qyriQwlOrhSvowK_l5UcA6zuSYEqOzpPe3HOZJ5eX1WtLaQwmYiSeqnJ-mXq1Gi3iE.woff2",
"u-4e0qyriQwlOrhSvowK_l5UcA6zuSYEqOzpPe3HOZJ5eX1WtLaQwmYiSequJ-mXq1Gi3iE.woff2",
"u-4e0qyriQwlOrhSvowK_l5UcA6zuSYEqOzpPe3HOZJ5eX1WtLaQwmYiSeqlJ-mXq1Gi3iE.woff2",
"u-4e0qyriQwlOrhSvowK_l5UcA6zuSYEqOzpPe3HOZJ5eX1WtLaQwmYiSeqkJ-mXq1Gi3iE.woff2",
"u-4e0qyriQwlOrhSvowK_l5UcA6zuSYEqOzpPe3HOZJ5eX1WtLaQwmYiSeqqJ-mXq1Gi.woff2"
]
},
"noto-sans": {
"prefix": "https://fonts.gstatic.com/s/notosans/v42",
"files": [
"o-0bIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjc5aPdu3mhPy1Fig.woff2",
"o-0bIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjc5ardu3mhPy1Fig.woff2",
"o-0bIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjc5a_du3mhPy1Fig.woff2",
"o-0bIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjc5aLdu3mhPy1Fig.woff2",
"o-0bIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjc5a3du3mhPy1Fig.woff2",
"o-0bIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjc5aHdu3mhPy1Fig.woff2",
"o-0bIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjc5aDdu3mhPy1Fig.woff2",
"o-0bIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjc5a7du3mhPy0.woff2"
]
},
"open-sans": {
"prefix": "https://fonts.gstatic.com/s/opensans/v44",
"files": [
"memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu0SC55K5gw.woff2",
"memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu0SC55K5gw.woff2",
"memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu0SC55K5gw.woff2",
"memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu0SC55K5gw.woff2",
"memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu0SC55K5gw.woff2",
"memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu0SC55K5gw.woff2",
"memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu0SC55K5gw.woff2",
"memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu0SC55K5gw.woff2",
"memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu0SC55K5gw.woff2",
"memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-mu0SC55I.woff2"
]
},
"overpass": {
"prefix": "https://fonts.gstatic.com/s/overpass/v19",
"files": [
"qFdH35WCmI96Ajtm81GoU9vgwBcIs1s.woff2",
"qFdH35WCmI96Ajtm81GhU9vgwBcIs1s.woff2",
"qFdH35WCmI96Ajtm81GqU9vgwBcIs1s.woff2",
"qFdH35WCmI96Ajtm81GrU9vgwBcIs1s.woff2",
"qFdH35WCmI96Ajtm81GlU9vgwBcI.woff2"
]
},
"poppins": {
"prefix": "https://fonts.gstatic.com/s/poppins/v24",
"files": [
"pxiEyp8kv8JHgFVrJJbecnFHGPezSQ.woff2",
"pxiEyp8kv8JHgFVrJJnecnFHGPezSQ.woff2",
"pxiEyp8kv8JHgFVrJJfecnFHGPc.woff2",
"pxiByp8kv8JHgFVrLGT9Z11lFd2JQEl8qw.woff2",
"pxiByp8kv8JHgFVrLGT9Z1JlFd2JQEl8qw.woff2",
"pxiByp8kv8JHgFVrLGT9Z1xlFd2JQEk.woff2",
"pxiByp8kv8JHgFVrLEj6Z11lFd2JQEl8qw.woff2",
"pxiByp8kv8JHgFVrLEj6Z1JlFd2JQEl8qw.woff2",
"pxiByp8kv8JHgFVrLEj6Z1xlFd2JQEk.woff2",
"pxiByp8kv8JHgFVrLCz7Z11lFd2JQEl8qw.woff2",
"pxiByp8kv8JHgFVrLCz7Z1JlFd2JQEl8qw.woff2",
"pxiByp8kv8JHgFVrLCz7Z1xlFd2JQEk.woff2"
]
},
"raleway": {
"prefix": "https://fonts.gstatic.com/s/raleway/v37",
"files": [
"1Ptug8zYS_SKggPNyCAIT4ttDfCmxA.woff2",
"1Ptug8zYS_SKggPNyCkIT4ttDfCmxA.woff2",
"1Ptug8zYS_SKggPNyCIIT4ttDfCmxA.woff2",
"1Ptug8zYS_SKggPNyCMIT4ttDfCmxA.woff2",
"1Ptug8zYS_SKggPNyC0IT4ttDfA.woff2"
]
},
"roboto": {
"prefix": "https://fonts.gstatic.com/s/roboto/v51",
"files": [
"KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3GUBHMdazTgWw.woff2",
"KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3iUBHMdazTgWw.woff2",
"KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3CUBHMdazTgWw.woff2",
"KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3-UBHMdazTgWw.woff2",
"KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMawCUBHMdazTgWw.woff2",
"KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMaxKUBHMdazTgWw.woff2",
"KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3OUBHMdazTgWw.woff2",
"KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBHMdazTgWw.woff2",
"KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBHMdazQ.woff2"
]
},
"roboto-slab": {
"prefix": "https://fonts.gstatic.com/s/robotoslab/v36",
"files": [
"BngMUXZYTXPIvIBgJJSb6ufA5qWr4xCCQ_k.woff2",
"BngMUXZYTXPIvIBgJJSb6ufJ5qWr4xCCQ_k.woff2",
"BngMUXZYTXPIvIBgJJSb6ufB5qWr4xCCQ_k.woff2",
"BngMUXZYTXPIvIBgJJSb6ufO5qWr4xCCQ_k.woff2",
"BngMUXZYTXPIvIBgJJSb6ufC5qWr4xCCQ_k.woff2",
"BngMUXZYTXPIvIBgJJSb6ufD5qWr4xCCQ_k.woff2",
"BngMUXZYTXPIvIBgJJSb6ufN5qWr4xCC.woff2"
]
},
"source-sans-3": {
"prefix": "https://fonts.gstatic.com/s/sourcesans3/v19",
"files": [
"nwpStKy2OAdR1K-IwhWudF-R3wIaZejf5HdF8Q.woff2",
"nwpStKy2OAdR1K-IwhWudF-R3wsaZejf5HdF8Q.woff2",
"nwpStKy2OAdR1K-IwhWudF-R3wMaZejf5HdF8Q.woff2",
"nwpStKy2OAdR1K-IwhWudF-R3wwaZejf5HdF8Q.woff2",
"nwpStKy2OAdR1K-IwhWudF-R3wAaZejf5HdF8Q.woff2",
"nwpStKy2OAdR1K-IwhWudF-R3wEaZejf5HdF8Q.woff2",
"nwpStKy2OAdR1K-IwhWudF-R3w8aZejf5Hc.woff2"
]
},
"ubuntu": {
"prefix": "https://fonts.gstatic.com/s/ubuntu/v21",
"files": [
"4iCs6KVjbNBYlgoKcg72nU6AF7xm.woff2",
"4iCs6KVjbNBYlgoKew72nU6AF7xm.woff2",
"4iCs6KVjbNBYlgoKcw72nU6AF7xm.woff2",
"4iCs6KVjbNBYlgoKfA72nU6AF7xm.woff2",
"4iCs6KVjbNBYlgoKcQ72nU6AF7xm.woff2",
"4iCs6KVjbNBYlgoKfw72nU6AFw.woff2",
"4iCv6KVjbNBYlgoCjC3jvWyNPYZvg7UI.woff2",
"4iCv6KVjbNBYlgoCjC3jtGyNPYZvg7UI.woff2",
"4iCv6KVjbNBYlgoCjC3jvGyNPYZvg7UI.woff2",
"4iCv6KVjbNBYlgoCjC3js2yNPYZvg7UI.woff2",
"4iCv6KVjbNBYlgoCjC3jvmyNPYZvg7UI.woff2",
"4iCv6KVjbNBYlgoCjC3jsGyNPYZvgw.woff2",
"4iCv6KVjbNBYlgoCxCvjvWyNPYZvg7UI.woff2",
"4iCv6KVjbNBYlgoCxCvjtGyNPYZvg7UI.woff2",
"4iCv6KVjbNBYlgoCxCvjvGyNPYZvg7UI.woff2",
"4iCv6KVjbNBYlgoCxCvjs2yNPYZvg7UI.woff2",
"4iCv6KVjbNBYlgoCxCvjvmyNPYZvg7UI.woff2",
"4iCv6KVjbNBYlgoCxCvjsGyNPYZvgw.woff2"
]
},
"ibm-plex-mono": {
"prefix": "https://fonts.gstatic.com/s/ibmplexmono/v20",
"files": [
"-F63fjptAgt5VM-kVkqdyU8n1iIq131nj-otFQ.woff2",
"-F63fjptAgt5VM-kVkqdyU8n1isq131nj-otFQ.woff2",
"-F63fjptAgt5VM-kVkqdyU8n1iAq131nj-otFQ.woff2",
"-F63fjptAgt5VM-kVkqdyU8n1iEq131nj-otFQ.woff2",
"-F63fjptAgt5VM-kVkqdyU8n1i8q131nj-o.woff2",
"-F6qfjptAgt5VM-kVkqdyU8n3twJwl1FgsAXHNlYzg.woff2",
"-F6qfjptAgt5VM-kVkqdyU8n3twJwlRFgsAXHNlYzg.woff2",
"-F6qfjptAgt5VM-kVkqdyU8n3twJwl9FgsAXHNlYzg.woff2",
"-F6qfjptAgt5VM-kVkqdyU8n3twJwl5FgsAXHNlYzg.woff2",
"-F6qfjptAgt5VM-kVkqdyU8n3twJwlBFgsAXHNk.woff2",
"-F6qfjptAgt5VM-kVkqdyU8n3vAOwl1FgsAXHNlYzg.woff2",
"-F6qfjptAgt5VM-kVkqdyU8n3vAOwlRFgsAXHNlYzg.woff2",
"-F6qfjptAgt5VM-kVkqdyU8n3vAOwl9FgsAXHNlYzg.woff2",
"-F6qfjptAgt5VM-kVkqdyU8n3vAOwl5FgsAXHNlYzg.woff2",
"-F6qfjptAgt5VM-kVkqdyU8n3vAOwlBFgsAXHNk.woff2",
"-F6qfjptAgt5VM-kVkqdyU8n3pQPwl1FgsAXHNlYzg.woff2",
"-F6qfjptAgt5VM-kVkqdyU8n3pQPwlRFgsAXHNlYzg.woff2",
"-F6qfjptAgt5VM-kVkqdyU8n3pQPwl9FgsAXHNlYzg.woff2",
"-F6qfjptAgt5VM-kVkqdyU8n3pQPwl5FgsAXHNlYzg.woff2",
"-F6qfjptAgt5VM-kVkqdyU8n3pQPwlBFgsAXHNk.woff2"
]
},
"dm-mono": {
"prefix": "https://fonts.gstatic.com/s/dmmono/v16",
"files": [
"aFTU7PB1QTsUX8KYthSQBK6PYK3EXw.woff2",
"aFTU7PB1QTsUX8KYthqQBK6PYK0.woff2",
"aFTR7PB1QTsUX8KYvumzEY2tbYf-Vlh3uA.woff2",
"aFTR7PB1QTsUX8KYvumzEYOtbYf-Vlg.woff2"
]
},
"fira-code": {
"prefix": "https://fonts.gstatic.com/s/firacode/v27",
"files": [
"uU9NCBsR6Z2vfE9aq3bh0NSDqFGedCMX.woff2",
"uU9NCBsR6Z2vfE9aq3bh2dSDqFGedCMX.woff2",
"uU9NCBsR6Z2vfE9aq3bh0dSDqFGedCMX.woff2",
"uU9NCBsR6Z2vfE9aq3bh3tSDqFGedCMX.woff2",
"uU9NCBsR6Z2vfE9aq3bhZ_Wmh3mUfBsu_Q.woff2",
"uU9NCBsR6Z2vfE9aq3bh09SDqFGedCMX.woff2",
"uU9NCBsR6Z2vfE9aq3bh3dSDqFGedA.woff2"
]
},
"inconsolata": {
"prefix": "https://fonts.gstatic.com/s/inconsolata/v37",
"files": [
"QlddNThLqRwH-OJ1UHjlKENVzkWGVkL3GZQmAwLyxq15IDhunJ_o.woff2",
"QlddNThLqRwH-OJ1UHjlKENVzkWGVkL3GZQmAwLyx615IDhunJ_o.woff2",
"QlddNThLqRwH-OJ1UHjlKENVzkWGVkL3GZQmAwLyya15IDhunA.woff2"
]
},
"jetbrains-mono": {
"prefix": "https://fonts.gstatic.com/s/jetbrainsmono/v24",
"files": [
"tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPx3cwgknk-6nFg.woff2",
"tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPxTcwgknk-6nFg.woff2",
"tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPxPcwgknk-6nFg.woff2",
"tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPx_cwgknk-6nFg.woff2",
"tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPx7cwgknk-6nFg.woff2",
"tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPxDcwgknk-4.woff2"
]
},
"roboto-mono": {
"prefix": "https://fonts.gstatic.com/s/robotomono/v31",
"files": [
"L0x5DF4xlVMF-BfR8bXMIjhGq3-cXbKDO1w.woff2",
"L0x5DF4xlVMF-BfR8bXMIjhPq3-cXbKDO1w.woff2",
"L0x5DF4xlVMF-BfR8bXMIjhIq3-cXbKDO1w.woff2",
"L0x5DF4xlVMF-BfR8bXMIjhEq3-cXbKDO1w.woff2",
"L0x5DF4xlVMF-BfR8bXMIjhFq3-cXbKDO1w.woff2",
"L0x5DF4xlVMF-BfR8bXMIjhLq3-cXbKD.woff2"
]
},
"source-code-pro": {
"prefix": "https://fonts.gstatic.com/s/sourcecodepro/v31",
"files": [
"HI_SiYsKILxRpg3hIP6sJ7fM7PqlMOvWnsUnxlC9.woff2",
"HI_SiYsKILxRpg3hIP6sJ7fM7PqlOevWnsUnxlC9.woff2",
"HI_SiYsKILxRpg3hIP6sJ7fM7PqlMevWnsUnxlC9.woff2",
"HI_SiYsKILxRpg3hIP6sJ7fM7PqlPuvWnsUnxlC9.woff2",
"HI_SiYsKILxRpg3hIP6sJ7fM7PqlMuvWnsUnxlC9.woff2",
"HI_SiYsKILxRpg3hIP6sJ7fM7PqlM-vWnsUnxlC9.woff2",
"HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevWnsUnxg.woff2"
]
},
"space-mono": {
"prefix": "https://fonts.gstatic.com/s/spacemono/v17",
"files": [
"i7dPIFZifjKcF5UAWdDRYE58RXi4EwSsbg.woff2",
"i7dPIFZifjKcF5UAWdDRYE98RXi4EwSsbg.woff2",
"i7dPIFZifjKcF5UAWdDRYEF8RXi4EwQ.woff2",
"i7dMIFZifjKcF5UAWdDRaPpZUFqaHi6WZ3S_Yg.woff2",
"i7dMIFZifjKcF5UAWdDRaPpZUFuaHi6WZ3S_Yg.woff2",
"i7dMIFZifjKcF5UAWdDRaPpZUFWaHi6WZ3Q.woff2"
]
},
"noto-color-emoji": {
"prefix": "https://fonts.gstatic.com/s/notocoloremoji/v40",
"files": [
"Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4gw.0.woff2",
"Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4gw.1.woff2",
"Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4gw.2.woff2",
"Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4gw.3.woff2",
"Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4gw.4.woff2",
"Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4gw.5.woff2",
"Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4gw.6.woff2",
"Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4gw.7.woff2",
"Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4gw.8.woff2",
"Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4gw.9.woff2"
]
}
},
"local": {
"abcfavorit/7bb31c73cb8d820e.woff2": "./ABCFavorit/ABCFavorit-Variable.woff2",
"abcfavorit/aed849de6e333605.woff2": "./ABCFavorit/ABCFavorit-BoldItalic.woff2",
"abcfavorit/b435fb7330f5e171.woff2": "./ABCFavorit/ABCFavorit-MediumItalic.woff2",
"abcfavorit/aee27c224ccd5167.woff2": "./ABCFavorit/ABCFavorit-RegularItalic.woff2"
}
}
+7 -12
View File
@@ -5,32 +5,27 @@ import type {
} from '@gitbook/api';
import { generateFontFacesCSS, getFontSourcesToPreload } from './custom';
import { generateDefaultFontFacesCSS } from './default';
export { DEFAULT_MONOSPACE_FONT, generateEmojiFontFacesCSS } from './default';
import { fonts } from './default';
/**
* Represents font data for either a default font or a custom font
*/
export type FontData = DefaultFontData | CustomFontData;
interface BaseFontData {
/** `@font-face` rules and the CSS variable, to inline in the document head. */
fontFaceRules: string;
}
/**
* Font data for a default font, self-hosted from our own assets
* Font data for a default font, currently handle with next/font
*/
interface DefaultFontData extends BaseFontData {
interface DefaultFontData {
type: 'default';
variable: string;
}
/**
* Font data for a custom font with @font-face rules
*/
interface CustomFontData extends BaseFontData {
interface CustomFontData {
type: 'custom';
fontFaceRules: string;
preloadSources: CustomizationFontDefinition['fontFaces'];
}
@@ -44,7 +39,7 @@ export function getFontData(
if (typeof font === 'string') {
return {
type: 'default',
fontFaceRules: generateDefaultFontFacesCSS(font),
variable: fonts[font].variable,
};
}
-36
View File
@@ -1,36 +0,0 @@
export interface FontVariantData {
weight: string;
style: string;
/** Path under `~gitbook/static/fonts`, index-aligned with the family's `subsets`. */
files: string[];
}
export interface FontFallbackFaceData {
family: string;
local: string;
ascentOverride: string;
descentOverride: string;
lineGapOverride: string;
sizeAdjust: string;
}
export interface FontFamilyData {
family: string;
variable: string;
/** Full value for the CSS variable, fallbacks included. */
fontFamilyValue: string;
/** `unicode-range` per subset. Held once per family rather than repeated on every variant. */
subsets: string[];
variants: FontVariantData[];
fallbackFace: FontFallbackFaceData | null;
/** Applied to every face of the family. */
ascentOverride?: string;
}
export type FontFacesData = Record<string, FontFamilyData>;
/** Where `scripts/download-fonts.ts` fetches (or copies) each file from. */
export interface FontSourcesData {
google: Record<string, { prefix: string; files: string[] }>;
local: Record<string, string>;
}
+1 -3
View File
@@ -5,9 +5,7 @@
"outputs": [
"public/~gitbook/static/icons/**/*",
"public/~gitbook/static/math/**/*",
"public/~gitbook/static/embed/**/*",
"public/~gitbook/static/css/**/*",
"public/~gitbook/static/fonts/**/*"
"public/~gitbook/static/embed/**/*"
],
"dependsOn": ["^generate", "@gitbook/embed#build"]
},