mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-23 11:03:39 +00:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 75e34fa299 | |||
| c8a2fd8351 | |||
| 9466913e18 | |||
| c730845e17 | |||
| ef57c608a5 | |||
| 7b5a3616c4 | |||
| c9373efdb5 | |||
| 015615de8a | |||
| 0f867e533a | |||
| 7d5a6d2255 | |||
| a1da4f023d | |||
| 666e8426e1 | |||
| df2fa42bda | |||
| b3a7ad6d2c | |||
| 2932077bf9 | |||
| 957afd967f | |||
| 4c9a9d0416 | |||
| 40df91a363 | |||
| af66ff71ed | |||
| d678a9162d | |||
| 6f8ecd6111 | |||
| fa3eb07617 | |||
| dc4268db64 | |||
| a0c06a72cc | |||
| af7cadc8e9 | |||
| 19c20c0be3 | |||
| e2b2468475 | |||
| fae34e56ac | |||
| 11f4241709 | |||
| 5a410feda0 | |||
| 31bfe77f74 | |||
| 34dceb2f97 | |||
| cced4f0d78 | |||
| ba0094a862 | |||
| b259009c9c | |||
| d88dd5ed8a | |||
| 57bb146075 | |||
| 4b67fe5317 | |||
| 938125e772 | |||
| 1c8d9febb3 | |||
| 223edae17d | |||
| 12a455dab5 | |||
| d00dc8ca72 | |||
| 2c85a302fc | |||
| 3460b78b45 | |||
| 95a1f652f9 | |||
| cc37e2ae15 | |||
| 08382ce62f | |||
| 80cb52a237 | |||
| c6637b09e2 | |||
| 778624af00 | |||
| e15757d01f | |||
| aed79fd6c3 | |||
| 0c973a3292 | |||
| 56c923c0ed | |||
| 7d7806df30 | |||
| cb5598dc19 | |||
| 47f01edcb8 | |||
| e6ddc0f07f | |||
| 373f18f451 | |||
| 3f292065cd |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Fix images in reusable content across spaces.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Fix openapi-select hover in responses
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Respect fullWidth and defaultWidth for images
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
fix nested a tag causing hydration error
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
fix href being empty in TOC
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix navigation between sections/variants when previewing a site in v2
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@gitbook/colors": patch
|
||||
---
|
||||
|
||||
Update chroma ratio per step
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Fix openapi CR preview
|
||||
Add authorization header for OAuth2
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
add a force-revalidate api route to force bust the cache in case of errors
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"@gitbook/react-openapi": minor
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Design tweaks to code blocks and OpenAPI pages
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
"@gitbook/react-openapi": patch
|
||||
"gitbook-v2": patch
|
||||
"gitbook": patch
|
||||
"@gitbook/colors": patch
|
||||
---
|
||||
|
||||
Fix code highlighting for HTTP
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix resolution of links in reusable contents
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
fix ISR on preview env
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"gitbook-v2": minor
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Add support for reusable content across spaces.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix invalid sitemap.xml generated with relative URLs instead of absolute ones
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Change OpenAPI schema-optional from `info` to `tint` color
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@gitbook/colors": patch
|
||||
---
|
||||
|
||||
Change lightness check for color step 9 to allow input colors with a higher-than-needed contrast
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Indent JSON python code sample
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Fix schemas disclosure label causing client error
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix missing title on button to close the announcement banner.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Missing top-level required OpenAPI alternatives
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Make TOC height dynamic based on visible header and footer elements
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
Improve error messages around undefined site sections.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix bold header links hover color
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
cache fonts and static image used in OGImage in memory
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Increase logging around caching behaviour causing page crashes.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Handle nested deprecated properties in generateSchemaExample
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
Add `urlObject.hash` to `linker.toLinkForContent` to pass through URL fragment identifiers, used in search
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Change `Dropdown`s to use Radix's `DropdownMenu`
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@gitbook/react-openapi": patch
|
||||
---
|
||||
|
||||
Fix Python code sample "null vs None"
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Add border to filled sidebar on gradient theme
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Deduplicate path parameters from OpenAPI spec
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
remove trailing slash from linker
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
Revert investigation work around URL caches.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix OpenAPI disclosure label ("Show properties") misalignment on mobile
|
||||
@@ -63,9 +63,9 @@ To start your local version of GitBook, run the command `bun dev`.
|
||||
|
||||
When running the development server, published GitBook sites can be rendered through your local version at `http://localhost:3000/`.
|
||||
|
||||
For example, our published docs can be viewed using the local version by visiting `http://localhost:3000/docs.gitbook.com` after running the development server.
|
||||
For example, our published docs can be viewed using the local version by visiting `http://localhost:3000/gitbook.com/docs` after running the development server.
|
||||
|
||||
You can visit any published GitBook site behind your development server. Please make sure your site is [published publicly](https://docs.gitbook.com/published-documentation/publish-your-content-as-a-docs-site) to ensure you can view the site correctly in your development version.
|
||||
You can visit any published GitBook site behind your development server. Please make sure your site is [published publicly](https://gitbook.com/docs/published-documentation/publish-your-content-as-a-docs-site) to ensure you can view the site correctly in your development version.
|
||||
|
||||
### Commit your update
|
||||
|
||||
|
||||
@@ -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 }}"
|
||||
@@ -152,7 +152,7 @@ jobs:
|
||||
|
||||
| Site | `v1` | `2v` | `2c` |
|
||||
| --- | --- | --- | --- |
|
||||
| GitBook | [${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/docs.gitbook.com](${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/docs.gitbook.com) | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/docs.gitbook.com](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/docs.gitbook.com) | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/docs.gitbook.com](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/docs.gitbook.com) |
|
||||
| GitBook | [${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/gitbook.com/docs](${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/gitbook.com/docs) | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.com/docs](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.com/docs) | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.com/docs](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.com/docs) |
|
||||
| E2E | [${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/gitbook.gitbook.io/test-gitbook-open) | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open) | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open) |
|
||||
edit-mode: replace
|
||||
visual-testing-v1:
|
||||
@@ -200,7 +200,6 @@ jobs:
|
||||
name: Visual Testing v2 (Cloudflare)
|
||||
needs: deploy-v2-cloudflare
|
||||
timeout-minutes: 10
|
||||
if: startsWith(github.head_ref || github.ref_name, 'cloudflare/') || github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -263,7 +262,6 @@ jobs:
|
||||
name: Visual Testing Customers v2 (Cloudflare)
|
||||
needs: deploy-v2-cloudflare
|
||||
timeout-minutes: 8
|
||||
if: startsWith(github.head_ref || github.ref_name, 'cloudflare/') || github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<h1 align="center">GitBook</h1>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://docs.gitbook.com/">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>
|
||||
<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">
|
||||
@@ -66,7 +66,7 @@ bun dev:v2
|
||||
|
||||
examples:
|
||||
|
||||
- http://localhost:3000/url/docs.gitbook.com
|
||||
- http://localhost:3000/url/gitbook.com/docs
|
||||
- http://localhost:3000/url/open-source.gitbook.io/midjourney
|
||||
|
||||
Any published GitBook site can be accessed through your local development instance, and any updates you make to the codebase will be reflected in your browser.
|
||||
@@ -150,11 +150,11 @@ See `LICENSE` for more information.
|
||||
</p>
|
||||
|
||||
```md
|
||||
[](https://gitbook.com/)
|
||||
[](https://www.gitbook.com/preview?utm_source=gitbook_readme_badge&utm_medium=organic&utm_campaign=preview_documentation&utm_content=link)
|
||||
```
|
||||
|
||||
```html
|
||||
<a href="https://gitbook.com">
|
||||
<a href="https://www.gitbook.com/preview?utm_source=gitbook_readme_badge&utm_medium=organic&utm_campaign=preview_documentation&utm_content=link">
|
||||
<img
|
||||
src="https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=3F89A1"
|
||||
/>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"name": "@gitbook/cache-tags",
|
||||
"version": "0.3.1",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "^0.111.0",
|
||||
"@gitbook/api": "^0.117.1",
|
||||
"assert-never": "^1.2.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -35,7 +35,7 @@
|
||||
},
|
||||
"packages/colors": {
|
||||
"name": "@gitbook/colors",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.3",
|
||||
},
|
||||
@@ -49,9 +49,9 @@
|
||||
},
|
||||
"packages/gitbook": {
|
||||
"name": "gitbook",
|
||||
"version": "0.11.1",
|
||||
"version": "0.12.0",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "*",
|
||||
"@gitbook/api": "^0.117.1",
|
||||
"@gitbook/cache-do": "workspace:*",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
"@gitbook/colors": "workspace:*",
|
||||
@@ -141,14 +141,15 @@
|
||||
},
|
||||
"packages/gitbook-v2": {
|
||||
"name": "gitbook-v2",
|
||||
"version": "0.2.5",
|
||||
"version": "0.3.0",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "*",
|
||||
"@gitbook/api": "^0.117.1",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
"@opennextjs/cloudflare": "1.1.0",
|
||||
"@sindresorhus/fnv1a": "^3.1.0",
|
||||
"assert-never": "^1.2.1",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"next": "canary",
|
||||
"next": "^15.3.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"rison": "^0.1.1",
|
||||
@@ -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",
|
||||
@@ -202,7 +202,7 @@
|
||||
"name": "@gitbook/react-contentkit",
|
||||
"version": "0.7.0",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "*",
|
||||
"@gitbook/api": "^0.117.1",
|
||||
"@gitbook/icons": "workspace:*",
|
||||
"classnames": "^2.5.1",
|
||||
},
|
||||
@@ -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",
|
||||
@@ -260,7 +260,7 @@
|
||||
},
|
||||
"overrides": {
|
||||
"@codemirror/state": "6.4.1",
|
||||
"@gitbook/api": "0.113.0",
|
||||
"@gitbook/api": "^0.117.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
},
|
||||
@@ -625,7 +625,7 @@
|
||||
|
||||
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="],
|
||||
|
||||
"@gitbook/api": ["@gitbook/api@0.113.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-PWMeAkdm4bHSl3b5OmtcmskZ6qRkkDhauCPybo8sGnjS03O14YAUtubAQiNCKX/uwbs+yiQ8KRPyeIwn+g42yw=="],
|
||||
"@gitbook/api": ["@gitbook/api@0.117.1", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-edyi4Y+Zs/9vdU6CvJfwtVSiEWrKzzlp+sIfsyw8WT+mJWnH9CN+o17nxvRWNmAznU+YZm+Qz7itVbG8k+vSkQ=="],
|
||||
|
||||
"@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"],
|
||||
|
||||
@@ -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.4", "", { "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-/bn9N/6dVu9+sC7AptaGJylKUzyDDgqe3yKfvUxXOpy7whIq/+3Bw+q2/bilyQg6FcskbCWUt9nLvNf1hAVmfA=="],
|
||||
|
||||
"@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.1.0", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.6.4", "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-8DOzpLiewivA1pwRNGVPDXbrF79bzJiHscY+M1tekwvKqEqM26CRYafhrA5IDCeyteNaL9AZD6X4DLuCn/eeYQ=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
@@ -4077,7 +4077,7 @@
|
||||
|
||||
"gaxios/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=="],
|
||||
|
||||
"gitbook-v2/next": ["next@15.4.0-canary.7", "", { "dependencies": { "@next/env": "15.4.0-canary.7", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.0-canary.7", "@next/swc-darwin-x64": "15.4.0-canary.7", "@next/swc-linux-arm64-gnu": "15.4.0-canary.7", "@next/swc-linux-arm64-musl": "15.4.0-canary.7", "@next/swc-linux-x64-gnu": "15.4.0-canary.7", "@next/swc-linux-x64-musl": "15.4.0-canary.7", "@next/swc-win32-arm64-msvc": "15.4.0-canary.7", "@next/swc-win32-x64-msvc": "15.4.0-canary.7", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-ZYjT0iu+4osz8XIlr31MuoXaNQKRU75UcwEgNBt93gftoh6tzV2Mebz6sOGeVReYuYUvYlLJJksMBTNcFcPbSA=="],
|
||||
"gitbook-v2/next": ["next@15.3.2", "", { "dependencies": { "@next/env": "15.3.2", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.3.2", "@next/swc-darwin-x64": "15.3.2", "@next/swc-linux-arm64-gnu": "15.3.2", "@next/swc-linux-arm64-musl": "15.3.2", "@next/swc-linux-x64-gnu": "15.3.2", "@next/swc-linux-x64-musl": "15.3.2", "@next/swc-win32-arm64-msvc": "15.3.2", "@next/swc-win32-x64-msvc": "15.3.2", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-CA3BatMyHkxZ48sgOCLdVHjFU36N7TF1HhqAHLFOkV6buwZnvMI84Cug8xD56B9mCuKrqXnLn94417GrZ/jjCQ=="],
|
||||
|
||||
"global-dirs/ini": ["ini@1.3.7", "", {}, "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ=="],
|
||||
|
||||
@@ -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=="],
|
||||
|
||||
@@ -4969,23 +4975,23 @@
|
||||
|
||||
"gaxios/https-proxy-agent/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
|
||||
|
||||
"gitbook-v2/next/@next/env": ["@next/env@15.4.0-canary.7", "", {}, "sha512-q8S7f2lQti3Y3gcAPzE8Pj8y0EwiWHVyyilMzoLbDPXGVfxlQhXLRiFdy2cDkKN4DyjGZWDeehEtw4huvJAa3Q=="],
|
||||
"gitbook-v2/next/@next/env": ["@next/env@15.3.2", "", {}, "sha512-xURk++7P7qR9JG1jJtLzPzf0qEvqCN0A/T3DXf8IPMKo9/6FfjxtEffRJIIew/bIL4T3C2jLLqBor8B/zVlx6g=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.4.0-canary.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+TMxUu5CAWNe+UFRc47BZAXQxCRqZfVbGyCldddiog4MorvL7kBxSd1qlmrwI73fRRKtXkHIH1TaeItyxzC9rQ=="],
|
||||
"gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2DR6kY/OGcokbnCsjHpNeQblqCZ85/1j6njYSkzRdpLn5At7OkSdmk7WyAmB9G0k25+VgqVZ/u356OSoQZ3z0g=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.4.0-canary.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-veXp8lg/X/7O+pG9BDQ3OizFz3B40v29jsvEWj+ULY/W8Z6+dCSd5XPP2M8fG/gKKKA0D6L0CnnM2Mj0RRSUJw=="],
|
||||
"gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ro/fdqaZWL6k1S/5CLv1I0DaZfDVJkWNaUU3un8Lg6m0YENWlDulmIWzV96Iou2wEYyEsZq51mwV8+XQXqMp3w=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.4.0-canary.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-KxNGfW7BO0Z5B9rJyl9p7YVjNrxAhu06mH6h1PSdouZG7YMYpdRCconVXeuBI0PEu6g3ywNrOVxZUk1V6G5u0Q=="],
|
||||
"gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-covwwtZYhlbRWK2HlYX9835qXum4xYZ3E2Mra1mdQ+0ICGoMiw1+nVAn4d9Bo7R3JqSmK1grMq/va+0cdh7bJA=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.4.0-canary.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-THgXgmP/cC4DsNwvC6uqB90CebB7Ep1KyZajQL3fYKT5V4SWr46yngKLyoyJVeAYWJH908MrWddf7Ya/Zq7cyg=="],
|
||||
"gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-KQkMEillvlW5Qk5mtGA/3Yz0/tzpNlSw6/3/ttsV1lNtMuOHcGii3zVeXZyi4EJmmLDKYcTcByV2wVsOhDt/zg=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.4.0-canary.7", "", { "os": "linux", "cpu": "x64" }, "sha512-kpLB3Jj7fProynQYj2ahFyZlJs0xwm71VzCVrNRu6u7qJGXn6dK5h7+hro8y/y1iqjXWgCLSdxWSHahhWK8XdQ=="],
|
||||
"gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uRBo6THWei0chz+Y5j37qzx+BtoDRFIkDzZjlpCItBRXyMPIg079eIkOCl3aqr2tkxL4HFyJ4GHDes7W8HuAUg=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.4.0-canary.7", "", { "os": "linux", "cpu": "x64" }, "sha512-rnGAKvl4cWPVV9D+SybWOGijm0VmKXyqQ+IN0A6WDgdlYZAZP0ZnJv/rq7DSvuOh19AXS8UpQc88SelXV/3j3Q=="],
|
||||
"gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-+uxFlPuCNx/T9PdMClOqeE8USKzj8tVz37KflT3Kdbx/LOlZBRI2yxuIcmx1mPNK8DwSOMNCr4ureSet7eyC0w=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.4.0-canary.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-/PRbn//EuR3UGiquk050gqvjxLliEgGBy1Cx9KkpAT7szaHOBj1mDDQmxMTEhRex4i3YfKGJXWn5mLMCveya6Q=="],
|
||||
"gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.3.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-LLTKmaI5cfD8dVzh5Vt7+OMo+AIOClEdIU/TSKbXXT2iScUTSxOGoBhfuv+FU8R9MLmrkIL1e2fBMkEEjYAtPQ=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.4.0-canary.7", "", { "os": "win32", "cpu": "x64" }, "sha512-7a92XL+DlrbWyycCpQjjQMHOrsA0p+VvS7iA2dyi89Xsq0qtOPzFH0Gb56fsjh6M6BQGFhboOSzjmpjlkMTilQ=="],
|
||||
"gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-aW5B8wOPioJ4mBdMDXkt5f3j8pUr9W8AnlX0Df35uRWNT1Y6RIybxjnSUe+PhM+M1bwgyY8PHLmXZC6zT1o5tA=="],
|
||||
|
||||
"gitbook-v2/next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||
|
||||
|
||||
+2
-2
@@ -7,10 +7,10 @@
|
||||
"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.113.0",
|
||||
"@gitbook/api": "^0.117.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
},
|
||||
"version": "0.3.1",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "^0.111.0",
|
||||
"@gitbook/api": "^0.117.1",
|
||||
"assert-never": "^1.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
# cloudflare
|
||||
.open-next
|
||||
.wrangler
|
||||
|
||||
# Symbolic links
|
||||
public
|
||||
|
||||
@@ -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
|
||||
|
||||
Vendored
+1
@@ -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.
|
||||
|
||||
@@ -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/middleware').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,14 @@
|
||||
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';
|
||||
|
||||
export default {
|
||||
name: 'GitbookISRQueue',
|
||||
send: async (msg) => {
|
||||
return trace({ operation: 'gitbookISRQueueSend', name: msg.MessageBody.url }, async () => {
|
||||
const { ctx } = getCloudflareContext();
|
||||
ctx.waitUntil(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,81 @@
|
||||
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,
|
||||
regionalReplication: {
|
||||
defaultRegion: 'enam',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
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;
|
||||
@@ -1,23 +1,23 @@
|
||||
{
|
||||
"name": "gitbook-v2",
|
||||
"version": "0.2.5",
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"next": "canary",
|
||||
"@gitbook/api": "^0.117.1",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
"@opennextjs/cloudflare": "1.1.0",
|
||||
"@sindresorhus/fnv1a": "^3.1.0",
|
||||
"assert-never": "^1.2.1",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"next": "^15.3.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"@gitbook/api": "*",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
"@sindresorhus/fnv1a": "^3.1.0",
|
||||
"server-only": "^0.0.1",
|
||||
"warn-once": "^0.1.1",
|
||||
"rison": "^0.1.1",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"assert-never": "^1.2.1"
|
||||
"server-only": "^0.0.1",
|
||||
"warn-once": "^0.1.1"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -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}`;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+417
-1011
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,55 +1,102 @@
|
||||
import { race, tryCatch } from '@/lib/async';
|
||||
import { joinPath, joinPathWithBaseURL } from '@/lib/paths';
|
||||
import { trace } from '@/lib/tracing';
|
||||
import type { PublishedSiteContentLookup } from '@gitbook/api';
|
||||
import type { GitBookAPI, PublishedSiteContentLookup, SiteVisitorPayload } from '@gitbook/api';
|
||||
import { apiClient } from './api';
|
||||
import { getExposableError } from './errors';
|
||||
import type { DataFetcherResponse } from './types';
|
||||
import { getURLLookupAlternatives, stripURLSearch } from './urls';
|
||||
|
||||
/**
|
||||
* Lookup a content by its URL using the GitBook API.
|
||||
* To optimize caching, we try multiple lookup alternatives and return the first one that matches.
|
||||
*/
|
||||
export async function getPublishedContentByURL(input: {
|
||||
interface LookupPublishedContentByUrlInput {
|
||||
url: string;
|
||||
visitorAuthToken: string | null;
|
||||
redirectOnError: boolean;
|
||||
apiToken: string | null;
|
||||
visitorPayload: SiteVisitorPayload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup a content by its URL using the GitBook resolvePublishedContentByUrl API endpoint.
|
||||
* To optimize caching, we try multiple lookup alternatives and return the first one that matches.
|
||||
*/
|
||||
export async function resolvePublishedContentByUrl(input: LookupPublishedContentByUrlInput) {
|
||||
return lookupPublishedContentByUrl({
|
||||
url: input.url,
|
||||
fetchLookupAPIResult: ({ url, signal }) => {
|
||||
const api = apiClient({ apiToken: input.apiToken });
|
||||
return trace(
|
||||
{
|
||||
operation: 'resolvePublishedContentByUrl',
|
||||
name: url,
|
||||
},
|
||||
() =>
|
||||
tryCatch(
|
||||
api.urls.resolvePublishedContentByUrl(
|
||||
{
|
||||
url,
|
||||
...(input.visitorPayload ? { visitor: input.visitorPayload } : {}),
|
||||
redirectOnError: input.redirectOnError,
|
||||
},
|
||||
{ signal }
|
||||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup a content by its URL using the GitBook getPublishedContentByUrl API endpoint.
|
||||
* To optimize caching, we try multiple lookup alternatives and return the first one that matches.
|
||||
*
|
||||
* @deprecated use resolvePublishedContentByUrl.
|
||||
*
|
||||
*/
|
||||
export async function getPublishedContentByURL(input: LookupPublishedContentByUrlInput) {
|
||||
return lookupPublishedContentByUrl({
|
||||
url: input.url,
|
||||
fetchLookupAPIResult: ({ url, signal }) => {
|
||||
const api = apiClient({ apiToken: input.apiToken });
|
||||
return trace(
|
||||
{
|
||||
operation: 'getPublishedContentByURL',
|
||||
name: url,
|
||||
},
|
||||
() =>
|
||||
tryCatch(
|
||||
api.urls.getPublishedContentByUrl(
|
||||
{
|
||||
url,
|
||||
visitorAuthToken: input.visitorPayload.jwtToken ?? undefined,
|
||||
redirectOnError: input.redirectOnError,
|
||||
// @ts-expect-error - cacheVersion is not a real query param
|
||||
cacheVersion: 'v2',
|
||||
},
|
||||
{ signal }
|
||||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
type TryCatch<T> = ReturnType<typeof tryCatch<T>>;
|
||||
|
||||
async function lookupPublishedContentByUrl(input: {
|
||||
url: string;
|
||||
fetchLookupAPIResult: (args: {
|
||||
url: string;
|
||||
signal: AbortSignal;
|
||||
}) => TryCatch<Awaited<ReturnType<GitBookAPI['urls']['resolvePublishedContentByUrl']>>>;
|
||||
}): Promise<DataFetcherResponse<PublishedSiteContentLookup>> {
|
||||
const lookupURL = new URL(input.url);
|
||||
const url = stripURLSearch(lookupURL);
|
||||
const lookup = getURLLookupAlternatives(url);
|
||||
|
||||
const result = await race(lookup.urls, async (alternative, { signal }) => {
|
||||
const api = await apiClient({ apiToken: input.apiToken });
|
||||
|
||||
const callResult = await trace(
|
||||
{
|
||||
operation: 'getPublishedContentByURL',
|
||||
name: alternative.url,
|
||||
},
|
||||
() =>
|
||||
tryCatch(
|
||||
api.urls.getPublishedContentByUrl(
|
||||
{
|
||||
url: alternative.url,
|
||||
visitorAuthToken: input.visitorAuthToken ?? undefined,
|
||||
redirectOnError: input.redirectOnError,
|
||||
|
||||
// As this endpoint is cached by our API, we version the request
|
||||
// to void getting stale data with missing properties.
|
||||
// this could be improved by ensuring our API cache layer is versioned
|
||||
// or invalidated when needed
|
||||
// @ts-expect-error - cacheVersion is not a real query param
|
||||
cacheVersion: 'v2',
|
||||
},
|
||||
{
|
||||
signal,
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
const callResult = await input.fetchLookupAPIResult({
|
||||
url: alternative.url,
|
||||
signal,
|
||||
});
|
||||
|
||||
if (callResult.error) {
|
||||
if (alternative.primary) {
|
||||
|
||||
@@ -189,5 +189,7 @@ export interface GitBookDataFetcher {
|
||||
input: api.AIMessageInput[];
|
||||
output: api.AIOutputFormat;
|
||||
model: api.AIModel;
|
||||
tools?: api.AIToolCapabilities;
|
||||
previousResponseId?: string;
|
||||
}): AsyncGenerator<api.AIStreamResponse, void, unknown>;
|
||||
}
|
||||
|
||||
@@ -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,81 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { SizableImageAction, checkIsSizableImageURL } from './checkIsSizableImageURL';
|
||||
|
||||
describe('checkIsSizableImageURL', () => {
|
||||
it('should return Skip for non-parsable URLs', () => {
|
||||
expect(checkIsSizableImageURL('not a url')).toBe(SizableImageAction.Skip);
|
||||
});
|
||||
|
||||
it('should return Skip for non-http(s) URLs', () => {
|
||||
expect(checkIsSizableImageURL('data:image/png;base64,abc')).toBe(SizableImageAction.Skip);
|
||||
expect(checkIsSizableImageURL('file:///path/to/image.jpg')).toBe(SizableImageAction.Skip);
|
||||
});
|
||||
|
||||
it('should return Skip for localhost URLs', () => {
|
||||
expect(checkIsSizableImageURL('http://localhost:3000/image.jpg')).toBe(
|
||||
SizableImageAction.Skip
|
||||
);
|
||||
expect(checkIsSizableImageURL('https://localhost/image.png')).toBe(SizableImageAction.Skip);
|
||||
});
|
||||
|
||||
it('should return Skip for GitBook image URLs', () => {
|
||||
expect(checkIsSizableImageURL('https://example.com/~gitbook/image/test.jpg')).toBe(
|
||||
SizableImageAction.Skip
|
||||
);
|
||||
});
|
||||
|
||||
it('should return Resize for supported image extensions', () => {
|
||||
expect(checkIsSizableImageURL('https://example.com/image.jpg')).toBe(
|
||||
SizableImageAction.Resize
|
||||
);
|
||||
expect(checkIsSizableImageURL('https://example.com/image.jpeg')).toBe(
|
||||
SizableImageAction.Resize
|
||||
);
|
||||
expect(checkIsSizableImageURL('https://example.com/image.png')).toBe(
|
||||
SizableImageAction.Resize
|
||||
);
|
||||
expect(checkIsSizableImageURL('https://example.com/image.gif')).toBe(
|
||||
SizableImageAction.Resize
|
||||
);
|
||||
expect(checkIsSizableImageURL('https://example.com/image.webp')).toBe(
|
||||
SizableImageAction.Resize
|
||||
);
|
||||
});
|
||||
|
||||
it('should return Resize for URLs without extensions', () => {
|
||||
expect(checkIsSizableImageURL('https://example.com/image')).toBe(SizableImageAction.Resize);
|
||||
});
|
||||
|
||||
it('should return Passthrough for unsupported image extensions', () => {
|
||||
expect(checkIsSizableImageURL('https://example.com/image.svg')).toBe(
|
||||
SizableImageAction.Passthrough
|
||||
);
|
||||
expect(checkIsSizableImageURL('https://example.com/image.bmp')).toBe(
|
||||
SizableImageAction.Passthrough
|
||||
);
|
||||
expect(checkIsSizableImageURL('https://example.com/image.tiff')).toBe(
|
||||
SizableImageAction.Passthrough
|
||||
);
|
||||
expect(checkIsSizableImageURL('https://example.com/image.ico')).toBe(
|
||||
SizableImageAction.Passthrough
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle URLs with query parameters correctly', () => {
|
||||
expect(checkIsSizableImageURL('https://example.com/image.jpg?width=100')).toBe(
|
||||
SizableImageAction.Resize
|
||||
);
|
||||
expect(checkIsSizableImageURL('https://example.com/image.svg?height=200')).toBe(
|
||||
SizableImageAction.Passthrough
|
||||
);
|
||||
});
|
||||
|
||||
it('should be case-insensitive for extensions', () => {
|
||||
expect(checkIsSizableImageURL('https://example.com/image.JPG')).toBe(
|
||||
SizableImageAction.Resize
|
||||
);
|
||||
expect(checkIsSizableImageURL('https://example.com/image.PNG')).toBe(
|
||||
SizableImageAction.Resize
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,15 @@
|
||||
import { checkIsHttpURL } from '@/lib/urls';
|
||||
import { getExtension } from '@/lib/paths';
|
||||
|
||||
export enum SizableImageAction {
|
||||
Resize = 'resize',
|
||||
Skip = 'skip',
|
||||
Passthrough = 'passthrough',
|
||||
}
|
||||
|
||||
/**
|
||||
* https://developers.cloudflare.com/images/transform-images/#supported-input-formats
|
||||
*/
|
||||
const SUPPORTED_IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
|
||||
|
||||
/**
|
||||
* Check if an image URL is resizable.
|
||||
@@ -6,22 +17,27 @@ import { checkIsHttpURL } from '@/lib/urls';
|
||||
* Skip it for SVGs.
|
||||
* Skip it for GitBook images (to avoid recursion).
|
||||
*/
|
||||
export function checkIsSizableImageURL(input: string): boolean {
|
||||
export function checkIsSizableImageURL(input: string): SizableImageAction {
|
||||
if (!URL.canParse(input)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (input.includes('/~gitbook/image')) {
|
||||
return false;
|
||||
return SizableImageAction.Skip;
|
||||
}
|
||||
|
||||
const parsed = new URL(input);
|
||||
if (parsed.pathname.endsWith('.svg') || parsed.pathname.endsWith('.avif')) {
|
||||
return false;
|
||||
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
||||
return SizableImageAction.Skip;
|
||||
}
|
||||
if (!checkIsHttpURL(parsed)) {
|
||||
return false;
|
||||
if (parsed.hostname === 'localhost') {
|
||||
return SizableImageAction.Skip;
|
||||
}
|
||||
if (parsed.pathname.includes('/~gitbook/image')) {
|
||||
return SizableImageAction.Skip;
|
||||
}
|
||||
|
||||
return true;
|
||||
const extension = getExtension(parsed.pathname).toLowerCase();
|
||||
if (!extension || SUPPORTED_IMAGE_EXTENSIONS.includes(extension)) {
|
||||
// If no extension, we consider it resizable.
|
||||
return SizableImageAction.Resize;
|
||||
}
|
||||
|
||||
return SizableImageAction.Passthrough;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'server-only';
|
||||
import { GITBOOK_IMAGE_RESIZE_SIGNING_KEY, GITBOOK_IMAGE_RESIZE_URL } from '../env';
|
||||
import type { GitBookLinker } from '../links';
|
||||
import { checkIsSizableImageURL } from './checkIsSizableImageURL';
|
||||
import { SizableImageAction, checkIsSizableImageURL } from './checkIsSizableImageURL';
|
||||
import { getImageSize } from './resizer';
|
||||
import { type SignatureVersion, generateImageSignature } from './signatures';
|
||||
import type { ImageResizer } from './types';
|
||||
@@ -24,7 +24,7 @@ export function createImageResizer({
|
||||
|
||||
return {
|
||||
getResizedImageURL: (urlInput) => {
|
||||
if (!checkIsSizableImageURL(urlInput)) {
|
||||
if (checkIsSizableImageURL(urlInput) === SizableImageAction.Skip) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ export function createImageResizer({
|
||||
},
|
||||
|
||||
getImageSize: async (input, options) => {
|
||||
if (!checkIsSizableImageURL(input)) {
|
||||
if (checkIsSizableImageURL(input) !== SizableImageAction.Resize) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'server-only';
|
||||
import assertNever from 'assert-never';
|
||||
import { GITBOOK_IMAGE_RESIZE_MODE } from '../../env';
|
||||
import { checkIsSizableImageURL } from '../checkIsSizableImageURL';
|
||||
import { SizableImageAction, checkIsSizableImageURL } from '../checkIsSizableImageURL';
|
||||
import { resizeImageWithCDNCgi } from './cdn-cgi';
|
||||
import { resizeImageWithCFFetch } from './cf-fetch';
|
||||
import type { CloudflareImageJsonFormat, CloudflareImageOptions } from './types';
|
||||
@@ -13,7 +13,7 @@ export async function getImageSize(
|
||||
input: string,
|
||||
defaultSize: Partial<CloudflareImageOptions> = {}
|
||||
): Promise<{ width: number; height: number } | null> {
|
||||
if (!checkIsSizableImageURL(input)) {
|
||||
if (checkIsSizableImageURL(input) !== SizableImageAction.Resize) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -48,13 +48,17 @@ export async function resizeImage(
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
): Promise<Response> {
|
||||
const parsed = new URL(input);
|
||||
if (parsed.protocol === 'data:') {
|
||||
throw new Error('Cannot resize data: URLs');
|
||||
const action = checkIsSizableImageURL(input);
|
||||
if (action === SizableImageAction.Skip) {
|
||||
throw new Error(
|
||||
'Cannot resize this image, this function should have never been called on this url'
|
||||
);
|
||||
}
|
||||
|
||||
if (parsed.hostname === 'localhost') {
|
||||
throw new Error('Cannot resize localhost URLs');
|
||||
if (action === SizableImageAction.Passthrough) {
|
||||
return fetch(input, {
|
||||
signal: options.signal,
|
||||
});
|
||||
}
|
||||
|
||||
switch (GITBOOK_IMAGE_RESIZE_MODE) {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -93,6 +93,11 @@ export function createLinker(
|
||||
},
|
||||
|
||||
toAbsoluteURL(absolutePath: string): string {
|
||||
// If the path is already a full URL, we return it as is.
|
||||
if (URL.canParse(absolutePath)) {
|
||||
return absolutePath;
|
||||
}
|
||||
|
||||
if (!servedOn.host) {
|
||||
return absolutePath;
|
||||
}
|
||||
@@ -123,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');
|
||||
});
|
||||
});
|
||||
@@ -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];
|
||||
}
|
||||
@@ -10,15 +10,16 @@ import {
|
||||
type ResponseCookies,
|
||||
getPathScopedCookieName,
|
||||
getResponseCookiesForVisitorAuth,
|
||||
getVisitorToken,
|
||||
getVisitorData,
|
||||
normalizeVisitorAuthURL,
|
||||
} from '@/lib/visitor-token';
|
||||
} from '@/lib/visitors';
|
||||
import { serveResizedImage } from '@/routes/image';
|
||||
import {
|
||||
DataFetcherError,
|
||||
getPublishedContentByURL,
|
||||
getVisitorAuthBasePath,
|
||||
normalizeURL,
|
||||
resolvePublishedContentByUrl,
|
||||
throwIfDataError,
|
||||
} from '@v2/lib/data';
|
||||
import { isGitBookAssetsHostURL, isGitBookHostURL } from '@v2/lib/env';
|
||||
@@ -33,6 +34,15 @@ export const config = {
|
||||
|
||||
type URLWithMode = { url: URL; mode: 'url' | 'url-host' };
|
||||
|
||||
/**
|
||||
* Temporary list of hosts to test adaptive content using the new resolution API.
|
||||
*/
|
||||
const ADAPTIVE_CONTENT_HOSTS = [
|
||||
'docs.gitbook.com',
|
||||
'adaptive-docs.gitbook-staging.com',
|
||||
'enriched-content-playground.gitbook-staging.io',
|
||||
];
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
try {
|
||||
const requestURL = new URL(request.url);
|
||||
@@ -85,17 +95,22 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
//
|
||||
// Detect and extract the visitor authentication token from the request
|
||||
//
|
||||
// @ts-ignore - request typing
|
||||
const visitorToken = getVisitorToken({
|
||||
const { visitorToken, unsignedClaims, visitorParamsCookie } = getVisitorData({
|
||||
cookies: request.cookies.getAll(),
|
||||
url: siteRequestURL,
|
||||
});
|
||||
|
||||
const withAPIToken = async (apiToken: string | null) => {
|
||||
const resolve = ADAPTIVE_CONTENT_HOSTS.includes(siteRequestURL.hostname)
|
||||
? resolvePublishedContentByUrl
|
||||
: getPublishedContentByURL;
|
||||
const siteURLData = await throwIfDataError(
|
||||
getPublishedContentByURL({
|
||||
resolve({
|
||||
url: siteRequestURL.toString(),
|
||||
visitorAuthToken: visitorToken?.token ?? null,
|
||||
visitorPayload: {
|
||||
jwtToken: visitorToken?.token ?? undefined,
|
||||
unsignedClaims,
|
||||
},
|
||||
// When the visitor auth token is pulled from the cookie, set redirectOnError when calling getPublishedContentByUrl to allow
|
||||
// redirecting when the token is invalid as we could be dealing with stale token stored in the cookie.
|
||||
// For example when the VA backend signature has changed but the token stored in the cookie is not yet expired.
|
||||
@@ -106,7 +121,13 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
apiToken,
|
||||
})
|
||||
);
|
||||
const cookies: ResponseCookies = [];
|
||||
|
||||
const cookies: ResponseCookies = visitorParamsCookie
|
||||
? [
|
||||
// If visitor.* params were passed to the site URL, include a session cookie to persist these params across navigation.
|
||||
visitorParamsCookie,
|
||||
]
|
||||
: [];
|
||||
|
||||
//
|
||||
// Handle redirects
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
@@ -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/*",
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
VISITOR_TOKEN_COOKIE,
|
||||
getVisitorAuthCookieName,
|
||||
getVisitorAuthCookieValue,
|
||||
} from '@/lib/visitor-token';
|
||||
} from '@/lib/visitors';
|
||||
|
||||
import { getSiteAPIToken } from '../tests/utils';
|
||||
import {
|
||||
@@ -269,7 +269,7 @@ const testCases: TestsCase[] = [
|
||||
},
|
||||
{
|
||||
name: 'GitBook',
|
||||
contentBaseURL: 'https://docs.gitbook.com',
|
||||
contentBaseURL: 'https://gitbook.com/docs/',
|
||||
tests: [
|
||||
{
|
||||
name: 'Home',
|
||||
|
||||
@@ -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) => ({
|
||||
@@ -345,19 +346,30 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
|
||||
*/
|
||||
async function waitForIcons(page: Page) {
|
||||
await page.waitForFunction(() => {
|
||||
const urlStates: Record<string, 'pending' | 'loaded'> =
|
||||
(window as any).__ICONS_STATES__ || {};
|
||||
const urlStates: Record<
|
||||
string,
|
||||
{ state: 'pending'; uri: null } | { state: 'loaded'; uri: string }
|
||||
> = (window as any).__ICONS_STATES__ || {};
|
||||
(window as any).__ICONS_STATES__ = urlStates;
|
||||
|
||||
const fetchSvgAsDataUri = async (url: string): Promise<string> => {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch SVG: ${response.status}`);
|
||||
}
|
||||
|
||||
const svgText = await response.text();
|
||||
const encoded = encodeURIComponent(svgText).replace(/'/g, '%27').replace(/"/g, '%22');
|
||||
|
||||
return `data:image/svg+xml;charset=utf-8,${encoded}`;
|
||||
};
|
||||
|
||||
const loadUrl = (url: string) => {
|
||||
// Mark the URL as pending.
|
||||
urlStates[url] = 'pending';
|
||||
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
urlStates[url] = 'loaded';
|
||||
};
|
||||
img.src = url;
|
||||
urlStates[url] = { state: 'pending', uri: null };
|
||||
fetchSvgAsDataUri(url).then((uri) => {
|
||||
urlStates[url] = { state: 'loaded', uri };
|
||||
});
|
||||
};
|
||||
|
||||
const icons = Array.from(document.querySelectorAll('svg.gb-icon'));
|
||||
@@ -393,18 +405,11 @@ async function waitForIcons(page: Page) {
|
||||
|
||||
// If the URL is already queued for loading, we return the state.
|
||||
if (urlStates[url]) {
|
||||
if (urlStates[url] === 'loaded') {
|
||||
if (urlStates[url].state === 'loaded') {
|
||||
icon.setAttribute('data-argos-state', 'pending');
|
||||
const bckMaskImage = icon.style.maskImage;
|
||||
const bckDisplay = icon.style.display;
|
||||
icon.style.maskImage = '';
|
||||
icon.style.display = 'none';
|
||||
icon.style.maskImage = `url("${urlStates[url].uri}")`;
|
||||
requestAnimationFrame(() => {
|
||||
icon.style.maskImage = bckMaskImage;
|
||||
icon.style.display = bckDisplay;
|
||||
requestAnimationFrame(() => {
|
||||
icon.setAttribute('data-argos-state', 'loaded');
|
||||
});
|
||||
icon.setAttribute('data-argos-state', 'loaded');
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -16,7 +16,7 @@
|
||||
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gitbook/api": "*",
|
||||
"@gitbook/api": "^0.117.1",
|
||||
"@gitbook/cache-do": "workspace:*",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
"@gitbook/colors": "workspace:*",
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { AIMessage } from '@gitbook/api';
|
||||
import { DocumentView } from '../DocumentView';
|
||||
|
||||
/**
|
||||
* Render a message from the API backend.
|
||||
*/
|
||||
export function MessageView(props: {
|
||||
message: AIMessage;
|
||||
}) {
|
||||
const { message } = props;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
{message.steps.map((step, index) => {
|
||||
return (
|
||||
<div key={index} className="flex flex-col gap-2">
|
||||
<DocumentView
|
||||
document={step.content}
|
||||
context={{
|
||||
mode: 'default',
|
||||
contentContext: undefined,
|
||||
wrapBlocksInSuspense: false,
|
||||
}}
|
||||
style={['space-y-5']}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
'use server';
|
||||
import { type AIMessageInput, AIModel, type AIStreamResponse } from '@gitbook/api';
|
||||
import type { GitBookBaseContext } from '@v2/lib/context';
|
||||
import { EventIterator } from 'event-iterator';
|
||||
import type { MaybePromise } from 'p-map';
|
||||
import * as partialJson from 'partial-json';
|
||||
import type { DeepPartial } from 'ts-essentials';
|
||||
import type { z } from 'zod';
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
|
||||
/**
|
||||
* Get the latest value from a stream and the response id.
|
||||
*/
|
||||
export async function generate<T>(
|
||||
promise: MaybePromise<{
|
||||
stream: EventIterator<T>;
|
||||
response: Promise<{ responseId: string }>;
|
||||
}>
|
||||
) {
|
||||
const input = await promise;
|
||||
let value: T | undefined;
|
||||
|
||||
for await (const event of input.stream) {
|
||||
value = event;
|
||||
}
|
||||
|
||||
const { responseId } = await input.response;
|
||||
return {
|
||||
responseId,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream the generation of an object using the AI.
|
||||
*/
|
||||
export async function streamGenerateObject<T>(
|
||||
context: GitBookBaseContext,
|
||||
{
|
||||
organizationId,
|
||||
siteId,
|
||||
}: {
|
||||
organizationId: string;
|
||||
siteId: string;
|
||||
},
|
||||
{
|
||||
schema,
|
||||
messages,
|
||||
model = AIModel.Fast,
|
||||
}: {
|
||||
schema: z.ZodSchema<T>;
|
||||
messages: AIMessageInput[];
|
||||
model?: AIModel;
|
||||
previousResponseId?: string;
|
||||
}
|
||||
) {
|
||||
const rawStream = context.dataFetcher.streamAIResponse({
|
||||
organizationId,
|
||||
siteId,
|
||||
input: messages,
|
||||
output: {
|
||||
type: 'object',
|
||||
schema: zodToJsonSchema(schema),
|
||||
},
|
||||
model,
|
||||
});
|
||||
|
||||
let json = '';
|
||||
return parseResponse<DeepPartial<T>>(rawStream, (event) => {
|
||||
if (event.type === 'response_object') {
|
||||
json += event.jsonChunk;
|
||||
|
||||
const parsed = partialJson.parse(json, partialJson.ALL);
|
||||
return parsed;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a stream from the API to extract the responseId.
|
||||
*/
|
||||
function parseResponse<T>(
|
||||
responseStream: EventIterator<AIStreamResponse>,
|
||||
parse: (response: AIStreamResponse) => T | undefined
|
||||
): {
|
||||
stream: EventIterator<T>;
|
||||
response: Promise<{ responseId: string }>;
|
||||
} {
|
||||
let resolveResponse: (value: { responseId: string }) => void;
|
||||
const response = new Promise<{ responseId: string }>((resolve) => {
|
||||
resolveResponse = resolve;
|
||||
});
|
||||
|
||||
const stream = new EventIterator<T>((queue) => {
|
||||
(async () => {
|
||||
let foundResponse = false;
|
||||
|
||||
for await (const event of responseStream) {
|
||||
if (event.type === 'response_finish') {
|
||||
foundResponse = true;
|
||||
resolveResponse({ responseId: event.responseId });
|
||||
} else {
|
||||
const parsed = parse(event);
|
||||
if (parsed !== undefined) {
|
||||
queue.push(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundResponse) {
|
||||
throw new Error('No response found');
|
||||
}
|
||||
})().then(
|
||||
() => {
|
||||
queue.stop();
|
||||
},
|
||||
(error) => {
|
||||
queue.fail(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return { stream, response };
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
'use server';
|
||||
import {
|
||||
type AIMessage,
|
||||
AIMessageRole,
|
||||
type AIMessageStep,
|
||||
type AIStreamResponse,
|
||||
} from '@gitbook/api';
|
||||
import type { GitBookBaseContext } from '@v2/lib/context';
|
||||
import type { GitBookDataFetcher } from '@v2/lib/data';
|
||||
import { EventIterator } from 'event-iterator';
|
||||
import type { MaybePromise } from 'p-map';
|
||||
import * as partialJson from 'partial-json';
|
||||
import type { DeepPartial } from 'ts-essentials';
|
||||
import type { z } from 'zod';
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
import { MessageView } from '../MessageView';
|
||||
|
||||
/**
|
||||
* Get the latest value from a stream and the response id.
|
||||
*/
|
||||
export async function generate<T>(
|
||||
promise: MaybePromise<{
|
||||
stream: EventIterator<T>;
|
||||
response: Promise<{ responseId: string }>;
|
||||
}>
|
||||
) {
|
||||
const input = await promise;
|
||||
let value: T | undefined;
|
||||
|
||||
for await (const event of input.stream) {
|
||||
value = event;
|
||||
}
|
||||
|
||||
const { responseId } = await input.response;
|
||||
return {
|
||||
responseId,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream the generation of an object using the AI.
|
||||
*/
|
||||
export async function streamGenerateObject<T>(
|
||||
context: GitBookBaseContext,
|
||||
{
|
||||
schema,
|
||||
...input
|
||||
}: Omit<Parameters<GitBookDataFetcher['streamAIResponse']>[0], 'output'> & {
|
||||
schema: z.ZodSchema<T>;
|
||||
}
|
||||
) {
|
||||
const rawStream = context.dataFetcher.streamAIResponse({
|
||||
...input,
|
||||
output: {
|
||||
type: 'object',
|
||||
schema: zodToJsonSchema(schema),
|
||||
},
|
||||
});
|
||||
|
||||
let json = '';
|
||||
return parseResponse<DeepPartial<T>>(rawStream, (event) => {
|
||||
if (event.type === 'response_object') {
|
||||
json += event.jsonChunk;
|
||||
|
||||
const parsed = partialJson.parse(json, partialJson.ALL);
|
||||
return parsed;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream the generation of a document.
|
||||
*/
|
||||
export async function streamGenerateDocument(
|
||||
context: GitBookBaseContext,
|
||||
input: Omit<Parameters<GitBookDataFetcher['streamAIResponse']>[0], 'output'>
|
||||
) {
|
||||
const rawStream = context.dataFetcher.streamAIResponse({
|
||||
...input,
|
||||
output: {
|
||||
type: 'document',
|
||||
},
|
||||
});
|
||||
|
||||
const message: AIMessage = {
|
||||
id: '',
|
||||
role: AIMessageRole.Assistant,
|
||||
steps: [],
|
||||
};
|
||||
|
||||
const updateProcessingMessageStep = (
|
||||
stepIndex: number,
|
||||
callback: (step: AIMessageStep) => void
|
||||
) => {
|
||||
if (stepIndex > message.steps.length) {
|
||||
throw new Error(
|
||||
`Step index out of bounds ${stepIndex} (${message.steps.length} steps)`
|
||||
);
|
||||
}
|
||||
|
||||
if (message.steps[stepIndex]) {
|
||||
message.steps = [...message.steps];
|
||||
message.steps[stepIndex] = { ...message.steps[stepIndex] };
|
||||
callback(message.steps[stepIndex]);
|
||||
} else {
|
||||
message.steps = [
|
||||
...message.steps,
|
||||
{
|
||||
content: {
|
||||
object: 'document',
|
||||
data: {},
|
||||
nodes: [],
|
||||
},
|
||||
},
|
||||
];
|
||||
callback(message.steps[stepIndex]);
|
||||
}
|
||||
};
|
||||
|
||||
return parseResponse<React.ReactNode>(rawStream, (event) => {
|
||||
switch (event.type) {
|
||||
/**
|
||||
* The agent is processing a tool call in a new message.
|
||||
*/
|
||||
case 'response_tool_call': {
|
||||
updateProcessingMessageStep(event.stepIndex, (step) => {
|
||||
step.toolCalls ??= [];
|
||||
step.toolCalls.push(event.toolCall);
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
* The agent is writing the content of a new message.
|
||||
*/
|
||||
case 'response_reasoning':
|
||||
case 'response_document': {
|
||||
updateProcessingMessageStep(event.stepIndex, (step) => {
|
||||
const container = event.type === 'response_reasoning' ? 'reasoning' : 'content';
|
||||
|
||||
step[container] ??= {
|
||||
object: 'document',
|
||||
data: {},
|
||||
nodes: [],
|
||||
};
|
||||
step[container] = {
|
||||
...step[container],
|
||||
nodes: [...step[container].nodes],
|
||||
};
|
||||
if (event.operation === 'insert') {
|
||||
step[container].nodes.push(...event.blocks);
|
||||
} else {
|
||||
step[container].nodes.splice(
|
||||
-event.blocks.length,
|
||||
event.blocks.length,
|
||||
...event.blocks
|
||||
);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return <MessageView message={message} />;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a stream from the API to extract the responseId.
|
||||
*/
|
||||
function parseResponse<T>(
|
||||
responseStream: EventIterator<AIStreamResponse>,
|
||||
parse: (response: AIStreamResponse) => T | undefined
|
||||
): {
|
||||
stream: EventIterator<T>;
|
||||
response: Promise<{ responseId: string }>;
|
||||
} {
|
||||
let resolveResponse: (value: { responseId: string }) => void;
|
||||
const response = new Promise<{ responseId: string }>((resolve) => {
|
||||
resolveResponse = resolve;
|
||||
});
|
||||
|
||||
const stream = new EventIterator<T>((queue) => {
|
||||
(async () => {
|
||||
let foundResponse = false;
|
||||
|
||||
for await (const event of responseStream) {
|
||||
if (event.type === 'response_finish') {
|
||||
foundResponse = true;
|
||||
resolveResponse({ responseId: event.responseId });
|
||||
} else {
|
||||
const parsed = parse(event);
|
||||
if (parsed !== undefined) {
|
||||
queue.push(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundResponse) {
|
||||
throw new Error('No response found');
|
||||
}
|
||||
})().then(
|
||||
() => {
|
||||
queue.stop();
|
||||
},
|
||||
(error) => {
|
||||
queue.fail(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return { stream, response };
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export * from './streamLinkPageSummary';
|
||||
export * from './api';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { filterOutNullable } from '@/lib/typescript';
|
||||
import { getV1BaseContext } from '@/lib/v1';
|
||||
import { isV2 } from '@/lib/v2';
|
||||
import { AIMessageRole } from '@gitbook/api';
|
||||
import { AIMessageRole, AIModel } from '@gitbook/api';
|
||||
import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';
|
||||
import { getServerActionBaseContext } from '@v2/lib/server-actions';
|
||||
import { z } from 'zod';
|
||||
@@ -32,23 +32,18 @@ export async function* streamLinkPageSummary({
|
||||
const baseContext = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
|
||||
const siteURLData = await getSiteURLDataFromMiddleware();
|
||||
|
||||
const { stream } = await streamGenerateObject(
|
||||
baseContext,
|
||||
{
|
||||
organizationId: siteURLData.organization,
|
||||
siteId: siteURLData.site,
|
||||
},
|
||||
{
|
||||
schema: z.object({
|
||||
highlight: z
|
||||
.string()
|
||||
.describe('The reason why the user should read the target page.'),
|
||||
// questions: z.array(z.string().describe('The questions to sea')).max(3),
|
||||
}),
|
||||
messages: [
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `# 1. Role
|
||||
const { stream } = await streamGenerateObject(baseContext, {
|
||||
organizationId: siteURLData.organization,
|
||||
siteId: siteURLData.site,
|
||||
model: AIModel.Fast,
|
||||
schema: z.object({
|
||||
highlight: z.string().describe('The reason why the user should read the target page.'),
|
||||
// questions: z.array(z.string().describe('The questions to sea')).max(3),
|
||||
}),
|
||||
input: [
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `# 1. Role
|
||||
You are a contextual fact extractor. Your job is to find the exact fact from the linked page that directly answers the implied question in the current paragraph.
|
||||
|
||||
# 2. Task
|
||||
@@ -64,60 +59,60 @@ Extract a contextually-relevant fact that:
|
||||
3. Ensure the fact relates directly to the context of the paragraph containing the link
|
||||
4. Avoid ALL instructional language including words like "use", "click", "select", "create"
|
||||
5. Keep it under 30 words, factual and declarative about what EXISTS or IS TRUE`,
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `# 4. Current page
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `# 4. Current page
|
||||
The content of the current page is:`,
|
||||
attachments: [
|
||||
{
|
||||
type: 'page' as const,
|
||||
spaceId: currentSpaceId,
|
||||
pageId: currentPageId,
|
||||
},
|
||||
],
|
||||
},
|
||||
...(visitedPages
|
||||
? [
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: '# 5. Previous pages',
|
||||
},
|
||||
...visitedPages.map(({ spaceId, pageId }) => ({
|
||||
role: AIMessageRole.Developer,
|
||||
content: `## Page ${pageId}`,
|
||||
attachments: [
|
||||
{
|
||||
type: 'page' as const,
|
||||
spaceId,
|
||||
pageId,
|
||||
},
|
||||
],
|
||||
})),
|
||||
]
|
||||
: []),
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `# 6. Target page
|
||||
attachments: [
|
||||
{
|
||||
type: 'page' as const,
|
||||
spaceId: currentSpaceId,
|
||||
pageId: currentPageId,
|
||||
},
|
||||
],
|
||||
},
|
||||
...(visitedPages
|
||||
? [
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: '# 5. Previous pages',
|
||||
},
|
||||
...visitedPages.map(({ spaceId, pageId }) => ({
|
||||
role: AIMessageRole.Developer,
|
||||
content: `## Page ${pageId}`,
|
||||
attachments: [
|
||||
{
|
||||
type: 'page' as const,
|
||||
spaceId,
|
||||
pageId,
|
||||
},
|
||||
],
|
||||
})),
|
||||
]
|
||||
: []),
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `# 6. Target page
|
||||
The content of the target page is:`,
|
||||
attachments: [
|
||||
{
|
||||
type: 'page' as const,
|
||||
spaceId: targetSpaceId,
|
||||
pageId: targetPageId,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `# 7. Link preview
|
||||
attachments: [
|
||||
{
|
||||
type: 'page' as const,
|
||||
spaceId: targetSpaceId,
|
||||
pageId: targetPageId,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `# 7. Link preview
|
||||
The content of the link preview is:
|
||||
> ${linkPreview}
|
||||
> Page ID: ${targetPageId}`,
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `# 8. Guidelines & Examples
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `# 8. Guidelines & Examples
|
||||
ALWAYS:
|
||||
- ALWAYS choose facts that directly fulfill the contextual need where the link appears
|
||||
- ALWAYS connect target page information specifically to the current paragraph context
|
||||
@@ -146,14 +141,13 @@ Current paragraph: "Your team mentioned issues with conflicting edits. Need to c
|
||||
Preview: "Live Edit: Real-time collaborative editing."
|
||||
✓ "Teams with GitHub repositories (like yours) cannot use this feature due to sync limitations."
|
||||
✗ "Incompatible with GitHub/GitLab sync and requires specific visibility settings."`,
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.User,
|
||||
content: `I'm considering reading the link titled "${linkTitle}" pointing to page ${targetPageId}. Why should I read it? Relate it to the paragraph I'm currently reading.`,
|
||||
},
|
||||
].filter(filterOutNullable),
|
||||
}
|
||||
);
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.User,
|
||||
content: `I'm considering reading the link titled "${linkTitle}" pointing to page ${targetPageId}. Why should I read it? Relate it to the paragraph I'm currently reading.`,
|
||||
},
|
||||
].filter(filterOutNullable),
|
||||
});
|
||||
|
||||
for await (const value of stream) {
|
||||
const highlight = value.highlight;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { tString, useLanguage } from '@/intl/client';
|
||||
import * as storage from '@/lib/local-storage';
|
||||
import type { ResolvedContentRef } from '@/lib/references';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
@@ -18,6 +19,8 @@ export function AnnouncementBanner(props: {
|
||||
}) {
|
||||
const { announcement, contentRef } = props;
|
||||
|
||||
const language = useLanguage();
|
||||
|
||||
const hasLink = announcement.link && contentRef?.href;
|
||||
const closeable = announcement.style !== 'danger';
|
||||
|
||||
@@ -25,7 +28,7 @@ export function AnnouncementBanner(props: {
|
||||
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
|
||||
@@ -81,6 +84,7 @@ export function AnnouncementBanner(props: {
|
||||
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}`}
|
||||
type="button"
|
||||
onClick={dismissAnnouncement}
|
||||
title={tString(language, 'close')}
|
||||
>
|
||||
<Icon icon="close" className="size-4" />
|
||||
</button>
|
||||
|
||||
@@ -72,7 +72,7 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShow(false)}
|
||||
aria-label={tString(language, 'cookies_close')}
|
||||
aria-label={tString(language, 'close')}
|
||||
className={tcls(
|
||||
'absolute',
|
||||
'top-3',
|
||||
@@ -85,7 +85,7 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
|
||||
'rounded-sm',
|
||||
'hover:bg-tint-hover'
|
||||
)}
|
||||
title={tString(language, 'cookies_close')}
|
||||
title={tString(language, 'close')}
|
||||
>
|
||||
<Icon icon="xmark" className={tcls('size-4')} />
|
||||
</button>
|
||||
|
||||
@@ -58,7 +58,7 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
|
||||
<code
|
||||
id={id}
|
||||
className={tcls(
|
||||
'inline-grid min-w-full grid-cols-[auto_1fr] p-2 [count-reset:line]',
|
||||
'inline-grid min-w-full grid-cols-[auto_1fr] p-2 [count-reset:line] print:whitespace-pre-wrap',
|
||||
withWrap && 'whitespace-pre-wrap'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Card } from '@/components/primitives';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { getDataOrNull } from '@v2/lib/data';
|
||||
import { Image } from '../utils';
|
||||
import type { BlockProps } from './Block';
|
||||
import { Caption } from './Caption';
|
||||
import { IntegrationBlock } from './Integration';
|
||||
@@ -52,7 +53,14 @@ export async function Embed(props: BlockProps<gitbookAPI.DocumentBlockEmbed>) {
|
||||
<Card
|
||||
leadingIcon={
|
||||
embed.icon ? (
|
||||
<img src={embed.icon} className={tcls('w-5', 'h-5')} alt="Logo" />
|
||||
<Image
|
||||
src={embed.icon}
|
||||
className={tcls('w-5', 'h-5')}
|
||||
alt="Logo"
|
||||
sources={{ light: { src: embed.icon } }}
|
||||
sizes={[{ width: 20 }]}
|
||||
resize={context.contentContext.imageResizer}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
href={block.data.url}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||
import type { DocumentBlockHeading, DocumentBlockTabs } from '@gitbook/api';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import { getBlockTextStyle } from './spacing';
|
||||
|
||||
/**
|
||||
* A hash icon which adds the block or active block item's ID in the URL hash.
|
||||
* The button needs to be wrapped in a container with `hashLinkButtonWrapperStyles`.
|
||||
*/
|
||||
export const hashLinkButtonWrapperStyles = tcls('relative', 'group/hash');
|
||||
|
||||
export function HashLinkButton(props: {
|
||||
id: string;
|
||||
block: DocumentBlockTabs | DocumentBlockHeading;
|
||||
label?: string;
|
||||
className?: ClassValue;
|
||||
iconClassName?: ClassValue;
|
||||
}) {
|
||||
const { id, block, className, iconClassName, label = 'Direct link to block' } = props;
|
||||
const textStyle = getBlockTextStyle(block);
|
||||
return (
|
||||
<div
|
||||
className={tcls(
|
||||
'relative',
|
||||
'hash',
|
||||
'grid',
|
||||
'grid-area-1-1',
|
||||
'h-[1em]',
|
||||
'border-0',
|
||||
'opacity-0',
|
||||
'group-hover/hash:opacity-[0]',
|
||||
'group-focus/hash:opacity-[0]',
|
||||
'md:group-hover/hash:md:opacity-[1]',
|
||||
'md:group-focus/hash:md:opacity-[1]',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<a
|
||||
href={`#${id}`}
|
||||
aria-label={label}
|
||||
className={tcls('inline-flex', 'h-full', 'items-start', textStyle.lineHeight)}
|
||||
>
|
||||
<Icon
|
||||
icon="hashtag"
|
||||
className={tcls(
|
||||
'size-3',
|
||||
'self-center',
|
||||
'transition-colors',
|
||||
'text-transparent',
|
||||
'group-hover/hash:text-tint-subtle',
|
||||
'contrast-more:group-hover/hash:text-tint-strong',
|
||||
iconClassName
|
||||
)}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { DocumentBlockHeading } from '@gitbook/api';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import type { BlockProps } from './Block';
|
||||
import { HashLinkButton, hashLinkButtonWrapperStyles } from './HashLinkButton';
|
||||
import { Inlines } from './Inlines';
|
||||
import { getBlockTextStyle } from './spacing';
|
||||
|
||||
@@ -23,50 +23,20 @@ export function Heading(props: BlockProps<DocumentBlockHeading>) {
|
||||
className={tcls(
|
||||
textStyle.textSize,
|
||||
'heading',
|
||||
'group',
|
||||
'relative',
|
||||
'grid',
|
||||
'scroll-m-12',
|
||||
hashLinkButtonWrapperStyles,
|
||||
style
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={tcls(
|
||||
'hash',
|
||||
'grid',
|
||||
'grid-area-1-1',
|
||||
'relative',
|
||||
'-ml-6',
|
||||
'w-7',
|
||||
'border-0',
|
||||
'opacity-0',
|
||||
'group-hover:opacity-[0]',
|
||||
'group-focus:opacity-[0]',
|
||||
'md:group-hover:md:opacity-[1]',
|
||||
'md:group-focus:md:opacity-[1]',
|
||||
textStyle.marginTop
|
||||
)}
|
||||
>
|
||||
<a
|
||||
href={`#${id}`}
|
||||
aria-label="Direct link to heading"
|
||||
className={tcls('inline-flex', 'h-full', 'items-start', textStyle.lineHeight)}
|
||||
>
|
||||
<Icon
|
||||
icon="hashtag"
|
||||
className={tcls(
|
||||
'w-3.5',
|
||||
'h-[1em]',
|
||||
'mt-0.5',
|
||||
'transition-colors',
|
||||
'text-transparent',
|
||||
'group-hover:text-tint-subtle',
|
||||
'contrast-more:group-hover:text-tint-strong',
|
||||
'lg:w-4'
|
||||
)}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<HashLinkButton
|
||||
id={id}
|
||||
block={block}
|
||||
className={tcls('-ml-6', textStyle.anchorButtonMarginTop)}
|
||||
iconClassName={tcls('size-4')}
|
||||
label="Direct link to heading"
|
||||
/>
|
||||
|
||||
<div
|
||||
className={tcls(
|
||||
'grid-area-1-1',
|
||||
|
||||
@@ -29,7 +29,7 @@ export function Images(props: BlockProps<DocumentBlockImages>) {
|
||||
align === 'center' && 'justify-center',
|
||||
align === 'right' && 'justify-end',
|
||||
align === 'left' && 'justify-start',
|
||||
isMultipleImages && ['grid', 'grid-flow-col', 'max-w-none']
|
||||
isMultipleImages && ['grid', 'grid-flow-col']
|
||||
)}
|
||||
>
|
||||
{block.nodes.map((node: any, _i: number) => (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user