Compare commits

..

82 Commits

Author SHA1 Message Date
Samy Pessé 32d6f5845d Version Packages (#3247)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-07 12:49:53 +00:00
Greg Bergé 4f5cbfe732 Upgrade @scalar/openapi-parser (#3442) 2025-07-07 14:33:40 +02:00
Steven H afba8fcb34 Rating columns in tables now use a solid icons instead of unfilled. (#3434) 2025-07-07 10:05:45 +01:00
conico974 377a4899e4 Adjust cache life for error responses (#3423)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-07-07 09:38:48 +02:00
Greg Bergé 938bdeb34b Fix visual tests (#3440) 2025-07-06 09:02:31 +02:00
Greg Bergé d216eaa3f6 Upgrade Argos & disable bad plugin (#3438) 2025-07-05 21:37:20 +02:00
conico974 5ca5da0f21 Fix handling of bad requests to prevent unnecessary 500 errors (#3432)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-07-04 17:06:37 +02:00
Greg Bergé ebe6eb3b39 Support custom monospace fonts (#3430) 2025-07-04 13:13:36 +00:00
Zeno Kapitein 3bfe347096 Show section tabs when there is a single section group present (#3387) 2025-07-04 14:55:40 +02:00
Samy Pessé 7b38f89078 Update API client and condition for AI assistant (#3429) 2025-07-03 20:34:26 +02:00
Greg Bergé 09b689b0ea Fix table display in print (#3428) 2025-07-03 17:44:55 +02:00
Nolann B. 2350baa75f Support for OpenAPI Array request body (#3420) 2025-07-03 17:18:26 +02:00
Nolann B. bc1eca815e Error handling for AI chat (#3411) 2025-07-03 14:26:40 +02:00
conico974 78c10340e7 Increase max line length for code highlighting (#3415)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-07-03 13:05:42 +02:00
Valentino Hudhra c16890a3a6 Align README with CONTRIBUTING guides regarding bun version (#3427) 2025-07-03 12:40:08 +02:00
Addison 3b4fe2827a Update community ad on sponsored/community sites (#3421) 2025-07-03 11:03:16 +02:00
Samy Pessé 7e807aabb5 Don't prevent sending message in AI chat when loading followup suggestions (#3426) 2025-07-03 10:16:42 +02:00
Samy Pessé 973c74ee69 Adapt AI assistant to upcoming API changes (#3422) 2025-07-02 20:58:23 +02:00
Zeno Kapitein efed0b0617 Turn on Docs Assistant for another site (#3425) 2025-07-02 18:02:12 +02:00
Zeno Kapitein ca3b9aca5d Improve AI Chat context popup (#3424) 2025-07-02 18:01:24 +02:00
Taran Vohra e2afc07ab2 Fix resolution of page by resolving site redirects before space redirects (#3414) 2025-07-02 17:30:18 +05:30
Brett Jephson e8fb84d362 Remove flip hash from columns (#3413) 2025-07-02 09:41:53 +01:00
Samy Pessé 57f951a7d9 Remove AI page link summaries (#3418) 2025-07-02 08:40:55 +02:00
conico974 28008667ed Make Emoji component synchronous (#3417)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-07-01 17:10:16 +02:00
Samy Pessé f3affc3034 Add support for MCP tools (#3410) 2025-07-01 09:58:59 +02:00
conico974 59da30f3c6 Add support for cover image repositioning (#3404)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-30 19:07:48 +02:00
Greg Bergé 2db721112a Fix OpenAPI error (#3408) 2025-06-30 15:56:22 +00:00
Samy Pessé b60039b7d5 Fix links to other spaces in a section (#3409)
Co-authored-by: Steven H <steven@gitbook.io>
2025-06-30 17:47:23 +02:00
Steven H 8fe9c9afea Fix an issue where same-site references across spaces created invalid links. (#3407) 2025-06-30 16:03:27 +01:00
Zeno Kapitein 216ba7a556 Add analytics on AI chat (#3405) 2025-06-30 08:41:12 +00:00
Samy Pessé a2ff57081b Fix changesets 2025-06-29 10:41:30 +02:00
Zeno Kapitein 40dbd9a14b AI Chat followup (#3403) 2025-06-28 17:55:56 +02:00
Nolann B. e5bac69c5c Fix markdown page generation for groups (#3402) 2025-06-27 23:32:35 +02:00
Zeno Kapitein 8d6598393f AI Chat UI (#3397)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2025-06-27 17:43:14 +02:00
Greg Bergé 59b86eb9dc Correctly handle OpenAPI errors (#3401) 2025-06-27 11:15:21 +02:00
Steven H 52c9f6da2f Fix an issue with link resolution in cross-space reusable content. (#3392)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2025-06-27 11:14:38 +02:00
Nolann B. dd65987c94 Include page group children under the .md route (#3400) 2025-06-27 09:53:18 +02:00
conico974 ace61901a4 Fix ScrollSectionsList with suspended component (#3390)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-26 15:37:51 +02:00
conico974 e6c3c7635d refactor toc components to client (#3394)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2025-06-26 11:35:02 +02:00
conico974 5134d9e7f8 Avoid unnecessary proxy URL lookups (#3377)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-25 17:21:09 +02:00
conico974 6821fb2937 Downgrade esbuild to fix wrangler dev issues (#3396)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-25 16:50:09 +02:00
Taran Vohra cfe4812045 Replace all usage of getPublishedContentByUrl (#3395) 2025-06-25 18:55:02 +05:30
Samy Pessé 14843ac343 Fix changesets (#3393) 2025-06-25 14:42:49 +02:00
Samy Pessé 8fb6d7465f Remove v1 and replace by v2 (#3391)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-25 13:28:04 +02:00
Samy Pessé 0ef647586f Hooks and APIs for AI v2 (#3385) 2025-06-25 09:48:28 +02:00
Nolann B. b4039627d9 Handle nullish OpenAPI mediaTypeObject (#3389) 2025-06-24 17:25:38 +00:00
Addison 4a295e6db4 Update contributing guide (#3381) 2025-06-24 17:55:24 +02:00
Greg Bergé 5726999a5a Fix viewing a page from a revision (#3388) 2025-06-24 15:31:06 +00:00
conico974 58d7f3c140 Fix revision ID handling for computed content (#3386)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-24 15:00:33 +02:00
Samy Pessé 07eab986d5 Optimize fetching of revisions (#3382) 2025-06-23 19:15:17 +02:00
Samy Pessé 711cf38f9b Use getRevision in getRevisionFile (#3379) 2025-06-23 18:43:41 +02:00
Zeno Kapitein 4f7c0eea9f Clicking an active TOC item toggles its descendants (#3378) 2025-06-23 17:27:34 +02:00
conico974 f3448de2da Improve efficiency of the queue (#3373)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-23 15:09:29 +02:00
Viktor Renkema ec92075ef8 Set tw classes correctly for horizontal alignment in tables (#3375) 2025-06-23 14:35:32 +02:00
Nolann B. 1e013cd87c Optional label in OpenAPI x-codeSamples (#3376) 2025-06-23 13:54:15 +02:00
Samy Pessé f033734308 Support for site customization option to change how external links open (#3372) 2025-06-22 15:49:15 +02:00
Nolann B. 50c05164f7 Bump @scalar/api-client-react (#3371) 2025-06-22 12:03:10 +02:00
conico974 7fb9004b43 Properly redirect to the root page (#3369)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-20 19:15:21 +02:00
Greg Bergé 4721403986 Hide scrollbar on sections (#3367) 2025-06-20 12:31:18 +00:00
Samy Pessé df848ef64e Icon in buttons and text alignment (#3365) 2025-06-20 12:08:28 +02:00
conico974 0b40ccd286 Remove buildId from composable cache key generation (#3366)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-20 12:06:44 +02:00
Greg Bergé caaa692f0b Allow to zoom image on mobile if relevant (#3364) 2025-06-20 10:25:58 +02:00
Greg Bergé 42d43e09c4 Always show scrollbars (#3363) 2025-06-20 08:41:21 +02:00
Samy Pessé 7ccbf55ba7 Set myself as codeowner for data cache (#3355) 2025-06-20 08:41:02 +02:00
conico974 81f5bfc2f9 Filter out malicious request (#3357)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-19 17:00:37 +02:00
Greg Bergé a7a713bca2 Scroll to active TOC when clicking a link (#3362) 2025-06-19 16:50:13 +02:00
conico974 d6613c787f Make skeletons client component (#3361)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-19 16:46:31 +02:00
Greg Bergé 427f748e1c Add metadata for adding site to Apple devices home (#3360) 2025-06-19 16:35:07 +02:00
Greg Bergé 4f5fec7213 Fix CodeBlock rendering (#3359) 2025-06-19 15:55:19 +02:00
Greg Bergé 8c0a53ab40 Fix page group not expanded by default (#3358) 2025-06-19 15:08:59 +02:00
conico974 2dfd5aab20 Update SpacesDropdown to manage active state based on current space (#3356)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-19 13:39:59 +02:00
conico974 8d3c6562a2 Implement a StylesProvider (#3352)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-18 19:02:32 +02:00
Zeno Kapitein dae019c115 Consistently show variant selector in section bar if site has sections (#3349) 2025-06-18 17:48:49 +02:00
Steven H 73e0cbb2d6 Fix an issue where PDF export URLs were not bringing their query params. (#3351) 2025-06-18 13:26:20 +01:00
Steven H 392f59450c Improve performance of InlineLinkTooltip (#3339) 2025-06-18 12:15:45 +01:00
conico974 b4918f60ce fix variant space change when pathname equals space-path (#3343)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-06-17 22:31:32 +02:00
Samy Pessé 8f7c304d58 Fix crash when rendering ogimage with invalid icon (#3348) 2025-06-17 20:25:21 +02:00
Samy Pessé a3a944d7dd Fix crash during rendering of ogimage for VA sites with default icon (#3347) 2025-06-17 19:38:47 +02:00
Samy Pessé 88a35ed057 Fix crash when integration is triggering invalid requests (#3346) 2025-06-17 19:23:52 +02:00
Samy Pessé 67998b6f15 Fix ogimage generation failing with some JPEG images (#3345) 2025-06-17 19:10:15 +02:00
Samy Pessé ff6d1150a5 Use getRevisionPageDocument on v2 (#3342) 2025-06-17 16:12:58 +02:00
Samy Pessé 500c8cb649 Don't crash ogimage generation on RTL text (#3341) 2025-06-17 13:04:50 +02:00
437 changed files with 6866 additions and 9594 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix UX issue about highlighting the search term in search result sections
-9
View File
@@ -1,9 +0,0 @@
---
"gitbook": patch
---
Fix three small visual issues
- Fix sidebar showing on `no-toc` pages in the gradient theme
- Fix variant selector truncating incorrectly in header when sections are present
- Fix page cover alignment on `lg` screens without TOC
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Respect fullWidth and defaultWidth for images
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook-v2": patch
---
Add docs.testgitbook.com to ADAPTIVE_CONTENT_HOSTS list
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
fix nested a tag causing hydration error
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
fix href being empty in TOC
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix navigation between sections/variants when previewing a site in v2
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": minor
---
Add support for inline icons.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": minor
---
Add circular corners and depth styling
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix crash when integration script fails to render block.
-5
View File
@@ -1,5 +0,0 @@
---
'@gitbook/react-openapi': patch
---
Add authorization header for OAuth2
-6
View File
@@ -1,6 +0,0 @@
---
'@gitbook/react-openapi': patch
'gitbook': patch
---
Improve support for OAuth2 security type
-6
View File
@@ -1,6 +0,0 @@
---
"gitbook-v2": patch
"gitbook": patch
---
Adds Columns layout block to GBO
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": minor
---
Support dark-mode specific page cover image
-5
View File
@@ -1,5 +0,0 @@
---
'gitbook': patch
---
Update Models page styling
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook-v2": patch
---
Optimize performances by using a smarter per-request cache arround data cached functions
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook-v2": patch
---
add a force-revalidate api route to force bust the cache in case of errors
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": minor
---
Fix rendering of ogimage with SVG logos.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": minor
---
Best effort at preserving current variant when navigating between sections by matching the pathname against site spaces in the new section.
-6
View File
@@ -1,6 +0,0 @@
---
"gitbook-v2": patch
"gitbook": patch
---
encode customization header
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": minor
---
Rework full-width layout, add support for full-width page option
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook-v2": patch
---
fix ISR on preview env
-6
View File
@@ -1,6 +0,0 @@
---
"gitbook-v2": patch
"gitbook": patch
---
Fix concurrent execution in Vercel causing pages to not be attached to the proper tags.
-5
View File
@@ -1,5 +0,0 @@
---
"@gitbook/react-contentkit": patch
---
Add basic error handling when transitioning between states.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook-v2": patch
---
Generate a llms-full.txt version of the docs site
-5
View File
@@ -1,5 +0,0 @@
---
"@gitbook/fonts": minor
---
Initial version of the package
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix rendering of ogimage when logo or icon are AVIF images.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Add margin to adjacent buttons
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook-v2": patch
---
Don't cache unexpected API errors for more than a few minutes.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook-v2": patch
---
add a global error boundary
-5
View File
@@ -1,5 +0,0 @@
---
'@gitbook/react-openapi': patch
---
Indent JSON python code sample
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix missing title on button to close the announcement banner.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Make icons for page groups more contrasting
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Ignore case while highlighting search results.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Make TOC height dynamic based on visible header and footer elements
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix bold header links hover color
-6
View File
@@ -1,6 +0,0 @@
---
"gitbook": patch
"gitbook-v2": patch
---
cache fonts and static image used in OGImage in memory
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix border being added to cards
-5
View File
@@ -1,5 +0,0 @@
---
'@gitbook/react-openapi': patch
---
Handle nested deprecated properties in generateSchemaExample
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook-v2": patch
---
Add `urlObject.hash` to `linker.toLinkForContent` to pass through URL fragment identifiers, used in search
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook-v2": patch
---
apply customization for dynamic context
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Reverse order of feedback smileys
-5
View File
@@ -1,5 +0,0 @@
---
'@gitbook/react-openapi': patch
---
Deduplicate path parameters from OpenAPI spec
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix ogimage using incorrect Google Font depending on language.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook-v2": patch
---
remove trailing slash from linker
+3
View File
@@ -0,0 +1,3 @@
# Changes to the API data cache functions can invalidate all existing data cache
# causing a massive amount of revalidation, impacting our API.
packages/gitbook/src/lib/data/api.ts @SamyPesse
+4 -9
View File
@@ -71,14 +71,9 @@ nvm use
bun install
```
3. Build the project:
3. Start the development server:
```bash
bun build:v2
```
4. Start the development server:
```bash
bun dev:v2
bun dev
```
Additional development commands:
@@ -89,9 +84,9 @@ Additional development commands:
#### 4. Preview your changes
When running the development server, published GitBook sites can be rendered through your local version at `http://localhost:3000/`.
When running the development server, published GitBook sites can be rendered through your local version at `http://localhost:3000/url`.
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.
For example, our published docs can be viewed using the local version by visiting `http://localhost:3000/url/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://gitbook.com/docs/published-documentation/publish-your-content-as-a-docs-site) to ensure you can view the site correctly in your development version.
@@ -32,7 +32,7 @@ runs:
workingDirectory: ./
wranglerVersion: '4.10.0'
environment: ${{ inputs.environment }}
command: deployments status --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
command: deployments status --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
# This step is used to get the version ID that is currently deployed to Cloudflare.
- id: extract_current_version
@@ -51,7 +51,7 @@ runs:
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
command: versions deploy ${{ steps.extract_current_version.outputs.version_id }}@100% ${{ inputs.serverVersionId }}@0% -y --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
# Since we use version overrides headers, we can directly deploy the middleware to 100%.
- id: deploy_middleware
@@ -63,7 +63,7 @@ runs:
workingDirectory: ./
wranglerVersion: '4.10.0'
environment: ${{ inputs.environment }}
command: versions deploy ${{ inputs.middlewareVersionId }}@100% -y --config ./packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc
command: versions deploy ${{ inputs.middlewareVersionId }}@100% -y --config ./packages/gitbook/openNext/customWorkers/middlewareWrangler.jsonc
- name: Deploy server to Cloudflare at 100%
uses: cloudflare/wrangler-action@v3.14.0
@@ -73,7 +73,7 @@ runs:
workingDirectory: ./
wranglerVersion: '4.10.0'
environment: ${{ inputs.environment }}
command: versions deploy ${{ inputs.serverVersionId }}@100% -y --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
command: versions deploy ${{ inputs.serverVersionId }}@100% -y --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
- name: Outputs
shell: bash
@@ -59,7 +59,7 @@ runs:
GITBOOK_ASSETS_PREFIX: ${{ inputs.opItem }}/GITBOOK_ASSETS_PREFIX
GITBOOK_FONTS_URL: ${{ inputs.opItem }}/GITBOOK_FONTS_URL
- name: Build worker
run: bun run turbo build:v2:cloudflare
run: bun run turbo build:cloudflare
env:
GITBOOK_RUNTIME: cloudflare
shell: bash
@@ -72,7 +72,7 @@ runs:
workingDirectory: ./
wranglerVersion: '4.10.0'
environment: ${{ inputs.environment }}
command: deploy --config ./packages/gitbook-v2/openNext/customWorkers/doWrangler.jsonc
command: deploy --config ./packages/gitbook/openNext/customWorkers/doWrangler.jsonc
- id: upload_server
name: Upload server to Cloudflare
@@ -83,7 +83,7 @@ runs:
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
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
- name: Extract server version worker ID
shell: bash
@@ -95,7 +95,7 @@ runs:
- 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 }}
bun run ./packages/gitbook/openNext/customWorkers/script/updateWrangler.ts ${{ steps.extract_server_version_id.outputs.version_id }}
- id: upload_middleware
name: Upload middleware to Cloudflare
@@ -106,7 +106,7 @@ runs:
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
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook/openNext/customWorkers/middlewareWrangler.jsonc
- name: Extract middleware version worker ID
shell: bash
+7 -103
View File
@@ -7,53 +7,6 @@ on:
env:
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
jobs:
deploy-v1-cloudflare:
name: Deploy v1 to Cloudflare Pages
runs-on: ubuntu-latest
environment:
name: ${{ github.ref == 'refs/heads/main' && '1c-production' || '1c-preview' }}
url: ${{ steps.deploy.outputs.deployment-url }}
permissions:
contents: read
deployments: write
issues: write
pull-requests: write
checks: write
statuses: write
outputs:
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
deployment-alias-url: ${{ steps.deploy.outputs.deployment-alias-url }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Sets env vars for production
if: github.ref == 'refs/heads/main'
run: |
echo "GITBOOK_ASSETS_PREFIX=https://static.gitbook.com" >> $GITHUB_ENV
- name: Build Next.js with next-on-pages
run: bun run turbo gitbook#build:cloudflare
env:
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ secrets.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY }}
GITBOOK_RUNTIME: cloudflare
- id: deploy
name: Deploy to Cloudflare
uses: cloudflare/wrangler-action@v3.14.0
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
workingDirectory: ./
wranglerVersion: '3.112.0'
command: pages deploy ./packages/gitbook/.vercel/output/static --project-name=${{ vars.CLOUDFLARE_PROJECT_NAME }} --branch=${{ github.ref == 'refs/heads/main' && 'main' || format('pr{0}', github.event.pull_request.number) }}
- name: Outputs
run: |
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
echo "Alias URL: ${{ steps.deploy.outputs.deployment-alias-url }}"
deploy-v2-vercel:
name: Deploy v2 to Vercel (preview)
runs-on: ubuntu-latest
@@ -114,7 +67,6 @@ jobs:
name: Comment Deployments (preview)
if: always() && !startsWith(github.ref, 'refs/heads/main')
needs:
- deploy-v1-cloudflare
- deploy-v2-vercel
- deploy-v2-cloudflare
steps:
@@ -134,15 +86,6 @@ jobs:
body: |
Summary of the deployments:
### Version 1
| Version | URL | Status |
| --- | --- | --- |
| Latest commit | [${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}](${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}) | ${{ needs.deploy-v1-cloudflare.result == 'success' && '✅' || '❌' }} |
| PR | [${{ needs.deploy-v1-cloudflare.outputs.deployment-alias-url }}](${{ needs.deploy-v1-cloudflare.outputs.deployment-alias-url }}) | ${{ needs.deploy-v1-cloudflare.result == 'success' && '✅' || '❌' }} |
### Version 2
| Version | URL | Status |
| --- | --- | --- |
| Vercel | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}](${{ needs.deploy-v2-vercel.outputs.deployment-url }}) | ${{ needs.deploy-v2-vercel.result == 'success' && '✅' || '❌' }} |
@@ -150,30 +93,11 @@ jobs:
### Test content
| Site | `v1` | `2v` | `2c` |
| --- | --- | --- | --- |
| 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) |
| Site | `2v` | `2c` |
| --- | --- | --- |
| GitBook | [${{ 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-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:
runs-on: ubuntu-latest
name: Visual Testing v1
needs: deploy-v1-cloudflare
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
- name: Run Playwright tests
run: bun e2e
env:
BASE_URL: ${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
visual-testing-v2-vercel:
runs-on: ubuntu-latest
name: Visual Testing v2
@@ -216,26 +140,6 @@ jobs:
SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: 'v2-cloudflare'
visual-testing-customers-v1:
runs-on: ubuntu-latest
name: Visual Testing Customers v1
needs: deploy-v1-cloudflare
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
- name: Run Playwright tests
run: bun e2e-customers
env:
BASE_URL: ${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: 'customers-v1'
visual-testing-customers-v2:
runs-on: ubuntu-latest
name: Visual Testing Customers v2
@@ -278,10 +182,10 @@ jobs:
SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: 'customers-v2'
pagespeed-testing-v1:
pagespeed-testing-v2:
runs-on: ubuntu-latest
name: PageSpeed Testing v1
needs: deploy-v1-cloudflare
needs: deploy-v2-vercel
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -294,5 +198,5 @@ jobs:
- name: Run pagespeed tests
run: bun ./packages/gitbook/tests/pagespeed-testing.ts
env:
BASE_URL: ${{needs.deploy-v1-cloudflare.outputs.deployment-url}}
BASE_URL: ${{needs.deploy-v2-vercel.outputs.deployment-url}}
PAGESPEED_API_KEY: ${{ secrets.PAGESPEED_API_KEY }}
+1 -19
View File
@@ -39,22 +39,4 @@ jobs:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
release-preview:
# For now it releases the cache-do to both preview and production
# Once we changed to deploy the app only on release, we should change `release:preview` in `cache-do`
name: Release Preview
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v3
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Release preview packages
run: bun run release:preview
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+8 -14
View File
@@ -1,7 +1,7 @@
<h1 align="center">GitBook</h1>
<p align="center">
<a href="https://gitbook.com/docs/">Docs</a> - <a href="https://github.com/GitbookIO/community">Community</a> - <a href="https://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> - <a href="https://github.com/orgs/GitbookIO/discussions/categories/feature-requests">Feature requests</a>
</p>
<p align="center">
@@ -35,10 +35,10 @@ To run a local version of this project, please follow these simple steps.
### Prerequisites
- Node.js (Version: >=20.6)
- Use nvm for easy Node management
- Bun (Version: >=1.2.1)
- We use a text-based lockfile which isn't supported below 1.2.1
- Node.js (Version: >=20.6)
- Use nvm for easy Node management
- [Bun](https://bun.sh/) (Version: >=1.2.15)
- We use a text-based lockfile which isn't supported below 1.2.15
### Set up
@@ -56,19 +56,13 @@ git clone https://github.com/gitbookIO/gitbook.git
bun install
```
4. Run build.
4. Start your local development server.
```
bun build:v2
bun dev
```
5. Start your local development server.
```
bun dev:v2
```
6. Open a published GitBook space in your web browser, prefixing it with `http://localhost:3000/`.
6. Open a published GitBook space in your web browser, prefixing it with `http://localhost:3000/url`.
examples:
+1 -2
View File
@@ -21,7 +21,7 @@
"packages/openapi-parser/src/fixtures/**/*",
"packages/emoji-codepoints/index.ts",
"packages/icons/src/data/*.json",
"packages/cache-do/worker-configuration.d.ts"
"packages/gitbook/worker-configuration.d.ts"
]
},
"formatter": {
@@ -127,7 +127,6 @@
{
"include": [
"packages/gitbook/**/*",
"packages/gitbook-v2/**/*",
"packages/react-openapi/**/*",
"packages/react-math/**/*",
"packages/react-contentkit/**/*",
+235 -433
View File
File diff suppressed because it is too large Load Diff
+3 -5
View File
@@ -11,14 +11,13 @@
"overrides": {
"@codemirror/state": "6.4.1",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"esbuild": "0.24.2"
},
"private": true,
"scripts": {
"dev": "turbo run dev",
"dev:v2": "turbo run dev:v2",
"build": "turbo run build",
"build:v2": "turbo run build:v2",
"clean-deps": "rm -rf node_modules && rm -rf packages/*/node_modules",
"typecheck": "turbo run typecheck",
"format": "biome check --write ./",
@@ -29,14 +28,13 @@
"changeset": "changeset",
"changeset-version": "changeset version && bun run format",
"release": "turbo run release && changeset publish",
"release:preview": "turbo run release:preview",
"download:env": "op read op://gitbook-x-dev/gitbook-open/.env.local >> .env.local",
"clean": "turbo run clean"
},
"workspaces": {
"packages": ["packages/*"],
"catalog": {
"@gitbook/api": "^0.121.0"
"@gitbook/api": "^0.128.0"
}
},
"patchedDependencies": {
-3
View File
@@ -1,3 +0,0 @@
.wrangler
worker-configuration.d.ts
dist/
-18
View File
@@ -1,18 +0,0 @@
# @gitbook/cache-do
## 0.1.1
### Patch Changes
- b7a5106: Disable cloudflare observability in production
## 0.1.0
### Minor Changes
- 9b8d519: Experiment with optimizing billable duration in Cloudflare by using multiple RPC sessions instead of one
- 636b868: First version of a new cache backend powered by Cloudflare Durable Objects
### Patch Changes
- 56f5fa1: Enable Workers observability with a sampling of 0.1
-22
View File
@@ -1,22 +0,0 @@
# `@gitbook/cache-do`
Cache backend, powered by Cloudflare Durable Objects. The cache is optimized for GitBook use-cases.
### Performances
The cache backend is optimized for performances by being distributed and accessible close to the worker locations that are reading it.
### Geo-distribution
To achieve a good balance between **performances** and **consistency**, cache objects are distributed over 7 locations, representing continents.
It makes it possible to purge all 7 locations in one go and achieve fast consistency.
### Concepts
**Cache tag**: unique tag in the cache environment. A cache tag groups multiple keys that should be purged together in one operation.
Cache tags should not contain a large set of unique keys. Exceeding thousands could lead to performances or reliability issues.
**Cache key**: unique key in the cache environment. Each key should be assigned to a `tag`.
**Location**: cache is distributed over 7 unique locations, one for each continent.
-35
View File
@@ -1,35 +0,0 @@
{
"name": "@gitbook/cache-do",
"type": "module",
"private": true,
"exports": {
".": {
"types": "./dist/index.d.ts",
"development": "./src/index.ts",
"default": "./dist/index.js"
},
"./api": {
"types": "./dist/api.d.ts",
"development": "./src/api.ts",
"default": "./dist/api.js"
}
},
"version": "0.1.1",
"dependencies": {
"@msgpack/msgpack": "^3.0.0-beta2",
"lru_map": "^0.4.1"
},
"devDependencies": {
"typescript": "^5.5.3",
"wrangler": "^4.10.0"
},
"scripts": {
"generate": "wrangler types",
"build": "tsc",
"typecheck": "tsc --noEmit",
"dev": "tsc -w",
"release": "wrangler deploy",
"release:preview": "wrangler deploy && wrangler deploy --env preview"
},
"files": ["dist", "src", "bin", "data", "README.md", "CHANGELOG.md"]
}
-292
View File
@@ -1,292 +0,0 @@
import { DurableObject } from 'cloudflare:workers';
import { decode, encode } from '@msgpack/msgpack';
import { LRUMap } from 'lru_map';
export interface CacheObjectDescriptor {
get: <Value = unknown>(key: string) => Promise<Value | undefined>;
set: <Value = unknown>(key: string, value: Value, expiresAt: number) => Promise<void>;
}
/**
* Value stored in a chunked binary msgpack format.
* Stored under the key `prop.${key}.${index}`.
*/
interface CacheObjectProp<Value = unknown> {
value: Value;
expiresAt: number;
}
/**
* Expiration clock stored under the key `exp.${expiresAt}.${key}`.
*/
interface CacheObjectExp {
/** Key of the property */
k: string;
/** Number of chunks */
c: number;
}
/**
* Durable Object class being deployed as a distributed cache.
*/
export class CacheObject extends DurableObject {
private lru = new LRUMap<string, { match: CacheObjectProp | undefined }>(500);
/**
* Open a descriptor to access the cache object.
* The goal is to minimize the amount of RPC sessions between the client and the cache object.
* One session is opened per request on the client side and used to perform multiple operations.
* https://developers.cloudflare.com/workers/runtime-apis/rpc/#return-functions-from-rpc-methods
*/
public open(): CacheObjectDescriptor {
return {
get: async <Value = unknown>(key: string) => {
return this.get<Value>(key);
},
set: async <Value = unknown>(key: string, value: Value, expiresAt: number) => {
await this.set(key, value, expiresAt);
},
};
}
/**
* Get the value of a property.
*/
public async get<Value = unknown>(key: string) {
return this.logOperation({ operation: 'get', key }, async (setLog) => {
// Try the memory state first.
const memoryEntry = this.lru.get(key);
if (memoryEntry) {
setLog({ memory: true });
setLog({ memoryMatch: !!memoryEntry.match });
if (!memoryEntry.match) {
return;
}
const isExpired = memoryEntry.match.expiresAt < Date.now();
setLog({ memoryExpired: isExpired });
if (!isExpired) {
return memoryEntry.match.value as Value;
}
}
return await this.getFromStorage<Value>(key);
});
}
/**
* Get the value of a property from the DO storage.
*/
public async getFromStorage<Value = unknown>(key: string) {
return this.logOperation({ operation: 'getFromStorage', key }, async (setLog) => {
const entries = await this.ctx.storage.list<Uint8Array>({
prefix: getStoragePropKey(key),
noCache: true,
});
if (entries.size) {
const entry = decodeChunks<CacheObjectProp<Value>>(entries);
setLog({ chunks: entries.size, chunksSize: entry?.size ?? 0 });
if (entry && entry.value.expiresAt > Date.now()) {
// Found
this.lru.set(key, { match: entry.value });
return entry.value.value;
}
}
// Not found
this.lru.set(key, { match: undefined });
});
}
/**
* Set a value in the cache object.
*/
public async set<Value = unknown>(key: string, value: Value, expiresAt: number) {
return this.logOperation({ operation: 'set', key }, async (setLog) => {
const prop: CacheObjectProp<Value> = {
value,
expiresAt,
};
this.lru.set(key, { match: prop });
await this.ctx.storage.transaction(async (tx) => {
const entries = encodeChunks(key, prop);
const chunks = Object.keys(entries).length;
setLog({ chunks });
const clockValue: CacheObjectExp = {
k: key,
c: chunks,
};
await tx.put(getGCClockKey(key, expiresAt), clockValue);
await tx.put(entries);
const currentAlarm = await tx.getAlarm();
if (!currentAlarm) {
// Set an alarm to garbage collect all entries that have expired in 12h.
await tx.setAlarm(Date.now() + 12 * 60 * 60 * 1000);
}
});
});
}
/**
* Purge all keys in the cache object.
*/
public async purge() {
return this.logOperation({ operation: 'purge' }, async (setLog) => {
const result = new Set<string>();
try {
// List all the keys in the cache object.
const entries = await this.ctx.storage.list<CacheObjectExp>({
prefix: 'exp.',
noCache: true,
});
setLog({ entries: entries.size });
entries.forEach((exp) => {
result.add(exp.k);
});
} catch (_error) {}
await this.reset();
return Array.from(result);
});
}
/**
* Alarm to garbage collect all entries that have expired.
*/
async alarm() {
return this.logOperation({ operation: 'alarm' }, async (setLog) => {
try {
const entries = await this.ctx.storage.list<CacheObjectExp>({
prefix: 'exp.',
noCache: true,
});
setLog({ entries: entries.size });
const toDeleteSet = new Set<string>();
for (const [key, exp] of entries) {
const timestamp = Number.parseInt(key.split('.')[1]);
if (timestamp < Date.now()) {
toDeleteSet.add(key);
for (let i = 0; i < exp.c; i++) {
toDeleteSet.add(getStoragePropChunkKey(exp.k, i));
}
}
}
// Delete the keys by batch of 128.
const toDelete = Array.from(toDeleteSet);
setLog({ toDelete: toDelete.length });
for (let i = 0; i < toDelete.length; i += 128) {
await this.ctx.storage.delete(toDelete.slice(i, i + 128));
}
// If there are still keys to delete, set an alarm to continue the deletion in 12h.
if (toDelete.length) {
await this.ctx.storage.setAlarm(Date.now() + 12 * 60 * 60 * 1000);
}
} catch (_error) {
await this.reset();
}
});
}
/**
* Reset the cache object.
*/
async reset() {
return this.logOperation({ operation: 'reset' }, async () => {
this.lru.clear();
await this.ctx.storage.deleteAll();
});
}
/**
* Time and log an operation.
*/
async logOperation<T>(
log: Record<string, unknown>,
fn: (update: (log: Record<string, unknown>) => void) => Promise<T>
): Promise<T> {
const objectId = this.ctx.id.name ?? this.ctx.id.toString();
const update: Record<string, unknown> = {};
const start = performance.now();
try {
return await fn((arg) => {
Object.assign(update, arg);
});
} finally {
const duration = performance.now() - start;
console.log({ ...log, ...update, objectId, duration });
}
}
}
function getStoragePropKey(key: string): string {
return `prop.${key}.`;
}
function getStoragePropChunkKey(key: string, index: number): string {
return `${getStoragePropKey(key)}${index}`;
}
function getGCClockRootKey(timestamp: number): string {
return `exp.${timestamp}.`;
}
function getGCClockKey(key: string, expiresAt: number): string {
return `${getGCClockRootKey(expiresAt)}${key}`;
}
function encodeChunks<T>(key: string, value: T): Record<string, Uint8Array> {
const buf = encode(value);
const entries: Record<string, Uint8Array> = {};
const chunks = chunkUint8Array(buf, 128 * 1024);
for (let index = 0; index < chunks.length; index++) {
entries[getStoragePropChunkKey(key, index)] = chunks[index];
}
return entries;
}
function decodeChunks<T>(entries: Map<string, Uint8Array>): { value: T; size: number } | undefined {
const chunks = Array.from(entries.entries())
.map(([key, value]) => {
const index = Number.parseInt(key.split('.').pop()!);
return [index, value] as const;
})
.sort(([a], [b]) => a - b)
.map(([, value]) => value);
if (chunks.length === 0) {
return;
}
const buf = mergeUint8Array(chunks);
return { value: decode(buf) as T, size: buf.length };
}
function chunkUint8Array(input: Uint8Array, chunkSize: number): Uint8Array[] {
const chunks: Uint8Array[] = [];
for (let i = 0; i < input.length; i += chunkSize) {
chunks.push(input.slice(i, i + chunkSize));
}
return chunks;
}
function mergeUint8Array(chunks: Uint8Array[]): Uint8Array {
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
const result = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
result.set(chunk, offset);
offset += chunk.length;
}
return result;
}
-97
View File
@@ -1,97 +0,0 @@
import type { CacheObject } from './CacheObject';
export type CacheLocationId = ContinentCode;
const allLocations: CacheLocationId[] = ['AF', 'AS', 'NA', 'SA', 'AN', 'EU', 'OC'];
/**
* Location hint for the CacheObject durable object.
*/
const doLocationHints: {
[key in CacheLocationId]: DurableObjectLocationHint;
} = {
AF: 'afr',
AS: 'apac',
NA: 'wnam',
SA: 'sam',
AN: 'oc',
EU: 'weur',
OC: 'oc',
};
/**
* Client to access a cache tag.
*/
export class CacheObjectStub {
private stub: DurableObjectStub<CacheObject>;
constructor(
/** Binding to the CacheObject durable object */
private doNamespace: DurableObjectNamespace<CacheObject>,
/** ID of the location to target */
private locationId: CacheLocationId,
/** Name of the tag */
private tag: string
) {
const groupId = getCacheObjectIdName(this.locationId, this.tag);
this.stub = this.doNamespace.get(this.doNamespace.idFromName(groupId), {
// Initialize the object with a locaiton hint,
// as we might want to purge all locations before the object is created.
// https://developers.cloudflare.com/durable-objects/reference/data-location/
locationHint: doLocationHints[this.locationId],
});
}
/**
* Open a descriptor to the cache object.
* It can be used to perform multiple operations in a single RPC session.
* Ex:
* ```ts
* using desc = cache.open();
* await desc.set('key', 'value', Date.now() + 1000);
* await desc.get('key');
* ```
*/
async open() {
return await this.stub.open();
}
/**
* Get a value from the cache.
*/
async get<Value = unknown>(key: string) {
return (await this.stub.get(key)) as Value | undefined;
}
/**
* Set a value in the cache.
*/
async set<Value = unknown>(key: string, value: Value, expiresAt: number) {
return await this.stub.set(key, value, expiresAt);
}
/**
* Purge all keys in the cache tag.
*/
async purge() {
const keys = new Set<string>();
await Promise.all(
allLocations.map(async (locationId) => {
const groupId = getCacheObjectIdName(locationId, this.tag);
const cacheGroup = this.doNamespace.get(this.doNamespace.idFromName(groupId), {
// Initialize the object with a locaiton hint,
// as we might want to purge all locations before the object is created.
// https://developers.cloudflare.com/durable-objects/reference/data-location/
locationHint: doLocationHints[this.locationId],
});
const locationkeys = await cacheGroup.purge();
locationkeys.forEach((key) => keys.add(key));
})
);
return keys;
}
}
function getCacheObjectIdName(locationId: CacheLocationId, tag: string): string {
return `${locationId}:${tag}`;
}
-1
View File
@@ -1 +0,0 @@
export * from './CacheObjectStub';
-9
View File
@@ -1,9 +0,0 @@
import { WorkerEntrypoint } from 'cloudflare:workers';
export * from './CacheObject';
export default class Worker extends WorkerEntrypoint {
fetch() {
return new Response('Hello, world!');
}
}
-21
View File
@@ -1,21 +0,0 @@
{
"compilerOptions": {
"target": "esnext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": false,
"declaration": true,
"outDir": "dist",
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"incremental": true,
"types": ["./worker-configuration.d.ts"]
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}
-8
View File
@@ -1,8 +0,0 @@
{
"extends": ["//"],
"tasks": {
"generate": {
"outputs": ["worker-configuration.d.ts"]
}
}
}
-27
View File
@@ -1,27 +0,0 @@
main = "./src/index.ts"
name = "gitbook-open-cache"
compatibility_date = "2024-09-02"
durable_objects.bindings = [
{name = "CACHE", class_name = "CacheObject"}
]
migrations = [
{tag = "v1", new_classes = ["CacheObject"]}
]
[observability]
enabled = false
[env.preview]
name = "gitbook-open-cache-preview"
durable_objects.bindings = [
{name = "CACHE", class_name = "CacheObject"}
]
migrations = [
{tag = "v1", new_classes = ["CacheObject"]}
]
[env.preview.observability]
enabled = true
head_sampling_rate = 0.1
+7
View File
@@ -0,0 +1,7 @@
# @gitbook/fonts
## 0.1.0
### Minor Changes
- fbfcca5: Initial version of the package
+1 -1
View File
@@ -8,7 +8,7 @@
"default": "./dist/index.js"
}
},
"version": "0.0.0",
"version": "0.1.0",
"dependencies": {
"@gitbook/api": "catalog:"
},
-12
View File
@@ -1,12 +0,0 @@
# next.js
/.next/
# vercel
.vercel
# cloudflare
.open-next
.wrangler
# Symbolic links
public
-81
View File
@@ -1,81 +0,0 @@
# 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
- Updated dependencies [77397ca]
- @gitbook/cache-tags@0.3.1
## 0.2.4
### Patch Changes
- 4234289: Fix incoming URL for requests that were proxied
- Updated dependencies [116575c]
- @gitbook/cache-tags@0.3.0
## 0.2.3
### Patch Changes
- 5b2bf82: Use stable site URL data for route rewrite in the middleware
## 0.2.2
### Patch Changes
- 54ee014: Add initial support for loading custom fonts
- bba2e52: Fix site redirects when it includes a section/variant path
## 0.2.1
### Patch Changes
- Updated dependencies [f32bf1f]
- @gitbook/cache-tags@0.2.0
## 0.2.0
### Minor Changes
- 76c7974: Add route to revalidate cached data
## 0.1.2
### Patch Changes
- 05ffd0e: Improving data cache management for computed content
- Updated dependencies [05ffd0e]
- @gitbook/cache-tags@0.1.0
## 0.1.1
### Patch Changes
- 3e11678: fix: lost section groups
## 0.1.0
### Minor Changes
- cfccc44: Setup structure and deployment for new version
-6
View File
@@ -1,6 +0,0 @@
/// <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.
-39
View File
@@ -1,39 +0,0 @@
{
"name": "gitbook-v2",
"version": "0.3.0",
"private": true,
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/cache-tags": "workspace:*",
"@opennextjs/cloudflare": "1.2.1",
"@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",
"rison": "^0.1.1",
"server-only": "^0.0.1",
"warn-once": "^0.1.1",
"object-identity": "^0.1.2"
},
"devDependencies": {
"gitbook": "*",
"@types/rison": "^0.0.9",
"tailwindcss": "^3.4.0",
"postcss": "^8"
},
"scripts": {
"generate": "rm -rf ./public && cp -r ../gitbook/public ./public",
"dev:v2": "env-cmd --silent -f ../../.env.local next --turbopack",
"build": "next build",
"build:v2": "next build",
"start": "next start",
"build:v2:cloudflare": "opennextjs-cloudflare build",
"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"
}
}
-6
View File
@@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
@@ -1,18 +0,0 @@
'use client';
import NextError from 'next/error';
export default function GlobalError({
error,
}: {
error: Error & { digest?: string };
}) {
console.error('Global error:', error);
return (
<html lang="en">
<body>
<NextError statusCode={undefined as any} />
</body>
</html>
);
}
@@ -1,2 +0,0 @@
import RootLayout from '@v2/app/~space/[spaceId]/~gitbook/pdf/layout';
export default RootLayout;
@@ -1,4 +0,0 @@
import PDFPage, { generateMetadata } from '@v2/app/~space/[spaceId]/~gitbook/pdf/page';
export default PDFPage;
export { generateMetadata };
@@ -1,2 +0,0 @@
import RootLayout from '@v2/app/~space/[spaceId]/~gitbook/pdf/layout';
export default RootLayout;
@@ -1,4 +0,0 @@
import PDFPage, { generateMetadata } from '@v2/app/~space/[spaceId]/~gitbook/pdf/page';
export default PDFPage;
export { generateMetadata };
-73
View File
@@ -1,73 +0,0 @@
import { waitUntil } from '@/lib/waitUntil';
import type { JSONDocument, RevisionPageDocument } from '@gitbook/api';
import type { GitBookSiteContext, GitBookSpaceContext } from '../context';
import { getDataOrNull } from './errors';
/**
* Get the document for a page.
*/
export async function getPageDocument(
context: GitBookSpaceContext | GitBookSiteContext,
page: RevisionPageDocument
): Promise<JSONDocument | null> {
const { dataFetcher, space } = context;
if (
'site' in context &&
(context.site.id === 'site_JOVzv' || context.site.id === 'site_IxAYj')
) {
return getDataOrNull(
dataFetcher.getRevisionPageDocument({
spaceId: space.id,
revisionId: space.revision,
pageId: page.id,
})
);
}
if (page.documentId) {
return getDataOrNull(
dataFetcher.getDocument({ spaceId: space.id, documentId: page.documentId })
);
}
if ('computed' in page && page.computed) {
return getDataOrNull(
dataFetcher.getComputedDocument({
organizationId: space.organization,
spaceId: space.id,
source: page.computed,
seed: page.computedSeed,
})
);
}
// Pre-fetch the document to start filling the cache before we migrate to this API.
if (process.env.NODE_ENV === 'development') {
// Disable for now to investigate side-effects
if (isInPercentRollout(space.id, 10) || process.env.VERCEL_ENV === 'preview') {
await waitUntil(
getDataOrNull(
dataFetcher.getRevisionPageDocument({
spaceId: space.id,
revisionId: space.revision,
pageId: page.id,
})
)
);
}
}
return null;
}
function isInPercentRollout(value: string, rollout: number) {
return getRandomPercent(value) < rollout;
}
function getRandomPercent(value: string) {
const hash = value.split('').reduce((acc, char) => {
return acc + char.charCodeAt(0);
}, 0);
return hash % 100;
}
-116
View File
@@ -1,116 +0,0 @@
import { CustomizationThemeMode } from '@gitbook/api';
import { headers } from 'next/headers';
import type { SiteURLData } from './context';
export enum MiddlewareHeaders {
/**
* Type of the route (static or dynamic)
*/
RouteType = 'x-gitbook-route-type',
/**
* The URL of the site (without the pathname)
*/
SiteURL = 'x-gitbook-site-url',
/**
* The data associated with the URL.
*/
SiteURLData = 'x-gitbook-site-url-data',
/**
* The mode of the URL (url or url-host)
*/
URLMode = 'x-gitbook-url-mode',
/**
* The theme of the page (light or dark)
*/
Theme = 'x-gitbook-theme',
/**
* The customization override to apply.
*/
Customization = 'x-gitbook-customization',
/**
* The API token used to fetch the content.
* This should only be passed for non-site dynamic routes.
*/
APIToken = 'x-gitbook-api-token',
}
/**
* Get the URL mode from the middleware headers.
* This function should only be called in a server action or a dynamic route.
*/
export async function getURLModeFromMiddleware(): Promise<'url' | 'url-host'> {
const headersList = await headers();
const mode = headersList.get(MiddlewareHeaders.URLMode);
if (!mode) {
throw new Error('URL mode is not set by the middleware');
}
return mode as 'url' | 'url-host';
}
/**
* Get the site URL data from the middleware headers.
* This function should only be called in a server action or a dynamic route.
*/
export async function getSiteURLDataFromMiddleware(): Promise<SiteURLData> {
const headersList = await headers();
const siteURLData = headersList.get(MiddlewareHeaders.SiteURLData);
if (!siteURLData) {
throw new Error(
'Site URL data is not set by the middleware. This should only be called in a server action or a dynamic route.'
);
}
return JSON.parse(siteURLData);
}
/**
* Get the URL from the middleware headers.
* This function should only be called in a server action or a dynamic route.
*/
export async function getSiteURLFromMiddleware(): Promise<string> {
const headersList = await headers();
const siteURL = headersList.get(MiddlewareHeaders.SiteURL);
if (!siteURL) {
throw new Error('URL mode is not set by the middleware');
}
return siteURL;
}
/**
* For preview, the theme can be set via query string (?theme=light).
* This function should only be called in a dynamic route.
*/
export async function getThemeFromMiddleware() {
const headersList = await headers();
const queryStringTheme = headersList.get(MiddlewareHeaders.Theme);
if (!queryStringTheme) {
return null;
}
return queryStringTheme === 'light'
? CustomizationThemeMode.Light
: CustomizationThemeMode.Dark;
}
/**
* Get the API token from the middleware headers.
* This function should only be called in a dynamic route.
*/
export async function getAPITokenFromMiddleware(): Promise<string> {
const headersList = await headers();
const apiToken = headersList.get(MiddlewareHeaders.APIToken);
if (!apiToken) {
throw new Error('API token is not set by the middleware');
}
return apiToken;
}
-21
View File
@@ -1,21 +0,0 @@
import { describe, expect, it } from 'bun:test';
import { getProxyRequestIdentifier, isProxyRequest } from './proxy';
describe('isProxyRequest', () => {
it('should return true for proxy requests', () => {
const proxyRequestURL = new URL('https://proxy.gitbook.site/sites/site_foo/hello/world');
expect(isProxyRequest(proxyRequestURL)).toBe(true);
});
it('should return false for non-proxy requests', () => {
const nonProxyRequestURL = new URL('https://example.com/docs/foo/hello/world');
expect(isProxyRequest(nonProxyRequestURL)).toBe(false);
});
});
describe('getProxyRequestIdentifier', () => {
it('should return the correct identifier for proxy requests', () => {
const proxyRequestURL = new URL('https://proxy.gitbook.site/sites/site_foo/hello/world');
expect(getProxyRequestIdentifier(proxyRequestURL)).toBe('sites/site_foo');
});
});
-15
View File
@@ -1,15 +0,0 @@
/**
* Check if the request to the site was through a proxy.
*/
export function isProxyRequest(requestURL: URL): boolean {
return (
requestURL.host === 'proxy.gitbook.site' || requestURL.host === 'proxy.gitbook-staging.site'
);
}
export function getProxyRequestIdentifier(requestURL: URL): string {
// For proxy requests, we extract the site ID from the pathname
// e.g. https://proxy.gitbook.site/site/siteId/...
const pathname = requestURL.pathname.slice(1).split('/');
return pathname.slice(0, 2).join('/');
}
-486
View File
@@ -1,486 +0,0 @@
import { CustomizationThemeMode } from '@gitbook/api';
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import rison from 'rison';
import { getContentSecurityPolicy } from '@/lib/csp';
import { validateSerializedCustomization } from '@/lib/customization';
import { removeLeadingSlash, removeTrailingSlash } from '@/lib/paths';
import {
type ResponseCookies,
getPathScopedCookieName,
getResponseCookiesForVisitorAuth,
getVisitorData,
normalizeVisitorAuthURL,
} from '@/lib/visitors';
import { serveResizedImage } from '@/routes/image';
import {
DataFetcherError,
getVisitorAuthBasePath,
lookupPublishedContentByUrl,
normalizeURL,
throwIfDataError,
} from '@v2/lib/data';
import { isGitBookAssetsHostURL, isGitBookHostURL } from '@v2/lib/env';
import { getImageResizingContextId } from '@v2/lib/images';
import { MiddlewareHeaders } from '@v2/lib/middleware';
import type { SiteURLData } from './lib/context';
export const config = {
matcher: [
'/((?!_next/static|_next/image|~gitbook/static|~gitbook/revalidate|~gitbook/monitoring|~scalar/proxy).*)',
],
};
type URLWithMode = { url: URL; mode: 'url' | 'url-host' };
export async function middleware(request: NextRequest) {
try {
const requestURL = new URL(request.url);
// Redirect to normalize the URL
const normalized = normalizeURL(requestURL);
if (normalized.toString() !== requestURL.toString()) {
return NextResponse.redirect(normalized.toString());
}
for (const handler of [serveSiteRoutes, serveSpacePDFRoutes]) {
const result = await handler(requestURL, request);
if (result) {
return result;
}
}
return NextResponse.next();
} catch (error) {
return serveErrorResponse(error as Error);
}
}
/**
* Handle request that are targetting the site routes group.
*/
async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
const match = getSiteURLFromRequest(request);
if (!match) {
return null;
}
const { url: siteRequestURL, mode } = match;
const imagesContextId = getImageResizingContextId(siteRequestURL);
/**
* Serve image resizing requests (all requests containing `/~gitbook/image`).
* All URLs containing `/~gitbook/image` are rewritten to `/~gitbook/image`
* and serve from a single route handler.
*
* In GitBook v1: image resizing was done at the root of the hostname (docs.company.com/~gitbook/image)
* In GitBook v2: image resizing is done at the content level (docs.company.com/section/variant/~gitbook/image)
*/
if (siteRequestURL.pathname.endsWith('/~gitbook/image')) {
return await serveResizedImage(request, {
imagesContextId: imagesContextId,
});
}
//
// Detect and extract the visitor authentication token from the request
//
const { visitorToken, unsignedClaims, visitorParamsCookie } = getVisitorData({
cookies: request.cookies.getAll(),
url: siteRequestURL,
});
const withAPIToken = async (apiToken: string | null) => {
const siteURLData = await throwIfDataError(
lookupPublishedContentByUrl({
url: siteRequestURL.toString(),
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.
redirectOnError: visitorToken?.source === 'visitor-auth-cookie',
// Use the API token passed in the request, if any
// as it could be used for .preview hostnames
apiToken,
})
);
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
//
if ('redirect' in siteURLData) {
// biome-ignore lint/suspicious/noConsole: we want to log the redirect
console.log('redirect', siteURLData.redirect);
if (siteURLData.target === 'content') {
let contentRedirect = new URL(siteURLData.redirect, request.url);
// For content redirects, we redirect using the /url/:url format
// during development and testing in 'url' mode.
if (mode === 'url') {
const urlObject = new URL(siteURLData.redirect);
contentRedirect = new URL(
`/url/${urlObject.host}${urlObject.pathname}${urlObject.search}`,
request.url
);
}
// Keep the same search params as the original request
// as it might contain a VA token
contentRedirect.search = request.nextUrl.search;
return NextResponse.redirect(contentRedirect);
}
return NextResponse.redirect(siteURLData.redirect);
}
cookies.push(
...getResponseCookiesForVisitorAuth(
getVisitorAuthBasePath(siteRequestURL, siteURLData),
visitorToken
)
);
// We use the host/origin from the canonical URL to ensure the links are
// correctly generated when the site is proxied. e.g. https://proxy.gitbook.com/site/siteId/...
const siteCanonicalURL = new URL(siteURLData.canonicalUrl);
let incomingURL = requestURL;
// For cases where the site is proxied, we use the canonical URL
// as the incoming URL along with all the search params from the request.
if (mode !== 'url') {
incomingURL = siteCanonicalURL;
incomingURL.search = requestURL.search;
}
//
// Make sure the URL is clean of any va token after a successful lookup
// The token is stored in a cookie that is set on the redirect response
//
const incomingURLWithoutToken = normalizeVisitorAuthURL(incomingURL);
if (incomingURLWithoutToken.toString() !== incomingURL.toString()) {
return writeResponseCookies(
NextResponse.redirect(incomingURLWithoutToken.toString()),
cookies
);
}
//
// Render and serve the content
//
// The route is static, except when using dynamic parameters from query params
// (customization override, theme, etc)
let routeType: 'dynamic' | 'static' = 'static';
// We pick only stable data from the siteURL data to prevent re-rendering of
// the root layout when changing pages..
const stableSiteURLData: SiteURLData = {
site: siteURLData.site,
siteSection: siteURLData.siteSection,
siteSpace: siteURLData.siteSpace,
siteBasePath: siteURLData.siteBasePath,
basePath: siteURLData.basePath,
space: siteURLData.space,
organization: siteURLData.organization,
changeRequest: siteURLData.changeRequest,
revision: siteURLData.revision,
shareKey: siteURLData.shareKey,
apiToken: siteURLData.apiToken,
imagesContextId: imagesContextId,
};
const requestHeaders = new Headers(request.headers);
requestHeaders.set(MiddlewareHeaders.RouteType, routeType);
requestHeaders.set(MiddlewareHeaders.URLMode, mode);
requestHeaders.set(
MiddlewareHeaders.SiteURL,
`${siteCanonicalURL.origin}${siteURLData.basePath}`
);
requestHeaders.set(MiddlewareHeaders.SiteURLData, JSON.stringify(stableSiteURLData));
// Preview of customization/theme
const customization = siteRequestURL.searchParams.get('customization');
if (customization && validateSerializedCustomization(customization)) {
routeType = 'dynamic';
// We need to encode the customization headers, otherwise it will fail for some customization values containing non ASCII chars on vercel.
requestHeaders.set(MiddlewareHeaders.Customization, encodeURIComponent(customization));
}
const theme = siteRequestURL.searchParams.get('theme');
if (theme === CustomizationThemeMode.Dark || theme === CustomizationThemeMode.Light) {
routeType = 'dynamic';
requestHeaders.set(MiddlewareHeaders.Theme, theme);
}
// We support forcing dynamic routes by setting a `gitbook-dynamic-route` cookie
// This is useful for testing dynamic routes.
if (request.cookies.has('gitbook-dynamic-route')) {
routeType = 'dynamic';
}
// Pass a x-forwarded-host and origin that are equal to ensure Next doesn't block server actions when proxied
requestHeaders.set('x-forwarded-host', request.nextUrl.host);
requestHeaders.set('origin', request.nextUrl.origin);
const siteURLWithoutProtocol = `${siteCanonicalURL.host}${siteURLData.basePath}`;
const { pathname, routeType: routeTypeFromPathname } = encodePathInSiteContent(
siteURLData.pathname
);
routeType = routeTypeFromPathname ?? routeType;
const route = [
'sites',
routeType,
mode,
encodeURIComponent(siteURLWithoutProtocol),
encodeURIComponent(
rison.encode(
// rison can't encode undefined values
Object.fromEntries(
Object.entries(stableSiteURLData).filter(
([_, v]) => typeof v !== 'undefined'
)
)
)
),
pathname,
].join('/');
console.log(`rewriting ${request.nextUrl.toString()} to ${route}`);
const rewrittenURL = new URL(`/${route}`, request.nextUrl.toString());
const response = NextResponse.rewrite(rewrittenURL, {
request: {
headers: requestHeaders,
},
});
// Add Content Security Policy header
response.headers.set('content-security-policy', getContentSecurityPolicy());
// Basic security headers
response.headers.set('strict-transport-security', 'max-age=31536000');
response.headers.set('referrer-policy', 'no-referrer-when-downgrade');
response.headers.set('x-content-type-options', 'nosniff');
// Debug header
response.headers.set('x-gitbook-route-type', routeType);
response.headers.set('x-gitbook-route-site', siteURLWithoutProtocol);
return writeResponseCookies(response, cookies);
};
// For https://preview/<siteURL> requests,
if (siteRequestURL.hostname === 'preview') {
return serveWithQueryAPIToken(
// We scope the API token to the site ID.
`${siteRequestURL.hostname}/${requestURL.pathname.slice(1).split('/')[0]}`,
request,
withAPIToken
);
}
return withAPIToken(null);
}
/**
* Serve routes for PDF export for a space: /~space/:spaceId/~gitbook/pdf
*/
async function serveSpacePDFRoutes(requestURL: URL, request: NextRequest) {
const pathnameParts = requestURL.pathname.slice(1).split('/');
if (pathnameParts[0] !== '~space' && pathnameParts[0] !== '~site') {
return null;
}
return serveWithQueryAPIToken(
pathnameParts.slice(0, 2).join('/'),
request,
async (apiToken) => {
// Handle the rest with the router default logic
return NextResponse.next({
headers: {
[MiddlewareHeaders.APIToken]: apiToken,
},
});
}
);
}
/**
* Serve an error response.
*/
function serveErrorResponse(error: Error) {
if (error instanceof DataFetcherError) {
return new Response(error.message, {
status: error.code,
headers: { 'content-type': 'text/plain' },
});
}
throw error;
}
/**
* Server a response with an API token obtained from the query params.
*/
async function serveWithQueryAPIToken(
scopePath: string,
request: NextRequest,
serve: (apiToken: string) => Promise<NextResponse>
) {
// We store the API token in a cookie that is scoped to the specific route
// to avoid errors when multiple previews are opened in different tabs.
const cookieName = getPathScopedCookieName('gitbook-api-token', scopePath);
// Extract a potential GitBook API token passed in the request
// If found, we redirect to the same URL but with the token in the cookie
const queryAPIToken = request.nextUrl.searchParams.get('token');
if (queryAPIToken) {
request.nextUrl.searchParams.delete('token');
return writeResponseCookies(NextResponse.redirect(request.nextUrl.toString()), [
{
name: cookieName,
value: queryAPIToken,
options: {
httpOnly: true,
sameSite: process.env.NODE_ENV === 'production' ? 'none' : undefined,
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 60, // 1 hour
},
},
]);
}
const apiToken = request.cookies.get(cookieName)?.value;
if (!apiToken) {
throw new DataFetcherError('Missing API token', 400);
}
return serve(apiToken);
}
/**
* The URL of the GitBook content can be passed in 3 different ways (in order of priority):
* - The request has a `X-GitBook-URL` header:
* URL is taken from the header.
* - The request has a `X-Forwarded-Host` header:
* Host is taken from the header, pathname is taken from the request URL.
* - The request URL is matching `/url/:url`:
* URL is taken from the pathname.
*/
function getSiteURLFromRequest(request: NextRequest): URLWithMode | null {
const xGitbookUrl = request.headers.get('x-gitbook-url');
if (xGitbookUrl) {
return {
url: appendQueryParams(new URL(xGitbookUrl), request.nextUrl.searchParams),
mode: 'url-host',
};
}
const isMainHost = isGitBookHostURL(request.url);
const isAssetsHost = isGitBookAssetsHostURL(request.url);
// /url/:url requests on the main host
const prefix = '/url/';
if (isMainHost && request.nextUrl.pathname.startsWith(prefix)) {
return {
url: appendQueryParams(
new URL(`https://${request.nextUrl.pathname.slice(prefix.length)}`),
request.nextUrl.searchParams
),
mode: 'url',
};
}
// Skip other requests to main hosts
if (isMainHost || isAssetsHost) {
return null;
}
const xForwardedHost = request.headers.get('x-forwarded-host');
// The x-forwarded-host is set by Vercel for all requests
// so we ignore it if the hostname is the same as the instance one.
if (xForwardedHost) {
return {
url: appendQueryParams(
new URL(`https://${xForwardedHost}${request.nextUrl.pathname}`),
request.nextUrl.searchParams
),
mode: 'url-host',
};
}
return null;
}
/**
* Encode path in a site content.
* Special paths are not encoded and passed to be handled by the route handlers.
*/
function encodePathInSiteContent(rawPathname: string): {
pathname: string;
routeType?: 'static' | 'dynamic';
} {
const pathname = removeLeadingSlash(removeTrailingSlash(rawPathname));
if (pathname.match(/^~gitbook\/ogimage\/\S+$/)) {
return { pathname };
}
// If the pathname is a markdown file, we rewrite it to ~gitbook/markdown/:pathname
if (pathname.match(/\.md$/)) {
const pagePathWithoutMD = pathname.slice(0, -3);
return {
pathname: `~gitbook/markdown/${encodeURIComponent(pagePathWithoutMD)}`,
// The markdown content is always static and doesn't depend on the dynamic parameter (customization, theme, etc)
routeType: 'static',
};
}
switch (pathname) {
case '~gitbook/icon':
return { pathname };
case 'llms.txt':
case 'sitemap.xml':
case 'sitemap-pages.xml':
case 'robots.txt':
// LLMs.txt, sitemap, sitemap-pages and robots.txt are always static
// as they only depend on the site structure / pages.
return { pathname, routeType: 'static' };
case '~gitbook/pdf':
// PDF routes are always dynamic as they depend on the search params.
return { pathname, routeType: 'dynamic' };
default:
return { pathname: encodeURIComponent(pathname || '/') };
}
}
/**
* Append all the query params from a URL to another URL.
*/
function appendQueryParams(url: URL, from: URLSearchParams) {
for (const [key, value] of from.entries()) {
url.searchParams.set(key, value);
}
return url;
}
/**
* Write the cookies to a response.
*/
function writeResponseCookies<R extends NextResponse>(response: R, cookies: ResponseCookies): R {
cookies.forEach((cookie) => {
response.cookies.set(cookie.name, cookie.value, cookie.options);
});
return response;
}
-10
View File
@@ -1,10 +0,0 @@
import config from '../gitbook/tailwind.config';
export default {
...config,
content: [
'../gitbook/src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'../gitbook/src/components/**/*.{js,ts,jsx,tsx,mdx}',
'../gitbook/src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
};
-44
View File
@@ -1,44 +0,0 @@
{
"compilerOptions": {
"target": "esnext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"incremental": true,
"module": "esnext",
"esModuleInterop": true,
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"plugins": [
{
"name": "next"
}
],
"paths": {
"@v2/*": ["./src/*"],
"@/*": ["../gitbook/src/*"]
},
"types": [
"bun-types" // add Bun global
]
},
"include": [
"next-env.d.ts",
".next/types/**/*.ts",
"**/*.ts",
"**/*.tsx",
"../gitbook/types/**/*.d.ts",
"../gitbook/cf-env.d.ts"
],
"exclude": [
"node_modules",
"packages/openapi-parser",
"packages/react-openapi",
"packages/react-math",
"packages/gitbook"
]
}
-11
View File
@@ -1,11 +0,0 @@
{
"extends": ["//"],
"tasks": {
"generate": {
"outputs": ["public"]
},
"typecheck": {
"dependsOn": ["^typecheck"]
}
}
}
-27
View File
@@ -1,27 +0,0 @@
# Configurations
### API ###
## API token to use when looking up the published content
# GITBOOK_API_URL=https://api.gitbook.com
# GITBOOK_TOKEN=xxx
## User agent to use when making requests to the API
# GITBOOK_USER_AGENT=GitBook/1.0.0
# GITBOOK_USER_AGENT_COMMENT=something
### URL of the application ###
# NEXT_PUBLIC_GITBOOK_APP_URL=https://app.gitbook.com
### Image resizing ###
# GITBOOK_IMAGE_RESIZE_SIGNING_KEY=1234567890
# GITBOOK_IMAGE_RESIZE_URL=https://mycompany.com/cdn-cgi/image/
### SEO ###
# GITBOOK_BLOCK_SEARCH_INDEXATION=true
## Caching
# GITBOOK_OUTPUT_CACHE=true
### Silent logs
# SILENT=true
+6
View File
@@ -1,3 +1,4 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# testing
@@ -31,3 +32,8 @@ screenshots/
# Generated public files
/public/~gitbook/static/*
!/public/~gitbook/static/images
# cloudflare
.open-next
.wrangler
worker-configuration.d.ts
+102
View File
@@ -1,5 +1,107 @@
# gitbook
## 0.13.0
### Minor Changes
- af98402: Add support for inline icons.
- 7d3fe23: Add circular corners and depth styling
- f3affc3: Display MCP tool calls in AI chat.
- fa12f9e: Support dark-mode specific page cover image
- df848ef: Add support for icons in buttons.
- b7a0db3: Fix rendering of ogimage with SVG logos.
- c3b620e: Best effort at preserving current variant when navigating between sections by matching the pathname against site spaces in the new section.
- 4fb2a4a: Rework full-width layout, add support for full-width page option
- df848ef: Add support for text alignment for headings and paragraphs.
- f033734: Add support for site customization option to change how external links open.
- 7b38f89: Enable AI chat when AI mode is configured to assistant
- 8d65983: Add AI chat
### Patch Changes
- 42d88da: Fix UX issue about highlighting the search term in search result sections
- 6aa3ff9: Fix three small visual issues
- Fix sidebar showing on `no-toc` pages in the gradient theme
- Fix variant selector truncating incorrectly in header when sections are present
- Fix page cover alignment on `lg` screens without TOC
- 015615d: Respect fullWidth and defaultWidth for images
- e8fb84d: Fix hash with align in columns
- 4721403: Hide scrollbar on sections
- d410381: Add docs.testgitbook.com to ADAPTIVE_CONTENT_HOSTS list
- 7d5a6d2: fix nested a tag causing hydration error
- bc1eca8: Error handling for AI Chat
- b3a7ad6: fix href being empty in TOC
- dc4268d: Fix navigation between sections/variants when previewing a site in v2
- 58d7f3c: Fix revision id for computed content
- 11a6511: Fix crash when integration script fails to render block.
- 7a00880: Improve support for OAuth2 security type
- c0ee60e: Adds Columns layout block to GBO
- 9316ccd: Update Models page styling
- 42d43e0: Show scrollbars
- 72cd0e5: Optimize performances by using a smarter per-request cache arround data cached functions
- af66ff7: add a force-revalidate api route to force bust the cache in case of errors
- e2afc07: Fix resolution of page by resolving site redirects before space redirects
- 88a35ed: Fix crash when integration is triggering invalid requests.
- 711cf38: Optimize the fetch of revision files by using only the getRevision cache.
- f58b904: encode customization header
- 4f5fec7: Fix CodeBlock layout
- 3bfe347: Show tabs when there is a single section group present
- a7a713b: Scroll to active TOC when clicking a link
- ba0094a: fix ISR on preview env
- 521052d: Fix concurrent execution in Vercel causing pages to not be attached to the proper tags.
- 33726c8: Generate a llms-full.txt version of the docs site
- 500c8cb: Don't crash ogimage generation on RTL text, as a workaround until we can support it.
- 6859f7d: Fix rendering of ogimage when logo or icon are AVIF images.
- a28a997: Add margin to adjacent buttons
- 67998b6: Fix ogimage generation failing with some JPEG images.
- 8c0a53a: Fix page group not expanded by default
- dfa8a37: Don't cache unexpected API errors for more than a few minutes.
- 73e0cbb: Fix an issue where PDF export URLs were not keeping their query params.
- e5bac69: Fix markdown page generation for groups
- 6294bbb: add a global error boundary
- b60039b: Fix links to other spaces within a section.
- 59da30f: Add support for cover repositioning
- b403962: Handle nullish OpenAPI mediaTypeObject
- c730845: Fix missing title on button to close the announcement banner.
- 231167d: Make icons for page groups more contrasting
- d99da6a: Ignore case while highlighting search results.
- dae019c: Consistently show variant selector in section bar if site has sections
- dd65987: Include page group children under the .md route
- 57bb146: Make TOC height dynamic based on visible header and footer elements
- 4f7c0ee: Clicking an active TOC item toggles its descendants
- ca3b9ac: Improve AI Chat context popup
- 5726999: Fix viewing a page from a revision
- 392f594: Fix InlineLinkTooltip having a negative impact on performance, especially on larger pages.
- c9373ef: Fix bold header links hover color
- fa3eb07: cache fonts and static image used in OGImage in memory
- e7a591d: Fix border being added to cards
- 427f748: Add metadata for adding site to Apple devices home
- a3a944d: Fix crash during rendering of ogimage for VA sites with default icon.
- 4b67fe5: Add `urlObject.hash` to `linker.toLinkForContent` to pass through URL fragment identifiers, used in search
- caaa692: Allow to zoom images on mobile if relevant
- 902c3c6: apply customization for dynamic context
- b6b5975: Reverse order of feedback smileys
- fbfcca5: Fix ogimage using incorrect Google Font depending on language.
- 2932077: remove trailing slash from linker
- 2350baa: Support for OpenAPI Array request body
- Updated dependencies [957afd9]
- Updated dependencies [7a00880]
- Updated dependencies [11a6511]
- Updated dependencies [fbfcca5]
- Updated dependencies [a0c06a7]
- Updated dependencies [b403962]
- Updated dependencies [1e013cd]
- Updated dependencies [4f5cbfe]
- Updated dependencies [4c9a9d0]
- Updated dependencies [40df91a]
- Updated dependencies [2350baa]
- @gitbook/react-openapi@1.3.1
- @gitbook/react-contentkit@0.7.1
- @gitbook/fonts@0.1.0
- @gitbook/openapi-parser@2.1.5
## 0.12.0
### Minor Changes
-4
View File
@@ -1,4 +0,0 @@
{
"version": 1,
"exclude": ["/~gitbook/static/*"]
}
-7
View File
@@ -1,7 +0,0 @@
import type { CacheObject } from '@gitbook/cache-do';
declare global {
interface CloudflareEnv {
CACHE?: DurableObjectNamespace<CacheObject>;
}
}
+5 -5
View File
@@ -117,11 +117,11 @@ const testCases: TestsCase[] = [
contentBaseURL: 'https://docs.gradient.network',
tests: [{ name: 'Home', url: '/' }],
},
{
name: 'mygate-network.gitbook.io',
contentBaseURL: 'https://mygate-network.gitbook.io',
tests: [{ name: 'Home', url: '/' }],
},
// {
// name: 'mygate-network.gitbook.io',
// contentBaseURL: 'https://mygate-network.gitbook.io',
// tests: [{ name: 'Home', url: '/' }],
// },
{
name: 'treasurenft.gitbook.io',
contentBaseURL: 'https://treasurenft.gitbook.io',
+33 -2
View File
@@ -1,6 +1,7 @@
import {
CustomizationBackground,
CustomizationCorners,
CustomizationDefaultMonospaceFont,
CustomizationDepth,
CustomizationHeaderPreset,
CustomizationIconsStyle,
@@ -464,6 +465,22 @@ const testCases: TestsCase[] = [
},
],
},
{
name: '[page].md',
skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel',
contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/',
tests: [
{
name: 'blocks.md',
url: 'blocks.md',
screenshot: false,
run: async (_page, response) => {
expect(response?.status()).toBe(200);
expect(response?.headers()['content-type']).toContain('text/markdown');
},
},
],
},
{
name: 'Site subdirectory (proxy)',
skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel',
@@ -613,6 +630,11 @@ const testCases: TestsCase[] = [
url: 'blocks/links',
run: waitForCookiesDialog,
},
{
name: 'Buttons',
url: 'blocks/buttons',
run: waitForCookiesDialog,
},
{
name: 'Lists',
url: 'blocks/lists',
@@ -777,6 +799,15 @@ const testCases: TestsCase[] = [
}),
run: waitForCookiesDialog,
},
{
name: `With custom monospace font - Theme mode ${themeMode}`,
url: `blocks/code${getCustomizationURL({
styling: {
monospaceFont: CustomizationDefaultMonospaceFont.SpaceMono,
},
})}`,
run: waitForCookiesDialog,
},
// New site themes
...allThemes.flatMap((theme) => [
...allTintColors.flatMap((tint) => [
@@ -905,7 +936,7 @@ const testCases: TestsCase[] = [
).toBeVisible();
const url = page.url();
expect(url.includes('shared-space-uno')).toBeTruthy(); // same uno site
expect(url.endsWith('/shared')).toBeTruthy(); // correct page
expect(url.endsWith('/shared/')).toBeTruthy(); // correct page
},
screenshot: false,
},
@@ -925,7 +956,7 @@ const testCases: TestsCase[] = [
).toBeVisible();
const url = page.url();
expect(url.includes('shared-space-dos')).toBeTruthy(); // same dos site
expect(url.endsWith('/shared')).toBeTruthy(); // correct page
expect(url.endsWith('/shared/')).toBeTruthy(); // correct page
},
screenshot: false,
},
+170
View File
@@ -0,0 +1,170 @@
import { argosScreenshot } from '@argos-ci/playwright';
import { expect, test } from '@playwright/test';
import { getContentTestURL } from '../tests/utils';
import { waitForIcons } from './util';
test.describe('PDF export', () => {
test('export all pages as PDF (e2e)', async ({ page }) => {
// Set the header to disable the Vercel toolbar
// But only on the main document as it'd cause CORS issues on other resources
await page.route('**/*', async (route, request) => {
if (request.resourceType() === 'document') {
await route.continue({
headers: {
...request.headers(),
'x-vercel-skip-toolbar': '1',
},
});
} else {
await route.continue();
}
});
await page.goto(
getContentTestURL(
'https://gitbook-open-e2e-sites.gitbook.io/gitbook-doc/~gitbook/pdf?limit=10'
)
);
const printBtn = page.getByTestId('print-button');
await expect(printBtn).toBeVisible();
await argosScreenshot(page, 'pdf - all pages', {
viewports: ['macbook-13'],
argosCSS: `
/* Hide Intercom */
.intercom-lightweight-app {
display: none !important;
}
`,
threshold: undefined,
fullPage: true,
beforeScreenshot: async ({ runStabilization }) => {
await runStabilization();
await waitForIcons(page);
},
});
});
test('export all pages as PDF (GitBook docs)', async ({ page }) => {
// Set the header to disable the Vercel toolbar
// But only on the main document as it'd cause CORS issues on other resources
await page.route('**/*', async (route, request) => {
if (request.resourceType() === 'document') {
await route.continue({
headers: {
...request.headers(),
'x-vercel-skip-toolbar': '1',
},
});
} else {
await route.continue();
}
});
await page.goto(getContentTestURL('https://gitbook.com/docs/~gitbook/pdf?limit=10'));
const printBtn = page.getByTestId('print-button');
await expect(printBtn).toBeVisible();
await argosScreenshot(page, 'pdf - all pages', {
viewports: ['macbook-13'],
argosCSS: `
/* Hide Intercom */
.intercom-lightweight-app {
display: none !important;
}
`,
threshold: undefined,
fullPage: true,
beforeScreenshot: async ({ runStabilization }) => {
await runStabilization();
await waitForIcons(page);
},
});
});
test('export a single page as PDF (e2e)', async ({ page }) => {
// Set the header to disable the Vercel toolbar
// But only on the main document as it'd cause CORS issues on other resources
await page.route('**/*', async (route, request) => {
if (request.resourceType() === 'document') {
await route.continue({
headers: {
...request.headers(),
'x-vercel-skip-toolbar': '1',
},
});
} else {
await route.continue();
}
});
await page.goto(
getContentTestURL(
'https://gitbook-open-e2e-sites.gitbook.io/gitbook-doc/~gitbook/pdf?page=Bw7LjWwgTjV8nIV4s7rs&only=yes&limit=2'
)
);
const printBtn = page.getByTestId('print-button');
await expect(printBtn).toBeVisible();
await argosScreenshot(page, 'pdf - all pages', {
viewports: ['macbook-13'],
argosCSS: `
/* Hide Intercom */
.intercom-lightweight-app {
display: none !important;
}
`,
threshold: undefined,
fullPage: true,
beforeScreenshot: async ({ runStabilization }) => {
await runStabilization();
await waitForIcons(page);
},
});
});
test('export a single page as PDF (GitBook docs)', async ({ page }) => {
// Set the header to disable the Vercel toolbar
// But only on the main document as it'd cause CORS issues on other resources
await page.route('**/*', async (route, request) => {
if (request.resourceType() === 'document') {
await route.continue({
headers: {
...request.headers(),
'x-vercel-skip-toolbar': '1',
},
});
} else {
await route.continue();
}
});
await page.goto(
getContentTestURL(
'https://gitbook.com/docs/~gitbook/pdf?page=DfnNkU49mvLe2ythHAyx&only=yes&limit=2'
)
);
const printBtn = page.getByTestId('print-button');
await expect(printBtn).toBeVisible();
await argosScreenshot(page, 'pdf - all pages', {
viewports: ['macbook-13'],
argosCSS: `
/* Hide Intercom */
.intercom-lightweight-app {
display: none !important;
}
`,
threshold: undefined,
fullPage: true,
beforeScreenshot: async ({ runStabilization }) => {
await runStabilization();
await waitForIcons(page);
},
});
});
});
+12 -1
View File
@@ -1,8 +1,10 @@
import { argosScreenshot } from '@argos-ci/playwright';
import {
CustomizationAIMode,
CustomizationBackground,
CustomizationCorners,
CustomizationDefaultFont,
CustomizationDefaultMonospaceFont,
CustomizationDepth,
type CustomizationHeaderItem,
CustomizationHeaderPreset,
@@ -16,6 +18,7 @@ import {
CustomizationThemeMode,
type CustomizationThemedColor,
type SiteCustomizationSettings,
SiteExternalLinksTarget,
} from '@gitbook/api';
import { type BrowserContext, type Page, type Response, expect, test } from '@playwright/test';
import deepMerge from 'deepmerge';
@@ -281,6 +284,7 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
corners: CustomizationCorners.Rounded,
depth: CustomizationDepth.Subtle,
font: CustomizationDefaultFont.Inter,
monospaceFont: CustomizationDefaultMonospaceFont.IBMPlexMono,
background: CustomizationBackground.Plain,
icons: CustomizationIconsStyle.Regular,
links: CustomizationLinksStyle.Default,
@@ -314,9 +318,16 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
feedback: {
enabled: false,
},
// TODO: remove aiSearch once the cache has been fully updated (after 11/07/2025)
aiSearch: {
enabled: true,
},
ai: {
mode: CustomizationAIMode.None,
},
externalLinks: {
target: SiteExternalLinksTarget.Self,
},
advancedCustomization: {
enabled: true,
},
@@ -346,7 +357,7 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
/**
* Wait for all icons present on the page to be loaded.
*/
async function waitForIcons(page: Page) {
export async function waitForIcons(page: Page) {
await page.waitForFunction(() => {
const urlStates: Record<
string,
-50
View File
@@ -1,50 +0,0 @@
module.exports = {
env: {
BUILD_VERSION: (process.env.GITHUB_SHA ?? '').slice(0, 7),
GITBOOK_ASSETS_PREFIX: process.env.GITBOOK_ASSETS_PREFIX,
GITBOOK_ICONS_URL: process.env.GITBOOK_ICONS_URL,
GITBOOK_ICONS_TOKEN: process.env.GITBOOK_ICONS_TOKEN,
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY,
GITBOOK_RUNTIME: process.env.GITBOOK_RUNTIME,
},
webpack(config) {
config.resolve.fallback = {
...config.resolve.fallback,
// Required for `swagger2openapi` to work:
fs: false,
path: false,
http: false,
};
return config;
},
async headers() {
return [
// Cache all static assets for 1 year
{
source: '/~gitbook/static/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
];
},
assetPrefix: process.env.GITBOOK_ASSETS_PREFIX,
poweredByHeader: false,
images: {
remotePatterns: [
{
protocol: 'https',
hostname: '*.gitbook.io',
},
],
},
};

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