Compare commits

..

1 Commits

Author SHA1 Message Date
Nolann Biron d8183c786c wip 2025-11-12 09:39:59 +01:00
535 changed files with 8560 additions and 30142 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix webframe height issue
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Skip AI questions when it matches the example provided to agents / crawlers.
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
SearchInput fixes — Translate Clear button, Bold theme color, "Esc" keyboard shortcut
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix search container shadow
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Revert contentref anchor title
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": minor
---
Add "Content-Signal" to "robots.txt"
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Move the "Ask <query>" to the bottom of search results if query is not a question
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Improve AI Search branding
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": minor
---
Allow AI agents to ask questions and get the answer in markdown when fetching with `?ask=<question>`.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": minor
---
Support variant selector in hidden section
-5
View File
@@ -1,5 +0,0 @@
---
"@gitbook/icons": patch
---
Icon clipping fix
+6
View File
@@ -0,0 +1,6 @@
---
'@gitbook/react-openapi': patch
'gitbook': patch
---
Add alt text support to card covers
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Improve ancestor label in references
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Tweak language dropdown styling
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Fix ToggeableLinkItem display
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix icon resolution for page group
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix embeddable search links
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Add confirmation to "Copy MCP URL" page action
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Update search scope UI
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Fix OpenAPI path display
+6
View File
@@ -0,0 +1,6 @@
---
'@gitbook/react-openapi': patch
'gitbook': patch
---
Add OpenAPI servers selection
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix opacity of ScrollContainer buttons on bold themes
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Pass visitor session to Ask AI.
@@ -30,7 +30,7 @@ runs:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.43.0'
wranglerVersion: '4.10.0'
environment: ${{ inputs.environment }}
command: deployments status --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
@@ -49,7 +49,7 @@ runs:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.43.0'
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/openNext/customWorkers/defaultWrangler.jsonc
@@ -61,7 +61,7 @@ runs:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.43.0'
wranglerVersion: '4.10.0'
environment: ${{ inputs.environment }}
command: versions deploy ${{ inputs.middlewareVersionId }}@100% -y --config ./packages/gitbook/openNext/customWorkers/middlewareWrangler.jsonc
@@ -71,7 +71,7 @@ runs:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.43.0'
wranglerVersion: '4.10.0'
environment: ${{ inputs.environment }}
command: versions deploy ${{ inputs.serverVersionId }}@100% -y --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
@@ -26,7 +26,7 @@ inputs:
description: 'Commit message to associate with the deployment'
required: true
outputs:
deployment-url:
deployment-url:
description: "Deployment URL"
value: ${{ steps.upload_middleware.outputs.deployment-url }}
runs:
@@ -52,8 +52,6 @@ runs:
GITBOOK_API_URL: ${{ inputs.opItem }}/GITBOOK_API_URL
GITBOOK_API_PUBLIC_URL: ${{ inputs.opItem }}/GITBOOK_API_PUBLIC_URL
GITBOOK_API_TOKEN: ${{ inputs.opItem }}/GITBOOK_API_TOKEN
GITBOOK_OAUTH_SERVER_URL: ${{ inputs.opItem }}/GITBOOK_OAUTH_SERVER_URL
GITBOOK_PREVIEW_BASE_URL: ${{ inputs.opItem }}/GITBOOK_PREVIEW_BASE_URL
GITBOOK_INTEGRATIONS_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_HOST
GITBOOK_IMAGE_RESIZE_SIGNING_KEY: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SIGNING_KEY
GITBOOK_IMAGE_RESIZE_URL: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_URL
@@ -64,7 +62,6 @@ runs:
run: bun run turbo build:cloudflare
env:
GITBOOK_RUNTIME: cloudflare
GITBOOK_BLOCK_SEARCH_INDEXATION: ${{ inputs.environment == 'preview' && 'true' || '' }}
shell: bash
- name: Upload the DO worker
@@ -73,7 +70,7 @@ runs:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.43.0'
wranglerVersion: '4.10.0'
environment: ${{ inputs.environment }}
command: deploy --config ./packages/gitbook/openNext/customWorkers/doWrangler.jsonc
@@ -84,7 +81,7 @@ runs:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.43.0'
wranglerVersion: '4.10.0'
environment: ${{ inputs.environment }}
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
@@ -107,10 +104,10 @@ runs:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.43.0'
wranglerVersion: '4.10.0'
environment: ${{ inputs.environment }}
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
id: extract_middleware_version_id
+1 -18
View File
@@ -19,11 +19,8 @@ inputs:
environment:
description: 'Environment to deploy to'
required: true
headSha:
description: 'Git ref to deploy, used for the deploymentId'
required: false
outputs:
deployment-url:
deployment-url:
description: "Deployment URL"
value: ${{ steps.deploy.outputs.deployment-url }}
runs:
@@ -54,25 +51,12 @@ runs:
GITBOOK_API_URL: ${{ inputs.opItem }}/GITBOOK_API_URL
GITBOOK_API_PUBLIC_URL: ${{ inputs.opItem }}/GITBOOK_API_PUBLIC_URL
GITBOOK_API_TOKEN: ${{ inputs.opItem }}/GITBOOK_API_TOKEN
GITBOOK_OAUTH_SERVER_URL: ${{ inputs.opItem }}/GITBOOK_OAUTH_SERVER_URL
GITBOOK_PREVIEW_BASE_URL: ${{ inputs.opItem }}/GITBOOK_PREVIEW_BASE_URL
GITBOOK_INTEGRATIONS_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_HOST
GITBOOK_IMAGE_RESIZE_SIGNING_KEY: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SIGNING_KEY
GITBOOK_IMAGE_RESIZE_URL: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_URL
GITBOOK_IMAGE_RESIZE_MODE: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_MODE
GITBOOK_ASSETS_PREFIX: ${{ inputs.opItem }}/GITBOOK_ASSETS_PREFIX
GITBOOK_FONTS_URL: ${{ inputs.opItem }}/GITBOOK_FONTS_URL
- name: Inject build env vars
if: ${{ inputs.environment == 'preview' }}
shell: bash
run: |
HEAD_SHA=$(git rev-parse HEAD)
echo "resolved HEAD_SHA: $HEAD_SHA"
echo "GITBOOK_HEAD_SHA=$HEAD_SHA" >> .vercel/.env.${{ inputs.environment }}.local
echo "GITBOOK_RUNTIME=vercel" >> .vercel/.env.${{ inputs.environment }}.local
echo "GITBOOK_BLOCK_SEARCH_INDEXATION=true" >> .vercel/.env.${{ inputs.environment }}.local
echo "--- .vercel/.env.${{ inputs.environment }}.local after inject ---"
cat .vercel/.env.${{ inputs.environment }}.local
- name: Build Project Artifacts
run: bun run vercel build --target=${{ inputs.environment }} --token=${{ inputs.vercelToken }}
shell: bash
@@ -80,7 +64,6 @@ runs:
VERCEL_ORG_ID: ${{ inputs.vercelOrg }}
VERCEL_PROJECT_ID: ${{ inputs.vercelProject }}
GITBOOK_RUNTIME: vercel
GITBOOK_HEAD_SHA: ${{ inputs.headSha }}
- name: Deploy Project Artifacts to Vercel
id: deploy
shell: bash
+7 -2
View File
@@ -5,6 +5,8 @@ on:
branches:
- main
- changeset-release/main
env:
BUN_NPM_TOKEN: ${{ secrets.NPM_TOKEN_READONLY }}
jobs:
format:
runs-on: ubuntu-latest
@@ -34,10 +36,13 @@ jobs:
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- run: bun unit
build:
build-oss:
# CI to check that the repository builds correctly on a machine without the credentials
runs-on: ubuntu-latest
name: Build
name: Build (Open Source)
timeout-minutes: 6
env:
BUN_NPM_TOKEN: ''
steps:
- name: Checkout
uses: actions/checkout@v4
+11 -43
View File
@@ -1,25 +1,15 @@
name: Preview
on:
pull_request_target:
pull_request:
push:
branches:
- main
branches:
- main
env:
BUN_NPM_TOKEN: ${{ secrets.NPM_TOKEN_READONLY }}
jobs:
approval:
# Ensure that external contributors' PRs are manually approved before deployment
name: Approve Preview Deployment
runs-on: ubuntu-latest
if: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.fork }}
environment:
name: preview-approval
steps:
- name: Approval gate
run: echo "Preview deployment approved."
deploy-v2-vercel:
name: Deploy v2 to Vercel (preview)
runs-on: ubuntu-latest
needs: approval
if: ${{ always() && (needs.approval.result == 'success' || needs.approval.result == 'skipped') }}
environment:
name: 2v-preview
url: ${{ steps.deploy.outputs.deployment-url }}
@@ -28,8 +18,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Deploy to Vercel
id: deploy
uses: ./.github/composite/deploy-vercel
@@ -40,12 +28,9 @@ jobs:
vercelToken: ${{ secrets.VERCEL_TOKEN }}
opItem: op://gitbook-open/2v-preview
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
headSha: ${{ github.event.pull_request.head.sha || github.sha }}
deploy-v2-cloudflare:
name: Deploy v2 to Cloudflare Worker (preview)
runs-on: ubuntu-latest
needs: approval
if: ${{ always() && (needs.approval.result == 'success' || needs.approval.result == 'skipped') }}
environment:
name: 2c-preview
url: ${{ steps.deploy.outputs.deployment-url }}
@@ -54,8 +39,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Deploy to Cloudflare
id: deploy
uses: ./.github/composite/deploy-cloudflare
@@ -82,7 +65,7 @@ jobs:
comment-deployments:
runs-on: ubuntu-latest
name: Comment Deployments (preview)
if: always() && github.event_name != 'push'
if: always() && !startsWith(github.ref, 'refs/heads/main')
needs:
- deploy-v2-vercel
- deploy-v2-cloudflare
@@ -119,13 +102,10 @@ jobs:
runs-on: ubuntu-latest
name: Visual Testing v2
needs: deploy-v2-vercel
if: ${{ always() && needs.deploy-v2-vercel.result == 'success' }}
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
@@ -138,18 +118,15 @@ jobs:
BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: v2-vercel
ARGOS_BUILD_NAME: 'v2-vercel'
visual-testing-v2-cloudflare:
runs-on: ubuntu-latest
name: Visual Testing v2 (Cloudflare)
needs: deploy-v2-cloudflare
if: ${{ always() && needs.deploy-v2-cloudflare.result == 'success' }}
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
@@ -162,18 +139,15 @@ jobs:
BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: v2-cloudflare
ARGOS_BUILD_NAME: 'v2-cloudflare'
visual-testing-customers-v2:
runs-on: ubuntu-latest
name: Visual Testing Customers v2
needs: deploy-v2-vercel
if: ${{ always() && needs.deploy-v2-vercel.result == 'success' }}
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
@@ -186,18 +160,15 @@ jobs:
BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: customers-v2-vercel
ARGOS_BUILD_NAME: 'customers-v2'
visual-testing-customers-v2-cloudflare:
runs-on: ubuntu-latest
name: Visual Testing Customers v2 (Cloudflare)
needs: deploy-v2-cloudflare
if: ${{ always() && needs.deploy-v2-cloudflare.result == 'success' }}
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
@@ -210,17 +181,14 @@ jobs:
BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: customers-v2-cloudflare
ARGOS_BUILD_NAME: 'customers-v2'
browserless-testing-v2-vercel:
runs-on: ubuntu-latest
name: Browserless Testing v2 (Vercel)
needs: deploy-v2-vercel
if: ${{ always() && needs.deploy-v2-vercel.result == 'success' }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
@@ -249,4 +217,4 @@ jobs:
# run: cd ./packages/gitbook && bun e2e-browserless
# env:
# BASE_URL: ${{needs.deploy-v2-cloudflare.outputs.deployment-url}}
# SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/
# SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/
+2
View File
@@ -3,6 +3,8 @@ on:
push:
branches:
- main
env:
BUN_NPM_TOKEN: ${{ secrets.NPM_TOKEN_READONLY }}
jobs:
deploy-v2-vercel:
name: Deploy v2 to Vercel (production)
+2
View File
@@ -3,6 +3,8 @@ on:
push:
branches:
- main
env:
BUN_NPM_TOKEN: ${{ secrets.NPM_TOKEN_READONLY }}
jobs:
deploy-v2-vercel:
name: Deploy v2 to Vercel (staging)
+9 -16
View File
@@ -4,6 +4,9 @@ on:
push:
branches:
- main
env:
BUN_NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
concurrency: ${{ github.workflow }}-${{ github.ref }}
@@ -11,38 +14,28 @@ jobs:
publish:
name: Publish
runs-on: ubuntu-latest
permissions:
id-token: write
pull-requests: write
contents: write
steps:
- name: Checkout Repo
uses: actions/checkout@v3
with:
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
registry-url: 'https://registry.npmjs.org'
- name: Update npm
run: |
corepack enable npm
corepack install -g npm@latest
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Create Release Pull Request
- name: Create Release Pull Request or Publish to npm
id: changesets
uses: changesets/action@v1
with:
publish: bun run publish-all-packages
version: bun run changeset-version
publish: npm run release
version: npm run changeset-version
env:
# Using a PAT instead of GITHUB_TOKEN because we need to run workflows when releases are created
# https://github.com/orgs/community/discussions/26875#discussioncomment-3253761
GITHUB_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+1 -4
View File
@@ -25,7 +25,4 @@ yarn-error.log*
# TypeScript
*.tsbuildinfo
.npmrc
# Bun pack artifacts
packages/*/*.tgz
.npmrc
-77
View File
@@ -1,77 +0,0 @@
# AGENTS Instructions
## Commands
```bash
bun install # Install dependencies
bun dev # Start dev server (all packages)
bun run build # Build all packages
bun run format # Format with Biome (run after every change)
bun run typecheck # Type-check all packages
bun run unit # Run unit tests
```
## Development
The dev server proxies published GitBook sites locally. After `bun dev`, access any published site at:
```
http://localhost:3000/url/<published-gitbook-url>
```
Examples:
- `http://localhost:3000/url/gitbook.com/docs`
- `http://localhost:3000/url/open-source.gitbook.io/midjourney`
## Architecture
```
packages/
gitbook/ # Main Next.js app
src/
app/ # Next.js App Router (sites/)
components/ # React components
lib/ # Server utilities, data fetching
intl/ # Internationalization (translations/)
openapi-parser/ # OpenAPI 3.0/3.1/Swagger parser
react-openapi/ # OpenAPI rendering components
react-contentkit/ # ContentKit component rendering
embed/ # Embeddable GitBook components
shared/ # Shared utilities
icons/ # Icon assets
fonts/ # Font assets
colors/ # Color tokens
expr/ # GitBook expression evaluator
cache-do/ # Cloudflare DO cache
cache-tags/ # Cache tag utilities
```
## Testing
```bash
bun run unit # Unit tests via bun test (not vitest)
bun run e2e # Playwright e2e tests (requires built app)
```
Run a specific test file:
```bash
cd packages/gitbook && bun test src/lib/cache.test.ts
```
## Changesets
After committing code changes, create a changeset for the affected package:
```md
---
"gitbook": patch
---
Provide a short description of the change.
```
Save as `.changeset/<name>.md`, then commit it separately with message: `changeset`
## Formatting
Uses Biome (not ESLint/Prettier). Always run `bun run format` before committing.
-1
View File
@@ -1 +0,0 @@
@AGENTS.md
+17 -1
View File
@@ -77,7 +77,23 @@ All pull-requests will be tested against both visual and performances testing to
## Fonts and Icons
GitBook Open uses fontawesome. For self-hosting and local development, for licensing reasons, only the icons from the free version should be used.
GitBook Open uses fontawesome. During development, your local environment will use the free version. However, only the pro version will be accepted by CI. If you see the following error:
```
The GitBook icon is missing. It indicates that the dependencies were installed without the correct font-awesome package. These changes have probably been persisted in the Bun lockfile. Read the README for more information.
```
It means that you've changed the GBO dependencies and bundled in the free version. Only GitBook staff can help with this - if you're not on the GitBook team, please ping us in the PR and we'll help get things moving.
If you are GitBook staff, you'll need our NPM token in your local environment.
```
.env.local
BUN_NPM_TOKEN=xxx
```
and then reinstall dependencies.
## Contributing
Binary file not shown.

Before

Width:  |  Height:  |  Size: 491 KiB

After

Width:  |  Height:  |  Size: 622 KiB

-1
View File
@@ -23,7 +23,6 @@
"packages/emoji-codepoints/index.ts",
"packages/icons/src/data/*.json",
"packages/gitbook/worker-configuration.d.ts",
"gitbook/tsconfig.json",
"**/*.css"
]
},
+1765 -1229
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
[install.scopes]
"gitbook" = { token = "$BUN_NPM_TOKEN", url = "https://registry.npmjs.org" }
+12 -15
View File
@@ -6,20 +6,18 @@
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@changesets/cli": "^2.30.0",
"turbo": "^2.9.6",
"vercel": "50.37.3"
"@changesets/cli": "^2.29.7",
"turbo": "^2.5.8",
"vercel": "^39.4.2"
},
"packageManager": "bun@1.3.7",
"packageManager": "bun@1.3.0",
"overrides": {
"@codemirror/state": "6.4.1",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"esbuild": "0.27.3",
"axios": "1.8.4",
"@radix-ui/react-slot": "1.2.4"
"esbuild": "0.24.2"
},
"private": true,
"scripts": {
@@ -34,7 +32,8 @@
"e2e-customers": "turbo run e2e-customers",
"changeset": "changeset",
"changeset-version": "changeset version && bun run format && bun update",
"publish-all-packages": "turbo run publish-to-npm --continue=dependencies-successful",
"release": "turbo run release && bun run publish-all-packages",
"publish-all-packages": "for dir in packages/*; do (cd \"$dir\" && bun publish || true); done && changeset tag",
"download:env": "op read op://gitbook-x-dev/gitbook-open/.env.local >> .env.local",
"clean": "turbo run clean"
},
@@ -43,7 +42,7 @@
"catalog": {
"@tsconfig/strictest": "^2.0.6",
"@tsconfig/node20": "^20.1.6",
"@gitbook/api": "0.179.0",
"@gitbook/api": "0.146.0",
"@scalar/api-client-react": "^1.3.46",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
@@ -51,17 +50,15 @@
"bidc": "^0.0.2",
"bun-types": "^1.1.20",
"classnames": "^2.5.1",
"react": "^19.0.1",
"react-dom": "^19.0.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tsdown": "^0.15.6",
"typescript": "^5.5.3",
"usehooks-ts": "^3.1.1",
"vercel": "^50.26.1"
"usehooks-ts": "^3.1.1"
}
},
"patchedDependencies": {
"decode-named-character-reference@1.0.2": "patches/decode-named-character-reference@1.0.2.patch",
"@vercel/next@4.4.2": "patches/@vercel%2Fnext@4.4.2.patch",
"next@16.2.3": "patches/next@16.2.3.patch"
"@vercel/next@4.4.2": "patches/@vercel%2Fnext@4.4.2.patch"
}
}
-23
View File
@@ -1,28 +1,5 @@
# @gitbook/browser-types
## 0.1.5
### Patch Changes
- 5f66860: Use isCookiesTrackingDisabled for cookie consent integrations
- 2e495cb: Add Global Privacy Control (GPC) support
## 0.1.4
### Patch Changes
- 7a11861: Add support for custom cookie banner registration
- Updated dependencies [6f1db32]
- @gitbook/icons@0.4.3
## 0.1.3
### Patch Changes
- 10995e0: Use NPM Trusted publishing for publishing the package.
- Updated dependencies [10995e0]
- @gitbook/icons@0.3.4
## 0.1.2
### Patch Changes
+2 -6
View File
@@ -9,7 +9,7 @@
}
},
"sideEffects": false,
"version": "0.1.5",
"version": "0.1.2",
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/icons": "workspace:"
@@ -22,15 +22,11 @@
"scripts": {
"build": "tsdown",
"typecheck": "tsc --noEmit",
"dev": "bun run build -- --watch ./src",
"publish-to-npm": "../../scripts/publish-if-new.sh"
"dev": "bun run build -- --watch ./src"
},
"files": ["dist", "README.md", "CHANGELOG.md"],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"repository": {
"url": "https://github.com/GitbookIO/gitbook"
}
}
-24
View File
@@ -46,11 +46,6 @@ export type GitBookAssistant = {
ui?: boolean;
};
export type GitBookCookieBannerHandler = (options: {
onApprove: () => void;
onReject: () => void;
}) => void;
export type GitBookGlobal = {
/**
* Register an event listener.
@@ -77,25 +72,6 @@ export type GitBookGlobal = {
* Register a custom assistant to be available on the site.
*/
registerAssistant: (assistant: GitBookAssistant) => () => void;
/**
* Register a custom cookie banner handler.
* When registered, the built-in cookie banner will not be displayed.
*/
registerCookieBanner: (handler: GitBookCookieBannerHandler) => void;
/**
* Indicates whether cookies tracking is disabled for the current user.
* Returns `true` when tracking is disabled, `false` when tracking is enabled,
* and `undefined` when the tracking preference is unknown or not yet determined.
*/
isCookiesTrackingDisabled: () => boolean | undefined;
/**
* Indicates whether global privacy control is enabled for the current user.
* Returns `true` when global privacy control is enabled, `false` when it is disabled.
*/
isGlobalPrivacyControlEnabled: () => boolean;
};
declare global {
-6
View File
@@ -1,11 +1,5 @@
# @gitbook/cache-tags
## 0.3.4
### Patch Changes
- 10995e0: Use NPM Trusted publishing for publishing the package.
## 0.3.3
### Patch Changes
+2 -6
View File
@@ -8,7 +8,7 @@
}
},
"sideEffects": false,
"version": "0.3.4",
"version": "0.3.3",
"dependencies": {
"@gitbook/api": "catalog:",
"assert-never": "catalog:"
@@ -21,15 +21,11 @@
"scripts": {
"build": "tsdown",
"typecheck": "tsc --noEmit",
"dev": "bun run build -- --watch ./src",
"publish-to-npm": "../../scripts/publish-if-new.sh"
"dev": "bun run build -- --watch ./src"
},
"files": ["dist", "README.md", "CHANGELOG.md"],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"repository": {
"url": "https://github.com/GitbookIO/gitbook"
}
}
-6
View File
@@ -1,11 +1,5 @@
# @gitbook/colors
## 0.4.3
### Patch Changes
- 10995e0: Use NPM Trusted publishing for publishing the package.
## 0.4.2
### Patch Changes
+2 -6
View File
@@ -8,7 +8,7 @@
}
},
"sideEffects": false,
"version": "0.4.3",
"version": "0.4.2",
"devDependencies": {
"bun-types": "catalog:",
"tsdown": "catalog:",
@@ -17,15 +17,11 @@
"scripts": {
"build": "tsdown",
"typecheck": "tsc --noEmit",
"dev": "bun run build -- --watch ./src",
"publish-to-npm": "../../scripts/publish-if-new.sh"
"dev": "bun run build -- --watch ./src"
},
"files": ["dist", "README.md", "CHANGELOG.md"],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"repository": {
"url": "https://github.com/GitbookIO/gitbook"
}
}
-56
View File
@@ -1,61 +1,5 @@
# @gitbook/embed
## 0.5.0
### Minor Changes
- 88c38fa: Add Search tab to Docs Embed, refactor search
### Patch Changes
- 8d2a95b: Docs Embed: Better support light/dark mode overrides
## 0.4.0
### Minor Changes
- b3875a1: Support `assistantName` property to override Assistant name
## 0.3.0
### Minor Changes
- 370b731: Docs Embed: Make trademark optional
## 0.2.2
### Patch Changes
- b4b471f: Fix unexpected error thrown when using the embed in standalone mode
## 0.2.1
### Patch Changes
- 96e24a1: Fix signed JWT token usage
- 0aa34cc: Fix Embed options in React
- Updated dependencies [6f1db32]
- @gitbook/icons@0.4.3
## 0.2.0
### Minor Changes
- 1430cee: Improve Docs Embed with separate Assistant and Docs tabs
### Patch Changes
- Updated dependencies [62438f4]
- @gitbook/icons@0.4.0
## 0.1.5
### Patch Changes
- 10995e0: Use NPM Trusted publishing for publishing the package.
- Updated dependencies [10995e0]
- @gitbook/icons@0.3.4
## 0.1.4
### Patch Changes
+8 -456
View File
@@ -1,64 +1,24 @@
# GitBook Docs Embed (`@gitbook/embed`)
# `@gitbook/embed`
Embed your GitBook docs in your product or website.
The Docs Embed can contain three tabs:
- **Assistant**: The [GitBook Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant) - an AI-powered chat interface to help users find answers
- **Search**: A search-focused surface for quickly finding pages and asking scoped questions
- **Docs**: A browser for navigating your documentation site
The embed is set up automatically based on your site's configuration. You can optionally customize and override the configuration with custom actions, tools, suggested questions, [Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access), and more. See the [Configuration](#configuration) section for all available options.
Embed the GitBook Docs Assistant in your product or website.
# Usage
## As a standalone script from your docs site
## As a script from your docs site
All GitBook docs sites include a script to easily add the Docs Embed as a widget on your website.
All GitBook docs site includes a script to easily embed the docs assistant as a widget on your website.
The script is served at `https://docs.company.com/~gitbook/embed/script.js`.
You can find the embed script from your docs site settings, or you can copy the following and replace `docs.company.com` with your docs site hostname.
You can find the embed script from your docs site settings, or you can copy the following and replace the `docs.company.com` by your docs site hostname.
```html
<script src="https://docs.company.com/~gitbook/embed/script.js"></script>
<script>
// Initialize with Authenticated Access (optional)
window.GitBook('init',
{ siteURL: 'https://docs.company.com' },
{ visitor: { token: 'your-jwt-token' } }
);
window.GitBook('show');
</script>
```
The standalone script provides a global `GitBook` function. See the [API Reference](#api-reference) section for all available methods.
### Example: Configuring the widget
```javascript
GitBook('configure', {
button: {
label: 'Ask',
icon: 'assistant' // 'assistant' | 'sparkle' | 'help' | 'book'
},
tabs: ['assistant', 'search', 'docs'],
actions: [
{
icon: 'circle-question',
label: 'Contact Support',
onClick: () => window.open('https://support.example.com', '_blank')
}
],
greeting: { title: 'Welcome!', subtitle: 'How can I help?' },
assistantName: 'Support Assistant',
suggestions: ['What is GitBook?', 'How do I get started?'],
tools: [/* ... */],
closeButton: true,
});
```
See the [Configuration](#configuration) section for all available options.
## As a package from NPM
Install the package: `npm install @gitbook/embed` and import it in your web application:
@@ -70,49 +30,10 @@ const gitbook = createGitBook({
siteURL: 'https://docs.company.com'
});
// Create an iframe and get its URL
const iframe = document.createElement('iframe');
iframe.src = gitbook.getFrameURL({
colorScheme: 'dark', // Optional: force the embed to render in dark mode
visitor: {
token: 'your-jwt-token', // Optional: for Adaptive Content or Authenticated Access
unsignedClaims: { // Optional: custom claims for dynamic expressions
userId: '123',
plan: 'premium'
}
}
});
iframe.src = gitbook.getFrameURL();
// Create a frame client to communicate with the iframe
const frame = gitbook.createFrame(iframe);
// Use the frame client methods
frame.navigateToPage('/getting-started'); // Navigate to a page in the docs tab
frame.navigateToAssistant(); // Switch to the assistant tab
frame.postUserMessage('How do I get started?');
frame.clearChat();
// Configure the embed (see Configuration section for all options)
frame.configure({
tabs: ['assistant', 'search', 'docs'],
actions: [
{
icon: 'circle-question',
label: 'Contact Support',
onClick: () => window.open('https://support.example.com', '_blank')
}
],
greeting: { title: 'Welcome!', subtitle: 'How can I help?' },
assistantName: 'Support Assistant',
suggestions: ['What is GitBook?', 'How do I get started?'],
tools: [/* ... */],
closeButton: true
});
// Listen to events
frame.on('close', () => {
console.log('Frame closed');
});
```
## As React components
@@ -120,378 +41,9 @@ frame.on('close', () => {
After installing the NPM package, you can import prebuilt React components:
```tsx
import { GitBookProvider, GitBookFrame } from '@gitbook/embed/react';
import { GitBookProvider, GitBookAssistantFrame } from '@gitbook/embed/react';
<GitBookProvider siteURL="https://docs.company.com">
<GitBookFrame
colorScheme="dark"
visitor={{
token: 'your-jwt-token', // Optional: for Adaptive Content or Authenticated Access
unsignedClaims: { userId: '123' } // Optional: custom claims for dynamic expressions
}}
tabs={['assistant', 'search', 'docs']}
greeting={{ title: 'Welcome!', subtitle: 'How can I help?' }}
assistantName="Support Assistant"
suggestions={['What is GitBook?', 'How do I get started?']}
actions={[
{
icon: 'circle-question',
label: 'Contact Support',
onClick: () => window.open('https://support.example.com', '_blank')
}
]}
tools={[/* ... */]}
closeButton
/>
<GitBookAssistantFrame />
</GitBookProvider>
```
You can also use the `useGitBook` hook to access the client:
```tsx
import { useGitBook } from '@gitbook/embed/react';
function MyComponent() {
const gitbook = useGitBook();
const frameURL = gitbook.getFrameURL({ colorScheme: 'dark', visitor: { token: '...' } });
// ...
}
```
# API Reference
## Method Comparison
| Method | Standalone Script | NPM Package | React Components |
|--------|------------------|-------------|------------------|
| **Initialize** | `GitBook('init', options, frameOptions)` | `createGitBook(options)` | `<GitBookProvider siteURL="...">` |
| **Get frame URL** | ❌ (handled internally) | `client.getFrameURL(options)` | `useGitBook().getFrameURL(options)` |
| **Create frame client** | ❌ (handled internally) | `client.createFrame(iframe)` | `useGitBook().createFrame(iframe)` |
| **Show/Hide widget** | `GitBook('show')` / `GitBook('hide')` | ❌ | ❌ |
| **Open/Close window** | `GitBook('open')` / `GitBook('close')` / `GitBook('toggle')` | ❌ | ❌ |
| **Navigate to page** | `GitBook('navigateToPage', path)` | `frame.navigateToPage(path)` | Via frame client |
| **Navigate to assistant** | `GitBook('navigateToAssistant')` | `frame.navigateToAssistant()` | Via frame client |
| **Post message** | `GitBook('postUserMessage', message)` | `frame.postUserMessage(message)` | Via frame client |
| **Clear chat** | `GitBook('clearChat')` | `frame.clearChat()` | Via frame client |
| **Configure** | `GitBook('configure', settings)` | `frame.configure(settings)` | Props on `<GitBookFrame>` |
| **Event listeners** | ❌ | `frame.on(event, listener)` | Via frame client |
| **Unload** | `GitBook('unload')` | ❌ | ❌ |
## Method Signatures
### Standalone Script
- `GitBook('init', options: { siteURL: string }, frameOptions?: { colorScheme?: 'light' | 'dark', visitor?: {...} })` - Initialize widget
- `GitBook('show')` - Show widget button
- `GitBook('hide')` - Hide widget button
- `GitBook('open')` - Open widget window
- `GitBook('close')` - Close widget window
- `GitBook('toggle')` - Toggle widget window
- `GitBook('navigateToPage', path: string)` - Navigate to page
- `GitBook('navigateToAssistant')` - Navigate to assistant tab
- `GitBook('postUserMessage', message: string)` - Post message to chat
- `GitBook('clearChat')` - Clear chat history
- `GitBook('configure', settings: {...})` - Configure widget
- `GitBook('unload')` - Unload widget
### NPM Package
**Client Factory:**
- `createGitBook(options: { siteURL: string })` → `GitBookClient`
- `client.getFrameURL(options?: { colorScheme?: 'light' | 'dark', visitor?: {...} })` → `string`
- `client.createFrame(iframe: HTMLIFrameElement)` → `GitBookFrameClient`
**Frame Client:**
- `frame.navigateToPage(path: string)` → `void`
- `frame.navigateToAssistant()` → `void`
- `frame.postUserMessage(message: string)` → `void`
- `frame.clearChat()` → `void`
- `frame.configure(settings: Partial<GitBookEmbeddableConfiguration>)` → `void`
- `frame.on(event: string, listener: Function)` → `() => void` (unsubscribe)
### React Components
**Components:**
- `<GitBookProvider siteURL: string>` - Provider component
- `<GitBookFrame {...props}>` - Frame component (accepts all config options as props)
**Hooks:**
- `useGitBook()` → `GitBookClient` (must be used within `<GitBookProvider>`)
# Configuration
Configuration options are available across usage methods as follows:
- **Standalone script**: via `GitBook('configure', {...})`
- **NPM package**: via `frame.configure({...})`
- **React components**: via props on `<GitBookFrame>`
### `tabs`
Available in: Standalone script, NPM package, React components
Override which tabs are displayed. Defaults to your site's configuration.
- **Type**: `('assistant' | 'search' | 'docs')[]`
```javascript
tabs: ['assistant', 'search', 'docs']
```
### `closeButton`
Available in: Standalone script, NPM package, React components
Display a close (`x`) button in the embed sidebar.
- **Type**: `boolean`
- **Default**: `false`
```javascript
closeButton: true
```
Behavior:
- When clicked, the embed sends a `close` event to the parent page.
- In the standalone script, this event is handled automatically and the widget closes.
- In custom iframe integrations (NPM package), you must listen for the `close` event and decide how to hide/collapse your UI.
- In React, this works automatically when using the standard widget flow. If you build custom frame wiring, handle `close` the same way as the NPM package.
NPM package example:
```typescript
const frame = gitbook.createFrame(iframe);
frame.configure({
closeButton: true
});
const unsubscribe = frame.on('close', () => {
// Hide your modal/drawer/container
container.classList.add('hidden');
});
```
Notes:
- The close button is rendered in the same sidebar area as tabs/actions.
- If your UI hides that sidebar or doesn't render it, the button will not be visible.
### `trademark`
Available in: Standalone script, NPM package, React components
Display GitBook branding in the embed. Defaults to true.
- **Type**: `boolean`
- **Default**: `true`
```javascript
trademark: true
```
### `actions`
Available in: Standalone script, NPM package, React components
Custom action buttons rendered in the sidebar alongside tabs. Each action button triggers a callback when clicked.
**Note**: This prop was previously named `buttons`. Use `actions` instead, it has the same functionality.
- **Type**: `GitBookEmbeddableActionDefinition[]`
- **Properties**:
- `icon`: `string` - Icon name. Any [FontAwesome icon](https://fontawesome.com/search) is supported. (e.g., `'rocket'`, `'comments'`, `'user-circle'`, ...)
- `label`: `string` - Button label text
- `onClick`: `() => void | Promise<void>` - Callback function when clicked
```javascript
actions: [
{
icon: 'comments',
label: 'Contact Support',
onClick: () => window.open('https://support.example.com', '_blank')
},
{
icon: 'rocket',
label: 'Get started',
onClick: () => {
GitBook('navigateToPage', '/getting-started');
}
}
]
```
### `greeting`
Available in: Standalone script, NPM package, React components
Welcome message displayed in the [Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant) tab.
- **Type**: `{ title: string, subtitle: string }`
```javascript
greeting: {
title: 'Welcome!',
subtitle: 'How can I help you today?'
}
```
### `assistantName`
Available in: Standalone script, NPM package, React components
Override the assistant name displayed in the chat header and assistant entry points (for example, sidebar tabs and action labels). This name will be limited to 32 characters to prevent text overflow.
- **Type**: `string`
```javascript
assistantName: 'Support Assistant'
```
### `suggestions`
Available in: Standalone script, NPM package, React components
Suggested questions displayed in the [Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant) welcome screen.
- **Type**: `string[]`
```javascript
suggestions: [
'What is GitBook?',
'How do I get started?',
'What can you do?'
]
```
### `tools`
Available in: Standalone script, NPM package, React components
Custom AI tools to extend the [Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant). Tools allow the assistant to execute functions and integrate with your own systems.
**Note**: In addition to custom tools, the Assistant will always have access to any [MCP servers you define](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant#extend-gitbook-assistant-with-mcp-servers) in your site's AI settings.
- **Type**: `GitBookToolDefinition[]`
- **Properties**:
- `name`: `string` - Unique tool identifier
- `description`: `string` - Description of what the tool does (used by the AI to decide when and how to use it).
- `inputSchema`: `object` - JSON schema defining the tool's input parameters
- `execute`: `(input: object) => Promise<{ output: any, summary: string }>` - Async function that executes the tool.
- `output`: The result of the tool execution, provided to the AI to continue working with. Not shown to the user.
- `summary`: The visual summary of the tool execution, shown in the user's chat window.
- `confirmation`: `{ icon?: string, label: string }` (optional) - Confirmation button shown before execution, useful for actions that require the user's express approval.
```javascript
tools: [
{
name: 'get_user_info',
description: 'Get information about the current user',
inputSchema: {
type: 'object',
properties: {
userId: {
type: 'string',
description: 'The user ID to look up'
}
},
required: ['userId']
},
execute: async (input) => {
const user = await fetch(`/api/users/${input.userId}`).then(r => r.json());
return {
output: { name: user.name, plan: user.plan },
summary: `Retrieved info for user ${user.name}`
};
}
},
{
name: 'create_ticket',
description: 'Create a support ticket',
confirmation: {
icon: 'circle-question',
label: 'Create support ticket?'
},
inputSchema: {
type: 'object',
properties: {
subject: { type: 'string' },
description: { type: 'string' }
},
required: ['subject', 'description']
},
execute: async (input) => {
const ticket = await fetch('/api/tickets', {
method: 'POST',
body: JSON.stringify(input)
}).then(r => r.json());
return {
output: { ticketId: ticket.id },
summary: `Created ticket #${ticket.id}`
};
}
}
]
```
### `visitor` (Authenticated Access)
Available in: Standalone script (via `init`), NPM package (via `getFrameURL()`), React components (as prop)
[Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access) options passed when creating the frame URL. Used for [Adaptive Content](https://gitbook.com/docs/publishing-documentation/adaptive-content) and [Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access).
**Note**: This is not a configuration option but rather a parameter when initializing the frame or creating the frame URL.
**Standalone script**: Pass as the second argument to `GitBook('init', options, frameOptions)`
**NPM package**: Pass to `getFrameURL({ visitor: {...} })`
**React components**: Pass as the `visitor` prop on `<GitBookFrame>`
- **Type**: `{ token?: string, unsignedClaims?: Record<string, unknown> }`
- **Properties**:
- `token`: `string` (optional) - Signed JWT token for [Adaptive Content](https://gitbook.com/docs/publishing-documentation/adaptive-content) or [Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access)
- `unsignedClaims`: `Record<string, unknown>` (optional) - Unsigned claims that can be used in dynamic expressions via `visitor.claims.unsigned.<claim-name>`
```javascript
visitor: {
token: 'your-jwt-token',
unsignedClaims: {
userId: '123',
plan: 'premium',
role: 'admin'
}
}
```
### `colorScheme`
Available in: Standalone script (via `init`), NPM package (via `getFrameURL()`), React components (as prop)
Override the embed's color scheme. When omitted, the embed follows the iframe's CSS `color-scheme`, which lets it inherit the parent page or browser preference.
**Note**: This is not a configuration option but rather a parameter when initializing the frame or creating the frame URL.
**Standalone script**: Pass as the second argument to `GitBook('init', options, frameOptions)`
**NPM package**: Pass to `getFrameURL({ colorScheme: 'dark' })`
**React components**: Pass as the `colorScheme` prop on `<GitBookFrame>`
- **Type**: `'light' | 'dark'`
```javascript
colorScheme: 'dark'
```
### `button`
Available in: Standalone script only
Configure the widget button for the standalone script. This option is not available when using the NPM package or React components, since they can be customized completely.
- **Type**: `{ label: string, icon: 'assistant' | 'sparkle' | 'help' | 'book' }`
- **Properties**:
- `label`: `string` - Button label text
- `icon`: `'assistant' | 'sparkle' | 'help' | 'book'` - Icon displayed on the button. Choose from one of 4 presets.
```javascript
button: {
label: 'Ask',
icon: 'assistant'
}
```
+2 -8
View File
@@ -13,7 +13,7 @@
}
},
"sideEffects": false,
"version": "0.5.0",
"version": "0.1.4",
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/icons": "workspace:",
@@ -23,7 +23,6 @@
"react": "*"
},
"devDependencies": {
"bun-types": "catalog:",
"@types/react": "catalog:",
"tsdown": "catalog:",
"typescript": "catalog:",
@@ -34,17 +33,12 @@
"build-lib": "tsdown",
"build-standalone": "bun build src/standalone/index.ts --bundle --minify --outdir=standalone",
"clean": "rm -rf ./dist",
"unit": "bun test",
"typecheck": "tsc --noEmit",
"dev": "bun run build -- --watch ./src",
"publish-to-npm": "../../scripts/publish-if-new.sh"
"dev": "bun run build -- --watch ./src"
},
"files": ["dist", "README.md", "CHANGELOG.md"],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"repository": {
"url": "https://github.com/GitbookIO/gitbook"
}
}
@@ -1,51 +0,0 @@
import { describe, expect, it } from 'bun:test';
import { createGitBook } from './createGitBook';
describe('createGitBook.getFrameURL', () => {
it('builds the embed URL when the site URL has no trailing slash', () => {
const client = createGitBook({ siteURL: 'https://example.com/docs' });
const url = new URL(client.getFrameURL({}));
expect(url.origin).toBe('https://example.com');
expect(url.pathname).toBe('/docs/~gitbook/embed');
expect(url.searchParams.toString()).toBe('');
});
it('handles a trailing slash and adds visitor parameters', () => {
const client = createGitBook({ siteURL: 'https://example.com/docs/' });
const url = new URL(
client.getFrameURL({
visitor: {
token: 'signed-token',
unsignedClaims: {
role: 'editor',
count: 3,
enabled: false,
},
},
})
);
expect(url.pathname).toBe('/docs/~gitbook/embed');
expect(url.searchParams.get('jwt_token')).toBe('signed-token');
expect(url.searchParams.get('visitor.role')).toBe('editor');
expect(url.searchParams.get('visitor.count')).toBe('3');
expect(url.searchParams.get('visitor.enabled')).toBe('false');
});
it('adds an explicit color scheme override when requested', () => {
const client = createGitBook({ siteURL: 'https://example.com/docs/' });
const url = new URL(
client.getFrameURL({
colorScheme: 'dark',
})
);
expect(url.pathname).toBe('/docs/~gitbook/embed');
expect(url.searchParams.get('theme')).toBe('dark');
});
});
+2 -12
View File
@@ -8,12 +8,6 @@ export type CreateGitBookOptions = {
};
export type GetFrameURLOptions = {
/**
* Override the color scheme used by the embedded docs.
* When omitted, the embed follows the iframe's CSS `color-scheme`.
*/
colorScheme?: 'light' | 'dark';
/**
* Authentication to use for the frame.
*/
@@ -46,14 +40,10 @@ export function createGitBook(options: CreateGitBookOptions) {
const client: GitBookClient = {
getFrameURL: (frameOptions) => {
const url = new URL(options.siteURL);
url.pathname = `${url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`}~gitbook/embed`;
if (frameOptions.colorScheme) {
url.searchParams.set('theme', frameOptions.colorScheme);
}
url.pathname = `${url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`}~gitbook/embed/assistant`;
if (frameOptions.visitor?.token) {
url.searchParams.set('jwt_token', frameOptions.visitor.token);
url.searchParams.set('token', frameOptions.visitor.token);
}
if (frameOptions.visitor?.unsignedClaims) {
@@ -64,12 +64,10 @@ export function createGitBookFrame(iframe: HTMLIFrameElement): GitBookFrameClien
const events = new Map<string, Array<(...args: any[]) => void>>();
const configuration: GitBookEmbeddableConfiguration = {
tabs: ['assistant', 'search', 'docs'],
actions: [],
greeting: { title: '', subtitle: '' },
buttons: [],
welcomeMessage: '',
suggestions: [],
tools: [],
trademark: true,
};
return {
+5 -31
View File
@@ -23,7 +23,7 @@ export type GitBookToolDefinition = AIToolDefinition & {
/**
* Custom button definition to be passed to the embeddable GitBook.
*/
export type GitBookEmbeddableActionDefinition = {
export type GitBookEmbeddableButtonDefinition = {
/**
* Icon to be displayed in the button.
*/
@@ -41,48 +41,22 @@ export type GitBookEmbeddableActionDefinition = {
};
/**
* Overall configuration for the layout of the GitBook embed.
* Overall configuration for the layout of the embeddable GitBook.
*/
export type GitBookEmbeddableConfiguration = {
/** Tabs to display in the embed (if enabled on the site). */
tabs: ('assistant' | 'docs' | 'search')[];
/** Additional buttons to be displayed in the header of the GitBook embed. */
actions: GitBookEmbeddableActionDefinition[];
/**
* Additional buttons to be displayed in the header of the GitBook embed.
* @deprecated Use `actions` instead.
* Buttons to be displayed in the header of the embeddable GitBook.
*/
buttons?: GitBookEmbeddableActionDefinition[];
buttons: GitBookEmbeddableButtonDefinition[];
/** Message to be displayed in the welcome page. */
greeting: {
title: string;
subtitle: string;
};
/**
* Override the assistant name displayed in the UI.
* Limited to 32 characters.
*/
assistantName?: string;
welcomeMessage: string;
/** Suggestions of questions to be displayed in the welcome page. */
suggestions: string[];
/** Tools to be provided to the assistant. */
tools: GitBookToolDefinition[];
/**
* Display GitBook branding in the embed.
*/
trademark?: boolean;
/**
* Display a close button inside the assistant.
*/
closeButton?: boolean;
};
/**
-1
View File
@@ -1,2 +1 @@
export * from './client';
export * from './standalone';
+6 -36
View File
@@ -11,34 +11,19 @@ import { useGitBook } from './GitBookProvider';
export type GitBookFrameProps = {
className?: string;
} & GetFrameURLOptions &
Partial<GitBookEmbeddableConfiguration>;
GitBookEmbeddableConfiguration;
/**
* Render a frame with the GitBook Assistant in it.
*/
export function GitBookFrame(props: GitBookFrameProps) {
const {
className,
colorScheme,
visitor,
actions = [],
greeting,
suggestions = [],
tools = [],
tabs = ['assistant', 'search', 'docs'],
trademark = true,
closeButton = false,
assistantName,
} = props;
const { className, visitor, buttons, welcomeMessage, suggestions, tools } = props;
const frameRef = useRef<HTMLIFrameElement>(null);
const gitbook = useGitBook();
const [gitbookFrame, setGitbookFrame] = useState<GitBookFrameClient | null>(null);
const frameURL = useMemo(
() => gitbook.getFrameURL({ visitor, colorScheme }),
[gitbook, visitor, colorScheme]
);
const frameURL = useMemo(() => gitbook.getFrameURL({ visitor }), [gitbook, visitor]);
useEffect(() => {
if (frameRef.current) {
@@ -48,26 +33,12 @@ export function GitBookFrame(props: GitBookFrameProps) {
useEffect(() => {
gitbookFrame?.configure({
tabs,
actions,
greeting,
buttons,
welcomeMessage,
suggestions,
tools,
closeButton,
trademark,
assistantName,
});
}, [
gitbookFrame,
actions,
greeting,
suggestions,
tools,
tabs,
closeButton,
trademark,
assistantName,
]);
}, [gitbookFrame, buttons, welcomeMessage, suggestions, tools]);
return (
<iframe
@@ -77,7 +48,6 @@ export function GitBookFrame(props: GitBookFrameProps) {
width="100%"
height="100%"
className={className}
style={colorScheme ? { colorScheme } : undefined}
/>
);
}
+30 -68
View File
@@ -31,51 +31,24 @@ type StandaloneCalls =
// Clear the chat
| ['clearChat']
// Configure the embed
| ['configure', Partial<GitBookEmbeddableConfiguration & StandaloneConfiguration>]
| ['configure', Partial<GitBookEmbeddableConfiguration>]
// Navigate to a page
| ['navigateToPage', string]
// Navigate to the assistant
| ['navigateToAssistant'];
type StandaloneConfiguration = {
/** Configure the button to open the embed */
button: {
/** Label to be displayed in the button. */
label: string;
/** Icon to be displayed in the button. */
icon: 'assistant' | 'sparkle' | 'help' | 'book';
};
};
export type GitBookStandalone = ((...args: StandaloneCalls) => void) & {
q?: StandaloneCalls[];
};
let widgetIframe: HTMLIFrameElement | undefined;
let _client: GitBookClient | undefined;
let _frame: GitBookFrameClient | undefined;
let frameOptions: GetFrameURLOptions | undefined;
let frameConfiguration: GitBookEmbeddableConfiguration & StandaloneConfiguration = {
button: {
label: 'Ask',
icon: 'assistant',
},
actions: [],
greeting: { title: '', subtitle: '' },
suggestions: [],
tools: [],
tabs: ['assistant', 'search', 'docs'],
trademark: true,
};
const widgetButton = document.createElement('button');
widgetButton.id = 'gitbook-widget-button';
widgetButton.addEventListener('click', () => {
GitBook('toggle');
});
widgetButton.innerHTML = `
<span id="gitbook-widget-button-icon" data-icon="${frameConfiguration.button.icon}"></span>
<span id="gitbook-widget-button-label">${frameConfiguration.button.label}</span>
<span id="gitbook-widget-button-icon"></span>
<span id="gitbook-widget-button-label">Ask</span>
`;
const widgetWindow = document.createElement('div');
@@ -85,6 +58,17 @@ widgetWindow.classList.add('hidden');
document.body.appendChild(widgetButton);
document.body.appendChild(widgetWindow);
let widgetIframe: HTMLIFrameElement | undefined;
let _client: GitBookClient | undefined;
let _frame: GitBookFrameClient | undefined;
let frameOptions: GetFrameURLOptions | undefined;
let frameConfiguration: GitBookEmbeddableConfiguration = {
buttons: [],
welcomeMessage: '',
suggestions: [],
tools: [],
};
function getClient() {
if (!_client) {
throw new Error(
@@ -101,19 +85,12 @@ function getIframe() {
widgetIframe?.remove();
widgetIframe = document.createElement('iframe');
widgetIframe.id = 'gitbook-widget-iframe';
if (frameOptions?.colorScheme) {
widgetIframe.style.colorScheme = frameOptions.colorScheme;
}
widgetIframe.src = client.getFrameURL({
...frameOptions,
});
widgetWindow.appendChild(widgetIframe);
_frame = client.createFrame(widgetIframe);
_frame.on('close', () => {
widgetWindow.classList.add('hidden');
widgetButton.classList.remove('open');
});
}
return { iframe: widgetIframe, frame: _frame };
}
@@ -158,44 +135,27 @@ const GitBook = (...args: StandaloneCalls) => {
case 'postUserMessage':
getIframe().frame.postUserMessage(args[1]);
break;
case 'configure': {
const settings = args[1];
// If trademark is disabled, change the (branded) icon to the sparkle icon
if (
settings.trademark === false &&
!settings.button?.icon &&
frameConfiguration.button.icon === 'assistant'
) {
settings.button = {
label: frameConfiguration.button.label,
icon: 'sparkle',
};
}
case 'configure':
frameConfiguration = {
...frameConfiguration,
...settings,
...args[1],
};
// Update the button label and icon
if (settings.button?.label) {
const label = widgetButton.querySelector('#gitbook-widget-button-label');
if (label) {
label.textContent = settings.button.label;
}
}
if (settings.button?.icon) {
const icon = widgetButton.querySelector('#gitbook-widget-button-icon');
if (icon) {
icon.setAttribute('data-icon', settings.button.icon);
}
}
getIframe().frame.configure({
...frameConfiguration,
buttons: [
...frameConfiguration.buttons,
// Always include a close button
{
icon: 'close',
label: 'Close',
onClick: () => {
GitBook('close');
},
},
],
});
break;
}
case 'clearChat':
getIframe().frame.clearChat();
break;
@@ -214,3 +174,5 @@ const precalls = (window.GitBook as GitBookStandalone | undefined)?.q ?? [];
// @ts-expect-error - GitBook is not defined in the global scope
window.GitBook = GitBook;
precalls.forEach((call) => GitBook(...call));
GitBook('configure', {});
+1 -24
View File
@@ -29,16 +29,6 @@
--gitbook-widget-easing-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
}
@media (prefers-color-scheme: dark) {
:root {
--gitbook-widget-text-color: #FFFFFF;
--gitbook-widget-border-color: #202020;
--gitbook-widget-background-translucent: rgba(15, 15, 15, 0.9);
--gitbook-widget-background-translucent-hover: rgba(20, 20, 20, 0.9);
--gitbook-widget-background-solid: #f0f0f0;
}
}
* {
box-sizing: border-box;
}
@@ -113,18 +103,6 @@
background-color: currentColor;
}
#gitbook-widget-button-icon[data-icon="sparkle"] {
mask-image: url("https://ka-p.fontawesome.com/releases/v6.6.0/svgs/regular/sparkle.svg?v=2&token=a463935e93");
}
#gitbook-widget-button-icon[data-icon="help"] {
mask-image: url("https://ka-p.fontawesome.com/releases/v6.6.0/svgs/regular/circle-question.svg?v=2&token=a463935e93");
}
#gitbook-widget-button-icon[data-icon="book"] {
mask-image: url("https://ka-p.fontawesome.com/releases/v6.6.0/svgs/regular/book-open.svg?v=2&token=a463935e93");
}
#gitbook-widget-button.open #gitbook-widget-button-icon {
mask-image: url('https://ka-p.fontawesome.com/releases/v6.6.0/svgs/regular/close.svg?v=2&token=a463935e93');
}
@@ -142,8 +120,7 @@
z-index: 9998;
width: calc(min(var(--gitbook-widget-window-width), calc(100vw - var(--gitbook-widget-right) - var(--gitbook-widget-left))));
height: calc(min(var(--gitbook-widget-window-height), calc(100vh - var(--gitbook-widget-window-bottom) - var(--gitbook-widget-top))));
background-color: var(--gitbook-widget-background-translucent);
backdrop-filter: blur(48px);
background-color: var(--gitbook-widget-background-solid);
border: 1px solid var(--gitbook-widget-border-color);
border-radius: var(--gitbook-widget-radius);
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
+1 -3
View File
@@ -15,9 +15,7 @@
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"types": [
"bun-types" // add Bun global
]
"types": []
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"]
-13
View File
@@ -1,18 +1,5 @@
# @gitbook/expr
## 1.2.5
### Patch Changes
- 8242f18: Extend gitbook/expr std lib with some additional methods
## 1.2.4
### Patch Changes
- 10995e0: Use NPM Trusted publishing for publishing the package.
- 10995e0: Use NPM dependency for eval-estree-expression
## 1.2.3
### Patch Changes
+5 -9
View File
@@ -1,7 +1,7 @@
{
"name": "@gitbook/expr",
"description": "Safely evaluate & parse user-defined GitBook expressions.",
"version": "1.2.5",
"version": "1.2.3",
"type": "module",
"exports": {
".": {
@@ -11,12 +11,12 @@
},
"sideEffects": false,
"dependencies": {
"acorn": "^8.15.0",
"acorn-loose": "^8.5.2",
"acorn": "^8.14.0",
"acorn-loose": "8.4.0",
"acorn-walk": "^8.3.4",
"assert-never": "catalog:",
"escodegen": "^2.1.0",
"eval-estree-expression": "github:jonschlinkert/eval-estree-expression#fb0246a"
"eval-estree-expression": "github:jonschlinkert/eval-estree-expression#9cf28d2"
},
"devDependencies": {
"@tsconfig/strictest": "catalog:",
@@ -33,15 +33,11 @@
"typecheck": "tsc --noEmit",
"unit": "bun test",
"clean": "rm -rf ./dist",
"dev": "bun run build -- --watch ./src",
"publish-to-npm": "../../scripts/publish-if-new.sh"
"dev": "bun run build -- --watch ./src"
},
"files": ["dist", "README.md", "CHANGELOG.md"],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"repository": {
"url": "https://github.com/GitbookIO/gitbook"
}
}
@@ -25,7 +25,6 @@ describe('autocomplete', () => {
description: 'An array of string',
items: SymbolString(),
});
const visitorClaimsKeyPropStringSymbol = SymbolString({ name: 'key' });
const symbols = {
visitor: SymbolObject({
name: 'visitor',
@@ -34,7 +33,7 @@ describe('autocomplete', () => {
name: 'claims',
description: 'The claims contained in the visitor JWT token',
properties: {
key: visitorClaimsKeyPropStringSymbol,
key: SymbolString({ name: 'key' }),
flags: SymbolObject({
name: 'flags',
properties: {
@@ -169,12 +168,8 @@ describe('autocomplete', () => {
childrenRefs: [
'visitor.claims.key.length',
'visitor.claims.key.at',
'visitor.claims.key.startsWith',
'visitor.claims.key.endsWith',
'visitor.claims.key.includes',
'visitor.claims.key.toLowerCase',
'visitor.claims.key.toUpperCase',
'visitor.claims.key.trim',
],
},
},
@@ -232,12 +227,8 @@ describe('autocomplete', () => {
childrenRefs: [
'visitor.claims.role.length',
'visitor.claims.role.at',
'visitor.claims.role.startsWith',
'visitor.claims.role.endsWith',
'visitor.claims.role.includes',
'visitor.claims.role.toLowerCase',
'visitor.claims.role.toUpperCase',
'visitor.claims.role.trim',
],
},
},
@@ -255,47 +246,13 @@ describe('autocomplete', () => {
childrenRefs: [
'visitor.claims.key.length',
'visitor.claims.key.at',
'visitor.claims.key.startsWith',
'visitor.claims.key.endsWith',
'visitor.claims.key.includes',
'visitor.claims.key.toLowerCase',
'visitor.claims.key.toUpperCase',
'visitor.claims.key.trim',
],
},
},
],
},
{
expressionWithCursor: 'visitor.claims.key.<cur>',
expectedSuggestions: [
{
type: 'symbol',
symbol: {
definition: SymbolNumber({
name: 'length',
description:
'The length data property of a String value contains the length of the string in UTF-16 code units.',
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length',
}),
ref: 'visitor.claims.key.length',
parentRef: 'visitor.claims.key',
childrenRefs: [],
},
},
...visitorClaimsKeyPropStringSymbol.methods.map<AutocompleteSymbolSuggestion>(
(method) => ({
type: 'symbol',
symbol: {
definition: method,
ref: `visitor.claims.key.${method.name}`,
parentRef: 'visitor.claims.key',
childrenRefs: [],
},
})
),
],
},
{
expressionWithCursor: 'visitor.claims.h<cur>',
expectedSuggestions: [
@@ -54,54 +54,12 @@ describe('ExpressionRuntime', () => {
inputs: { reviews: [{ status: 'approved' }, { status: 'approved' }] },
expectedResult: true,
},
{
scenario: 'array includes',
condition: 'reviews.includes("approved")',
inputs: { reviews: ['pending', 'approved'] },
expectedResult: true,
},
{
scenario: 'array map',
condition: '[1, 2, 3].map(n => n * x)',
inputs: { x: 2 },
expectedResult: [2, 4, 6],
},
{
scenario: 'string startsWith',
condition: 'user.role.startsWith("ad")',
inputs: { user: { role: 'admin' } },
expectedResult: true,
},
{
scenario: 'string endsWith',
condition: 'user.role.endsWith("min")',
inputs: { user: { role: 'admin' } },
expectedResult: true,
},
{
scenario: 'string includes',
condition: 'user.role.includes("dm")',
inputs: { user: { role: 'admin' } },
expectedResult: true,
},
{
scenario: 'string toLowerCase',
condition: 'user.role.toLowerCase() === "admin"',
inputs: { user: { role: 'ADMIN' } },
expectedResult: true,
},
{
scenario: 'string toUpperCase',
condition: 'user.role.toUpperCase() === "ADMIN"',
inputs: { user: { role: 'admin' } },
expectedResult: true,
},
{
scenario: 'string trim',
condition: 'user.role.trim() === "admin"',
inputs: { user: { role: ' admin ' } },
expectedResult: true,
},
])(
'should properly evaluate/safeEvaluate a valid conditional expression: $scenario',
({ condition, inputs, expectedResult }) => {
@@ -255,12 +255,8 @@ describe('ExpressionRuntime', () => {
childrenRefs: [
'visitor.claims.key.length',
'visitor.claims.key.at',
'visitor.claims.key.startsWith',
'visitor.claims.key.endsWith',
'visitor.claims.key.includes',
'visitor.claims.key.toLowerCase',
'visitor.claims.key.toUpperCase',
'visitor.claims.key.trim',
],
});
@@ -305,12 +301,8 @@ describe('ExpressionRuntime', () => {
childrenRefs: [
'visitor.claims.flags.FLAG1.length',
'visitor.claims.flags.FLAG1.at',
'visitor.claims.flags.FLAG1.startsWith',
'visitor.claims.flags.FLAG1.endsWith',
'visitor.claims.flags.FLAG1.includes',
'visitor.claims.flags.FLAG1.toLowerCase',
'visitor.claims.flags.FLAG1.toUpperCase',
'visitor.claims.flags.FLAG1.trim',
],
});
@@ -326,12 +318,8 @@ describe('ExpressionRuntime', () => {
childrenRefs: [
'visitor.claims.flags.FLAG2.length',
'visitor.claims.flags.FLAG2.at',
'visitor.claims.flags.FLAG2.startsWith',
'visitor.claims.flags.FLAG2.endsWith',
'visitor.claims.flags.FLAG2.includes',
'visitor.claims.flags.FLAG2.toLowerCase',
'visitor.claims.flags.FLAG2.toUpperCase',
'visitor.claims.flags.FLAG2.trim',
],
});
-58
View File
@@ -156,31 +156,6 @@ const StandardLibrary: Partial<
members: [SymbolString(), SymbolUndefined()],
}),
}),
SymbolFunction({
name: 'startsWith',
description: `Returns true if the given characters are found at the beginning of the string, including when searchString
is an empty string. Otherwise returns false.`,
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith',
args: [
SymbolString({
name: 'searchString',
description: `The characters to be searched for at the start of this string. Cannot be a regex.
All values that are not regexes are coerced to strings, so omitting it or passing undefined causes startsWith() to search for
the string "undefined", which is rarely what you want.`,
}),
OptionalFunctionArg(
SymbolNumber({
name: 'position',
description: `The start position at which searchString is expected to be found
(the index of searchString's first character). Defaults to 0.`,
})
),
],
returns: SymbolBoolean({
description: `true if the given characters are found at the beginning of the string, including when searchString is an empty string;
otherwise, false.`,
}),
}),
SymbolFunction({
name: 'endsWith',
description: `Returns true if the sequence of elements of searchString converted to a String is the same as the corresponding
@@ -230,39 +205,6 @@ const StandardLibrary: Partial<
otherwise, false.`,
}),
}),
SymbolFunction({
name: 'toLowerCase',
description:
'Returns the value of the string converted to lower case. toLowerCase() does not affect the value of the string str itself.',
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toLowerCase',
args: [],
returns: SymbolString({
description:
'A new string representing the calling string converted to lower case.',
}),
}),
SymbolFunction({
name: 'toUpperCase',
description:
'Returns the value of the string converted to uppercase. toUpperCase() does not affect the value of the string str itself.',
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase',
args: [],
returns: SymbolString({
description:
'A new string representing the calling string converted to upper case.',
}),
}),
SymbolFunction({
name: 'trim',
description:
'Returns new string representing str stripped of whitespace from both its beginning and end.',
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trim',
args: [],
returns: SymbolString({
description:
'A new string representing str stripped of whitespace from both its beginning and end.',
}),
}),
],
},
[SymbolType.Array]: (arraySymbolDef: ArraySymbolDef) => ({
-6
View File
@@ -1,11 +1,5 @@
# @gitbook/fonts
## 0.1.3
### Patch Changes
- 10995e0: Use NPM Trusted publishing for publishing the package.
## 0.1.2
### Patch Changes
+2 -6
View File
@@ -8,7 +8,7 @@
}
},
"sideEffects": false,
"version": "0.1.3",
"version": "0.1.2",
"dependencies": {
"@gitbook/api": "catalog:"
},
@@ -24,8 +24,7 @@
"typecheck": "tsc --noEmit",
"dev": "bun run build -- --watch ./src",
"clean": "rm -rf ./dist && rm -rf ./src/data",
"unit": "bun test",
"publish-to-npm": "../../scripts/publish-if-new.sh"
"unit": "bun test"
},
"files": ["dist", "bin", "README.md", "CHANGELOG.md"],
"engines": {
@@ -34,8 +33,5 @@
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"repository": {
"url": "https://github.com/GitbookIO/gitbook"
}
}
-314
View File
@@ -1,319 +1,5 @@
# gitbook
## 0.25.1
### Patch Changes
- aa1fc24: Add support for sticky first column in tables
## 0.25.0
### Minor Changes
- 88c38fa: Add Search tab to Docs Embed, refactor search
- 709964f: Rework page layout
### Patch Changes
- 789c2fa: Adjust empty TOC logic and layout
- 8d2a95b: Docs Embed: Better support light/dark mode overrides
- 7fd381c: Hide empty sidebar on OpenAPI pages
- 723e579: Fix rendering multi-expandable block in a column
- 68c842b: Translations dropdown should show language spaces that best matches the current space title
- Updated dependencies [8d2a95b]
- Updated dependencies [88c38fa]
- @gitbook/embed@0.5.0
## 0.24.0
### Minor Changes
- 50653ab: Redirect sitemap.md and .well-known/sitemap.md to llms.txt.
- 50653ab: Improve AI agent detection and readability by outputting 200 for page not found.
### Patch Changes
- 126d4b7: Improve Assistant form controls
- Updated dependencies [8242f18]
- @gitbook/expr@1.2.5
- @gitbook/react-openapi@1.5.12
## 0.23.2
### Patch Changes
- Updated dependencies [ab17ff4]
- @gitbook/icons@0.4.6
- @gitbook/react-contentkit@0.7.15
## 0.23.1
### Patch Changes
- Updated dependencies [3c141f0]
- @gitbook/icons@0.4.5
- @gitbook/react-contentkit@0.7.14
## 0.23.0
### Minor Changes
- 8e5a2aa: Pan and zoom controls for mermaid diagrams
### Patch Changes
- b77c4fc: Add getPage MCP tool
- 31e77d4: Track new events for site analytics.
- f884477: Associate questions asked in the embeddable assistant to the proper channel.
- deabce9: Sort results by score to mix records and pages.
- 4909044: Add Claude Code and Codex connect actions
- da648ca: Fix regressions in Assistant UX
- 3ce7e52: Fix Variant dropdown not showing in GitBook embed
- 7f4e644: Small OpenAPI Improvements
- 4787b7e: Use localized titles/descriptions for site entities
- 7ab6351: Import site redirect patterns from @gitbook/api
- b40465e: Fix normalization of URL causing invalid redirections.
- b5d6f2e: Fix translation quality across 11 locales
- 3151864: Track events from the proper server URL.
- 16bfafe: Add option to sticky header row in tables
- 7c2823c: Fix full width header alignment
- c4a2604: Fix PDF export crash when page contains an OpenAPI block
- c56b569: Support expand-all options for OpenAPI
- Updated dependencies [7f4e644]
- Updated dependencies [b5d6f2e]
- Updated dependencies [9dd2db6]
- Updated dependencies [c56b569]
- Updated dependencies [ccd21e4]
- @gitbook/react-openapi@1.5.11
- @gitbook/openapi-parser@3.0.11
- @gitbook/icons@0.4.4
- @gitbook/react-contentkit@0.7.13
## 0.22.3
### Patch Changes
- f182680: Add backward compatibility for OpenAPI spec content URL
- bdc6cd9: Use OpenAPI publicURL as Scalar specUrl
- b3875a1: Refactor embeddable context to merge local & site properties in one unified way
- eceec6f: Move ~scalar/proxy route to site route group
- 887c3f8: Add token verification on OpenAPI proxy
- d666212: Add support for localized site section titles
- Updated dependencies [b3875a1]
- Updated dependencies [bdc6cd9]
- Updated dependencies [8ab419a]
- Updated dependencies [eceec6f]
- Updated dependencies [887c3f8]
- @gitbook/embed@0.4.0
- @gitbook/react-openapi@1.5.10
- @gitbook/openapi-parser@3.0.10
## 0.22.2
### Patch Changes
- e074283: Add server-side proxy for Scalar API client to bypass CORS
- afa868e: Show "Ask Assistant" search result immediately
- 0803db0: Track opening of search results for records and in the assistant.
- cb71511: Wrap tracking call with waitUntil
- 4a6f91a: Show social accounts in header
- 370b731: Docs Embed: Make trademark optional
- afa476c: Add server-side insight event tracking for MCP route
- Updated dependencies [e074283]
- Updated dependencies [370b731]
- @gitbook/openapi-parser@3.0.9
- @gitbook/react-openapi@1.5.9
- @gitbook/embed@0.3.0
## 0.22.1
### Patch Changes
- 9d5e397: Remove code syntax highlighting patch for Safari
- 3d8e89c: Show favicon for record search results in the assistant.
- 5f66860: Use isCookiesTrackingDisabled for cookie consent integrations
- 426aabe: Fix TOC header logo on bold themes
- ae30a57: Integrate Ask AI with upcoming insights by passing visitor session IDs.
- f7c251e: Update Reddit social account rendering
- e31a8f4: Hide cookie banner for AI user agents
- 2e495cb: Add Global Privacy Control (GPC) support
- Updated dependencies [1a7ef78]
- Updated dependencies [93eea0b]
- Updated dependencies [b4b471f]
- Updated dependencies [5f66860]
- Updated dependencies [2ebb18d]
- Updated dependencies [11d9b80]
- Updated dependencies [2e495cb]
- Updated dependencies [f517035]
- @gitbook/react-openapi@1.5.8
- @gitbook/embed@0.2.2
- @gitbook/browser-types@0.1.5
## 0.22.0
### Minor Changes
- e00d1c5: Support code themes customization
- f294818: Serve a markdown version of the page when the "Accept" header contains "text/markdown".
### Patch Changes
- 9d6457f: OpenAPI styling tweaks
- d738e11: Prevent form submission during IME composition
- 1e53376: Add sidesheet component, use it for TOC and AIChat
- f7f6f9d: Set <html lang> based on translated language
- 89de966: Support social links
- b53cb1d: Align styling of PageLinkItem and PageDocumentItem
- 27a34b7: Support `defaultExpanded` in Expandable block
- a5ef44a: Better conditional display of page actions
- 07c02eb: Header & TOC tweaks
- 29e2b22: Fix ordered list item index calculation
- f6cf62a: Standardise toggle chevrons across the app
- b57561d: Fix rendering of images in reusable content from an external space with the new dereferencing in the API.
- e172b25: Grow InlineActionButton to take available width on line
- 7a11861: Add support for custom cookie banner registration
- 8f32f4f: Better handle huge section groups
- 0aa34cc: Fix Embed options in React
- Updated dependencies [e00d1c5]
- Updated dependencies [96e24a1]
- Updated dependencies [4e2d863]
- Updated dependencies [d3f6c38]
- Updated dependencies [3ba9e46]
- Updated dependencies [717ad0b]
- Updated dependencies [6f1db32]
- Updated dependencies [7a11861]
- Updated dependencies [0aa34cc]
- Updated dependencies [48ca21e]
- @gitbook/react-openapi@1.5.7
- @gitbook/embed@0.2.1
- @gitbook/openapi-parser@3.0.8
- @gitbook/icons@0.4.3
- @gitbook/browser-types@0.1.4
- @gitbook/react-contentkit@0.7.12
## 0.21.3
### Patch Changes
- Updated dependencies [bf63045]
- @gitbook/react-openapi@1.5.6
## 0.21.2
### Patch Changes
- ca68236: Fix 404 when accessing rss.xml for the root page.
- 21851d3: Round page cover corners on extrawide screens
- 695e99f: Update button sizing
- 7517e98: Clean up button and input across the app
- 99d788e: Update page cover styling
- ebf3b2b: Fix an issue where content refs to spaces outside of site was resolving with share links
- 35efc9d: Fix TOCPageIcons backgrounds on gradient and bold themes
- a7f58ab: Fix embed tab button size regression
- 360b525: Switch icon masking method to fix visual cutoffs in FA7
- Updated dependencies [985c1b4]
- Updated dependencies [360b525]
- @gitbook/openapi-parser@3.0.7
- @gitbook/icons@0.4.2
- @gitbook/react-openapi@1.5.5
- @gitbook/react-contentkit@0.7.11
## 0.21.1
### Patch Changes
- Updated dependencies [e7fb771]
- @gitbook/icons@0.4.1
- @gitbook/react-contentkit@0.7.10
## 0.21.0
### Minor Changes
- 3553db0: Add a RSS feed to all pages with update blocks.
### Patch Changes
- f478ddc: Add Input component
- 3817289: Fix overflowing section groups
- 86e7aa6: Fix search results cursor
- 1430cee: Improve Docs Embed with separate Assistant and Docs tabs
- ed9e8fe: Add support for button actions
- 9c69cc4: Add customizable suggested questions
- c51076e: Fix links to other spaces and root page in embeddable view.
- 6278a18: Support `greeting` and fix suggested questions if there are no custom ones defined
- 04a4175: Remove hidden site spaces from navigation
- 74f8931: Fix hidden section not found
- 344842f: Improve OpenAPI circular references
- Updated dependencies [4766092]
- Updated dependencies [8761cee]
- Updated dependencies [461e15f]
- Updated dependencies [1430cee]
- Updated dependencies [62438f4]
- Updated dependencies [3e40b4d]
- Updated dependencies [87d68ea]
- Updated dependencies [344842f]
- @gitbook/react-openapi@1.5.4
- @gitbook/openapi-parser@3.0.6
- @gitbook/embed@0.2.0
- @gitbook/icons@0.4.0
- @gitbook/react-contentkit@0.7.9
## 0.20.0
### Minor Changes
- 1d92dc1: Update site layout and blocks with container queries and better transitions
### Patch Changes
- 8fdc5c8: Fix webframe height issue
- 9022d87: Add breadcrumbs to search results
- 0c01832: SearchInput fixes — Translate Clear button, Bold theme color, "Esc" keyboard shortcut
- 1d1f0a7: Fix search container shadow
- 2aa4f2e: Update dropdown menu styling
- a49781e: Printable collapsed code block
- 7d6a316: Revert contentref anchor title
- e27986b: Reword "Everywhere" search scope to "All docs"
- 758b34d: Move the "Ask <query>" to the bottom of search results if query is not a question
- 0ae7cfd: Improve AI Search branding
- f9f8011: Add alt text support to card covers
- ebbb476: Improve ancestor label in references
- 1b0613e: Tweak language dropdown styling
- 3355960: Fix ToggeableLinkItem display
- c31e564: Fix icon resolution for page group
- 55c0b03: Support translations and generic variants together
- 3876f70: Edgeless Assistant window on `depth: flat` sites
- 683bb8f: Add confirmation to "Copy MCP URL" page action
- 4534ea8: Implement unique title tags for sections & variants
- 6d56df7: Treat pages without visible TOC items as `no-toc`
- 8606f22: Add padding to TOC page group items
- 3676d64: Update search scope UI
- c32d449: Fix OpenAPI path display
- 8ce7322: Add OpenAPI servers selection
- 2c3066e: Improve OAuth2 scopes handling in OpenAPI
- 6b1c9fe: Fix opacity of ScrollContainer buttons on bold themes
- Updated dependencies [b4a021a]
- Updated dependencies [a512c90]
- Updated dependencies [df1966d]
- Updated dependencies [b45feaf]
- Updated dependencies [10995e0]
- Updated dependencies [f9f8011]
- Updated dependencies [10995e0]
- Updated dependencies [8ce7322]
- Updated dependencies [2c3066e]
- @gitbook/react-openapi@1.5.3
- @gitbook/openapi-parser@3.0.5
- @gitbook/react-contentkit@0.7.8
- @gitbook/browser-types@0.1.3
- @gitbook/cache-tags@0.3.4
- @gitbook/react-math@0.6.4
- @gitbook/colors@0.4.3
- @gitbook/embed@0.1.5
- @gitbook/fonts@0.1.3
- @gitbook/icons@0.3.4
- @gitbook/expr@1.2.4
## 0.19.2
### Patch Changes
@@ -1,76 +0,0 @@
import { expect } from '@playwright/test';
import { type TestsCase, getCustomizationURL, runTestCases } from './util';
const testCases: TestsCase[] = [
{
name: 'Cookie Banner',
contentBaseURL: 'https://gitbook.com/docs/',
tests: [
{
name: 'should show built-in banner when no custom banner is registered',
url: getCustomizationURL({
privacyPolicy: {
url: 'https://policies.gitbook.com/privacy/cookies',
},
}),
screenshot: false,
run: async (page) => {
// Check that built-in banner is visible
const dialog = page.getByTestId('cookies-dialog');
await expect(dialog).toBeVisible({ timeout: 5000 });
},
},
{
name: 'should not show built-in banner when UserAgent is AI',
url: getCustomizationURL({
privacyPolicy: {
url: 'https://policies.gitbook.com/privacy/cookies',
},
}),
screenshot: false,
run: async (page) => {
// Override navigator.userAgent to simulate AI crawler (affects isAIUserAgent())
await page.addInitScript(() => {
Object.defineProperty(navigator, 'userAgent', {
get: () =>
'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; ChatGPT-User/1.0; +https://openai.com/bot',
configurable: true,
});
});
await page.reload();
// Check that built-in banner is not visible for AI UserAgent
const dialog = page.getByTestId('cookies-dialog');
await expect(dialog).not.toBeVisible({ timeout: 5000 });
},
},
{
name: 'should not show built-in banner when custom banner is registered',
url: getCustomizationURL({
privacyPolicy: {
url: 'https://policies.gitbook.com/privacy/cookies',
},
}),
screenshot: false,
run: async (page) => {
// Register a custom cookie banner handler
await page.waitForFunction(() => {
return typeof window !== 'undefined' && window.GitBook !== undefined;
});
await page.evaluate(() => {
window.GitBook?.registerCookieBanner(() => {
// Custom cookie banner handler - no-op for testing to avoid reload
});
});
// Check that built-in banner is not visible
const dialog = page.getByTestId('cookies-dialog');
await expect(dialog).not.toBeVisible({ timeout: 5000 });
},
},
],
},
];
runTestCases(testCases);
+39 -47
View File
@@ -1,18 +1,5 @@
import { type TestsCase, runTestCases, waitForCookiesDialog } from './util';
const ONE_HOUR_IN_MS = 60 * 60 * 1000;
const CLOSED_ONETRUST_COOKIE_BANNER = [
{
name: '__gitbook_cookie_granted',
value: 'no',
},
{
name: 'OptanonAlertBoxClosed',
value: new Date(Date.now() - ONE_HOUR_IN_MS).toISOString(),
},
];
/** A list of test cases to run on the customers' docs sites. */
const testCases: TestsCase[] = [
{
@@ -23,18 +10,18 @@ const testCases: TestsCase[] = [
{ name: 'OpenAPI', url: '/snyk-api/reference/apps', run: waitForCookiesDialog },
],
},
{
name: 'Nexthink',
contentBaseURL: 'https://docs.nexthink.com',
tests: [
{
name: 'Home',
url: '/',
screenshot: { waitForTOCScrolling: false },
run: waitForCookiesDialog,
},
],
},
// {
// name: 'Nexthink',
// contentBaseURL: 'https://docs.nexthink.com',
// tests: [
// {
// name: 'Home',
// url: '/',
// screenshot: { waitForTOCScrolling: false },
// run: waitForCookiesDialog,
// },
// ],
// },
{
name: 'asiksupport-stg.dto.kemkes.go.id',
contentBaseURL: 'https://asiksupport-stg.dto.kemkes.go.id',
@@ -50,6 +37,11 @@ const testCases: TestsCase[] = [
contentBaseURL: 'https://faq.deltaemulator.com',
tests: [{ name: 'Home', url: '/' }],
},
{
name: 'docs.dify.ai',
contentBaseURL: 'https://docs.dify.ai',
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
},
{
name: 'seeddao.gitbook.io',
contentBaseURL: 'https://seeddao.gitbook.io',
@@ -110,22 +102,21 @@ const testCases: TestsCase[] = [
contentBaseURL: 'https://docs.midas.app',
tests: [{ name: 'Home', url: '/' }],
},
// Disabling for now, one of the image is making the tests timeout
// {
// name: 'docs.keeper.io',
// contentBaseURL: 'https://docs.keeper.io',
// tests: [{ name: 'Home', url: '/en', run: waitForCookiesDialog }],
// },
{
name: 'docs.keeper.io',
contentBaseURL: 'https://docs.keeper.io',
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
},
{
name: 'adiblar.gitbook.io',
contentBaseURL: 'https://adiblar.gitbook.io',
tests: [{ name: 'Home', url: '/' }],
},
// {
// name: 'docs.gradient.network',
// contentBaseURL: 'https://docs.gradient.network',
// tests: [{ name: 'Home', url: '/' }],
// },
{
name: 'docs.gradient.network',
contentBaseURL: 'https://docs.gradient.network',
tests: [{ name: 'Home', url: '/' }],
},
// {
// name: 'mygate-network.gitbook.io',
// contentBaseURL: 'https://mygate-network.gitbook.io',
@@ -181,6 +172,11 @@ const testCases: TestsCase[] = [
contentBaseURL: 'https://docs.realapp.link',
tests: [{ name: 'Home', url: '/' }],
},
{
name: 'docs.plaza.finance',
contentBaseURL: 'https://docs.plaza.finance',
tests: [{ name: 'Home', url: '/' }],
},
{
name: 'docs.publicai.io',
contentBaseURL: 'https://docs.publicai.io',
@@ -223,12 +219,11 @@ const testCases: TestsCase[] = [
contentBaseURL: 'https://docs.soniclabs.com',
tests: [{ name: 'Home', url: '/' }],
},
// This one redirects to binance now
// {
// name: 'docs.meshchain.ai',
// contentBaseURL: 'https://docs.meshchain.ai',
// tests: [{ name: 'Home', url: '/' }],
// },
{
name: 'docs.meshchain.ai',
contentBaseURL: 'https://docs.meshchain.ai',
tests: [{ name: 'Home', url: '/' }],
},
{
name: 'docs.thousandeyes.com',
contentBaseURL: 'https://docs.thousandeyes.com',
@@ -247,12 +242,9 @@ const testCases: TestsCase[] = [
{
name: 'run-ai-docs.nvidia.com',
contentBaseURL: 'https://run-ai-docs.nvidia.com',
skip: process.env.ARGOS_BUILD_NAME !== 'customers-v2',
tests: [
{
name: 'Home',
url: '/',
cookies: CLOSED_ONETRUST_COOKIE_BANNER,
},
{ name: 'Home', url: '/' },
{ name: 'OG Image', url: '/~gitbook/ogimage/h17zQIFwy3MaafVNmItO', mode: 'image' },
],
},
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -29,7 +29,7 @@ test.describe('PDF export', () => {
const printBtn = page.getByTestId('print-button');
await expect(printBtn).toBeVisible();
await argosScreenshot(page, 'pdf - e2e - all pages', {
await argosScreenshot(page, 'pdf - all pages', {
viewports: ['macbook-13'],
argosCSS: `
/* Hide Intercom */
@@ -67,7 +67,7 @@ test.describe('PDF export', () => {
const printBtn = page.getByTestId('print-button');
await expect(printBtn).toBeVisible();
await argosScreenshot(page, 'pdf - gb-docs - all pages', {
await argosScreenshot(page, 'pdf - all pages', {
viewports: ['macbook-13'],
argosCSS: `
/* Hide Intercom */
@@ -109,7 +109,7 @@ test.describe('PDF export', () => {
const printBtn = page.getByTestId('print-button');
await expect(printBtn).toBeVisible();
await argosScreenshot(page, 'pdf - e2e - single-page', {
await argosScreenshot(page, 'pdf - all pages', {
viewports: ['macbook-13'],
argosCSS: `
/* Hide Intercom */
@@ -151,7 +151,7 @@ test.describe('PDF export', () => {
const printBtn = page.getByTestId('print-button');
await expect(printBtn).toBeVisible();
await argosScreenshot(page, 'pdf - gb-docs - single-page', {
await argosScreenshot(page, 'pdf - all pages', {
viewports: ['macbook-13'],
argosCSS: `
/* Hide Intercom */
+15 -43
View File
@@ -2,11 +2,9 @@ import { argosScreenshot } from '@argos-ci/playwright';
import {
CustomizationAIMode,
CustomizationBackground,
CustomizationCodeTheme,
CustomizationCorners,
CustomizationDefaultFont,
CustomizationDefaultMonospaceFont,
CustomizationDefaultThemeMode,
CustomizationDepth,
type CustomizationHeaderItem,
CustomizationHeaderPreset,
@@ -17,6 +15,7 @@ import {
CustomizationSidebarBackgroundStyle,
CustomizationSidebarListStyle,
CustomizationTheme,
CustomizationThemeMode,
type CustomizationThemedColor,
type SiteCustomizationSettings,
SiteExternalLinksTarget,
@@ -75,10 +74,6 @@ export type TestsCase = {
skip?: boolean;
tests: Array<Test>;
contentBaseURL?: string;
/**
* Whether screenshots in this test case should capture the full scrollable page by default.
*/
fullPage?: boolean;
};
export const allLocales: CustomizationLocale[] = [
@@ -88,9 +83,9 @@ export const allLocales: CustomizationLocale[] = [
CustomizationLocale.Zh,
];
export const allThemeModes: CustomizationDefaultThemeMode[] = [
CustomizationDefaultThemeMode.Light,
CustomizationDefaultThemeMode.Dark,
export const allThemeModes: CustomizationThemeMode[] = [
CustomizationThemeMode.Light,
CustomizationThemeMode.Dark,
];
export const allTintColors: Array<{
@@ -159,19 +154,9 @@ export async function waitForNotFound(_page: Page, response: Response | null) {
expect(response?.status()).toBe(404);
}
export async function setTimeToMorning(page: Page) {
const now = new Date();
now.setHours(8, 0, 0, 0); // 8:00:00.000 AM (local time)
await page.clock.install({ time: now });
}
export async function waitForCoverImages(page: Page, options?: { darkMode?: boolean }) {
const selector = options?.darkMode
? 'img[alt="Page cover"].dark\\:inline'
: 'img[alt="Page cover"]:not(.dark\\:inline)';
export async function waitForCoverImages(page: Page) {
// Wait for cover images to exist (not the shimmer placeholder)
await expect(page.locator(selector)).toBeVisible({
await expect(page.locator('img[alt="Page cover"]').first()).toBeVisible({
timeout: 10_000,
});
}
@@ -244,9 +229,9 @@ export function runTestCases(testCases: TestsCase[]) {
.intercom-lightweight-app {
display: none !important;
}
`,
`,
threshold: screenshotOptions?.threshold ?? undefined,
fullPage: testEntry.fullPage ?? testCase.fullPage ?? false,
fullPage: testEntry.fullPage ?? false,
beforeScreenshot: async ({ runStabilization }) => {
await runStabilization();
if (screenshotOptions?.waitForTOCScrolling !== false) {
@@ -313,16 +298,6 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
background: CustomizationBackground.Plain,
icons: CustomizationIconsStyle.Regular,
links: CustomizationLinksStyle.Default,
codeTheme: {
default: {
light: CustomizationCodeTheme.DefaultLight,
dark: CustomizationCodeTheme.DefaultDark,
},
openapi: {
light: CustomizationCodeTheme.DefaultLight,
dark: CustomizationCodeTheme.DefaultDark,
},
},
sidebar: {
background: CustomizationSidebarBackgroundStyle.Default,
list: CustomizationSidebarListStyle.Default,
@@ -344,7 +319,7 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
groups: [],
},
themes: {
default: CustomizationDefaultThemeMode.System,
default: CustomizationThemeMode.Light,
toggeable: true,
},
pdf: {
@@ -380,7 +355,6 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
url: 'https://www.gitbook.com/privacy',
},
socialPreview: {},
socialAccounts: [],
};
const encoded = rison.encode_object(deepMerge(DEFAULT_CUSTOMIZATION, partial));
@@ -443,15 +417,14 @@ export async function waitForIcons(page: Page) {
return true;
}
const maskImage = icon.querySelector('[data-testid="mask-image"]');
if (!maskImage) {
throw new Error('No mask-image element');
}
// url("https://ka-p.fontawesome.com/releases/v6.6.0/svgs/light/moon.svg?v=2&token=a463935e93")
const maskImage = window.getComputedStyle(icon).getPropertyValue('mask-image');
const urlMatch = maskImage.match(/url\("([^"]+)"\)/);
const url = urlMatch?.[1];
const url = maskImage.getAttribute('href');
// If URL is invalid we throw an error.
if (!url) {
throw new Error('No mask-image url');
throw new Error('No mask-image');
}
// If the URL is already queued for loading, we return the state.
@@ -481,8 +454,7 @@ export async function waitForIcons(page: Page) {
*/
async function waitForTOCScrolling(page: Page) {
const viewport = await page.viewportSize();
if (viewport && viewport.width >= 1024 && !page.url().includes('~gitbook/embed/demo')) {
// The embed demo is an iframe, which means the viewport is only a fraction of the main document. So there is no open TOC to scroll to.
if (viewport && viewport.width >= 1024) {
const toc = page.getByTestId('table-of-contents');
await expect(toc).toBeVisible();
await page.evaluate(() => {
+1 -24
View File
@@ -1,27 +1,9 @@
// @ts-check
// We don't use the deployment ID yet on 2c, we need to remove it because of https://github.com/opennextjs/opennextjs-aws/issues/1136
let deploymentId =
process.env.GITBOOK_RUNTIME === 'cloudflare'
? undefined
: process.env.GITBOOK_HEAD_SHA || process.env.GITHUB_SHA || Date.now().toString(); // Needed because we use a custom deployment method i.e. https://vercel.com/docs/skew-protection#custom-deployment-id
const { VERCEL_TARGET_ENV } = process.env;
// Because preview, staging and prod shares the same SHA, the deployment will fail if we don't prefix it with the environment name.
if (VERCEL_TARGET_ENV === 'preview') {
deploymentId = `t-${deploymentId}`;
} else if (VERCEL_TARGET_ENV === 'staging') {
deploymentId = `s-${deploymentId}`;
} else if (VERCEL_TARGET_ENV === 'production') {
deploymentId = `p-${deploymentId}`;
}
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
deploymentId: deploymentId?.slice(0, 32), // Vercel's deployment ID has a max length of 32 characters
experimental: {
// This is needed to throw "forbidden" when the api token expired during revalidation
authInterrupts: true,
@@ -35,18 +17,14 @@ const nextConfig = {
// Since content is fully static, we don't want to fetch on hover again
optimisticClientCache: false,
// Disable splitting the RSC in like 5 chunks
prefetchInlining: true,
},
env: {
BUILD_VERSION: (process.env.GITBOOK_HEAD_SHA ?? process.env.GITHUB_SHA ?? '').slice(0, 7),
BUILD_VERSION: (process.env.GITHUB_SHA ?? '').slice(0, 7),
// GitBook envs
GITBOOK_API_URL: process.env.GITBOOK_API_URL,
GITBOOK_APP_URL: process.env.GITBOOK_APP_URL,
GITBOOK_OAUTH_SERVER_URL: process.env.GITBOOK_OAUTH_SERVER_URL,
GITBOOK_PREVIEW_BASE_URL: process.env.GITBOOK_PREVIEW_BASE_URL,
GITBOOK_INTEGRATIONS_HOST: process.env.GITBOOK_INTEGRATIONS_HOST,
GITBOOK_IMAGE_RESIZE_URL: process.env.GITBOOK_IMAGE_RESIZE_URL,
GITBOOK_ICONS_URL: process.env.GITBOOK_ICONS_URL,
@@ -59,7 +37,6 @@ const nextConfig = {
GITBOOK_IMAGE_RESIZE_MODE: process.env.GITBOOK_IMAGE_RESIZE_MODE,
GITBOOK_FONTS_URL: process.env.GITBOOK_FONTS_URL,
GITBOOK_RUNTIME: process.env.GITBOOK_RUNTIME,
GITBOOK_BLOCK_SEARCH_INDEXATION: process.env.GITBOOK_BLOCK_SEARCH_INDEXATION,
// Next.js envs
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY,
@@ -2,14 +2,14 @@
"main": "default.js",
"name": "gitbook-open-v2-server",
"keep_names": false,
"compatibility_date": "2026-04-02",
"compatibility_date": "2025-04-14",
"compatibility_flags": [
"nodejs_compat",
"allow_importable_env",
"global_fetch_strictly_public"
],
"observability": {
"enabled": false
"enabled": true
},
"vars": {
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
@@ -9,7 +9,7 @@
"global_fetch_strictly_public"
],
"observability": {
"enabled": false
"enabled": true
},
"env": {
"preview": {
@@ -1,20 +1,20 @@
{
"main": "middleware.js",
"name": "gitbook-open-v2",
"compatibility_date": "2026-04-02",
"compatibility_date": "2025-04-14",
"keep_names": false,
"compatibility_flags": [
"nodejs_compat",
"allow_importable_env",
"global_fetch_strictly_public"
],
"observability": {
"enabled": false
},
"assets": {
"directory": "../../.open-next/assets",
"binding": "ASSETS"
},
"observability": {
"enabled": true
},
"vars": {
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
},
@@ -6,13 +6,12 @@ import { softTagFilter } from '@opennextjs/cloudflare/overrides/tag-cache/tag-ca
const originalTagCache = doShardedTagCache({
baseShardSize: 12,
regionalCache: true,
// We can set a long TTL for the regional cache, as we invalidate it on update
regionalCacheTtlSec: 24 * 60 * 60, // 1 day
regionalCacheTtlSec: 60 * 5 /* 5 minutes */,
// Because we invalidate the Cache API on update, we can safely set this to true
regionalCacheDangerouslyPersistMissingTags: true,
shardReplication: {
numberOfSoftReplicas: 2,
numberOfHardReplicas: 2,
numberOfHardReplicas: 1,
regionalReplication: {
defaultRegion: 'enam',
},
+21 -36
View File
@@ -1,6 +1,6 @@
{
"name": "gitbook",
"version": "0.25.1",
"version": "0.19.2",
"private": true,
"dependencies": {
"@cloudflare/workers-types": "^4.20251011.0",
@@ -8,55 +8,48 @@
"@gitbook/browser-types": "workspace:*",
"@gitbook/cache-tags": "workspace:*",
"@gitbook/colors": "workspace:*",
"@gitbook/embed": "workspace:*",
"@gitbook/emoji-codepoints": "workspace:*",
"@gitbook/expr": "workspace:*",
"@gitbook/embed": "workspace:*",
"@gitbook/fonts": "workspace:*",
"@gitbook/icons": "workspace:*",
"@gitbook/openapi-parser": "workspace:*",
"@gitbook/react-contentkit": "workspace:*",
"@gitbook/react-math": "workspace:*",
"@gitbook/react-openapi": "workspace:*",
"@mermaid-js/mermaid-zenuml": "^0.2.2",
"@modelcontextprotocol/sdk": "1.17.5",
"@opennextjs/aws": "3.10.1",
"@opennextjs/cloudflare": "1.19.1",
"@panzoom/panzoom": "^4.6.1",
"@modelcontextprotocol/sdk": "^1.17.5",
"@opennextjs/aws": "^3.8.5",
"@opennextjs/cloudflare": "^1.11.0",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.1.12",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-navigation-menu": "^1.2.3",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-tooltip": "^1.1.8",
"@radix-ui/react-collapsible": "^1.1.12",
"@sindresorhus/fnv1a": "^3.1.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/typography": "^0.5.16",
"@tusbar/cache-control": "^1.0.2",
"@vercel/agent-readability": "^0.4.0",
"ai": "^4.2.2",
"assert-never": "catalog:",
"bidc": "catalog:",
"classnames": "catalog:",
"direction": "^2.0.1",
"event-iterator": "^2.0.0",
"feed": "^5.1.0",
"flexsearch": "^0.8.212",
"image-size": "^2.0.2",
"js-cookie": "^3.0.5",
"jsontoxml": "^1.0.1",
"jwt-decode": "^4.0.0",
"leven": "^4.1.0",
"mcp-handler": "^1.0.2",
"mdast-util-from-markdown": "^2.0.2",
"mdast-util-frontmatter": "^2.0.1",
"mdast-util-gfm": "^3.1.0",
"mdast-util-to-markdown": "^2.1.2",
"memoizee": "^0.4.17",
"mermaid": "^11.14.0",
"micromark-extension-frontmatter": "^2.0.0",
"micromark-extension-gfm": "^3.0.0",
"motion": "^12.23.24",
"next": "^16.2.3",
"next": "15.4.0",
"next-themes": "^0.4.6",
"nuqs": "^2.2.3",
"object-hash": "^3.0.0",
@@ -64,10 +57,9 @@
"openapi-types": "^12.1.3",
"p-map": "^7.0.3",
"quick-lru": "^7.0.1",
"react": "19.2.4",
"react-dom": "19.2.4",
"react": "catalog:",
"react-dom": "catalog:",
"react-hotkeys-hook": "^4.4.1",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"rehype-stringify": "^10.0.1",
"remark-gfm": "^4.0.1",
@@ -75,7 +67,7 @@
"remark-rehype": "^11.1.1",
"rison": "^0.1.1",
"server-only": "^0.0.1",
"shiki": "^3.21.0",
"shiki": "^3.2.0",
"tailwind-merge": "^2.2.0",
"tailwind-shades": "^1.1.2",
"unified": "^11.0.5",
@@ -85,12 +77,11 @@
"usehooks-ts": "catalog:",
"warn-once": "^0.1.1",
"zod": "^3",
"zod-to-json-schema": "^3.25.1",
"zustand": "^5.0.3"
},
"devDependencies": {
"@argos-ci/playwright": "^6.4.2",
"@playwright/test": "^1.58.2",
"@argos-ci/playwright": "^5.0.9",
"@playwright/test": "^1.54.2",
"@scalar/api-client-react": "catalog:",
"@tailwindcss/postcss": "^4.1.11",
"@types/js-cookie": "^3.0.6",
@@ -100,10 +91,9 @@
"@types/node": "^20",
"@types/object-hash": "^3.0.6",
"@types/parse-cache-control": "^1.0.4",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@types/rison": "^0.0.9",
"@types/negotiator": "^0.6.4",
"bun-types": "catalog:",
"deepmerge": "^4.3.1",
"env-cmd": "^10.1.0",
@@ -113,22 +103,21 @@
"tailwindcss": "^4.1.11",
"ts-essentials": "^10.0.1",
"typescript": "catalog:",
"vercel": "50.37.3",
"wrangler": "^4.79.0",
"rss-parser": "^3.13.0"
"vercel": "^39.3.0",
"wrangler": "^4.43.0"
},
"scripts": {
"generate": "./scripts/generate.sh",
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math",
"dev": "env-cmd --silent -f ../../.env.local next --webpack",
"build": "next build --webpack",
"build:local": "GITBOOK_URL=http://localhost:3000 next build --webpack",
"dev": "env-cmd --silent -f ../../.env.local next",
"build": "next build",
"build:local": "GITBOOK_URL=http://localhost:3000 next build",
"start": "GITBOOK_URL=http://localhost:3000 next start",
"build:cloudflare": "GITBOOK_RUNTIME=cloudflare opennextjs-cloudflare build",
"build:cloudflare": "opennextjs-cloudflare build",
"dev:cloudflare": "wrangler dev --port 8771 --env preview",
"dev:cf:middleware": "wrangler dev --port 8771 --inspector-port 9230 --env dev --config ./openNext/customWorkers/middlewareWrangler.jsonc",
"dev:cf:server": "wrangler dev --port 8772 --env dev --config ./openNext/customWorkers/defaultWrangler.jsonc",
"e2e": "playwright test e2e/internal.spec.ts e2e/cookie-banner.spec.ts e2e/pdf.spec.ts --project=chromium",
"e2e": "playwright test e2e/internal.spec.ts e2e/pdf.spec.ts --project=chromium",
"e2e-customers": "playwright test e2e/customers.spec.ts --project=chromium",
"unit": "bun test {src,packages} --preload ./tests/preload-bun.ts",
"e2e-browserless": "bun test ./tests/",
@@ -140,9 +129,5 @@
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"overrides": {
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3"
}
}
@@ -16,7 +16,7 @@ export default async function Page(props: PageProps) {
const { context } = await getDynamicSiteContext(params);
const pathname = getPagePathFromParams(params);
return <SitePage context={context} pageParams={{ pathname }} staticRoute={false} />;
return <SitePage context={context} pageParams={{ pathname }} />;
}
export async function generateViewport(props: PageProps): Promise<Viewport> {
@@ -22,12 +22,7 @@ export default async function SiteDynamicLayout({
const withTracking = shouldTrackEvents(await headers());
return (
<CustomizationRootLayout
htmlClassName="sheet-open:gutter-stable"
bodyClassName="site-background"
forcedTheme={forcedTheme}
context={context}
>
<CustomizationRootLayout forcedTheme={forcedTheme} context={context}>
<SiteLayout
context={context}
forcedTheme={forcedTheme}
@@ -1,30 +0,0 @@
import { type RouteLayoutParams, getDynamicSiteContext } from '@/app/utils';
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
/**
* Redirect to the upstream auth provider login URL of site, or to the site root when not configured.
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<RouteLayoutParams> }
) {
const { context } = await getDynamicSiteContext(await params);
const noLoginFallbackURL = context.linker.toAbsoluteURL(context.linker.toPathInSite(''));
if (!context.site.urls.login) {
return NextResponse.redirect(noLoginFallbackURL);
}
try {
const loginURL = new URL(context.site.urls.login);
const location = request.nextUrl.searchParams.get('location');
if (location) {
loginURL.searchParams.set('location', location);
}
return NextResponse.redirect(loginURL);
} catch (_error) {
return NextResponse.redirect(noLoginFallbackURL);
}
}
@@ -1,36 +0,0 @@
import {
type RouteLayoutParams,
getDynamicSiteContext,
getSiteURLDataFromParams,
} from '@/app/utils';
import { getVisitorAuthBasePath } from '@/lib/data';
import { getVisitorAuthCookieName } from '@/lib/visitors';
import { cookies } from 'next/headers';
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
/**
* Clear the site-scoped auth session cookies and redirect to the site root.
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<RouteLayoutParams> }
) {
const resolvedParams = await params;
const [{ context }, siteURLData, cookieStore] = await Promise.all([
getDynamicSiteContext(resolvedParams),
getSiteURLDataFromParams(resolvedParams),
cookies(),
]);
cookieStore.delete(
getVisitorAuthCookieName(
getVisitorAuthBasePath(new URL(request.nextUrl.toString()), siteURLData)
)
);
// TODO: Redirect to the site root for now. Once the API supports it,
// optionally redirect to a logoutURL (e.g when needing to logout from upstream auth too)
// when defined in visitor auth settings.
return NextResponse.redirect(context.linker.toAbsoluteURL(context.linker.toPathInSite('')));
}
@@ -1,28 +1,16 @@
import type { RouteLayoutParams } from '@/app/utils';
import { EmbeddableAssistantPage } from '@/components/Embeddable';
import { getEmbeddableDynamicContext } from '@/lib/embeddable';
import { CustomizationAIMode } from '@gitbook/api';
import { redirect } from 'next/navigation';
export const dynamic = 'force-static';
type PageProps = {
params: Promise<RouteLayoutParams>;
};
export const dynamic = 'force-static';
export default async function Page(props: PageProps) {
const params = await props.params;
const { context } = await getEmbeddableDynamicContext(params);
// If the assistant is not enabled, redirect to the docs
if (context.customization.ai.mode !== CustomizationAIMode.Assistant) {
redirect(`${context.linker.toPathInSite('~gitbook/embed/page/')}`);
}
return (
<EmbeddableAssistantPage
baseURL={context.linker.toPathInSite('~gitbook/embed/')}
siteTitle={context.site.title}
/>
);
return <EmbeddableAssistantPage context={context} />;
}
@@ -5,7 +5,6 @@ import {
generateEmbeddableViewport,
} from '@/components/Embeddable';
import { getEmbeddableStaticContext } from '@/lib/embeddable';
import { getThemeFromMiddleware } from '@/lib/middleware';
import { shouldTrackEvents } from '@/lib/tracking';
import { headers } from 'next/headers';
@@ -19,14 +18,12 @@ export default async function RootLayout({
}: React.PropsWithChildren<SiteStaticLayoutProps>) {
const { context, visitorAuthClaims } = await getEmbeddableStaticContext(await params);
const withTracking = shouldTrackEvents(await headers());
const forcedTheme = await getThemeFromMiddleware();
return (
<EmbeddableRootLayout
context={context}
withTracking={withTracking}
visitorAuthClaims={visitorAuthClaims}
forcedTheme={forcedTheme}
>
{children}
</EmbeddableRootLayout>
@@ -1,21 +0,0 @@
import type { RouteLayoutParams } from '@/app/utils';
import { getEmbeddableDynamicContext } from '@/lib/embeddable';
import { CustomizationAIMode } from '@gitbook/api';
import { redirect } from 'next/navigation';
type PageProps = {
params: Promise<RouteLayoutParams>;
};
export default async function Page(props: PageProps) {
const params = await props.params;
const { context } = await getEmbeddableDynamicContext(params);
const baseURL = context.linker.toPathInSite('~gitbook/embed/');
// If assistant is enabled, redirect to assistant, otherwise to docs
if (context.customization.ai.mode === CustomizationAIMode.Assistant) {
redirect(`${baseURL}/assistant`);
} else {
redirect(`${baseURL}/page/`);
}
}
@@ -12,7 +12,7 @@ export default async function Page(props: PageProps) {
const { context } = await getEmbeddableDynamicContext(params);
const pathname = getPagePathFromParams(params);
return <EmbeddableDocsPage context={context} pageParams={{ pathname }} staticRoute={false} />;
return <EmbeddableDocsPage context={context} pageParams={{ pathname }} />;
}
export async function generateMetadata(props: PageProps): Promise<Metadata> {
@@ -1,16 +0,0 @@
import type { RouteLayoutParams } from '@/app/utils';
import { EmbeddableSearchPage } from '@/components/Embeddable';
import { getEmbeddableDynamicContext } from '@/lib/embeddable';
type PageProps = {
params: Promise<RouteLayoutParams>;
};
export const dynamic = 'force-static';
export default async function Page(props: PageProps) {
const params = await props.params;
const { context } = await getEmbeddableDynamicContext(params);
return <EmbeddableSearchPage context={context} />;
}
@@ -1,12 +0,0 @@
import type { RouteLayoutParams } from '@/app/utils';
import type { NextRequest } from 'next/server';
import { handleMcpRequest } from '../handler';
async function handler(
rawRequest: NextRequest,
{ params }: { params: Promise<RouteLayoutParams> }
) {
return handleMcpRequest(rawRequest, await params, '~gitbook/mcp/auth');
}
export { handler as GET, handler as POST };
@@ -1,227 +0,0 @@
import { SiteInsightsDisplayContext } from '@gitbook/api';
import { type RouteLayoutParams, getDynamicSiteContext } from '@/app/utils';
import { getExposableError, throwIfDataError } from '@/lib/data';
import { getMarkdownForPageInSpace } from '@/lib/markdownPage';
import { resolvePagePath } from '@/lib/pages';
import { joinPathWithBaseURL } from '@/lib/paths';
import { findSiteSpaceBy, findSiteSpaceByUrl } from '@/lib/sites';
import { trackServerInsightsEvents } from '@/lib/tracking';
import { waitUntil } from '@/lib/waitUntil';
import { createMcpHandler } from 'mcp-handler';
import type { NextRequest } from 'next/server';
import { z } from 'zod';
export async function handleMcpRequest(
rawRequest: NextRequest,
params: RouteLayoutParams,
endpoint: '~gitbook/mcp' | '~gitbook/mcp/auth'
) {
const { context } = await getDynamicSiteContext(params);
const { dataFetcher, linker, site } = context;
if (!context.customization.pageActions.mcp) {
return new Response('Not Found', { status: 404 });
}
// Next.js request.url is the original URL and not the rewritten one from the middleware
const requestURL = new URL(context.linker.toAbsoluteURL(context.linker.toPathInSite(endpoint)));
requestURL.search = rawRequest.nextUrl.search;
const request = new Request(requestURL, rawRequest);
waitUntil(
trackServerInsightsEvents({
organizationId: context.organizationId,
siteId: context.site.id,
events: [
{
type: 'mcp_request',
location: {
displayContext: SiteInsightsDisplayContext.Server,
},
},
],
request,
})
);
const mcpHandler = createMcpHandler(
(server) => {
server.tool(
'searchDocumentation',
`Search across the documentation to find relevant information, code examples, API references, and guides. Use this tool when you need to answer questions about ${site.title}, find specific documentation, understand how features work, or locate implementation details. The search returns contextual content with titles and direct links to the documentation pages.`,
{
query: z.string(),
},
async ({ query }) => {
const results = await throwIfDataError(
dataFetcher.searchSiteContent({
organizationId: context.organizationId,
siteId: site.id,
query,
scope: { mode: 'all' },
})
);
waitUntil(
trackServerInsightsEvents({
organizationId: context.organizationId,
siteId: site.id,
events: [
{
type: 'search_type_query',
query,
location: {
displayContext: SiteInsightsDisplayContext.Mcp,
},
},
],
request,
})
);
return {
content: results.flatMap((result) => {
if (result.type === 'record') {
return {
type: 'text',
text: [
`Title: ${result.title}`,
`Link: ${result.url}`,
result.description ? `Content: ${result.description}` : '',
]
.filter(Boolean)
.join('\n'),
};
}
const found = findSiteSpaceBy(
context.structure,
(siteSpace) => siteSpace.space.id === result.id
);
const spaceURL = found?.siteSpace.urls.published;
if (!spaceURL) {
return [];
}
return result.pages.map((pageResult) => {
const pageURL = linker.toAbsoluteURL(
linker.toLinkForContent(
joinPathWithBaseURL(spaceURL, pageResult.path)
)
);
const body = pageResult.sections
?.map((section) => section.body)
.join('\n');
return {
type: 'text',
text: [
`Title: ${pageResult.title}`,
`Link: ${pageURL}`,
body ? `Content: ${body}` : '',
]
.filter(Boolean)
.join('\n'),
};
});
}),
};
}
);
const siteUrl = context.siteSpace.urls.published;
server.tool(
'getPage',
`Fetch the full markdown content of a specific documentation page from ${site.title}. Use this when you have a page URL and want to read its content. Accepts full URLs (e.g. ${siteUrl}/getting-started). Since \`searchDocumentation\` returns partial content, use \`getPage\` to retrieve the complete page when you need more details. The content includes links you can follow to navigate to related pages.`,
{
url: z
.string()
.describe('The URL of the page to fetch')
.transform((value, ctx) => {
if (URL.canParse(value)) {
return value;
}
if (URL.canParse(`https://${value}`)) {
return `https://${value}`;
}
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `"${value}" is not a valid URL. Expected a full URL like ${siteUrl}/getting-started`,
});
return z.NEVER;
}),
},
async ({ url }) => {
try {
const match = findSiteSpaceByUrl(context.structure, url);
if (!match) {
return {
content: [{ type: 'text', text: `Page not found: "${url}"` }],
isError: true,
};
}
const revision = await throwIfDataError(
dataFetcher.getRevision({
spaceId: match.siteSpace.space.id,
revisionId: match.siteSpace.space.revision,
})
);
const resolved = resolvePagePath(revision.pages, match.pagePath ?? '');
if (!resolved) {
return {
content: [{ type: 'text', text: `Page not found: "${url}"` }],
isError: true,
};
}
const markdown = await getMarkdownForPageInSpace(
context,
match.siteSpace,
resolved.page
);
waitUntil(
trackServerInsightsEvents({
organizationId: context.organizationId,
siteId: site.id,
events: [
{
type: 'page_view',
location: {
displayContext: SiteInsightsDisplayContext.Mcp,
page: resolved.page.id,
space: match.siteSpace.space.id,
revision: match.siteSpace.space.revision,
},
},
],
request,
})
);
return { content: [{ type: 'text', text: markdown }] };
} catch (error) {
const exposable = getExposableError(error);
return {
content: [{ type: 'text', text: exposable.message }],
isError: true,
};
}
}
);
},
{},
{
streamableHttpEndpoint: context.linker.toPathInSite(endpoint),
maxDuration: 60,
verboseLogs: true,
disableSse: true,
}
);
return mcpHandler(request);
}
@@ -1,12 +0,0 @@
import type { RouteLayoutParams } from '@/app/utils';
import type { NextRequest } from 'next/server';
import { handleMcpRequest } from './handler';
async function handler(
rawRequest: NextRequest,
{ params }: { params: Promise<RouteLayoutParams> }
) {
return handleMcpRequest(rawRequest, await params, '~gitbook/mcp');
}
export { handler as GET, handler as POST };
@@ -1,204 +0,0 @@
import type {
ComputedPageResult,
ComputedSectionResult,
OrderedComputedResult,
SearchSiteContentRequest,
} from '@/components/Search/search-types';
import { throwIfDataError } from '@/lib/data';
import { toEmbeddableLinkForPublishedContent } from '@/lib/embeddable-linker';
import { getSiteURLDataFromMiddleware } from '@/lib/middleware';
import { joinPathWithBaseURL } from '@/lib/paths';
import { getServerActionBaseContext } from '@/lib/server-actions';
import { findSiteSpaceBy, getLocalizedTitle } from '@/lib/sites';
import type {
SearchPageResult,
SearchSpaceResult,
SiteSection,
SiteSectionGroup,
SiteSpace,
} from '@gitbook/api';
import type { IconName } from '@gitbook/icons';
import { type NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const { asEmbeddable, query, scope } = (await request.json()) as SearchSiteContentRequest;
const [context, siteURLData] = await Promise.all([
getServerActionBaseContext({ isEmbeddable: asEmbeddable }),
getSiteURLDataFromMiddleware(),
]);
if (query.length <= 1) {
return NextResponse.json([]);
}
const [searchResults, { structure }] = await Promise.all([
throwIfDataError(
context.dataFetcher.searchSiteContent({
organizationId: siteURLData.organization,
siteId: siteURLData.site,
query,
scope,
})
),
throwIfDataError(
context.dataFetcher.getPublishedContentSite({
organizationId: siteURLData.organization,
siteId: siteURLData.site,
siteShareKey: siteURLData.shareKey,
})
),
]);
const results = searchResults
.flatMap((resultItem) => {
if (resultItem.type === 'record') {
const result: OrderedComputedResult = {
type: 'record',
id: resultItem.id,
title: resultItem.title,
description: resultItem.description,
href: resultItem.url,
score: resultItem.score,
};
return [{ score: resultItem.score, items: [result] }];
}
const found = findSiteSpaceBy(
structure,
(siteSpace) => siteSpace.space.id === resultItem.id
);
return resultItem.pages.map((pageItem) => ({
score: pageItem.score,
items: transformSitePageResult({
asEmbeddable: Boolean(asEmbeddable),
linker: context.linker,
pageItem,
spaceItem: resultItem,
siteSpace: found?.siteSpace,
siteSection: found?.siteSection ?? undefined,
siteSectionGroup: found?.siteSectionGroup ?? undefined,
}),
}));
})
.sort((a, b) => b.score - a.score)
.flatMap((group) => group.items);
return NextResponse.json(results);
}
function transformSitePageResult(args: {
asEmbeddable: boolean;
linker: Awaited<ReturnType<typeof getServerActionBaseContext>>['linker'];
pageItem: SearchPageResult;
spaceItem: SearchSpaceResult;
siteSpace?: SiteSpace;
siteSection?: SiteSection;
siteSectionGroup?: SiteSectionGroup | null;
}): OrderedComputedResult[] {
const { asEmbeddable, pageItem, spaceItem, siteSection, siteSectionGroup, siteSpace, linker } =
args;
const currentLanguage = siteSpace?.space.language;
const spaceURL = siteSpace?.urls.published;
const breadcrumbs: NonNullable<ComputedPageResult['breadcrumbs']> = [];
if (siteSectionGroup) {
breadcrumbs.push({
icon: siteSectionGroup.icon as IconName,
label: getLocalizedTitle(siteSectionGroup, currentLanguage),
});
}
if (siteSection) {
breadcrumbs.push({
icon: siteSection.icon as IconName,
label: getLocalizedTitle(siteSection, currentLanguage),
});
}
if (
(siteSection?.siteSpaces?.filter(
(space) =>
siteSection.siteSpaces?.filter(
(candidate) => candidate.space.language === space.space.language
).length > 1
).length ?? 0) > 1 &&
siteSpace
) {
breadcrumbs.push({
label: getLocalizedTitle(siteSpace, currentLanguage),
});
}
breadcrumbs.push(
...pageItem.ancestors.map((ancestor) => ({
label: ancestor.title,
}))
);
const pageHref = !spaceURL
? linker.toPathInSpace(pageItem.path)
: asEmbeddable
? toEmbeddableLinkForPublishedContent(linker, spaceURL, pageItem.path)
: linker.toLinkForContent(joinPathWithBaseURL(spaceURL, pageItem.path));
const page: ComputedPageResult = {
type: 'page',
id: `${spaceItem.id}/${pageItem.id}`,
title: pageItem.title,
href: pageHref,
pageId: pageItem.id,
spaceId: spaceItem.id,
score: pageItem.score,
breadcrumbs,
};
const pageSections =
pageItem.sections
?.filter((section) => section.title || section.body)
.map<ComputedSectionResult>((section) => {
let sectionHref = linker.toPathInSpace(pageItem.path);
if (spaceURL) {
if (asEmbeddable) {
sectionHref = toEmbeddableLinkForPublishedContent(
linker,
spaceURL,
section.path
);
} else {
sectionHref = linker.toLinkForContent(
joinPathWithBaseURL(spaceURL, section.path)
);
}
}
return {
type: 'section',
id: `${page.id}/${section.id}`,
title: section.title,
href: sectionHref,
body: section.body,
pageId: pageItem.id,
spaceId: spaceItem.id,
score: section.score,
};
}) ?? [];
// Find the best-scoring section to use as a body preview on the page result.
const bestSection = pageSections.reduce<ComputedSectionResult | undefined>(
(best, section) => (!best || section.score > best.score ? section : best),
undefined
);
if (bestSection) {
page.bestSection = {
href: bestSection.href,
title: bestSection.title,
body: bestSection.body,
score: bestSection.score,
};
}
return [page];
}
@@ -1,32 +0,0 @@
import { handleOpenAPIProxyOptions, handleOpenAPIProxyRequest } from '@/routes/openapi-proxy';
import type { NextRequest } from 'next/server';
export const dynamic = 'force-dynamic';
export async function GET(request: NextRequest) {
return handleOpenAPIProxyRequest(request);
}
export async function POST(request: NextRequest) {
return handleOpenAPIProxyRequest(request);
}
export async function PUT(request: NextRequest) {
return handleOpenAPIProxyRequest(request);
}
export async function DELETE(request: NextRequest) {
return handleOpenAPIProxyRequest(request);
}
export async function PATCH(request: NextRequest) {
return handleOpenAPIProxyRequest(request);
}
export async function HEAD(request: NextRequest) {
return handleOpenAPIProxyRequest(request);
}
export async function OPTIONS() {
return handleOpenAPIProxyOptions();
}
@@ -18,7 +18,7 @@ export default async function Page(props: PageProps) {
const { context } = await getStaticSiteContext(params);
const pathname = getPagePathFromParams(params);
return <SitePage context={context} pageParams={{ pathname }} staticRoute />;
return <SitePage context={context} pageParams={{ pathname }} />;
}
export async function generateViewport(props: PageProps): Promise<Viewport> {
@@ -19,11 +19,7 @@ export default async function SiteStaticLayout({
const withTracking = shouldTrackEvents();
return (
<CustomizationRootLayout
htmlClassName="sheet-open:gutter-stable"
bodyClassName="site-background"
context={context}
>
<CustomizationRootLayout context={context}>
<SiteLayout
context={context}
withTracking={withTracking}
@@ -10,6 +10,5 @@ export async function GET(
{ params }: { params: Promise<RouteLayoutParams> }
) {
const { context } = await getStaticSiteContext(await params);
return serveLLMsTxt(context);
return serveLLMsTxt(context, { withMarkdownPages: true });
}
@@ -1,28 +1,16 @@
import type { RouteParams } from '@/app/utils';
import type { RouteLayoutParams } from '@/app/utils';
import { EmbeddableAssistantPage } from '@/components/Embeddable';
import { getEmbeddableStaticContext } from '@/lib/embeddable';
import { CustomizationAIMode } from '@gitbook/api';
import { redirect } from 'next/navigation';
export const dynamic = 'force-static';
type PageProps = {
params: Promise<RouteParams>;
params: Promise<RouteLayoutParams>;
};
export default async function Page(props: PageProps) {
const params = await props.params;
const { context } = await getEmbeddableStaticContext(params);
// If the assistant is not enabled, redirect to the docs
if (context.customization.ai.mode !== CustomizationAIMode.Assistant) {
redirect(`${context.linker.toPathInSite('~gitbook/embed/page/')}`);
}
return (
<EmbeddableAssistantPage
baseURL={context.linker.toPathInSite('~gitbook/embed/')}
siteTitle={context.site.title}
/>
);
return <EmbeddableAssistantPage context={context} />;
}
@@ -18,14 +18,6 @@ export async function GET(
<html>
<head>
<meta name="color-scheme" content="light dark">
<style type="text/css">
.control {
display: none;
}
.custom-trigger .control {
display: inline-flex;
}
</style>
</head>
<body>
<svg style="position: absolute; bottom: 6rem; right: 4rem;" width="719" height="644" viewBox="0 0 719 644" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -113,29 +105,17 @@ export async function GET(
<line x1="658.186" y1="444" x2="635.471" y2="444" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
</g>
</svg>
<button type="button" class="control" onclick="window.GitBook('open')">Open</button>
<button type="button" class="control" onclick="window.GitBook('close')">Close</button>
</body>
<script src="${context.linker.toAbsoluteURL(context.linker.toPathInSite('~gitbook/embed/script.js'))}"></script>
<script>
const useCustomTrigger = new URLSearchParams(window.location.search).get('trigger') === 'custom';
window.GitBook('configure', {
suggestions: [
'Help me get started',
'What can I ask you?',
'Show me tips and tricks',
],
tabs: ['assistant', 'search', 'docs'],
closeButton: useCustomTrigger
});
if (useCustomTrigger) {
document.body.classList.add('custom-trigger');
window.GitBook('hide');
} else {
window.GitBook('open');
}
window.GitBook('configure', {
suggestions: [
'Help me get started',
'What can I ask you?',
'Show me tips and tricks',
],
});
window.GitBook('open');
</script>
</html>
`,
@@ -1,23 +0,0 @@
import type { RouteLayoutParams } from '@/app/utils';
import { getEmbeddableStaticContext } from '@/lib/embeddable';
import { CustomizationAIMode } from '@gitbook/api';
import { redirect } from 'next/navigation';
export const dynamic = 'force-static';
type PageProps = {
params: Promise<RouteLayoutParams>;
};
export default async function Page(props: PageProps) {
const params = await props.params;
const { context } = await getEmbeddableStaticContext(params);
const baseURL = context.linker.toPathInSite('~gitbook/embed/');
// If assistant is enabled, redirect to assistant, otherwise to docs
if (context.customization.ai.mode === CustomizationAIMode.Assistant) {
redirect(`${baseURL}/assistant`);
} else {
redirect(`${baseURL}/page/`);
}
}
@@ -14,7 +14,7 @@ export default async function Page(props: PageProps) {
const { context } = await getEmbeddableStaticContext(params);
const pathname = getPagePathFromParams(params);
return <EmbeddableDocsPage context={context} pageParams={{ pathname }} staticRoute />;
return <EmbeddableDocsPage context={context} pageParams={{ pathname }} />;
}
export async function generateMetadata(props: PageProps): Promise<Metadata> {

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