Compare commits

..

33 Commits

Author SHA1 Message Date
Steven H 11ef360467 Merge branch 'main' into stevenh/fix-empty-href 2025-06-02 14:23:26 +01:00
conico974 7d5a6d2255 Fix hydration error on V2 (#3272)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-02 14:23:15 +01:00
Steven H 30e9e18ab4 Merge branch 'main' into stevenh/fix-empty-href 2025-06-02 10:28:56 +01:00
conico974 a1da4f023d Move DO to a 3rd worker (#3270)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-05-30 16:19:11 +02:00
conico974 666e8426e1 External middleware in cloudflare (#3254)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-05-30 14:56:00 +02:00
conico974 df2fa42bda Chores: create server worker (#3265)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-05-30 14:31:21 +02:00
conico974 b3a7ad6d2c Fix href being empty in TOC (#3269)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-05-28 17:07:33 +02:00
conico974 2932077bf9 Remove trailing slash from linker (#3268)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-05-27 21:13:55 +02:00
Nolann B. 957afd967f Add authorization header for OAuth2 (#3266) 2025-05-26 20:54:07 +02:00
Nolann B. 4c9a9d0416 Handle nested deprecated properties in generateSchemaExample (#3267) 2025-05-26 20:28:26 +02:00
Nolann B. 40df91a363 Deduplicate path parameters from OpenAPI spec (#3264) 2025-05-26 18:55:11 +02:00
conico974 af66ff71ed Add a force-revalidate api route (#3263)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-05-25 16:55:45 +02:00
Samy Pessé 6f8ecd6111 Use more strict context ID for images during preview (#3261) 2025-05-23 13:16:43 +02:00
conico974 fa3eb07617 Cache fonts and static image in memory for OG Image (#3258)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-05-23 10:20:07 +02:00
Samy Pessé dc4268db64 Compute correct urls for variants/sections when previewing unpublished site (#3259) 2025-05-23 10:01:15 +02:00
Nolann B. a0c06a72cc Indent JSON python code sample (#3260) 2025-05-23 09:13:20 +02:00
conico974 19c20c0be3 Fix OGImage icon fetch on cloudflare (#3253)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-05-21 15:39:52 +02:00
Viktor Renkema fae34e56ac Calculate class more explicitly & separately to fix horizontal table cell alignment issue (#3252) 2025-05-21 14:20:39 +02:00
Steven H fcae8b0bf3 Merge branch 'main' into stevenh/fix-empty-href 2025-05-21 09:42:34 +01:00
Steven Hall a47fb6c2bd changeset 2025-05-21 09:28:34 +01:00
Steven Hall d86eb47bf7 Fix invalid HTML on an Announcement banner without a CTA. 2025-05-21 09:27:58 +01:00
conico974 ba0094a862 fix ISR in preview in cloudflare (#3250)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-05-21 09:52:05 +02:00
conico974 b259009c9c Add temporary logging for Cloudflare debugging (#3249)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-05-20 19:33:33 +02:00
conico974 d88dd5ed8a Update OpenNext (#3248)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-05-20 19:04:07 +02:00
Zeno Kapitein 57bb146075 Dynamic TOC height (#3233) 2025-05-20 14:13:57 +02:00
Zeno Kapitein 4b67fe5317 V2: Add urlObject.hash to linker.toLinkForContent (#3246) 2025-05-20 10:25:35 +02:00
Samy Pessé 938125e772 Version Packages (#3180)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-19 19:10:57 +02:00
conico974 1c8d9febb3 Custom Incremental Cache (#3239)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-05-19 17:00:09 +02:00
conico974 223edae17d Add env variables for the DOQueue (#3243)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-05-19 13:47:33 +02:00
Nolann B. 12a455dab5 Fix OpenAPI layout issues (#3242) 2025-05-18 14:50:05 +02:00
Nolann B. d00dc8ca72 Pass spec validation errors through OpenAPIParseError (#3240) 2025-05-18 13:20:17 +02:00
Samy Pessé 2c85a302fc Update opennext and leverage "use cache" on CF (#3204)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-05-16 13:58:35 +01:00
Addison 3460b78b45 Update docs.gitbook.com to gitbook.com/docs (#3187)
Co-authored-by: Tal Gluck <talagluck@gmail.com>
Co-authored-by: Taran Vohra <taranvohra@outlook.com>
2025-05-16 17:59:53 +05:30
116 changed files with 2349 additions and 1441 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": minor
---
Fix images in reusable content across spaces.
-5
View File
@@ -1,5 +0,0 @@
---
'gitbook': patch
---
Fix openapi-select hover in responses
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
fix nested a tag causing hydration error
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
fix href being empty in TOC
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix navigation between sections/variants when previewing a site in v2
-5
View File
@@ -1,5 +0,0 @@
---
"@gitbook/colors": patch
---
Update chroma ratio per step
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook-v2": patch
---
Only resize images with supported extensions.
@@ -1,6 +1,5 @@
---
'@gitbook/react-openapi': patch
'gitbook': patch
---
Fix openapi CR preview
Add authorization header for OAuth2
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Decrease rounding of header logo
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook-v2": patch
---
add a force-revalidate api route to force bust the cache in case of errors
+6
View File
@@ -0,0 +1,6 @@
---
"gitbook-v2": patch
"gitbook": patch
---
Fix invalid HTML on an Announcement banner without a CTA.
-6
View File
@@ -1,6 +0,0 @@
---
"gitbook-v2": patch
"gitbook": patch
---
Fix URL in sitemap
-6
View File
@@ -1,6 +0,0 @@
---
"@gitbook/react-openapi": minor
"gitbook": minor
---
Design tweaks to code blocks and OpenAPI pages
-8
View File
@@ -1,8 +0,0 @@
---
"@gitbook/react-openapi": patch
"gitbook-v2": patch
"gitbook": patch
"@gitbook/colors": patch
---
Fix code highlighting for HTTP
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook-v2": patch
---
Fix crash on Cloudflare by using latest stable version of Next.js instead of canary
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix resolution of links in reusable contents
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook-v2": patch
---
fix ISR on preview env
-6
View File
@@ -1,6 +0,0 @@
---
"gitbook-v2": minor
"gitbook": minor
---
Add support for reusable content across spaces.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Better print layouts: wrap code blocks & force table column auto-sizing
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix invalid sitemap.xml generated with relative URLs instead of absolute ones
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Change OpenAPI schema-optional from `info` to `tint` color
-5
View File
@@ -1,5 +0,0 @@
---
"@gitbook/colors": patch
---
Change lightness check for color step 9 to allow input colors with a higher-than-needed contrast
@@ -2,4 +2,4 @@
'@gitbook/react-openapi': patch
---
Handle invalid OpenAPI Responses
Indent JSON python code sample
-6
View File
@@ -1,6 +0,0 @@
---
'@gitbook/react-openapi': patch
'gitbook': patch
---
Fix schemas disclosure label causing client error
-5
View File
@@ -1,5 +0,0 @@
---
'@gitbook/react-openapi': patch
---
Missing top-level required OpenAPI alternatives
-5
View File
@@ -1,5 +0,0 @@
---
'@gitbook/react-openapi': patch
---
Handle OpenAPI alternatives from schema.items
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Make TOC height dynamic based on visible header and footer elements
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook-v2": patch
---
Improve error messages around undefined site sections.
+6
View File
@@ -0,0 +1,6 @@
---
"gitbook": patch
"gitbook-v2": patch
---
cache fonts and static image used in OGImage in memory
-6
View File
@@ -1,6 +0,0 @@
---
"gitbook-v2": patch
"gitbook": patch
---
Increase logging around caching behaviour causing page crashes.
-5
View File
@@ -1,5 +0,0 @@
---
'@gitbook/react-openapi': patch
---
Use default value if string number or boolean in generateSchemaExample
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': patch
---
Handle nested deprecated properties in generateSchemaExample
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook-v2": patch
---
Add `urlObject.hash` to `linker.toLinkForContent` to pass through URL fragment identifiers, used in search
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Prevent section group popovers from opening on click
-6
View File
@@ -1,6 +0,0 @@
---
"gitbook": patch
"gitbook-v2": patch
---
Update the regex for validating site redirect
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Always link main logo to the root of the site
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Change `Dropdown`s to use Radix's `DropdownMenu`
-5
View File
@@ -1,5 +0,0 @@
---
"@gitbook/react-openapi": patch
---
Fix Python code sample "null vs None"
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Add border to filled sidebar on gradient theme
-6
View File
@@ -1,6 +0,0 @@
---
"gitbook-v2": minor
"gitbook": minor
---
Pass SVG images through image resizing without resizing them to serve them from optimal host.
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': patch
---
Deduplicate path parameters from OpenAPI spec
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook-v2": patch
---
remove trailing slash from linker
-6
View File
@@ -1,6 +0,0 @@
---
"gitbook": patch
"gitbook-v2": patch
---
Revert investigation work around URL caches.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix OpenAPI disclosure label ("Show properties") misalignment on mobile
@@ -0,0 +1,83 @@
name: Gradual Deploy to Cloudflare
description: Use gradual deployment to deploy to Cloudflare. This action will upload the middleware and server versions to Cloudflare and kept them bound together
inputs:
apiToken:
description: 'Cloudflare API token'
required: true
accountId:
description: 'Cloudflare account ID'
required: true
environment:
description: 'Cloudflare environment to deploy to (staging, production, preview)'
required: true
middlewareVersionId:
description: 'Middleware version ID to deploy'
required: true
serverVersionId:
description: 'Server version ID to deploy'
required: true
outputs:
deployment-url:
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.10.0'
environment: ${{ inputs.environment }}
command: deployments status --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
# This step is used to get the version ID that is currently deployed to Cloudflare.
- 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.10.0'
environment: ${{ inputs.environment }}
command: versions deploy ${{ steps.extract_current_version.outputs.version_id }}@100% ${{ inputs.serverVersionId }}@0% -y --config ./packages/gitbook-v2/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.10.0'
environment: ${{ inputs.environment }}
command: versions deploy ${{ inputs.middlewareVersionId }}@100% -y --config ./packages/gitbook-v2/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.10.0'
environment: ${{ inputs.environment }}
command: versions deploy ${{ inputs.serverVersionId }}@100% -y --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
- name: Outputs
shell: bash
env:
DEPLOYMENT_URL: ${{ steps.deploy_middleware.outputs.deployment-url }}
run: |
echo "URL: ${{ steps.deploy_middleware.outputs.deployment-url }}"
@@ -28,7 +28,7 @@ inputs:
outputs:
deployment-url:
description: "Deployment URL"
value: ${{ steps.deploy.outputs.deployment-url }}
value: ${{ steps.upload_middleware.outputs.deployment-url }}
runs:
using: 'composite'
steps:
@@ -63,8 +63,8 @@ runs:
env:
GITBOOK_RUNTIME: cloudflare
shell: bash
- id: deploy
name: Deploy to Cloudflare
- name: Upload the DO worker
uses: cloudflare/wrangler-action@v3.14.0
with:
apiToken: ${{ inputs.apiToken }}
@@ -72,10 +72,67 @@ runs:
workingDirectory: ./
wranglerVersion: '4.10.0'
environment: ${{ inputs.environment }}
command: ${{ inputs.deploy == 'true' && 'deploy' || format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook-v2/wrangler.jsonc
command: deploy --config ./packages/gitbook-v2/openNext/customWorkers/doWrangler.jsonc
- id: upload_server
name: Upload server to Cloudflare
uses: cloudflare/wrangler-action@v3.14.0
with:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.10.0'
environment: ${{ inputs.environment }}
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
- name: Extract server version worker ID
shell: bash
id: extract_server_version_id
run: |
version_id=$(echo '${{ steps.upload_server.outputs.command-output }}' | grep "Worker Version ID" | awk '{print $4}')
echo "version_id=$version_id" >> $GITHUB_OUTPUT
- name: Run updateWrangler scripts
shell: bash
run: |
bun run ./packages/gitbook-v2/openNext/customWorkers/script/updateWrangler.ts ${{ steps.extract_server_version_id.outputs.version_id }}
- id: upload_middleware
name: Upload middleware to Cloudflare
uses: cloudflare/wrangler-action@v3.14.0
with:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.10.0'
environment: ${{ inputs.environment }}
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc
- name: Extract middleware version worker ID
shell: bash
id: extract_middleware_version_id
run: |
version_id=$(echo '${{ steps.upload_middleware.outputs.command-output }}' | grep "Worker Version ID" | awk '{print $4}')
echo "version_id=$version_id" >> $GITHUB_OUTPUT
- name: Deploy server and middleware to Cloudflare
if: ${{ inputs.deploy == 'true' }}
uses: ./.github/actions/gradual-deploy-cloudflare
with:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
opServiceAccount: ${{ inputs.opServiceAccount }}
opItem: ${{ inputs.opItem }}
environment: ${{ inputs.environment }}
serverVersionId: ${{ steps.extract_server_version_id.outputs.version_id }}
middlewareVersionId: ${{ steps.extract_middleware_version_id.outputs.version_id }}
deploy: ${{ inputs.deploy }}
- name: Outputs
shell: bash
env:
DEPLOYMENT_URL: ${{ steps.deploy.outputs.deployment-url }}
DEPLOYMENT_URL: ${{ steps.upload_middleware.outputs.deployment-url }}
run: |
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
echo "URL: ${{ steps.upload_middleware.outputs.deployment-url }}"
echo "Output server: ${{ steps.upload_server.outputs.command-output }}"
+2 -2
View File
@@ -1,7 +1,7 @@
<h1 align="center">GitBook</h1>
<p align="center">
<a href="https://gitbook.com/docs">Docs</a> - <a href="https://github.com/GitbookIO/community">Community</a> - <a href="https://gitbook.com/docs/developers">Developer Docs</a> - <a href="https://changelog.gitbook.com/">Changelog</a> - <a href="https://github.com/GitbookIO/gitbook/issues/new?assignees=&labels=bug&template=bug_report.md">Bug reports</a>
<a href="https://gitbook.com/docs/">Docs</a> - <a href="https://github.com/GitbookIO/community">Community</a> - <a href="https://developer.gitbook.com/">Developer Docs</a> - <a href="https://changelog.gitbook.com/">Changelog</a> - <a href="https://github.com/GitbookIO/gitbook/issues/new?assignees=&labels=bug&template=bug_report.md">Bug reports</a>
</p>
<p align="center">
@@ -118,7 +118,7 @@ Encounter a bug or find an issue you'd like to fix? Helping us fix issues relate
## Deployment
> [!WARNING]
> [!WARNING]
> While it is possible to self-host this project, we do not recommend this unless you are certain this option fits your need.
>
> _Looking to add a specific feature in GitBook? Head to our [contributing guide](https://github.com/GitbookIO/gitbook/blob/main/.github/CONTRIBUTING.md) to get started._
+37 -31
View File
@@ -35,7 +35,7 @@
},
"packages/colors": {
"name": "@gitbook/colors",
"version": "0.3.2",
"version": "0.3.3",
"devDependencies": {
"typescript": "^5.5.3",
},
@@ -49,7 +49,7 @@
},
"packages/gitbook": {
"name": "gitbook",
"version": "0.11.1",
"version": "0.12.0",
"dependencies": {
"@gitbook/api": "^0.115.0",
"@gitbook/cache-do": "workspace:*",
@@ -141,10 +141,11 @@
},
"packages/gitbook-v2": {
"name": "gitbook-v2",
"version": "0.2.5",
"version": "0.3.0",
"dependencies": {
"@gitbook/api": "^0.115.0",
"@gitbook/cache-tags": "workspace:*",
"@opennextjs/cloudflare": "1.0.4",
"@sindresorhus/fnv1a": "^3.1.0",
"assert-never": "^1.2.1",
"jwt-decode": "^4.0.0",
@@ -156,7 +157,6 @@
"warn-once": "^0.1.1",
},
"devDependencies": {
"@opennextjs/cloudflare": "^1.0.0-beta.3",
"@types/rison": "^0.0.9",
"gitbook": "*",
"postcss": "^8",
@@ -185,7 +185,7 @@
},
"packages/openapi-parser": {
"name": "@gitbook/openapi-parser",
"version": "2.1.3",
"version": "2.1.4",
"dependencies": {
"@scalar/openapi-parser": "^0.10.10",
"@scalar/openapi-types": "^0.1.9",
@@ -232,7 +232,7 @@
},
"packages/react-openapi": {
"name": "@gitbook/react-openapi",
"version": "1.2.1",
"version": "1.3.0",
"dependencies": {
"@gitbook/openapi-parser": "workspace:*",
"@scalar/api-client-react": "^1.2.19",
@@ -791,9 +791,9 @@
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
"@opennextjs/aws": ["@opennextjs/aws@3.5.7", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.19.2", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-YjyHJrkIHI7YwQRCp8GjDOudu86oOc1RiwxvBBpPHrplsS18H4ZmkzGggAKhK6B4myGsJQ/q9kNP2TraoZiNzg=="],
"@opennextjs/aws": ["@opennextjs/aws@3.6.2", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.25.4", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-26/3GSoj7mKN7XpQFikYM2Lrwal5jlMc4fJO//QAdd5bCSnBaWBAkzr7+VvXAFVIC6eBeDLdtlWiQuUQVEAPZQ=="],
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.0.0-beta.3", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.5.7", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^3.114.3 || ^4.7.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-qKBXQZhUeQ+iGvfJeF7PO30g59LHnPOlRVZd77zxwn6Uc9C+c0LSwo8N28XRIWyQPkY007rKk9pSIxOrP4MHtQ=="],
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.0.4", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "^3.6.2", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.14.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-DVudGpOSJ91ruPiBJ0kuFmPhEQbXIXLTbvUjAx1OlbwFskG2gvdNIAmF3ZXV6z1VGDO7Q/u2W2ybMZLf7avlrA=="],
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
@@ -3649,7 +3649,7 @@
"@node-minify/core/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
"@opennextjs/aws/esbuild": ["esbuild@0.19.2", "", { "optionalDependencies": { "@esbuild/android-arm": "0.19.2", "@esbuild/android-arm64": "0.19.2", "@esbuild/android-x64": "0.19.2", "@esbuild/darwin-arm64": "0.19.2", "@esbuild/darwin-x64": "0.19.2", "@esbuild/freebsd-arm64": "0.19.2", "@esbuild/freebsd-x64": "0.19.2", "@esbuild/linux-arm": "0.19.2", "@esbuild/linux-arm64": "0.19.2", "@esbuild/linux-ia32": "0.19.2", "@esbuild/linux-loong64": "0.19.2", "@esbuild/linux-mips64el": "0.19.2", "@esbuild/linux-ppc64": "0.19.2", "@esbuild/linux-riscv64": "0.19.2", "@esbuild/linux-s390x": "0.19.2", "@esbuild/linux-x64": "0.19.2", "@esbuild/netbsd-x64": "0.19.2", "@esbuild/openbsd-x64": "0.19.2", "@esbuild/sunos-x64": "0.19.2", "@esbuild/win32-arm64": "0.19.2", "@esbuild/win32-ia32": "0.19.2", "@esbuild/win32-x64": "0.19.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-G6hPax8UbFakEj3hWO0Vs52LQ8k3lnBhxZWomUJDxfz3rZTLqF5k/FCzuNdLx2RbpBiQQF9H9onlDDH1lZsnjg=="],
"@opennextjs/aws/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="],
"@radix-ui/react-collection/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="],
@@ -4721,49 +4721,55 @@
"@node-minify/core/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
"@opennextjs/aws/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.19.2", "", { "os": "android", "cpu": "arm" }, "sha512-tM8yLeYVe7pRyAu9VMi/Q7aunpLwD139EY1S99xbQkT4/q2qa6eA4ige/WJQYdJ8GBL1K33pPFhPfPdJ/WzT8Q=="],
"@opennextjs/aws/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
"@opennextjs/aws/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.19.2", "", { "os": "android", "cpu": "arm64" }, "sha512-lsB65vAbe90I/Qe10OjkmrdxSX4UJDjosDgb8sZUKcg3oefEuW2OT2Vozz8ef7wrJbMcmhvCC+hciF8jY/uAkw=="],
"@opennextjs/aws/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="],
"@opennextjs/aws/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.19.2", "", { "os": "android", "cpu": "x64" }, "sha512-qK/TpmHt2M/Hg82WXHRc/W/2SGo/l1thtDHZWqFq7oi24AjZ4O/CpPSu6ZuYKFkEgmZlFoa7CooAyYmuvnaG8w=="],
"@opennextjs/aws/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.4", "", { "os": "android", "cpu": "arm64" }, "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A=="],
"@opennextjs/aws/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.19.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Ora8JokrvrzEPEpZO18ZYXkH4asCdc1DLdcVy8TGf5eWtPO1Ie4WroEJzwI52ZGtpODy3+m0a2yEX9l+KUn0tA=="],
"@opennextjs/aws/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.4", "", { "os": "android", "cpu": "x64" }, "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ=="],
"@opennextjs/aws/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.19.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-tP+B5UuIbbFMj2hQaUr6EALlHOIOmlLM2FK7jeFBobPy2ERdohI4Ka6ZFjZ1ZYsrHE/hZimGuU90jusRE0pwDw=="],
"@opennextjs/aws/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g=="],
"@opennextjs/aws/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.19.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-YbPY2kc0acfzL1VPVK6EnAlig4f+l8xmq36OZkU0jzBVHcOTyQDhnKQaLzZudNJQyymd9OqQezeaBgkTGdTGeQ=="],
"@opennextjs/aws/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A=="],
"@opennextjs/aws/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.19.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-nSO5uZT2clM6hosjWHAsS15hLrwCvIWx+b2e3lZ3MwbYSaXwvfO528OF+dLjas1g3bZonciivI8qKR/Hm7IWGw=="],
"@opennextjs/aws/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ=="],
"@opennextjs/aws/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.19.2", "", { "os": "linux", "cpu": "arm" }, "sha512-Odalh8hICg7SOD7XCj0YLpYCEc+6mkoq63UnExDCiRA2wXEmGlK5JVrW50vZR9Qz4qkvqnHcpH+OFEggO3PgTg=="],
"@opennextjs/aws/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ=="],
"@opennextjs/aws/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.19.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-ig2P7GeG//zWlU0AggA3pV1h5gdix0MA3wgB+NsnBXViwiGgY77fuN9Wr5uoCrs2YzaYfogXgsWZbm+HGr09xg=="],
"@opennextjs/aws/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.4", "", { "os": "linux", "cpu": "arm" }, "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ=="],
"@opennextjs/aws/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.19.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-mLfp0ziRPOLSTek0Gd9T5B8AtzKAkoZE70fneiiyPlSnUKKI4lp+mGEnQXcQEHLJAcIYDPSyBvsUbKUG2ri/XQ=="],
"@opennextjs/aws/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ=="],
"@opennextjs/aws/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.19.2", "", { "os": "linux", "cpu": "none" }, "sha512-hn28+JNDTxxCpnYjdDYVMNTR3SKavyLlCHHkufHV91fkewpIyQchS1d8wSbmXhs1fiYDpNww8KTFlJ1dHsxeSw=="],
"@opennextjs/aws/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ=="],
"@opennextjs/aws/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.19.2", "", { "os": "linux", "cpu": "none" }, "sha512-KbXaC0Sejt7vD2fEgPoIKb6nxkfYW9OmFUK9XQE4//PvGIxNIfPk1NmlHmMg6f25x57rpmEFrn1OotASYIAaTg=="],
"@opennextjs/aws/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA=="],
"@opennextjs/aws/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.19.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-dJ0kE8KTqbiHtA3Fc/zn7lCd7pqVr4JcT0JqOnbj4LLzYnp+7h8Qi4yjfq42ZlHfhOCM42rBh0EwHYLL6LEzcw=="],
"@opennextjs/aws/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg=="],
"@opennextjs/aws/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.19.2", "", { "os": "linux", "cpu": "none" }, "sha512-7Z/jKNFufZ/bbu4INqqCN6DDlrmOTmdw6D0gH+6Y7auok2r02Ur661qPuXidPOJ+FSgbEeQnnAGgsVynfLuOEw=="],
"@opennextjs/aws/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag=="],
"@opennextjs/aws/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.19.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-U+RinR6aXXABFCcAY4gSlv4CL1oOVvSSCdseQmGO66H+XyuQGZIUdhG56SZaDJQcLmrSfRmx5XZOWyCJPRqS7g=="],
"@opennextjs/aws/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA=="],
"@opennextjs/aws/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.19.2", "", { "os": "linux", "cpu": "x64" }, "sha512-oxzHTEv6VPm3XXNaHPyUTTte+3wGv7qVQtqaZCrgstI16gCuhNOtBXLEBkBREP57YTd68P0VgDgG73jSD8bwXQ=="],
"@opennextjs/aws/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g=="],
"@opennextjs/aws/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.19.2", "", { "os": "none", "cpu": "x64" }, "sha512-WNa5zZk1XpTTwMDompZmvQLHszDDDN7lYjEHCUmAGB83Bgs20EMs7ICD+oKeT6xt4phV4NDdSi/8OfjPbSbZfQ=="],
"@opennextjs/aws/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.4", "", { "os": "linux", "cpu": "x64" }, "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA=="],
"@opennextjs/aws/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.19.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-S6kI1aT3S++Dedb7vxIuUOb3oAxqxk2Rh5rOXOTYnzN8JzW1VzBd+IqPiSpgitu45042SYD3HCoEyhLKQcDFDw=="],
"@opennextjs/aws/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.4", "", { "os": "none", "cpu": "arm64" }, "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ=="],
"@opennextjs/aws/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.19.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-VXSSMsmb+Z8LbsQGcBMiM+fYObDNRm8p7tkUDMPG/g4fhFX5DEFmjxIEa3N8Zr96SjsJ1woAhF0DUnS3MF3ARw=="],
"@opennextjs/aws/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.4", "", { "os": "none", "cpu": "x64" }, "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw=="],
"@opennextjs/aws/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.19.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-5NayUlSAyb5PQYFAU9x3bHdsqB88RC3aM9lKDAz4X1mo/EchMIT1Q+pSeBXNgkfNmRecLXA0O8xP+x8V+g/LKg=="],
"@opennextjs/aws/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A=="],
"@opennextjs/aws/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.19.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-47gL/ek1v36iN0wL9L4Q2MFdujR0poLZMJwhO2/N3gA89jgHp4MR8DKCmwYtGNksbfJb9JoTtbkoe6sDhg2QTA=="],
"@opennextjs/aws/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw=="],
"@opennextjs/aws/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.2", "", { "os": "win32", "cpu": "x64" }, "sha512-tcuhV7ncXBqbt/Ybf0IyrMcwVOAPDckMK9rXNHtF17UTK18OKLpg08glminN06pt2WCoALhXdLfSPbVvK/6fxw=="],
"@opennextjs/aws/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q=="],
"@opennextjs/aws/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ=="],
"@opennextjs/aws/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg=="],
"@opennextjs/aws/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="],
"@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="],
+1 -1
View File
@@ -7,7 +7,7 @@
"turbo": "^2.5.0",
"vercel": "^39.3.0"
},
"packageManager": "bun@1.2.8",
"packageManager": "bun@1.2.11",
"overrides": {
"@codemirror/state": "6.4.1",
"@gitbook/api": "^0.115.0",
+8
View File
@@ -1,5 +1,13 @@
# @gitbook/colors
## 0.3.3
### Patch Changes
- c3f6b8c: Update chroma ratio per step
- 5e975ab: Fix code highlighting for HTTP
- f7a3470: Change lightness check for color step 9 to allow input colors with a higher-than-needed contrast
## 0.3.2
### Patch Changes
+1 -1
View File
@@ -8,7 +8,7 @@
"default": "./dist/index.js"
}
},
"version": "0.3.2",
"version": "0.3.3",
"devDependencies": {
"typescript": "^5.5.3"
},
+1
View File
@@ -6,6 +6,7 @@
# cloudflare
.open-next
.wrangler
# Symbolic links
public
+19
View File
@@ -1,5 +1,24 @@
# gitbook-v2
## 0.3.0
### Minor Changes
- 3119066: Add support for reusable content across spaces.
- 7d7806d: Pass SVG images through image resizing without resizing them to serve them from optimal host.
### Patch Changes
- 1c8d9fe: keep data cache in OpenNext between deployment
- 778624a: Only resize images with supported extensions.
- e6ddc0f: Fix URL in sitemap
- 5e975ab: Fix code highlighting for HTTP
- e15757d: Fix crash on Cloudflare by using latest stable version of Next.js instead of canary
- 634e0b4: Improve error messages around undefined site sections.
- 97b7c79: Increase logging around caching behaviour causing page crashes.
- 3f29206: Update the regex for validating site redirect
- dd043df: Revert investigation work around URL caches.
## 0.2.5
### Patch Changes
+1
View File
@@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+27 -16
View File
@@ -1,18 +1,29 @@
import { defineCloudflareConfig } from '@opennextjs/cloudflare';
import r2IncrementalCache from '@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache';
import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache';
import doQueue from '@opennextjs/cloudflare/overrides/queue/do-queue';
import doShardedTagCache from '@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache';
import type { OpenNextConfig } from '@opennextjs/cloudflare';
export default defineCloudflareConfig({
incrementalCache: withRegionalCache(r2IncrementalCache, { mode: 'long-lived' }),
tagCache: doShardedTagCache({
baseShardSize: 12,
regionalCache: true,
shardReplication: {
numberOfSoftReplicas: 2,
numberOfHardReplicas: 1,
export default {
default: {
override: {
wrapper: 'cloudflare-node',
converter: 'edge',
proxyExternalRequest: 'fetch',
queue: () => import('./openNext/queue/server').then((m) => m.default),
incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default),
tagCache: () => import('./openNext/tagCache/middleware').then((m) => m.default),
},
}),
queue: doQueue,
});
},
middleware: {
external: true,
override: {
wrapper: 'cloudflare-edge',
converter: 'edge',
proxyExternalRequest: 'fetch',
queue: () => import('./openNext/queue/middleware').then((m) => m.default),
incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default),
tagCache: () => import('./openNext/tagCache/middleware').then((m) => m.default),
},
},
dangerous: {
enableCacheInterception: true,
},
edgeExternals: ['node:crypto'],
} satisfies OpenNextConfig;
@@ -0,0 +1,36 @@
import { runWithCloudflareRequestContext } from '../../.open-next/cloudflare/init.js';
import { DurableObject } from 'cloudflare:workers';
// Only needed to run locally, in prod we'll use the one from do.js
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 default {
async fetch(request, env, ctx) {
return runWithCloudflareRequestContext(request, env, ctx, async () => {
// We can't move the handler import to the top level, otherwise the runtime will not be properly initialized
const { handler } = await import(
'../../.open-next/server-functions/default/handler.mjs'
);
// - `Request`s are handled by the Next server
return handler(request, env, ctx);
});
},
};
@@ -0,0 +1,163 @@
{
"main": "default.js",
"name": "gitbook-open-v2-server",
"compatibility_date": "2025-04-14",
"compatibility_flags": [
"nodejs_compat",
"allow_importable_env",
"global_fetch_strictly_public"
],
"observability": {
"enabled": true
},
"vars": {
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
},
"env": {
"dev": {
"vars": {
"STAGE": "dev"
},
"r2_buckets": [
{
"binding": "NEXT_INC_CACHE_R2_BUCKET",
"bucket_name": "gitbook-open-v2-cache-preview"
}
],
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "gitbook-open-v2-server-dev"
}
],
"durable_objects": {
"bindings": [
{
"name": "WRITE_BUFFER",
"class_name": "R2WriteBuffer"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["R2WriteBuffer"]
}
]
},
"preview": {
"vars": {
"STAGE": "preview",
// Just as a test for the preview environment to check that everything works
"NEXT_PRIVATE_DEBUG_CACHE": "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-server-preview"
}
],
"durable_objects": {
"bindings": [
{
"name": "WRITE_BUFFER",
"class_name": "R2WriteBuffer",
"script_name": "gitbook-open-v2-do-preview"
},
{
"name": "NEXT_TAG_CACHE_DO_SHARDED",
"class_name": "DOShardedTagCache",
"script_name": "gitbook-open-v2-do-preview"
},
{
"name": "NEXT_CACHE_DO_QUEUE",
"class_name": "DOQueueHandler",
"script_name": "gitbook-open-v2-do-preview"
}
]
}
},
"staging": {
"r2_buckets": [
{
"binding": "NEXT_INC_CACHE_R2_BUCKET",
"bucket_name": "gitbook-open-v2-cache-staging"
}
],
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "gitbook-open-v2-server-staging"
}
],
"durable_objects": {
"bindings": [
{
"name": "WRITE_BUFFER",
"class_name": "R2WriteBuffer",
"script_name": "gitbook-open-v2-do-staging"
},
{
"name": "NEXT_TAG_CACHE_DO_SHARDED",
"class_name": "DOShardedTagCache",
"script_name": "gitbook-open-v2-do-staging"
},
{
"name": "NEXT_CACHE_DO_QUEUE",
"class_name": "DOQueueHandler",
"script_name": "gitbook-open-v2-do-staging"
}
]
},
"tail_consumers": [
{
"service": "gitbook-x-staging-tail"
}
]
},
"production": {
"r2_buckets": [
{
"binding": "NEXT_INC_CACHE_R2_BUCKET",
"bucket_name": "gitbook-open-v2-cache-production"
}
],
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "gitbook-open-v2-server-production"
}
],
"durable_objects": {
"bindings": [
{
"name": "WRITE_BUFFER",
"class_name": "R2WriteBuffer",
"script_name": "gitbook-open-v2-do-production"
},
{
"name": "NEXT_TAG_CACHE_DO_SHARDED",
"class_name": "DOShardedTagCache",
"script_name": "gitbook-open-v2-do-production"
},
{
"name": "NEXT_CACHE_DO_QUEUE",
"class_name": "DOQueueHandler",
"script_name": "gitbook-open-v2-do-production"
}
]
},
"tail_consumers": [
{
"service": "gitbook-x-prod-tail"
}
]
}
}
}
@@ -0,0 +1,38 @@
// 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,127 @@
{
"main": "do.js",
"name": "gitbook-open-v2-do",
"compatibility_date": "2025-04-14",
"compatibility_flags": [
"nodejs_compat",
"allow_importable_env",
"global_fetch_strictly_public"
],
"observability": {
"enabled": true
},
"env": {
"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"
}
],
"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"]
}
]
},
"staging": {
"vars": {
"STAGE": "staging",
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
},
"r2_buckets": [
{
"binding": "NEXT_INC_CACHE_R2_BUCKET",
"bucket_name": "gitbook-open-v2-cache-staging"
}
],
"tail_consumers": [
{
"service": "gitbook-x-staging-tail"
}
],
"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"]
}
]
},
"production": {
"vars": {
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true",
"STAGE": "production"
},
"r2_buckets": [
{
"binding": "NEXT_INC_CACHE_R2_BUCKET",
"bucket_name": "gitbook-open-v2-cache-production"
}
],
"tail_consumers": [
{
"service": "gitbook-x-prod-tail"
}
],
"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"]
}
]
}
}
}
@@ -0,0 +1,42 @@
import { WorkerEntrypoint } from 'cloudflare:workers';
import { runWithCloudflareRequestContext } from '../../.open-next/cloudflare/init.js';
import { handler as middlewareHandler } from '../../.open-next/middleware/handler.mjs';
export { DOQueueHandler } from '../../.open-next/.build/durable-objects/queue.js';
export { DOShardedTagCache } from '../../.open-next/.build/durable-objects/sharded-tag-cache.js';
export default class extends WorkerEntrypoint {
async fetch(request) {
return runWithCloudflareRequestContext(request, this.env, this.ctx, async () => {
// - `Request`s are handled by the Next server
const reqOrResp = await middlewareHandler(request, this.env, this.ctx);
if (reqOrResp instanceof Response) {
return reqOrResp;
}
if (this.env.STAGE !== 'preview') {
// https://developers.cloudflare.com/workers/configuration/versions-and-deployments/gradual-deployments/#version-affinity
reqOrResp.headers.set(
'Cloudflare-Workers-Version-Overrides',
`gitbook-open-v2-${this.env.STAGE}="${this.env.WORKER_VERSION_ID}"`
);
return this.env.DEFAULT_WORKER?.fetch(reqOrResp, {
cf: {
cacheEverything: false,
},
});
}
// If we are in preview mode, we need to send the request to the preview URL
const modifiedUrl = new URL(reqOrResp.url);
modifiedUrl.hostname = this.env.PREVIEW_HOSTNAME;
const nextRequest = new Request(modifiedUrl, reqOrResp);
return fetch(nextRequest, {
cf: {
cacheEverything: false,
},
});
});
}
}
@@ -0,0 +1,216 @@
{
"main": "middleware.js",
"name": "gitbook-open-v2",
"compatibility_date": "2025-04-14",
"compatibility_flags": [
"nodejs_compat",
"allow_importable_env",
"global_fetch_strictly_public"
],
"assets": {
"directory": "../../.open-next/assets",
"binding": "ASSETS"
},
"observability": {
"enabled": true
},
"vars": {
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
},
"env": {
"dev": {
"vars": {
"STAGE": "dev",
"NEXT_PRIVATE_DEBUG_CACHE": "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-dev"
},
{
"binding": "DEFAULT_WORKER",
"service": "gitbook-open-v2-server-dev"
}
]
},
"preview": {
"vars": {
"STAGE": "preview",
"PREVIEW_HOSTNAME": "TO_REPLACE",
"WORKER_VERSION_ID": "TO_REPLACE"
},
"r2_buckets": [
{
"binding": "NEXT_INC_CACHE_R2_BUCKET",
"bucket_name": "gitbook-open-v2-cache-preview"
}
],
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "gitbook-open-v2-preview"
},
{
"binding": "DEFAULT_WORKER",
"service": "gitbook-open-v2-server-preview"
}
],
"durable_objects": {
"bindings": [
{
"name": "WRITE_BUFFER",
"class_name": "R2WriteBuffer",
"script_name": "gitbook-open-v2-do-preview"
},
{
"name": "NEXT_TAG_CACHE_DO_SHARDED",
"class_name": "DOShardedTagCache",
"script_name": "gitbook-open-v2-do-preview"
},
{
"name": "NEXT_CACHE_DO_QUEUE",
"class_name": "DOQueueHandler",
"script_name": "gitbook-open-v2-do-preview"
}
]
}
},
"staging": {
"vars": {
"STAGE": "staging",
"WORKER_VERSION_ID": "TO_REPLACE"
},
"routes": [
{
"pattern": "open-2c.gitbook-staging.com/*",
"zone_name": "gitbook-staging.com"
},
{
"pattern": "static-2c.gitbook-staging.com/*",
"zone_name": "gitbook-staging.com"
}
],
"r2_buckets": [
{
"binding": "NEXT_INC_CACHE_R2_BUCKET",
"bucket_name": "gitbook-open-v2-cache-staging"
}
],
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "gitbook-open-v2-staging"
},
{
"binding": "DEFAULT_WORKER",
"service": "gitbook-open-v2-server-staging"
}
],
"tail_consumers": [
{
"service": "gitbook-x-staging-tail"
}
],
"durable_objects": {
"bindings": [
{
"name": "WRITE_BUFFER",
"class_name": "R2WriteBuffer",
"script_name": "gitbook-open-v2-do-staging"
},
{
"name": "NEXT_TAG_CACHE_DO_SHARDED",
"class_name": "DOShardedTagCache",
"script_name": "gitbook-open-v2-do-staging"
},
{
"name": "NEXT_CACHE_DO_QUEUE",
"class_name": "DOQueueHandler",
"script_name": "gitbook-open-v2-do-staging"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache"]
}
]
},
"production": {
"vars": {
// This is a bit misleading, but it means that we can have 500 concurrent revalidations
// This means that we'll have up to 100 durable objects instance running at the same time
"MAX_REVALIDATE_CONCURRENCY": "100",
// Temporary variable to find the issue once deployed
// TODO: remove this once the issue is fixed
"DEBUG_CLOUDFLARE": "true",
"WORKER_VERSION_ID": "TO_REPLACE",
"STAGE": "production"
},
"routes": [
{
"pattern": "open-2c.gitbook.com/*",
"zone_name": "gitbook.com"
},
{
"pattern": "static-2c.gitbook.com/*",
"zone_name": "gitbook.com"
}
],
"r2_buckets": [
{
"binding": "NEXT_INC_CACHE_R2_BUCKET",
"bucket_name": "gitbook-open-v2-cache-production"
}
],
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "gitbook-open-v2-production"
},
{
"binding": "DEFAULT_WORKER",
"service": "gitbook-open-v2-server-production"
}
],
"tail_consumers": [
{
"service": "gitbook-x-prod-tail"
}
],
"durable_objects": {
"bindings": [
{
"name": "WRITE_BUFFER",
"class_name": "R2WriteBuffer",
"script_name": "gitbook-open-v2-do-production"
},
{
"name": "NEXT_TAG_CACHE_DO_SHARDED",
"class_name": "DOShardedTagCache",
"script_name": "gitbook-open-v2-do-production"
},
{
"name": "NEXT_CACHE_DO_QUEUE",
"class_name": "DOQueueHandler",
"script_name": "gitbook-open-v2-do-production"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache"]
}
]
}
}
}
@@ -0,0 +1,26 @@
// In this script, we use the args from the cli to update the PREVIEW_URL vars in the wrangler config file for the middleware
import fs from 'node:fs';
import path from 'node:path';
const wranglerConfigPath = path.join(__dirname, '../middlewareWrangler.jsonc');
const file = fs.readFileSync(wranglerConfigPath, 'utf-8');
const args = process.argv.slice(2);
// The versionId is in the format xxx-xxx-xxx-xxx, we need the first part to reconstruct the preview URL
const versionId = args[0];
// The preview URL is in the format https://<versionId>-gitbook-open-v2-server-preview.gitbook.workers.dev
const previewHostname = `${versionId.split('-')[0]}-gitbook-open-v2-server-preview.gitbook.workers.dev`;
let updatedFile = file.replace(
/"PREVIEW_HOSTNAME": "TO_REPLACE"/,
`"PREVIEW_HOSTNAME": "${previewHostname}"`
);
updatedFile = updatedFile.replaceAll(
/"WORKER_VERSION_ID": "TO_REPLACE"/g,
`"WORKER_VERSION_ID": "${versionId}"`
);
fs.writeFileSync(wranglerConfigPath, updatedFile);
@@ -0,0 +1,202 @@
import { createHash } from 'node:crypto';
import { trace } from '@/lib/tracing';
import type {
CacheEntryType,
CacheValue,
IncrementalCache,
WithLastModified,
} from '@opennextjs/aws/types/overrides.js';
import { getCloudflareContext } from '@opennextjs/cloudflare';
import type { DurableObjectNamespace, Rpc } from '@cloudflare/workers-types';
export const BINDING_NAME = 'NEXT_INC_CACHE_R2_BUCKET';
export const DEFAULT_PREFIX = 'incremental-cache';
export type KeyOptions = {
cacheType?: CacheEntryType;
};
/**
*
* It is very similar to the `R2IncrementalCache` in the `@opennextjs/cloudflare` package, but it allow us to trace
* the cache operations. It also integrates both R2 and Cache API in a single class.
* Having our own, will allow us to customize it in the future if needed.
*/
class GitbookIncrementalCache implements IncrementalCache {
name = 'GitbookIncrementalCache';
protected localCache: Cache | undefined;
async get<CacheType extends CacheEntryType = 'cache'>(
key: string,
cacheType?: CacheType
): Promise<WithLastModified<CacheValue<CacheType>> | null> {
const cacheKey = this.getR2Key(key, cacheType);
return trace(
{
operation: 'openNextIncrementalCacheGet',
name: cacheKey,
},
async (span) => {
span.setAttribute('cacheType', cacheType ?? 'cache');
const r2 = getCloudflareContext().env[BINDING_NAME];
const localCache = await this.getCacheInstance();
if (!r2) throw new Error('No R2 bucket');
try {
// Check local cache first if available
const localCacheEntry = await localCache.match(this.getCacheUrlKey(cacheKey));
if (localCacheEntry) {
span.setAttribute('cacheHit', 'local');
return localCacheEntry.json();
}
const r2Object = await r2.get(cacheKey);
if (!r2Object) return null;
span.setAttribute('cacheHit', 'r2');
return {
value: await r2Object.json(),
lastModified: r2Object.uploaded.getTime(),
};
} catch (e) {
console.error('Failed to get from cache', e);
return null;
}
}
);
}
async set<CacheType extends CacheEntryType = 'cache'>(
key: string,
value: CacheValue<CacheType>,
cacheType?: CacheType
): Promise<void> {
const cacheKey = this.getR2Key(key, cacheType);
return trace(
{
operation: 'openNextIncrementalCacheSet',
name: cacheKey,
},
async (span) => {
span.setAttribute('cacheType', cacheType ?? 'cache');
const localCache = await this.getCacheInstance();
try {
await this.writeToR2(cacheKey, JSON.stringify(value));
//TODO: Check if there is any places where we don't have tags
// Ideally we should always have tags, but in case we don't, we need to decide how to handle it
// For now we default to a build ID tag, which allow us to invalidate the cache in case something is wrong in this deployment
const tags = this.getTagsFromCacheEntry(value) ?? [
`build_id/${process.env.NEXT_BUILD_ID}`,
];
// We consider R2 as the source of truth, so we update the local cache
// only after a successful R2 write
await localCache.put(
this.getCacheUrlKey(cacheKey),
new Response(
JSON.stringify({
value,
// Note: `Date.now()` returns the time of the last IO rather than the actual time.
// See https://developers.cloudflare.com/workers/reference/security-model/
lastModified: Date.now(),
}),
{
headers: {
// Cache-Control default to 30 minutes, will be overridden by `revalidate`
// In theory we should always get the `revalidate` value
'cache-control': `max-age=${value.revalidate ?? 60 * 30}`,
'cache-tag': tags.join(','),
},
}
)
);
} catch (e) {
console.error('Failed to set to cache', e);
}
}
);
}
async delete(key: string): Promise<void> {
const cacheKey = this.getR2Key(key);
return trace(
{
operation: 'openNextIncrementalCacheDelete',
name: cacheKey,
},
async () => {
const r2 = getCloudflareContext().env[BINDING_NAME];
const localCache = await this.getCacheInstance();
if (!r2) throw new Error('No R2 bucket');
try {
await r2.delete(cacheKey);
// Here again R2 is the source of truth, so we delete from local cache first
await localCache.delete(this.getCacheUrlKey(cacheKey));
} catch (e) {
console.error('Failed to delete from cache', e);
}
}
);
}
async writeToR2(key: string, value: string): Promise<void> {
const env = getCloudflareContext().env as {
WRITE_BUFFER: DurableObjectNamespace<
Rpc.DurableObjectBranded & {
write: (key: string, value: string) => Promise<void>;
}
>;
};
const id = env.WRITE_BUFFER.idFromName(key);
// A stub is a client used to invoke methods on the Durable Object
const stub = env.WRITE_BUFFER.get(id);
await stub.write(key, value);
}
async getCacheInstance(): Promise<Cache> {
if (this.localCache) return this.localCache;
this.localCache = await caches.open('incremental-cache');
return this.localCache;
}
// Utility function to generate keys for R2/Cache API
getR2Key(key: string, cacheType: CacheEntryType = 'cache'): string {
const hash = createHash('sha256').update(key).digest('hex');
return `${DEFAULT_PREFIX}/${cacheType === 'cache' ? process.env?.NEXT_BUILD_ID : 'dataCache'}/${hash}.${cacheType}`.replace(
/\/+/g,
'/'
);
}
getCacheUrlKey(cacheKey: string): string {
return `http://cache.local/${cacheKey}`;
}
getTagsFromCacheEntry<CacheType extends CacheEntryType>(
entry: CacheValue<CacheType>
): string[] | undefined {
if ('tags' in entry && entry.tags) {
return entry.tags;
}
if ('meta' in entry && entry.meta && 'headers' in entry.meta && entry.meta.headers) {
const rawTags = entry.meta.headers['x-next-cache-tags'];
if (typeof rawTags === 'string') {
return rawTags.split(',');
}
}
if ('value' in entry) {
return entry.tags;
}
}
}
export default new GitbookIncrementalCache();
@@ -0,0 +1,21 @@
import { trace } from '@/lib/tracing';
import type { Queue } from '@opennextjs/aws/types/overrides.js';
import { getCloudflareContext } from '@opennextjs/cloudflare';
import doQueue from '@opennextjs/cloudflare/overrides/queue/do-queue';
import memoryQueue from '@opennextjs/cloudflare/overrides/queue/memory-queue';
interface Env {
STAGE?: string;
}
export default {
name: 'GitbookISRQueue',
send: async (msg) => {
return trace({ operation: 'gitbookISRQueueSend', name: msg.MessageBody.url }, async () => {
const { ctx, env } = getCloudflareContext();
const hasDurableObject =
(env as Env).STAGE !== 'dev' && (env as Env).STAGE !== 'preview';
ctx.waitUntil(hasDurableObject ? memoryQueue.send(msg) : doQueue.send(msg));
});
},
} satisfies Queue;
@@ -0,0 +1,9 @@
import type { Queue } from '@opennextjs/aws/types/overrides.js';
export default {
name: 'GitbookISRQueue',
send: async (msg) => {
// We should never reach this point in the server. If that's the case, we should log it.
console.warn('GitbookISRQueue: send called on server side, this should not happen.', msg);
},
} satisfies Queue;
@@ -0,0 +1,78 @@
import { trace } from '@/lib/tracing';
import type { NextModeTagCache } 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';
const originalTagCache = doShardedTagCache({
baseShardSize: 12,
regionalCache: true,
// We can probably increase this value even further
regionalCacheTtlSec: 60,
shardReplication: {
numberOfSoftReplicas: 2,
numberOfHardReplicas: 1,
},
});
export default {
name: 'GitbookTagCache',
mode: 'nextMode',
getLastRevalidated: async (tags: string[]) => {
const tagsToCheck = tags.filter(softTagFilter);
if (tagsToCheck.length === 0) {
// If we reach here, it probably means that there is an issue that we'll need to address.
console.warn(
'getLastRevalidated - No valid tags to check for last revalidation, original tags:',
tags
);
return 0; // If no tags to check, return 0
}
return trace(
{
operation: 'gitbookTagCacheGetLastRevalidated',
name: tagsToCheck.join(', '),
},
async () => {
return await originalTagCache.getLastRevalidated(tagsToCheck);
}
);
},
hasBeenRevalidated: async (tags: string[], lastModified?: number) => {
const tagsToCheck = tags.filter(softTagFilter);
if (tagsToCheck.length === 0) {
// If we reach here, it probably means that there is an issue that we'll need to address.
console.warn(
'hasBeenRevalidated - No valid tags to check for revalidation, original tags:',
tags
);
return false; // If no tags to check, return false
}
return trace(
{
operation: 'gitbookTagCacheHasBeenRevalidated',
name: tagsToCheck.join(', '),
},
async () => {
const result = await originalTagCache.hasBeenRevalidated(tagsToCheck, lastModified);
return result;
}
);
},
writeTags: async (tags: string[]) => {
return trace(
{
operation: 'gitbookTagCacheWriteTags',
name: tags.join(', '),
},
async () => {
const tagsToWrite = tags.filter(softTagFilter);
if (tagsToWrite.length === 0) {
console.warn('writeTags - No valid tags to write');
return; // If no tags to write, exit early
}
// Write only the filtered tags
await originalTagCache.writeTags(tagsToWrite);
}
);
},
} satisfies NextModeTagCache;
+5 -3
View File
@@ -1,10 +1,11 @@
{
"name": "gitbook-v2",
"version": "0.2.5",
"version": "0.3.0",
"private": true,
"dependencies": {
"@gitbook/api": "^0.115.0",
"@gitbook/cache-tags": "workspace:*",
"@opennextjs/cloudflare": "1.0.4",
"@sindresorhus/fnv1a": "^3.1.0",
"assert-never": "^1.2.1",
"jwt-decode": "^4.0.0",
@@ -17,7 +18,6 @@
},
"devDependencies": {
"gitbook": "*",
"@opennextjs/cloudflare": "^1.0.0-beta.3",
"@types/rison": "^0.0.9",
"tailwindcss": "^3.4.0",
"postcss": "^8"
@@ -29,7 +29,9 @@
"build:v2": "next build",
"start": "next start",
"build:v2:cloudflare": "opennextjs-cloudflare build",
"dev:v2:cloudflare": "wrangler dev --port 8771",
"dev:v2:cloudflare": "wrangler dev --port 8771 --env preview",
"dev:v2:cf:middleware": "wrangler dev --port 8771 --inspector-port 9230 --env dev --config ./openNext/customWorkers/middlewareWrangler.jsonc",
"dev:v2:cf:server": "wrangler dev --port 8772 --env dev --config ./openNext/customWorkers/defaultWrangler.jsonc",
"unit": "bun test",
"typecheck": "tsc --noEmit"
}
+1 -1
View File
@@ -165,7 +165,7 @@ export function getBaseContext(input: {
// Create link in the same format for links to other sites/sections.
linker.toLinkForContent = (rawURL: string) => {
const urlObject = new URL(rawURL);
return `/url/${urlObject.host}${urlObject.pathname}${urlObject.search}`;
return `/url/${urlObject.host}${urlObject.pathname}${urlObject.search}${urlObject.hash}`;
};
}
File diff suppressed because it is too large Load Diff
@@ -4,5 +4,4 @@ export * from './pages';
export * from './urls';
export * from './errors';
export * from './lookup';
export * from './proxy';
export * from './visitor';
+1 -1
View File
@@ -1,6 +1,6 @@
import { withLeadingSlash, withTrailingSlash } from '@/lib/paths';
import type { PublishedSiteContent } from '@gitbook/api';
import { getProxyRequestIdentifier, isProxyRequest } from './proxy';
import { getProxyRequestIdentifier, isProxyRequest } from '@v2/lib/proxy';
/**
* Get the appropriate base path for the visitor authentication cookie.
@@ -0,0 +1,19 @@
import { describe, expect, it } from 'bun:test';
import { getImageResizingContextId } from './getImageResizingContextId';
describe('getImageResizingContextId', () => {
it('should return proxy identifier for proxy requests', () => {
const proxyRequestURL = new URL('https://proxy.gitbook.site/sites/site_foo/hello/world');
expect(getImageResizingContextId(proxyRequestURL)).toBe('sites/site_foo');
});
it('should return preview identifier for preview requests', () => {
const previewRequestURL = new URL('https://preview/site_foo/hello/world');
expect(getImageResizingContextId(previewRequestURL)).toBe('site_foo');
});
it('should return host for regular requests', () => {
const regularRequestURL = new URL('https://example.com/docs/foo/hello/world');
expect(getImageResizingContextId(regularRequestURL)).toBe('example.com');
});
});
@@ -1,4 +1,5 @@
import { getProxyRequestIdentifier, isProxyRequest } from '../data';
import { getPreviewRequestIdentifier, isPreviewRequest } from '@v2/lib/preview';
import { getProxyRequestIdentifier, isProxyRequest } from '@v2/lib/proxy';
/**
* Get the site identifier to use for image resizing for an incoming request.
@@ -8,6 +9,9 @@ export function getImageResizingContextId(url: URL): string {
if (isProxyRequest(url)) {
return getProxyRequestIdentifier(url);
}
if (isPreviewRequest(url)) {
return getPreviewRequestIdentifier(url);
}
return url.host;
}
+21 -1
View File
@@ -19,7 +19,7 @@ const siteGitBookIO = createLinker({
siteBasePath: '/sitename/',
});
describe('toPathInContent', () => {
describe('toPathInSpace', () => {
it('should return the correct path', () => {
expect(root.toPathInSpace('some/path')).toBe('/some/path');
expect(variantInSection.toPathInSpace('some/path')).toBe('/section/variant/some/path');
@@ -29,6 +29,16 @@ describe('toPathInContent', () => {
expect(root.toPathInSpace('/some/path')).toBe('/some/path');
expect(variantInSection.toPathInSpace('/some/path')).toBe('/section/variant/some/path');
});
it('should remove the trailing slash', () => {
expect(root.toPathInSpace('some/path/')).toBe('/some/path');
expect(variantInSection.toPathInSpace('some/path/')).toBe('/section/variant/some/path');
});
it('should not add a trailing slash', () => {
expect(root.toPathInSpace('')).toBe('');
expect(variantInSection.toPathInSpace('')).toBe('/section/variant');
});
});
describe('toPathInSite', () => {
@@ -36,6 +46,16 @@ describe('toPathInSite', () => {
expect(root.toPathInSite('some/path')).toBe('/some/path');
expect(siteGitBookIO.toPathInSite('some/path')).toBe('/sitename/some/path');
});
it('should remove the trailing slash', () => {
expect(root.toPathInSite('some/path/')).toBe('/some/path');
expect(siteGitBookIO.toPathInSite('some/path/')).toBe('/sitename/some/path');
});
it('should not add a trailing slash', () => {
expect(root.toPathInSite('')).toBe('');
expect(siteGitBookIO.toPathInSite('')).toBe('/sitename');
});
});
describe('toRelativePathInSite', () => {
+5 -1
View File
@@ -128,5 +128,9 @@ export function createLinker(
function joinPaths(prefix: string, path: string): string {
const prefixPath = prefix.endsWith('/') ? prefix : `${prefix}/`;
const suffixPath = path.startsWith('/') ? path.slice(1) : path;
return prefixPath + suffixPath;
return removeTrailingSlash(prefixPath + suffixPath);
}
function removeTrailingSlash(path: string): string {
return path.endsWith('/') ? path.slice(0, -1) : path;
}
@@ -0,0 +1,21 @@
import { describe, expect, it } from 'bun:test';
import { getPreviewRequestIdentifier, isPreviewRequest } from './preview';
describe('isPreviewRequest', () => {
it('should return true for preview requests', () => {
const previewRequestURL = new URL('https://preview/site_foo/hello/world');
expect(isPreviewRequest(previewRequestURL)).toBe(true);
});
it('should return false for non-preview requests', () => {
const nonPreviewRequestURL = new URL('https://example.com/docs/foo/hello/world');
expect(isPreviewRequest(nonPreviewRequestURL)).toBe(false);
});
});
describe('getPreviewRequestIdentifier', () => {
it('should return the correct identifier for preview requests', () => {
const previewRequestURL = new URL('https://preview/site_foo/hello/world');
expect(getPreviewRequestIdentifier(previewRequestURL)).toBe('site_foo');
});
});
+13
View File
@@ -0,0 +1,13 @@
/**
* Check if the request to the site is a preview request.
*/
export function isPreviewRequest(requestURL: URL): boolean {
return requestURL.host === 'preview';
}
export function getPreviewRequestIdentifier(requestURL: URL): string {
// For preview requests, we extract the site ID from the pathname
// e.g. https://preview/site_id/...
const pathname = requestURL.pathname.slice(1).split('/');
return pathname[0];
}
+2 -1
View File
@@ -104,6 +104,7 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
const resolve = ADAPTIVE_CONTENT_HOSTS.includes(siteRequestURL.hostname)
? resolvePublishedContentByUrl
: getPublishedContentByURL;
const siteURLData = await throwIfDataError(
resolve({
url: siteRequestURL.toString(),
@@ -290,7 +291,7 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
return writeResponseCookies(response, cookies);
};
// For https://preview/<siteURL> requests,
// For https://preview/<siteId> requests,
if (siteRequestURL.hostname === 'preview') {
return serveWithQueryAPIToken(
// We scope the API token to the site ID.
@@ -0,0 +1,49 @@
import crypto from 'node:crypto';
import type { NextApiRequest, NextApiResponse } from 'next';
interface JsonBody {
// The paths need to be the rewritten one, `res.revalidate` call don't go through the middleware
paths: string[];
}
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// Only allow POST requests
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
const signatureHeader = req.headers['x-gitbook-signature'] as string | undefined;
if (!signatureHeader) {
return res.status(400).json({ error: 'Missing signature header' });
}
// We cannot use env from `@/v2/lib/env` here as it make it crash because of the import "server-only" in the file.
if (process.env.GITBOOK_SECRET) {
try {
const computedSignature = crypto
.createHmac('sha256', process.env.GITBOOK_SECRET)
.update(JSON.stringify(req.body))
.digest('hex');
if (computedSignature === signatureHeader) {
const results = await Promise.allSettled(
(req.body as JsonBody).paths.map((path) => {
// biome-ignore lint/suspicious/noConsole: we want to log here
console.log(`Revalidating path: ${path}`);
return res.revalidate(path);
})
);
return res.status(200).json({
success: results.every((result) => result.status === 'fulfilled'),
errors: results
.filter((result) => result.status === 'rejected')
.map((result) => (result as PromiseRejectedResult).reason),
});
}
return res.status(401).json({ error: 'Invalid signature' });
} catch (error) {
console.error('Error during revalidation:', error);
return res.status(400).json({ error: 'Invalid request or unable to parse JSON' });
}
}
// If no secret is set, we do not allow revalidation
return res.status(403).json({ error: 'Revalidation is disabled' });
}
+20 -1
View File
@@ -2,7 +2,11 @@
"main": ".open-next/worker.js",
"name": "gitbook-open-v2",
"compatibility_date": "2025-04-14",
"compatibility_flags": ["nodejs_compat", "allow_importable_env"],
"compatibility_flags": [
"nodejs_compat",
"allow_importable_env",
"global_fetch_strictly_public"
],
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS"
@@ -10,8 +14,15 @@
"observability": {
"enabled": true
},
"vars": {
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true",
"IS_PREVIEW": "false"
},
"env": {
"preview": {
"vars": {
"IS_PREVIEW": "true"
},
"r2_buckets": [
{
"binding": "NEXT_INC_CACHE_R2_BUCKET",
@@ -83,6 +94,14 @@
]
},
"production": {
"vars": {
// This is a bit misleading, but it means that we can have 500 concurrent revalidations
// This means that we'll have up to 100 durable objects instance running at the same time
"MAX_REVALIDATE_CONCURRENCY": "100",
// Temporary variable to find the issue once deployed
// TODO: remove this once the issue is fixed
"DEBUG_CLOUDFLARE": "true"
},
"routes": [
{
"pattern": "open-2c.gitbook.com/*",
+47 -1
View File
@@ -1,5 +1,51 @@
# gitbook
## 0.12.0
### Minor Changes
- 8339e91: Fix images in reusable content across spaces.
- 326e28e: Design tweaks to code blocks and OpenAPI pages
- 3119066: Add support for reusable content across spaces.
- 7d7806d: Pass SVG images through image resizing without resizing them to serve them from optimal host.
### Patch Changes
- c4ebb3f: Fix openapi-select hover in responses
- aed79fd: Decrease rounding of header logo
- 42ca7e1: Fix openapi CR preview
- e6ddc0f: Fix URL in sitemap
- 5e975ab: Fix code highlighting for HTTP
- 5d504ff: Fix resolution of links in reusable contents
- 95a1f65: Better print layouts: wrap code blocks & force table column auto-sizing
- 0499966: Fix invalid sitemap.xml generated with relative URLs instead of absolute ones
- 2a805cc: Change OpenAPI schema-optional from `info` to `tint` color
- 580101d: Fix schemas disclosure label causing client error
- 12a455d: Fix OpenAPI layout issues
- 97b7c79: Increase logging around caching behaviour causing page crashes.
- 373f18f: Prevent section group popovers from opening on click
- 3f29206: Update the regex for validating site redirect
- 0c973a3: Always link main logo to the root of the site
- ae5f1ab: Change `Dropdown`s to use Radix's `DropdownMenu`
- 0e201d5: Add border to filled sidebar on gradient theme
- dd043df: Revert investigation work around URL caches.
- 89a5816: Fix OpenAPI disclosure label ("Show properties") misalignment on mobile
- Updated dependencies [c3f6b8c]
- Updated dependencies [d00dc8c]
- Updated dependencies [42ca7e1]
- Updated dependencies [326e28e]
- Updated dependencies [5e975ab]
- Updated dependencies [f7a3470]
- Updated dependencies [580101d]
- Updated dependencies [20ebecb]
- Updated dependencies [80cb52a]
- Updated dependencies [cb5598d]
- Updated dependencies [c6637b0]
- Updated dependencies [a3ec264]
- @gitbook/colors@0.3.3
- @gitbook/openapi-parser@2.1.4
- @gitbook/react-openapi@1.3.0
## 0.11.1
### Patch Changes
@@ -564,7 +610,7 @@
- 4cbcc5b: Rollback of scalar modal while fixing perf issue
- 3996110: Optimize images rendered in community ads
- 133c3e7: Update design of Checkbox to be more consistent and readable
- 5096f7f: Disable KV cache for docs.gitbook.com as a test, also disable it for change-request to improve consistency
- 5096f7f: Disable KV cache for gitbook.com/docs as a test, also disable it for change-request to improve consistency
- 0f1565c: Add optional env `GITBOOK_INTEGRATIONS_HOST` to configure the host serving the integrations
- 2ff7ed1: Fix table of contents being visible on mobile when disabled at the page level
- b075f0f: Fix accessibility of the table of contents by using `aria-current` instead of `aria-selected`
+1
View File
@@ -168,6 +168,7 @@ export function runTestCases(testCases: TestsCase[]) {
new URL(testEntryPathname, testCase.contentBaseURL).toString()
)
: getTestURL(testEntryPathname);
if (testEntry.cookies) {
await context.addCookies(
testEntry.cookies.map((cookie) => ({
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gitbook",
"version": "0.11.1",
"version": "0.12.0",
"private": true,
"scripts": {
"dev": "env-cmd --silent -f ../../.env.local next dev",
@@ -18,35 +18,18 @@ export function AnnouncementBanner(props: {
}) {
const { announcement, contentRef } = props;
const hasLink = announcement.link && contentRef?.href;
const hasLink = contentRef?.href;
const closeable = announcement.style !== 'danger';
const Tag = hasLink ? Link : 'div';
const style = BANNER_STYLES[announcement.style];
return (
<div className="announcement-banner theme-bold:bg-header-background pt-4 pb-2">
<div id="announcement-banner" className="theme-bold:bg-header-background pt-4 pb-2">
<div className="scroll-nojump">
<div className={tcls('relative', CONTAINER_STYLE)}>
<Tag
href={contentRef?.href ?? ''}
className={tcls(
'flex w-full items-start justify-center overflow-hidden rounded-md straight-corners:rounded-none px-4 py-3 text-neutral-strong text-sm theme-bold:ring-1 theme-gradient:ring-1 ring-inset transition-colors',
style.container,
closeable && 'pr-12',
hasLink && style.hover
)}
insights={
announcement.link
? {
type: 'link_click',
link: {
target: announcement.link.to,
position: SiteInsightsLinkPosition.Announcement,
},
}
: undefined
}
<AnnouncementBannerParent
announcement={announcement}
closeable={closeable}
contentRef={contentRef}
>
<Icon
icon={style.icon as IconName}
@@ -75,7 +58,7 @@ export function AnnouncementBanner(props: {
</div>
) : null}
</div>
</Tag>
</AnnouncementBannerParent>
{closeable ? (
<button
className={`absolute top-0 right-4 mt-2 mr-2 rounded straight-corners:rounded-none p-1.5 transition-all hover:ring-1 sm:right-6 md:right-8 ${style.close}`}
@@ -91,6 +74,48 @@ export function AnnouncementBanner(props: {
);
}
/**
* Render the appropriate parent for the announcement banner depending on the presence of a link.
*/
function AnnouncementBannerParent(props: {
announcement: CustomizationAnnouncement;
children: React.ReactNode;
closeable: boolean;
contentRef: ResolvedContentRef | null;
}) {
const { announcement, contentRef, closeable, children } = props;
const style = BANNER_STYLES[announcement.style];
const classNames = [
'flex w-full items-start justify-center overflow-hidden rounded-md straight-corners:rounded-none px-4 py-3 text-neutral-strong text-sm theme-bold:ring-1 theme-gradient:ring-1 ring-inset transition-colors',
style.container,
];
if (contentRef?.href) {
return (
<Link
href={contentRef.href}
className={tcls(classNames, closeable && 'pr-12', style.hover)}
insights={
announcement.link
? {
type: 'link_click',
link: {
target: announcement.link.to,
position: SiteInsightsLinkPosition.Announcement,
},
}
: undefined
}
>
{children}
</Link>
);
}
return <div className={tcls(classNames)}>{children}</div>;
}
/**
* Dismiss the announcement banner and store the dismissal state in local storage.
* @see AnnouncementScript
@@ -145,7 +145,7 @@
}
.openapi-column-preview-body {
@apply flex flex-col gap-4 sticky top-4 site-header:top-20 site-header-sections:top-32 page-api-block:xl:max-2xl:top-32 print-mode:static;
@apply flex flex-col gap-4 sticky top-4 site-header:top-20 site-header:xl:max-2xl:top-32 site-header-sections:top-32 site-header-sections:xl:max-2xl:top-44 print-mode:static;
}
.openapi-column-preview pre {
@@ -770,7 +770,7 @@ body:has(.openapi-select-popover) {
}
.openapi-disclosure:has(> .openapi-disclosure-trigger:hover) {
@apply bg-tint-subtle;
@apply bg-tint-subtle overflow-hidden;
}
.openapi-disclosure:has(> .openapi-disclosure-trigger:hover),
@@ -778,10 +778,10 @@ body:has(.openapi-select-popover) {
@apply ring-1 shadow-sm;
}
.openapi-disclosure[data-expanded="true"]:not(:first-child) {
.openapi-disclosure[data-expanded="true"]:not(.openapi-schemas-disclosure):not(:first-child) {
@apply mt-2;
}
.openapi-disclosure[data-expanded="true"]:not(:last-child) {
.openapi-disclosure[data-expanded="true"]:not(.openapi-schemas-disclosure):not(:last-child) {
@apply mb-2;
}
@@ -4,7 +4,7 @@ import {
SiteInsightsLinkPosition,
} from '@gitbook/api';
import { Link } from '@/components/primitives';
import { LinkBox, LinkOverlay } from '@/components/primitives';
import { Image } from '@/components/utils';
import { resolveContentRef } from '@/lib/references';
import { type ClassValue, tcls } from '@/lib/tailwind';
@@ -44,7 +44,6 @@ export async function RecordCard(
<div
className={tcls(
'grid-area-1-1',
'z-0',
'relative',
'grid',
'bg-tint-base',
@@ -151,7 +150,6 @@ export async function RecordCard(
'rounded-md',
'straight-corners:rounded-none',
'dark:shadow-transparent',
'z-0',
'before:pointer-events-none',
'before:grid-area-1-1',
@@ -167,19 +165,22 @@ export async function RecordCard(
if (target && targetRef) {
return (
<Link
href={target.href}
className={tcls(style, 'hover:before:ring-tint-12/5')}
insights={{
type: 'link_click',
link: {
target: targetRef,
position: SiteInsightsLinkPosition.Content,
},
}}
>
// We don't use `Link` directly here because we could end up in a situation where
// a link is rendered inside a link, which is not allowed in HTML.
// It causes an hydration error in React.
<LinkBox href={target.href} className={tcls(style, 'hover:before:ring-tint-12/5')}>
<LinkOverlay
href={target.href}
insights={{
type: 'link_click',
link: {
target: targetRef,
position: SiteInsightsLinkPosition.Content,
},
}}
/>
{body}
</Link>
</LinkBox>
);
}
@@ -115,7 +115,8 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
return <Tag className={tcls(['w-full', verticalAlignment])}>{''}</Tag>;
}
const horizontalAlignment = `[&_*]:${getColumnAlignment(definition)} ${getColumnAlignment(definition)}`;
const horizontalAlignment = getColumnAlignment(definition);
const childrenHorizontalAlignment = `[&_*]:${horizontalAlignment}`;
return (
<Blocks
@@ -131,6 +132,7 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
'leading-normal',
verticalAlignment,
horizontalAlignment,
childrenHorizontalAlignment,
]}
context={context}
blockStyle={['w-full', 'max-w-[unset]']}
@@ -25,6 +25,7 @@ export function Footer(props: { context: GitBookSiteContext }) {
return (
<footer
id="site-footer"
className={tcls(
'border-tint-subtle border-t',
// If the footer only contains a mode toggle, we only show it on smaller screens
@@ -2,6 +2,7 @@ import type { SiteSpace } from '@gitbook/api';
import { tcls } from '@/lib/tailwind';
import { joinPath } from '@/lib/paths';
import type { GitBookSiteContext } from '@v2/lib/context';
import { DropdownChevron, DropdownMenu } from './DropdownMenu';
import { SpacesDropdownMenuItem } from './SpacesDropdownMenuItem';
@@ -74,7 +75,7 @@ export function SpacesDropdown(props: {
title: otherSiteSpace.title,
url: otherSiteSpace.urls.published
? linker.toLinkForContent(otherSiteSpace.urls.published)
: otherSiteSpace.space.urls.app,
: getFallbackSiteSpaceURL(otherSiteSpace, context),
}}
active={otherSiteSpace.id === siteSpace.id}
/>
@@ -82,3 +83,14 @@ export function SpacesDropdown(props: {
</DropdownMenu>
);
}
/**
* When the site is not published yet, `urls.published` is not available.
* To ensure navigation works in preview, we compute a relative URL from the siteSpace path.
*/
function getFallbackSiteSpaceURL(siteSpace: SiteSpace, context: GitBookSiteContext) {
const { linker, sections } = context;
return linker.toPathInSite(
sections?.current ? joinPath(sections.current.path, siteSpace.path) : siteSpace.path
);
}
@@ -87,7 +87,7 @@ export function PageAside(props: {
'page-api-block:xl:max-2xl:dark:hover:shadow-tint-1/1',
'page-api-block:xl:max-2xl:rounded-md',
'page-api-block:xl:max-2xl:h-auto',
'page-api-block:xl:max-2xl:my-8',
'page-api-block:xl:max-2xl:my-4',
'page-api-block:p-2'
)}
>
@@ -75,7 +75,7 @@ export async function CustomizationRootLayout(props: {
lang={customization.internationalization.locale}
className={tcls(
customization.header.preset === CustomizationHeaderPreset.None
? 'site-header-none'
? null
: 'scroll-pt-[76px]', // Take the sticky header in consideration for the scrolling
customization.styling.corners === CustomizationCorners.Straight
? ' straight-corners'
@@ -148,6 +148,13 @@
width: 100%;
}
}
.elevate-link {
& a[href]:not(.link-overlay) {
position: relative;
z-index: 20;
}
}
}
html {
@@ -161,6 +168,6 @@ html.dark {
color-scheme: dark light;
}
html.announcement-hidden .announcement-banner {
html.announcement-hidden #announcement-banner {
@apply hidden;
}
@@ -53,6 +53,8 @@ function encodeSection(context: GitBookSiteContext, section: SiteSection) {
description: section.description,
icon: section.icon,
object: section.object,
url: section.urls.published ? linker.toLinkForContent(section.urls.published) : '',
url: section.urls.published
? linker.toLinkForContent(section.urls.published)
: linker.toPathInSite(section.path),
};
}
@@ -17,7 +17,11 @@ export async function PageDocumentItem(props: {
context: GitBookSiteContext;
}) {
const { rootPages, page, context } = props;
const href = context.linker.toPathForPage({ pages: rootPages, page });
let href = context.linker.toPathForPage({ pages: rootPages, page });
// toPathForPage can returns an empty path, this will cause all links to point to the current page.
if (href === '') {
href = '/';
}
return (
<li className="flex flex-col">
@@ -6,6 +6,7 @@ import { tcls } from '@/lib/tailwind';
import { PagesList } from './PagesList';
import { TOCScrollContainer } from './TOCScroller';
import { TableOfContentsScript } from './TableOfContentsScript';
import { Trademark } from './Trademark';
export function TableOfContents(props: {
@@ -17,97 +18,107 @@ export function TableOfContents(props: {
const { space, customization, pages } = context;
return (
<aside // Sidebar container, responsible for setting the right dimensions and position for the sidebar.
data-testid="table-of-contents"
className={tcls(
'group',
'text-sm',
'grow-0',
'shrink-0',
'basis-full',
'lg:basis-72',
'page-no-toc:lg:basis-56',
'relative',
'z-[1]',
'lg:sticky',
// Without header
'lg:top-0',
'lg:h-screen',
// With header
'site-header:lg:top-16',
'site-header:lg:h-[calc(100vh_-_4rem)]',
// With header and sections
'site-header-sections:lg:top-[6.75rem]',
'site-header-sections:lg:h-[calc(100vh_-_6.75rem)]',
'pt-6',
'pb-4',
'sidebar-filled:lg:pr-6',
'page-no-toc:lg:pr-0',
'hidden',
'navigation-open:!flex',
'lg:flex',
'page-no-toc:lg:hidden',
'page-no-toc:xl:flex',
'site-header-none:page-no-toc:lg:flex',
'flex-col',
'gap-4',
'navigation-open:border-b',
'border-tint-subtle'
)}
>
{header && header}
<div // The actual sidebar, either shown with a filled bg or transparent.
<>
<aside // Sidebar container, responsible for setting the right dimensions and position for the sidebar.
data-testid="table-of-contents"
id="table-of-contents"
className={tcls(
'lg:-ms-5',
'relative flex flex-grow flex-col overflow-hidden border-tint-subtle',
'group',
'text-sm',
'sidebar-filled:bg-tint-subtle',
'theme-muted:bg-tint-subtle',
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
'[html.sidebar-filled.theme-muted_&]:bg-tint-base',
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base',
'[html.sidebar-filled.theme-gradient_&]:border',
'page-no-toc:!bg-transparent',
'grow-0',
'shrink-0',
'basis-full',
'lg:basis-72',
'page-no-toc:lg:basis-56',
'sidebar-filled:rounded-xl',
'straight-corners:rounded-none'
'relative',
'z-[1]',
'lg:sticky',
// Server-side static positioning
'lg:top-0',
'lg:h-screen',
'announcement:lg:h-[calc(100vh-4.25rem)]',
'site-header:lg:top-16',
'site-header:lg:h-[calc(100vh-4rem)]',
'announcement:site-header:lg:h-[calc(100vh-4rem-4.25rem)]',
'site-header-sections:lg:top-[6.75rem]',
'site-header-sections:lg:h-[calc(100vh-6.75rem)]',
'announcement:site-header-sections:lg:h-[calc(100vh-6.75rem-4.25rem)]',
// Client-side dynamic positioning (CSS vars applied by script)
'[html[style*="--toc-top-offset"]_&]:lg:!top-[var(--toc-top-offset)]',
'[html[style*="--toc-height"]_&]:lg:!h-[var(--toc-height)]',
'pt-6',
'pb-4',
'sidebar-filled:lg:pr-6',
'page-no-toc:lg:pr-0',
'hidden',
'navigation-open:!flex',
'lg:flex',
'page-no-toc:lg:hidden',
'page-no-toc:xl:flex',
'site-header-none:page-no-toc:lg:flex',
'flex-col',
'gap-4',
'navigation-open:border-b',
'border-tint-subtle'
)}
>
{innerHeader && <div className="px-5 *:my-4">{innerHeader}</div>}
<TOCScrollContainer // The scrollview inside the sidebar
{header && header}
<div // The actual sidebar, either shown with a filled bg or transparent.
className={tcls(
'flex flex-grow flex-col p-2',
customization.trademark.enabled && 'lg:pb-20',
'lg:gutter-stable overflow-y-auto',
'[&::-webkit-scrollbar]:bg-transparent',
'[&::-webkit-scrollbar-thumb]:bg-transparent',
'group-hover:[&::-webkit-scrollbar]:bg-tint-subtle',
'group-hover:[&::-webkit-scrollbar-thumb]:bg-tint-7',
'group-hover:[&::-webkit-scrollbar-thumb:hover]:bg-tint-8'
'lg:-ms-5',
'relative flex flex-grow flex-col overflow-hidden border-tint-subtle',
'sidebar-filled:bg-tint-subtle',
'theme-muted:bg-tint-subtle',
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
'[html.sidebar-filled.theme-muted_&]:bg-tint-base',
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base',
'[html.sidebar-filled.theme-gradient_&]:border',
'page-no-toc:!bg-transparent',
'sidebar-filled:rounded-xl',
'straight-corners:rounded-none'
)}
>
<PagesList
rootPages={pages}
pages={pages}
context={context}
style="page-no-toc:hidden border-tint-subtle sidebar-list-line:border-l"
/>
{customization.trademark.enabled ? (
<Trademark
space={space}
customization={customization}
placement={SiteInsightsTrademarkPlacement.Sidebar}
{innerHeader && <div className="px-5 *:my-4">{innerHeader}</div>}
<TOCScrollContainer // The scrollview inside the sidebar
className={tcls(
'flex flex-grow flex-col p-2',
customization.trademark.enabled && 'lg:pb-20',
'lg:gutter-stable overflow-y-auto',
'[&::-webkit-scrollbar]:bg-transparent',
'[&::-webkit-scrollbar-thumb]:bg-transparent',
'group-hover:[&::-webkit-scrollbar]:bg-tint-subtle',
'group-hover:[&::-webkit-scrollbar-thumb]:bg-tint-7',
'group-hover:[&::-webkit-scrollbar-thumb:hover]:bg-tint-8'
)}
>
<PagesList
rootPages={pages}
pages={pages}
context={context}
style="page-no-toc:hidden border-tint-subtle sidebar-list-line:border-l"
/>
) : null}
</TOCScrollContainer>
</div>
</aside>
{customization.trademark.enabled ? (
<Trademark
space={space}
customization={customization}
placement={SiteInsightsTrademarkPlacement.Sidebar}
/>
) : null}
</TOCScrollContainer>
</div>
</aside>
<TableOfContentsScript />
</>
);
}
@@ -0,0 +1,73 @@
'use client';
import { useEffect } from 'react';
/**
* Adjusts TableOfContents height based on visible elements
*/
export function TableOfContentsScript() {
useEffect(() => {
const root = document.documentElement;
// Calculate and set TOC dimensions
const updateTocLayout = () => {
// Get key elements
const header = document.getElementById('site-header');
const banner = document.getElementById('announcement-banner');
const footer = document.getElementById('site-footer');
// Set sticky top position based on header
const headerHeight = header?.offsetHeight ?? 0;
root.style.setProperty('--toc-top-offset', `${headerHeight}px`);
// Start with full viewport height minus header
let height = window.innerHeight - headerHeight;
// Subtract visible banner (if any)
if (banner && window.getComputedStyle(banner).display !== 'none') {
const bannerRect = banner.getBoundingClientRect();
if (bannerRect.height > 0 && bannerRect.bottom > 0) {
height -= Math.min(bannerRect.height, bannerRect.bottom);
}
}
// Subtract visible footer (if any)
if (footer) {
const footerRect = footer.getBoundingClientRect();
if (footerRect.top < window.innerHeight) {
height -= Math.min(footerRect.height, window.innerHeight - footerRect.top);
}
}
// Update height
root.style.setProperty('--toc-height', `${height}px`);
};
// Initial update
updateTocLayout();
// Let the browser handle scroll throttling naturally
window.addEventListener('scroll', updateTocLayout, { passive: true });
window.addEventListener('resize', updateTocLayout, { passive: true });
// Use MutationObserver for DOM changes
const observer = new MutationObserver(() => {
requestAnimationFrame(updateTocLayout);
});
// Only observe what matters
observer.observe(document.documentElement, {
subtree: true,
attributes: true,
attributeFilter: ['style', 'class'],
});
return () => {
observer.disconnect();
window.removeEventListener('scroll', updateTocLayout);
window.removeEventListener('resize', updateTocLayout);
};
}, []);
return null;
}
@@ -1 +1,4 @@
export * from './TableOfContents';
export { TableOfContents } from './TableOfContents';
export { PagesList } from './PagesList';
export { TOCScrollContainer } from './TOCScroller';
export { Trademark } from './Trademark';
@@ -3,6 +3,7 @@
import NextLink, { type LinkProps as NextLinkProps } from 'next/link';
import React from 'react';
import { tcls } from '@/lib/tailwind';
import { type TrackEventInput, useTrackEvent } from '../Insights';
// Props from Next, which includes NextLinkProps and all the things anchor elements support.
@@ -75,6 +76,46 @@ export const Link = React.forwardRef(function Link(
);
});
/**
* A box used to contain a link overlay.
* It is used to create a clickable area that can contain other elements.
*/
export const LinkBox = React.forwardRef(function LinkBox(
props: React.BaseHTMLAttributes<HTMLDivElement>,
ref: React.Ref<HTMLDivElement>
) {
const { children, className, ...domProps } = props;
return (
<div ref={ref} {...domProps} className={tcls('elevate-link relative', className)}>
{children}
</div>
);
});
/**
* A link overlay that can be used to create a clickable area on top of other elements.
* It is used to create a link that covers the entire area of the element without encapsulating it in a link tag.
* This is useful to avoid nesting links inside links.
*/
export const LinkOverlay = React.forwardRef(function LinkOverlay(
props: LinkProps,
ref: React.Ref<HTMLAnchorElement>
) {
const { children, className, ...domProps } = props;
return (
<Link
ref={ref}
{...domProps}
className={tcls(
'link-overlay static before:absolute before:top-0 before:left-0 before:z-10 before:h-full before:w-full',
className
)}
>
{children}
</Link>
);
});
/**
* Check if a link is external, compared to an origin.
*/

Some files were not shown because too many files have changed in this diff Show More