Compare commits

..

6 Commits

Author SHA1 Message Date
taranvohra d6b9fe6643 Bump api package 2024-10-11 12:58:44 +05:30
taranvohra 766c97a100 Merge remote-tracking branch 'origin/main' into cleanup-legacy-code 2024-10-11 12:58:05 +05:30
taranvohra 29fca1e835 bun.lockb 2024-10-07 02:01:02 +05:30
taranvohra 48b5c0707b Merge main 2024-10-07 01:52:50 +05:30
taranvohra 434cfd7a08 WIP 2024-09-12 18:02:33 +02:00
taranvohra e3587d59aa use gitbook/api version 0.61 2024-09-12 17:40:55 +02:00
243 changed files with 3330 additions and 5485 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Avoid multiple <h1> in the page by using a <div> for the title in the header
+6
View File
@@ -0,0 +1,6 @@
---
'@gitbook/react-openapi': patch
'gitbook': patch
---
Adds content-visibility css property to OpenAPI Operation for better render performance
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/cache-do': minor
---
Experiment with optimizing billable duration in Cloudflare by using multiple RPC sessions instead of one
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Fix scroll position when navigating pages on mobile
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': minor
---
Add Python support in Code Samples
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
RND-3532: drop down menu for hidden links at small screen size
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Change dark mode shadow for multi-space search toolbar
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Use ellipsis and fix icon color for more links in the header on small screen
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Improve consistency of change request preview by removing cache-control on response
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/icons': patch
---
Update version for static assets to fix invalid caching
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': minor
---
Revalidate change request cached content when pressing refresh button
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/icons': patch
---
Fix first run of the package in development mode
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Improve performances of loading pages with embeds by caching them
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': patch
---
bump: scalar from 1.0.5 to 1.0.7
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': minor
---
Replace all icons, previously imported from Geist, by new package `@gitbook/icons`
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/cache-do': minor
---
First version of a new cache backend powered by Cloudflare Durable Objects
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': minor
---
Add support for Norwegian language
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': minor
---
Optimize layout shift while transitioning between pages with full width blocks (ex: OpenAPI blocks)
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': minor
---
Add support for page icons
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Fix GitBook specific static assets not being served correctly when deployed on Cloudflare
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Only display spaces dropdown in compact header when site is multi-variants
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/cache-do': patch
---
Enable Workers observability with a sampling of 0.1
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/icons': minor
---
Initial release for `@gitbook/icons`, a package of content icons to be used for pages, documents, etc.
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Automatically scroll to active item in TOC
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': minor
---
Persist state of tabs and dynamically sync them based on title
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Ensure "Sponsored via GitBook" can be translated in all languages
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': minor
---
Show the caption for file blocks
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Use url hash to open Expandable and scroll to anchor
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Fix margin for paragraphs in quote blocks
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Use new cache backend, powered by Durable Objects, alongside the existing ones (KV, etc).
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Include offset in calculations of whether scrollable element is in view
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': minor
---
Add required query parameters to the code sample
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': minor
---
Display pattern if available in parmas in OpenAPI block
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Fix inconsistent click area in table because of scroll indicator
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Stop using KV cache backend for now, but also improves it for higher performances
+6
View File
@@ -0,0 +1,6 @@
---
'@gitbook/react-openapi': minor
'gitbook': patch
---
Synchronize response and response example tabs
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Show definition title when visible in cards
+6
View File
@@ -0,0 +1,6 @@
---
'@gitbook/react-openapi': minor
'gitbook': patch
---
Rollback of scalar modal while fixing perf issue
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Optimize images rendered in community ads
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Update design of Checkbox to be more consistent and readable
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-contentkit': patch
---
Fix rendering of webframe with SSR causing wrong communication between frame and renderer
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Disable KV cache for docs.gitbook.com as a test, also disable it for change-request to improve consistency
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Add optional env `GITBOOK_INTEGRATIONS_HOST` to configure the host serving the integrations
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': patch
---
Fix crash when `example` is undefined for a response
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': minor
---
Improve the toolbar for change-requests and revisions to show more actions
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': minor
---
Add support for icons style customization for sites
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': patch
---
Adds tabs to Response Example section e.g. for status code examples
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': patch
---
Render mandatory headers in code sample
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': patch
---
Update Scalar client to latest version
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': minor
---
Prevent search indexation for pages where it's configured as disabled
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Fix table of contents being visible on mobile when disabled at the page level
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Fix accessibility of the table of contents by using `aria-current` instead of `aria-selected`
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': minor
---
Update rendering of community ads to match new API response, and make it possible to preview ads.
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-math': minor
---
Export binary `gitbook-math` to copy assets to a directory
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Fix "ip" being passed to BSA for community ads
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': minor
---
Changed the alternative URL resolution criteria in order to support site URLs without /v/ prefix
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/icons': minor
---
Fix bin gitbook-icons when installed without the Pro token
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Don't show hidden pages in the empty state of a page
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': minor
---
Fix linking to a tab itself
+1 -1
View File
@@ -12,7 +12,7 @@ runs:
# Cache browser binaries, cache key is based on Playwright version and OS
- name: 🧰 Cache Playwright browser binaries
uses: actions/cache@v4
uses: actions/cache@v3
id: playwright-cache
with:
path: '~/.cache/ms-playwright'
+21 -43
View File
@@ -13,12 +13,8 @@ jobs:
permissions:
contents: read
deployments: write
issues: write
pull-requests: write
checks: write
statuses: write
outputs:
deployment_url: ${{ steps.deploy.outputs.deployment-url }}
deployment_url: ${{ steps.cloudflare.outputs.url }}
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -30,6 +26,16 @@ jobs:
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Cache Next.js build
uses: actions/cache@v3
with:
path: |
${{ github.workspace }}/.next/cache
# Generate a new cache whenever packages or source files change.
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/bun.lockb') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
# If source files changed but packages didn't, rebuild from a prior cache.
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/bun.lockb') }}-
- name: Sets env vars for production
run: |
echo "SENTRY_ENVIRONMENT=production" >> $GITHUB_ENV
@@ -46,55 +52,27 @@ jobs:
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}
SENTRY_DSN: ${{ vars.SENTRY_DSN }}
- id: deploy
name: Deploy to Cloudflare
uses: cloudflare/wrangler-action@v3.11.0
- id: cloudflare
name: Publish to Cloudflare Pages
uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: ${{ vars.CLOUDFLARE_PROJECT_NAME }}
directory: ./packages/gitbook/.vercel/output/static
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
workingDirectory: ./
wranglerVersion: '3.82.0'
command: pages deploy ./packages/gitbook/.vercel/output/static --project-name=${{ vars.CLOUDFLARE_PROJECT_NAME }} --branch=${{ github.ref == 'refs/heads/main' && 'main' || format('pr{0}', github.event.pull_request.number) }}
- name: Outputs
run: |
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
echo "Alias URL: ${{ steps.deploy.outputs.deployment-alias-url }}"
echo "ID: ${{ steps.cloudflare.outputs.id }}"
echo "URL: ${{ steps.cloudflare.outputs.url }}"
echo "Environment: ${{ steps.cloudflare.outputs.environment }}"
echo "Alias: ${{ steps.cloudflare.outputs.alias }}"
- name: Archive build output
uses: actions/upload-artifact@v4
with:
name: build-output
path: .vercel/
# Until https://github.com/cloudflare/wrangler-action/issues/301 is done
- name: Update Deployment Status to Success
env:
DEPLOYMENT_URL: ${{ steps.deploy.outputs.deployment-url }}
run: |
curl -X POST \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github.v3+json" \
-d '{"state": "success", "target_url": "${{ steps.deploy.outputs.deployment-url }}", "description": "Deployed Preview URL for commit", "context": "cloudflare/preview"}' \
https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.sha }}
- name: Find GitHub Comment
uses: peter-evans/find-comment@v3
id: fc
if: 1 && !startsWith(github.ref, 'refs/heads/main')
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: GitBook Preview
- name: Create or update GitHub comment
uses: peter-evans/create-or-update-comment@v4
if: 1 && !startsWith(github.ref, 'refs/heads/main')
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body: |
**GitBook Preview**
Latest commit: [${{ steps.deploy.outputs.deployment-url }}](${{ steps.deploy.outputs.deployment-url }})
PR: [${{ steps.deploy.outputs.deployment-alias-url }}](${{ steps.deploy.outputs.deployment-alias-url }})
edit-mode: replace
visual-testing:
runs-on: ubuntu-latest
name: Visual Testing
BIN
View File
Binary file not shown.
+1 -2
View File
@@ -8,8 +8,7 @@
},
"packageManager": "bun@1.1.18",
"patchedDependencies": {
"@vercel/next@4.3.15": "patches/@vercel%2Fnext@4.3.15.patch",
"@cloudflare/next-on-pages@1.13.5": "patches/@cloudflare%2Fnext-on-pages@1.13.5.patch"
"@vercel/next@4.3.6": "patches/@vercel%2Fnext@4.3.6.patch"
},
"private": true,
"scripts": {
-18
View File
@@ -1,18 +0,0 @@
# @gitbook/cache-do
## 0.1.1
### Patch Changes
- b7a5106: Disable cloudflare observability in production
## 0.1.0
### Minor Changes
- 9b8d519: Experiment with optimizing billable duration in Cloudflare by using multiple RPC sessions instead of one
- 636b868: First version of a new cache backend powered by Cloudflare Durable Objects
### Patch Changes
- 56f5fa1: Enable Workers observability with a sampling of 0.1
+2 -2
View File
@@ -14,14 +14,14 @@
"default": "./dist/api.js"
}
},
"version": "0.1.1",
"version": "0.0.0",
"dependencies": {
"@msgpack/msgpack": "^3.0.0-beta2",
"lru_map": "^0.4.1"
},
"devDependencies": {
"typescript": "^5.5.3",
"wrangler": "3.82.0"
"wrangler": "^3.78.7"
},
"scripts": {
"generate": "wrangler types --experimental-include-runtime",
+3 -2
View File
@@ -11,7 +11,8 @@ migrations = [
]
[observability]
enabled = false
enabled = true
head_sampling_rate = 0.01
[env.preview]
name = "gitbook-open-cache-preview"
@@ -24,4 +25,4 @@ migrations = [
[env.preview.observability]
enabled = true
head_sampling_rate = 0.1
head_sampling_rate = 1
+1 -2
View File
@@ -32,5 +32,4 @@ screenshots/
/playwright/.cache/
# Generated public files
/public/~gitbook/static/*
!/public/~gitbook/static/images
/public/~gitbook/static/
-135
View File
@@ -1,140 +1,5 @@
# gitbook
## 0.5.0
### Minor Changes
- 57cdd25: GitBook Open now supports Ask AI in sites. When asking a question to Ask AI, GitBook will use context from across your site sections and variants to provide the best answer.
- ca134c8: Fix an issue where the active site section indicator appeared above any dropdowns.
- d48926e: Fix an issue where the space dropdown was shown under the site sections in Safari.
- 9fe8142: Fix an issue where Ask AI was erroring due to an object being passed as a param.
- d843e5e: Fix an issue where the space dropdown could appear behind the header.
- a2e5647: Fix the styling of site section tabs on smaller screens.
### Patch Changes
- 076dc48: Fix expandable block anchore resolution
- d9bb9f9: Fix an issue with the cookie banner buttons being non responsive
- 23584c9: Update the site header with new styling, a new search button, and refactored layout
- 664debc: Add support for tint color
- 4d56f11: Update styling of search+ask modal
- 061c0c1: Fix a regression in variant drop-down caused by missing z-index.
- 2f76712: Add breadcrumbs above page title
- 07cf835: Add scroll margin to the top when there are sections
- 5d72b35: Smoother tab transition for sections
- 7c71363: Don't adjust fallback font for mono font.
- 7675c2c: Optimize performances by using new API endpoint for fetching site data.
- 87eea73: Fix margin and image resolution of header logo
- aa2ed0f: Restyle hint blocks
- ffd3937: Fix security issue with image resizing that could be used for phishing
- 2ce59d7: Fix - whitespace added to site section tabs with icons.
- c73e07d: Increase token max length to fix code not highlighted
- 3b3d6e2: Add icons to sections
- 1ed18c0: style: adds missing scalar css variables
- Updated dependencies [b7a5106]
- Updated dependencies [4771c78]
- Updated dependencies [ff50ac2]
- Updated dependencies [867481c]
- Updated dependencies [7ba67fd]
- Updated dependencies [a78c1ec]
- @gitbook/cache-do@0.1.1
- @gitbook/react-openapi@0.7.1
## 0.4.0
### Minor Changes
- e09f747: Revalidate change request cached content when pressing refresh button
- 2fa0851: Add navigation tabs for sections
- a4b63b8: Support resolution of new site URLs with sections
- 5c35f36: Replace all icons, previously imported from Geist, by new package `@gitbook/icons`
- e9b31a5: Unify section tab styles with page item styles
- f12a215: Add support for Norwegian language
- f4c9536: Optimize layout shift while transitioning between pages with full width blocks (ex: OpenAPI blocks)
- 1f24fe4: Add support for page icons
- cda08a9: Add support for searching results in a sections site
- b32e40c: Persist state of tabs and dynamically sync them based on title
- 15d2ee3: Show the caption for file blocks
- f885e88: Improve the toolbar for change-requests and revisions to show more actions
- 07ea45b: Remove deprecated synced block from GitBook Open
- c3675fd: Added support for new Reusable Content block.
- 1f24fe4: Add support for icons style customization for sites
- 4c19014: Prevent search indexation for pages where it's configured as disabled
- 3422ad4: Update rendering of community ads to match new API response, and make it possible to preview ads.
- 1152445: Changed the alternative URL resolution criteria in order to support site URLs without /v/ prefix
- 2c437f7: Fix linking to a tab itself
### Patch Changes
- aa32198: Avoid multiple <h1> in the page by using a <div> for the title in the header
- 51fa3ab: Adds content-visibility css property to OpenAPI Operation for better render performance
- a7066cc: Fix scroll position when navigating pages on mobile
- c754fc9: Add automatic color contrast in site header, restyle search button
- 5fe7adb: RND-3532: drop down menu for hidden links at small screen size
- 6295881: Change dark mode shadow for multi-space search toolbar
- f89b31c: Upgrade the scalar api client package
- 13c7534: Use ellipsis and fix icon color for more links in the header on small screen
- f885e88: Improve consistency of change request preview by removing cache-control on response
- 16e6171: Improve performances of loading pages with embeds by caching them
- 34d36c6: Fix GitBook specific static assets not being served correctly when deployed on Cloudflare
- af9e66e: Only display spaces dropdown in compact header when site is multi-variants
- e3a3d6a: Improve perception of fast loading by not rendering skeletons for individual blocks in the top part of the viewport
- 042b850: Automatically scroll to active item in TOC
- d43202f: Optimize bundle size of the server output by reducing bundle size of shiki (skipping themes)
- bfbed1a: Ensure "Sponsored via GitBook" can be translated in all languages
- fe9e6c1: Update ogimage with new design
- 17f71ba: Use url hash to open Expandable and scroll to anchor
- 3c07e65: Fix margin for paragraphs in quote blocks
- 636b868: Use new cache backend, powered by Durable Objects, alongside the existing ones (KV, etc).
- f16560c: Include offset in calculations of whether scrollable element is in view
- 689f553: Fix inconsistent click area in table because of scroll indicator
- 6ce3cea: Stop using KV cache backend for now, but also improves it for higher performances
- e914903: Synchronize response and response example tabs
- 0f990c7: Show definition title when visible in cards
- e3a3d6a: Fix flickering when displaying an "Ask" answer with code blocks
- 4cbcc5b: Rollback of scalar modal while fixing perf issue
- 3996110: Optimize images rendered in community ads
- 133c3e7: Update design of Checkbox to be more consistent and readable
- 5096f7f: Disable KV cache for docs.gitbook.com as a test, also disable it for change-request to improve consistency
- 0f1565c: Add optional env `GITBOOK_INTEGRATIONS_HOST` to configure the host serving the integrations
- 2ff7ed1: Fix table of contents being visible on mobile when disabled at the page level
- b075f0f: Fix accessibility of the table of contents by using `aria-current` instead of `aria-selected`
- cb782a7: Fix "ip" being passed to BSA for community ads
- a7af3ca: Improving the look and feel of new section tabs
- 0bf985a: Don't show hidden pages in the empty state of a page
- d6c28a0: Update header styling of sections, variant selector, and button links
- Change position of variant selector depending on context (next to logo or in table of contents)
- Update section tab styling and animation
- Make header buttons smaller with a new `medium` button size
- Updated dependencies [51fa3ab]
- Updated dependencies [9b8d519]
- Updated dependencies [cf3045a]
- Updated dependencies [f89b31c]
- Updated dependencies [d0f4860]
- Updated dependencies [ef9d012]
- Updated dependencies [094e9cd]
- Updated dependencies [636b868]
- Updated dependencies [56f5fa1]
- Updated dependencies [5c35f36]
- Updated dependencies [4247361]
- Updated dependencies [aa8c49e]
- Updated dependencies [e914903]
- Updated dependencies [4cbcc5b]
- Updated dependencies [0f1565c]
- Updated dependencies [237b703]
- Updated dependencies [51955da]
- Updated dependencies [a679e72]
- Updated dependencies [c079c3c]
- Updated dependencies [5c35f36]
- Updated dependencies [776bc31]
- @gitbook/react-openapi@0.7.0
- @gitbook/cache-do@0.1.0
- @gitbook/icons@0.1.0
- @gitbook/react-contentkit@0.5.1
- @gitbook/react-math@0.6.0
## 0.3.0
### Minor Changes
+20 -418
View File
@@ -1,16 +1,11 @@
import { argosScreenshot } from '@argos-ci/playwright';
import {
CustomizationBackground,
CustomizationCorners,
CustomizationFont,
CustomizationHeaderPreset,
CustomizationIconsStyle,
CustomizationLocale,
CustomizationThemeMode,
SiteCustomizationSettings,
} from '@gitbook/api';
import { test, expect, Page } from '@playwright/test';
import deepMerge from 'deepmerge';
import jwt from 'jsonwebtoken';
import rison from 'rison';
import { DeepPartial } from 'ts-essentials';
@@ -39,18 +34,6 @@ const allLocales: CustomizationLocale[] = [
CustomizationLocale.Zh,
];
const allThemeModes: CustomizationThemeMode[] = [
CustomizationThemeMode.Light,
CustomizationThemeMode.Dark,
];
const allThemePresets: CustomizationHeaderPreset[] = [
CustomizationHeaderPreset.Default,
CustomizationHeaderPreset.Bold,
CustomizationHeaderPreset.Contrast,
CustomizationHeaderPreset.Custom,
];
async function waitForCookiesDialog(page: Page) {
const dialog = page.getByRole('dialog', { name: 'Cookies' });
const accept = dialog.getByRole('button', { name: 'Accept' });
@@ -112,9 +95,7 @@ const testCases: TestsCase[] = [
name: 'Variants dropdown',
url: '',
run: async (page) => {
const spaceDrowpdown = page
.locator('[data-testid="space-dropdown-button"]')
.locator('visible=true');
const spaceDrowpdown = page.locator('[data-testid="space-dropdown-button"]');
await spaceDrowpdown.waitFor();
},
},
@@ -130,9 +111,7 @@ const testCases: TestsCase[] = [
name: 'Customized variant titles are displayed',
url: '',
run: async (page) => {
const spaceDrowpdown = page
.locator('[data-testid="space-dropdown-button"]')
.locator('visible=true');
const spaceDrowpdown = page.locator('[data-testid="space-dropdown-button"]');
await spaceDrowpdown.click();
const variantSelectionDropdown = page.locator(
@@ -164,9 +143,9 @@ const testCases: TestsCase[] = [
url: 'api-multi-versions/reference/api-reference/pets',
screenshot: false,
run: async (page) => {
const spaceDrowpdown = await page
.locator('[data-testid="space-dropdown-button"]')
.locator('visible=true');
const spaceDrowpdown = await page.waitForSelector(
'[data-testid="space-dropdown-button"]',
);
await spaceDrowpdown.click();
// Click the second variant in the dropdown
@@ -187,9 +166,9 @@ const testCases: TestsCase[] = [
url: 'api-multi-versions-share-links/8tNo6MeXg7CkFMzSSz81/reference/api-reference/pets',
screenshot: false,
run: async (page) => {
const spaceDrowpdown = await page
.locator('[data-testid="space-dropdown-button"]')
.locator('visible=true');
const spaceDrowpdown = await page.waitForSelector(
'[data-testid="space-dropdown-button"]',
);
await spaceDrowpdown.click();
// Click the second variant in the dropdown
@@ -222,9 +201,9 @@ const testCases: TestsCase[] = [
return `api-multi-versions-va/reference/api-reference/pets?jwt_token=${token}`;
})(),
run: async (page) => {
const spaceDrowpdown = await page
.locator('[data-testid="space-dropdown-button"]')
.locator('visible=true');
const spaceDrowpdown = await page.waitForSelector(
'[data-testid="space-dropdown-button"]',
);
await spaceDrowpdown.click();
// Click the second variant in the dropdown
@@ -416,10 +395,6 @@ const testCases: TestsCase[] = [
await page.click('[data-testid="annotation-button"]');
},
},
{
name: 'Stepper',
url: 'blocks/stepper',
},
],
},
{
@@ -456,38 +431,30 @@ const testCases: TestsCase[] = [
{
name: 'Customization',
baseUrl: 'https://gitbook.gitbook.io/test-gitbook-open/',
tests: allThemeModes.flatMap((theme) => [
tests: [
{
name: `Without header - Theme ${theme}`,
name: 'Without header',
url: getCustomizationURL({
header: {
preset: CustomizationHeaderPreset.None,
links: [],
},
themes: {
default: theme,
toggeable: false,
},
}),
run: waitForCookiesDialog,
},
{
name: `With duotone icons - Theme ${theme}`,
name: 'With duotone icons',
url:
'page-options/page-with-icon' +
getCustomizationURL({
styling: {
icons: CustomizationIconsStyle.Duotone,
},
themes: {
default: theme,
toggeable: false,
},
}),
run: waitForCookiesDialog,
},
{
name: `With header buttons - Theme ${theme}`,
name: 'With header buttons',
url: getCustomizationURL({
header: {
preset: CustomizationHeaderPreset.Default,
@@ -495,110 +462,21 @@ const testCases: TestsCase[] = [
{
title: 'Secondary button',
to: { kind: 'url', url: 'https://www.gitbook.com' },
// @ts-ignore Remove once we upgrade to the latest version of the API
style: 'button-secondary',
},
{
title: 'Primary button',
to: { kind: 'url', url: 'https://www.gitbook.com' },
// @ts-ignore Remove once we upgrade to the latest version of the API
style: 'button-primary',
},
],
},
themes: {
default: theme,
toggeable: false,
},
}),
run: waitForCookiesDialog,
},
{
name: `Without tint - Default preset - Theme ${theme}`,
url: getCustomizationURL({
header: {
preset: CustomizationHeaderPreset.Default,
links: [
{
title: 'Secondary button',
to: { kind: 'url', url: 'https://www.gitbook.com' },
style: 'button-secondary',
},
{
title: 'Primary button',
to: { kind: 'url', url: 'https://www.gitbook.com' },
style: 'button-primary',
},
],
},
themes: {
default: theme,
toggeable: false,
},
}),
run: waitForCookiesDialog,
},
...allThemePresets.flatMap((preset) => ({
name: `With tint - Preset ${preset} - Theme ${theme}`,
url: getCustomizationURL({
styling: {
tint: { color: { light: '#346DDB', dark: '#346DDB' } },
},
header: {
preset,
...(preset === CustomizationHeaderPreset.Custom
? {
backgroundColor: { light: '#C62C68', dark: '#EF96B8' },
linkColor: { light: '#4DDE98', dark: '#0C693D' },
}
: {}),
links: [
{
title: 'Secondary button',
to: { kind: 'url', url: 'https://www.gitbook.com' },
style: 'button-secondary',
},
{
title: 'Primary button',
to: { kind: 'url', url: 'https://www.gitbook.com' },
style: 'button-primary',
},
],
},
themes: {
default: theme,
toggeable: false,
},
}),
run: waitForCookiesDialog,
})),
{
name: `With tint - Legacy background match - Theme ${theme}`,
url: getCustomizationURL({
styling: {
background: CustomizationBackground.Match,
},
header: {
preset: CustomizationHeaderPreset.Default,
links: [
{
title: 'Secondary button',
to: { kind: 'url', url: 'https://www.gitbook.com' },
style: 'button-secondary',
},
{
title: 'Primary button',
to: { kind: 'url', url: 'https://www.gitbook.com' },
style: 'button-primary',
},
],
},
themes: {
default: theme,
toggeable: false,
},
}),
run: waitForCookiesDialog,
},
]),
],
},
{
name: 'Ads',
@@ -611,62 +489,13 @@ const testCases: TestsCase[] = [
},
],
},
{
name: 'Shared space navigation (first site)',
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/shared-space-uno/',
tests: [
{
name: 'Navigation to shared space',
url: '',
run: async (page) => {
const sharedSpaceLink = page.locator('a.underline');
await sharedSpaceLink.click();
expect(page.locator('h1')).toHaveText('shared');
const url = page.url();
expect(url.includes('shared-space-uno')).toBeTruthy(); // same uno site
expect(url.endsWith('/shared')).toBeTruthy(); // correct page
},
},
],
},
{
name: 'Shared space navigation (second site)',
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/shared-space-dos/',
tests: [
{
name: 'Navigation to shared space',
url: '',
run: async (page) => {
const sharedSpaceLink = page.locator('a.underline');
await sharedSpaceLink.click();
expect(page.locator('h1')).toHaveText('shared');
const url = page.url();
expect(url.includes('shared-space-dos')).toBeTruthy(); // same dos site
expect(url.endsWith('/shared')).toBeTruthy(); // correct page
},
},
],
},
{
name: 'Site Redirects',
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/gitbook-doc/',
tests: [
{
name: 'Redirect to SSO page',
url: 'a/redirect/to/sso',
run: async (page) => {
await expect(page.locator('h1')).toHaveText('SSO');
},
},
],
},
{
name: 'Share links',
baseUrl: 'https://gitbook.gitbook.io/gbo-tests-share-links/',
tests: [
{
name: 'Valid link',
url: 'thDznyWXCeEoT55WB7HC/',
url: 'TGs8PkF4GWVtbmPnWhYL/',
},
{
name: 'Invalid link',
@@ -934,178 +763,6 @@ const testCases: TestsCase[] = [
},
],
},
{
name: 'Adaptive Content - VA',
baseUrl: `https://gitbook-open-e2e-sites.gitbook.io/adaptive-content-va/`,
tests: [
{
name: 'isAlphaUser',
url: (() => {
const privateKey = 'afe09cdf-0f43-480a-b54c-8b1f62f174f9';
const token = jwt.sign(
{
name: 'gitbook-open-tests',
isAlphaUser: true,
},
privateKey,
{
expiresIn: '24h',
},
);
return `?jwt_token=${token}`;
})(),
run: async (page) => {
const alphaUserPage = page
.locator('a[class*="group\\/toclink"]')
.filter({ hasText: 'Alpha users' });
const betaUserPage = page
.locator('a[class*="group\\/toclink"]')
.filter({ hasText: 'Beta users' });
await expect(alphaUserPage).toBeVisible();
await expect(betaUserPage).toHaveCount(0);
},
},
{
name: 'isBetaUser',
url: (() => {
const privateKey = 'afe09cdf-0f43-480a-b54c-8b1f62f174f9';
const token = jwt.sign(
{
name: 'gitbook-open-tests',
isBetaUser: true,
},
privateKey,
{
expiresIn: '24h',
},
);
return `?jwt_token=${token}`;
})(),
run: async (page) => {
const alphaUserPage = page
.locator('a[class*="group\\/toclink"]')
.filter({ hasText: 'Alpha users' });
const betaUserPage = page
.locator('a[class*="group\\/toclink"]')
.filter({ hasText: 'Beta users' });
await expect(betaUserPage).toBeVisible();
await expect(alphaUserPage).toHaveCount(0);
},
},
{
name: 'isAlphaUser & isBetaUser',
url: (() => {
const privateKey = 'afe09cdf-0f43-480a-b54c-8b1f62f174f9';
const token = jwt.sign(
{
name: 'gitbook-open-tests',
isAlphaUser: true,
isBetaUser: true,
},
privateKey,
{
expiresIn: '24h',
},
);
return `?jwt_token=${token}`;
})(),
run: async (page) => {
const alphaUserPage = page
.locator('a[class*="group\\/toclink"]')
.filter({ hasText: 'Alpha users' });
const betaUserPage = page
.locator('a[class*="group\\/toclink"]')
.filter({ hasText: 'Beta users' });
await expect(alphaUserPage).toBeVisible();
await expect(betaUserPage).toBeVisible();
},
},
],
},
{
name: 'Tables',
baseUrl: 'https://gitbook.gitbook.io/test-gitbook-open/',
tests: [
{
name: 'Default table',
url: 'blocks/tables',
run: waitForCookiesDialog,
fullPage: true,
},
{
name: 'Table with straight corners',
url:
'blocks/tables' +
getCustomizationURL({
styling: {
corners: CustomizationCorners.Straight,
},
}),
run: waitForCookiesDialog,
fullPage: true,
},
{
name: 'Table with primary color',
url:
'blocks/tables' +
getCustomizationURL({
styling: {
tint: { color: { light: '#346DDB', dark: '#346DDB' } },
},
}),
run: waitForCookiesDialog,
fullPage: true,
},
// Test dark mode for each variant
...allThemeModes.flatMap((theme) => [
{
name: `Table in ${theme} mode`,
url:
'blocks/tables' +
getCustomizationURL({
themes: {
default: theme,
toggeable: false,
},
}),
run: waitForCookiesDialog,
fullPage: true,
},
{
name: `Table with straight corners in ${theme} mode`,
url:
'blocks/tables' +
getCustomizationURL({
styling: {
corners: CustomizationCorners.Straight,
},
themes: {
default: theme,
toggeable: false,
},
}),
run: waitForCookiesDialog,
fullPage: true,
},
{
name: `Table with primary color in ${theme} mode`,
url:
'blocks/tables' +
getCustomizationURL({
styling: {
tint: { color: { light: '#346DDB', dark: '#346DDB' } },
},
themes: {
default: theme,
toggeable: false,
},
}),
run: waitForCookiesDialog,
fullPage: true,
},
]),
],
},
];
for (const testCase of testCases) {
@@ -1140,62 +797,7 @@ for (const testCase of testCases) {
* Create a URL with customization settings.
*/
function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettings>): string {
/**
* Default customization settings.
*
* The customization object passed to the URL should be a valid API settings object. Hence we extend the test with necessary defaults.
*/
const DEFAULT_CUSTOMIZATION: SiteCustomizationSettings = {
styling: {
primaryColor: { light: '#346DDB', dark: '#346DDB' },
corners: CustomizationCorners.Rounded,
font: CustomizationFont.Inter,
background: CustomizationBackground.Plain,
icons: CustomizationIconsStyle.Regular,
},
internationalization: {
locale: CustomizationLocale.En,
},
favicon: {},
header: {
preset: CustomizationHeaderPreset.Default,
links: [],
},
footer: {
groups: [],
},
themes: {
default: CustomizationThemeMode.Light,
toggeable: true,
},
pdf: {
enabled: true,
},
feedback: {
enabled: false,
},
aiSearch: {
enabled: true,
},
advancedCustomization: {
enabled: true,
},
git: {
showEditLink: false,
},
pagination: {
enabled: true,
},
trademark: {
enabled: true,
},
privacyPolicy: {
url: 'https://www.gitbook.com/privacy',
},
socialPreview: {},
};
const encoded = rison.encode_object(deepMerge(DEFAULT_CUSTOMIZATION, partial));
const encoded = rison.encode_object(partial);
const searchParams = new URLSearchParams();
searchParams.set('customization', encoded);
+14 -15
View File
@@ -1,11 +1,11 @@
{
"name": "gitbook",
"version": "0.5.0",
"version": "0.3.0",
"private": true,
"scripts": {
"dev": "env-cmd --silent -f ../../.env.local next dev",
"build": "next build",
"build:cloudflare": "next-on-pages --custom-entrypoint=./src/cloudflare-entrypoint.ts",
"build:cloudflare": "next-on-pages",
"start": "next start",
"lint": "next lint",
"typecheck": "tsc --noEmit",
@@ -16,7 +16,7 @@
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static"
},
"dependencies": {
"@gitbook/api": "^0.81.0",
"@gitbook/api": "0.64.1",
"@gitbook/cache-do": "workspace:*",
"@gitbook/emoji-codepoints": "workspace:*",
"@gitbook/icons": "workspace:*",
@@ -41,7 +41,7 @@
"katex": "^0.16.9",
"mathjax": "^3.2.2",
"memoizee": "^0.4.15",
"next": "14.2.15",
"next": "^14.2.5",
"next-themes": "^0.2.1",
"nuqs": "^1.17.4",
"object-hash": "^3.0.0",
@@ -49,8 +49,8 @@
"p-map": "^7.0.0",
"parse-cache-control": "^1.0.1",
"postcss-color-contrast": "^1.1.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react": "^18",
"react-dom": "^18",
"react-hotkeys-hook": "^4.4.1",
"recoil": "^0.7.7",
"rehype-sanitize": "^6.0.0",
@@ -60,7 +60,7 @@
"remark-rehype": "^11.1.0",
"rison": "^0.1.1",
"server-only": "^0.0.1",
"shiki": "^1.23.1",
"shiki": "^1.11.1",
"tailwind-merge": "^2.2.0",
"tailwind-shades": "^1.1.2",
"unified": "^11.0.4",
@@ -68,9 +68,9 @@
},
"devDependencies": {
"@argos-ci/playwright": "^2.0.0",
"@cloudflare/next-on-pages": "^1.13.5",
"@cloudflare/workers-types": "^4.20240725.0",
"@playwright/test": "^1.42.1",
"@cloudflare/next-on-pages": "^1.13.3",
"@cloudflare/workers-types": "^4.20240725.0",
"@types/js-cookie": "^3.0.6",
"@types/jsontoxml": "^1.0.5",
"@types/jsonwebtoken": "^9.0.6",
@@ -78,12 +78,10 @@
"@types/object-hash": "^3.0.6",
"@types/parse-cache-control": "^1.0.4",
"@types/psi": "^4.1.6",
"@types/react": "18.3.13",
"@types/react-dom": "18.3.1",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/rison": "^0.0.9",
"autoprefixer": "^10",
"deepmerge": "^4.3.1",
"env-cmd": "^10.1.0",
"eslint": "^8",
"eslint-config-next": "^14.2.5",
"eslint-plugin-import": "^2.29.1",
@@ -91,7 +89,8 @@
"postcss": "^8",
"psi": "^4.1.0",
"tailwindcss": "^3.4.0",
"ts-essentials": "^10.0.1",
"typescript": "^5.5.3"
"typescript": "^5.5.3",
"env-cmd": "^10.1.0",
"ts-essentials": "^10.0.1"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

@@ -4,7 +4,6 @@ import {
verifyImageSignature,
resizeImage,
CloudflareImageOptions,
imagesResizingSignVersion,
checkIsSizableImageURL,
} from '@/lib/images';
import { parseImageAPIURL } from '@/lib/urls';
@@ -19,29 +18,28 @@ export const runtime = 'edge';
export async function GET(request: NextRequest) {
let urlParam = request.nextUrl.searchParams.get('url');
const signature = request.nextUrl.searchParams.get('sign');
// The current signature algorithm sets version as 2, but we need to support the older version as well
const signatureVersion = request.nextUrl.searchParams.get('sv') as string | undefined;
// The current signature algorithm sets version as 1, but we need to support the older version as well
// for previously generated content. In this case, we default to version 0.
const signatureVersion = (request.nextUrl.searchParams.get('sv') as '1') || '0';
if (!urlParam || !signature) {
return new Response('Missing url/sign parameters', { status: 400 });
}
const url = parseImageAPIURL(urlParam);
// Prevent infinite loops
if (url.includes('/~gitbook/image')) {
return new Response('Invalid url parameter', { status: 400 });
}
// Check again if the image can be sized, even though we checked when rendering the Image component
// Otherwise, it's possible to pass just any link to this endpoint and trigger HTML injection on the domain
// Also prevent infinite redirects.
if (!checkIsSizableImageURL(url)) {
return new Response('Invalid url parameter', { status: 400 });
}
// For older signatures, we redirect to the url.
if (signatureVersion !== imagesResizingSignVersion) {
return Response.redirect(url, 302);
}
// Verify the signature
const verified = await verifyImageSignature(url, { signature });
const verified = await verifyImageSignature(url, { signature, version: signatureVersion });
if (!verified) {
return new Response(`Invalid signature "${signature ?? ''}" for "${url}"`, { status: 400 });
}
@@ -1,223 +0,0 @@
import { CustomizationHeaderPreset } from '@gitbook/api';
import { redirect } from 'next/navigation';
import { ImageResponse } from 'next/og';
import { NextRequest } from 'next/server';
import colorContrast from 'postcss-color-contrast/js';
import React from 'react';
import { absoluteHref } from '@/lib/links';
import { tcls } from '@/lib/tailwind';
import { getContentTitle } from '@/lib/utils';
import { PageIdParams, fetchPageData } from '../../../../fetch';
export const runtime = 'edge';
/**
* Render the OpenGraph image for a space.
*/
export async function GET(req: NextRequest, { params }: { params: PageIdParams }) {
const { space, page, customization, site } = await fetchPageData(params);
if (customization.socialPreview.url) {
// If user configured a custom social preview, we redirect to it.
redirect(customization.socialPreview.url);
}
// TODO: Support all fonts available in GitBook
// Right now this is impossible since next/font/google does not expose the cached font file
// Another option would be to use the Satori prop `loadAdditionalAsset` [example](https://github.com/vercel/satori/blob/main/playground/pages/index.tsx),
// but this prop isn't (yet) exposed through `ImageResponse`.
const interRegular = await fetch(
new URL('../../../../../../fonts/Inter/Inter-Regular.ttf', import.meta.url),
).then((res) => res.arrayBuffer());
const interBold = await fetch(
new URL('../../../../../../fonts/Inter/Inter-Bold.ttf', import.meta.url),
).then((res) => res.arrayBuffer());
const theme = customization.themes.default;
const useLightTheme = theme === 'light';
// We have no access to CSS variables, so we'll have to hardcode some values
const baseColors = {
light: '#ffffff',
dark: '#111827',
};
let colors = {
background: baseColors[theme],
gradient: customization.styling.primaryColor[theme],
title: customization.styling.primaryColor[theme],
body: baseColors[useLightTheme ? 'dark' : 'light'], // Invert text on background
};
const gridWhite = absoluteHref('~gitbook/static/images/ogimage-grid-white.png', true);
const gridBlack = absoluteHref('~gitbook/static/images/ogimage-grid-black.png', true);
let gridAsset = useLightTheme ? gridBlack : gridWhite;
switch (customization.header.preset) {
case CustomizationHeaderPreset.Custom:
colors = {
background: customization.header.backgroundColor?.[theme] || colors.background,
gradient: customization.header.linkColor?.[theme] || colors.gradient,
title: customization.header.linkColor?.[theme] || colors.title,
body: colorContrast(
customization.header.backgroundColor?.[theme] || colors.background,
[baseColors.light, baseColors.dark],
),
};
gridAsset = colors.body == baseColors.light ? gridWhite : gridBlack;
break;
case CustomizationHeaderPreset.Bold:
colors = {
background: customization.styling.primaryColor[theme],
gradient: colorContrast(customization.styling.primaryColor[theme], [
baseColors.light,
baseColors.dark,
]),
title: colorContrast(customization.styling.primaryColor[theme], [
baseColors.light,
baseColors.dark,
]),
body: colorContrast(customization.styling.primaryColor[theme], [
baseColors.light,
baseColors.dark,
]),
};
gridAsset = colors.body == baseColors.light ? gridWhite : gridBlack;
break;
}
const favicon = function () {
if ('icon' in customization.favicon)
return (
<img
src={customization.favicon.icon[theme]}
width={40}
height={40}
tw={tcls('mr-4')}
alt="Icon"
/>
);
if ('emoji' in customization.favicon)
return (
<span tw={tcls('text-4xl', 'mr-4')}>
{String.fromCodePoint(parseInt('0x' + customization.favicon.emoji))}
</span>
);
return (
<img
src={absoluteHref(
`~gitbook/icon?size=medium&theme=${customization.themes.default}`,
true,
)}
alt="Icon"
width={40}
height={40}
tw={tcls('mr-4')}
/>
);
};
return new ImageResponse(
(
<div
tw={tcls(
'justify-between',
'p-20',
'relative',
'w-full',
'h-full',
'flex',
'flex-col',
`bg-[${colors.background}]`,
`text-[${colors.body}]`,
)}
>
{/* Gradient */}
<div
tw={tcls('absolute', 'inset-0')}
style={{
backgroundImage: `radial-gradient(ellipse 100% 100% at top right , ${colors.gradient}, ${colors.gradient}00)`,
opacity: 0.5,
}}
></div>
{/* Grid */}
<img
tw={tcls('absolute', 'inset-0', 'w-[100vw]', 'h-[100vh]')}
src={gridAsset}
alt="Grid"
/>
{/* Logo */}
{customization.header.logo ? (
<img
alt="Logo"
height={60}
src={
useLightTheme
? customization.header.logo.light
: customization.header.logo.dark
}
/>
) : (
<div tw={tcls('flex')}>
{favicon()}
<h3 tw={tcls('text-4xl', 'my-0')}>
{getContentTitle(space, customization, site ?? null)}
</h3>
</div>
)}
{/* Title and description */}
<div tw={tcls('flex', 'flex-col')}>
<h1
tw={tcls(
'text-8xl',
'my-0',
'tracking-tight',
'leading-none',
'text-left',
`text-[${colors.title}]`,
'font-bold',
)}
>
{page
? page.title.length > 64
? page.title.slice(0, 64) + '...'
: page.title
: 'Not found'}
</h1>
{page?.description && page?.title.length <= 64 ? (
<h2 tw={tcls('text-4xl', 'mb-0', 'mt-8', 'w-[75%]', 'font-normal')}>
{page.description.length > 164
? page.description.slice(0, 164) + '...'
: page.description}
</h2>
) : null}
</div>
</div>
),
{
width: 1200,
height: 630,
fonts: [
{
name: 'Inter',
data: interRegular,
weight: 400,
style: 'normal',
},
{
name: 'Inter',
data: interBold,
weight: 700,
style: 'normal',
},
],
},
);
}
-145
View File
@@ -1,145 +0,0 @@
import { RevisionPage } from '@gitbook/api';
import { redirect } from 'next/navigation';
import {
getRevisionPageByPath,
getDocument,
getSpaceContentData,
getSiteData,
getSiteRedirectBySource,
} from '@/lib/api';
import { resolvePagePath, resolvePageId } from '@/lib/pages';
import { getSiteContentPointer } from '@/lib/pointer';
export interface PagePathParams {
pathname?: string[];
}
export interface PageIdParams {
pageId: string;
}
/**
* Fetch all the data needed to render the content layout.
*/
export async function fetchContentData() {
const content = getSiteContentPointer();
const [{ space, contentTarget, pages }, { customization, site, sections, spaces, scripts }] =
await Promise.all([
getSpaceContentData(content, content.siteShareKey),
getSiteData(content),
]);
// we grab the space attached to the parent as it contains overriden customizations
const spaceRelativeToParent = spaces?.find((space) => space.id === content.spaceId);
return {
content,
contentTarget,
space: spaceRelativeToParent ?? space,
pages,
site,
sections,
spaces,
shareKey: content.siteShareKey,
customization,
scripts,
ancestors: [],
};
}
/**
* Fetch all the data needed to render the content.
* Optimized to fetch in parallel as much as possible.
*/
export async function fetchPageData(params: PagePathParams | PageIdParams) {
const contentData = await fetchContentData();
const page = await resolvePage({
organizationId: contentData.space.organization,
siteId: contentData.site.id,
spaceId: contentData.contentTarget.spaceId,
revisionId: contentData.contentTarget.revisionId,
pages: contentData.pages,
shareKey: contentData.shareKey,
params,
});
const document = page?.page.documentId
? await getDocument(contentData.space.id, page.page.documentId)
: null;
return {
...contentData,
...page,
document,
};
}
/**
* Resolve a page from the params.
* If the path can't be found, we try to resolve it from the API to handle redirects.
*/
async function resolvePage(input: {
organizationId: string;
siteId: string;
spaceId: string;
revisionId: string;
shareKey: string | undefined;
pages: RevisionPage[];
params: PagePathParams | PageIdParams;
}) {
const { organizationId, siteId, spaceId, revisionId, pages, shareKey, params } = input;
if ('pageId' in params) {
return resolvePageId(pages, params.pageId);
}
const rawPathname = getPathnameParam(params);
const pathname = normalizePathname(rawPathname);
// When resolving a page, we use the lowercased pathname
const page = resolvePagePath(pages, pathname);
if (page) {
return page;
}
// We don't test path that are too long as GitBook doesn't support them and will return a 404 anyway.
if (rawPathname.length <= 512) {
// If page can't be found, we try with the API, in case we have a redirect at space level.
// We use the raw pathname to handle special/malformed redirects setup by users in the GitSync.
// The page rendering will take care of redirecting to a normalized pathname.
const resolved = await getRevisionPageByPath(spaceId, revisionId, rawPathname);
if (resolved) {
return resolvePageId(pages, resolved.id);
}
// If a page still can't be found, we try with the API, in case we have a redirect at site level.
const resolvedSiteRedirect = await getSiteRedirectBySource({
organizationId,
siteId,
source: rawPathname.startsWith('/') ? rawPathname : `/${rawPathname}`,
siteShareKey: input.shareKey,
});
if (resolvedSiteRedirect) {
return redirect(resolvedSiteRedirect.target);
}
}
return undefined;
}
/**
* Get the page path from the params.
*/
export function getPathnameParam(params: PagePathParams): string {
const { pathname } = params;
return pathname ? pathname.map((part) => decodeURIComponent(part)).join('/') : '';
}
/**
* Normalize the URL pathname into the format used in the revision page path.
*/
export function normalizePathname(pathname: string) {
return pathname.toLowerCase();
}
@@ -1,18 +0,0 @@
import { CustomizationRootLayout } from '@/components/RootLayout';
import { getSiteData } from '@/lib/api';
import { getSiteContentPointer } from '@/lib/pointer';
/**
* Layout to be used for the site root. It fetches the customization data for the site
* and initializes the CustomizationRootLayout with it.
*/
export default async function SiteRootLayout(props: { children: React.ReactNode }) {
const { children } = props;
const pointer = getSiteContentPointer();
const { customization } = await getSiteData(pointer);
return (
<CustomizationRootLayout customization={customization}>{children}</CustomizationRootLayout>
);
}
@@ -8,10 +8,10 @@ import { useScrollPage } from '@/components/hooks';
/**
* Client component to initialize interactivity for a page.
*/
export function PageClientLayout(props: { withSections?: boolean }) {
export function PageClientLayout(props: {}) {
// We use this hook in the page layout to ensure the elements for the blocks
// are rendered before we scroll to a hash or to the top of the page
useScrollPage({ scrollMarginTop: props.withSections ? 50 : undefined });
useScrollPage();
useStripFallbackQueryParam();
return null;
@@ -1,11 +1,12 @@
import { getSpaceLanguage, t } from '@/intl/server';
import { getSiteData } from '@/lib/api';
import { getSiteContentPointer } from '@/lib/pointer';
import { getCurrentSiteLayoutData } from '@/lib/api';
import { tcls } from '@/lib/tailwind';
import { getSiteContentPointer } from '../../fetch';
export default async function NotFound() {
const pointer = getSiteContentPointer();
const { customization } = await getSiteData(pointer);
const { customization } = await getCurrentSiteLayoutData(pointer);
const language = getSpaceLanguage(customization);
@@ -29,13 +29,11 @@ export default async function Page(props: {
const {
content: contentPointer,
contentTarget,
sections,
space,
site,
parent,
customization,
pages,
page,
ancestors,
document,
} = await getPageDataWithFallback({
pagePathParams: params,
@@ -74,46 +72,41 @@ export default async function Page(props: {
page,
};
const withSections = Boolean(sections && sections.list.length > 0);
const headerOffset = { sectionsHeader: withSections, topHeader: withTopHeader };
return (
<>
{withFullPageCover && page.cover ? (
<PageCover as="full" page={page} cover={page.cover} context={contentRefContext} />
) : null}
{/* We use a flex row reverse to render the aside first because the page is streamed. */}
<div className="flex flex-row-reverse justify-end">
{page.layout.outline ? (
<PageAside
space={space}
site={site}
customization={customization}
page={page}
document={document}
withHeaderOffset={headerOffset}
withFullPageCover={withFullPageCover}
withPageFeedback={withPageFeedback}
context={contentRefContext}
/>
) : null}
<div className={tcls('flex', 'flex-row')}>
<PageBody
space={space}
pointer={contentPointer}
contentPointer={contentPointer}
contentTarget={contentTarget}
customization={customization}
context={contentRefContext}
page={page}
ancestors={ancestors}
document={document}
withPageFeedback={
// Display the page feedback in the page footer if the aside is not visible
withPageFeedback && !page.layout.outline
}
/>
{page.layout.outline ? (
<PageAside
space={space}
site={parent?.object === 'site' ? parent : undefined}
customization={customization}
page={page}
document={document}
withHeaderOffset={withTopHeader}
withFullPageCover={withFullPageCover}
withPageFeedback={withPageFeedback}
context={contentRefContext}
/>
) : null}
</div>
<React.Suspense fallback={null}>
<PageClientLayout withSections={withSections} />
<PageClientLayout />
</React.Suspense>
</>
);
@@ -137,7 +130,7 @@ export async function generateMetadata({
params: PagePathParams;
searchParams: { fallback?: string };
}): Promise<Metadata> {
const { space, pages, page, customization, site, ancestors } = await getPageDataWithFallback({
const { space, pages, page, customization, parent, ancestors } = await getPageDataWithFallback({
pagePathParams: params,
searchParams,
});
@@ -147,13 +140,12 @@ export async function generateMetadata({
}
return {
title: [page.title, getContentTitle(space, customization, site ?? null)]
title: [page.title, getContentTitle(space, customization, parent)]
.filter(Boolean)
.join(' | '),
description: page.description ?? '',
alternates: {
// Trim trailing slashes in canonical URL to match the redirect behavior
canonical: absoluteHref(getPagePath(pages, page), true).replace(/\/+$/, ''),
canonical: absoluteHref(getPagePath(pages, page), true),
},
openGraph: {
images: [
@@ -162,7 +154,7 @@ export async function generateMetadata({
],
},
robots:
isSpaceIndexable({ space, site: site ?? null }) && isPageIndexable(ancestors, page)
isSpaceIndexable({ space, parent }) && isPageIndexable(ancestors, page)
? 'index, follow'
: 'noindex, nofollow',
};
@@ -18,7 +18,7 @@ import { getContentTitle } from '@/lib/utils';
import { ClientContexts } from './ClientContexts';
import { RocketLoaderDetector } from './RocketLoaderDetector';
import { fetchContentData } from '../fetch';
import { fetchSpaceData } from '../fetch';
export const runtime = 'edge';
@@ -35,14 +35,13 @@ export default async function ContentLayout(props: { children: React.ReactNode }
contentTarget,
customization,
pages,
site,
parent,
spaces,
ancestors,
scripts,
sections,
} = await fetchContentData();
} = await fetchSpaceData();
ReactDOM.preconnect(api().client.endpoint);
ReactDOM.preconnect(api().endpoint);
if (assetsDomain) {
ReactDOM.preconnect(assetsDomain);
}
@@ -65,9 +64,8 @@ export default async function ContentLayout(props: { children: React.ReactNode }
<SpaceLayout
space={space}
contentTarget={contentTarget}
site={site}
parent={parent}
spaces={spaces}
sections={sections}
customization={customization}
pages={pages}
ancestors={ancestors}
@@ -99,7 +97,7 @@ export default async function ContentLayout(props: { children: React.ReactNode }
}
export async function generateViewport(): Promise<Viewport> {
const { customization } = await fetchContentData();
const { customization } = await fetchSpaceData();
return {
colorScheme: customization.themes.toggeable
? customization.themes.default === CustomizationThemeMode.Dark
@@ -110,11 +108,11 @@ export async function generateViewport(): Promise<Viewport> {
}
export async function generateMetadata(): Promise<Metadata> {
const { space, site, customization } = await fetchContentData();
const { space, parent, customization } = await fetchSpaceData();
const customIcon = 'icon' in customization.favicon ? customization.favicon.icon : null;
return {
title: getContentTitle(space, customization, site),
title: getContentTitle(space, customization, parent),
generator: `GitBook (${buildVersion()})`,
metadataBase: new URL(baseUrl()),
icons: {
@@ -135,7 +133,7 @@ export async function generateMetadata(): Promise<Metadata> {
},
],
},
robots: isSpaceIndexable({ space, site }) ? 'index, follow' : 'noindex, nofollow',
robots: isSpaceIndexable({ space, parent }) ? 'index, follow' : 'noindex, nofollow',
};
}
@@ -1,10 +1,12 @@
import { ContentVisibility } from '@gitbook/api';
import { NextRequest } from 'next/server';
import { getSpace, getSite } from '@/lib/api';
import { getCollection, getSite, getSpace } from '@/lib/api';
import { absoluteHref } from '@/lib/links';
import { getSiteContentPointer } from '@/lib/pointer';
import { isSpaceIndexable } from '@/lib/seo';
import { getSiteContentPointer } from '../../fetch';
export const runtime = 'edge';
/**
@@ -12,15 +14,13 @@ export const runtime = 'edge';
*/
export async function GET(req: NextRequest) {
const pointer = getSiteContentPointer();
const [site, space] = await Promise.all([
getSite(pointer.organizationId, pointer.siteId),
getSpace(pointer.spaceId, pointer.siteShareKey),
]);
const space = await getSpace(pointer.spaceId, pointer.siteShareKey);
const parent = await getSite(pointer.organizationId, pointer.siteId);
const lines = [
`User-agent: *`,
'Disallow: /~gitbook/',
...(isSpaceIndexable({ space, site })
...(isSpaceIndexable({ space, parent })
? [`Allow: /`, `Sitemap: ${absoluteHref(`/sitemap.xml`, true)}`]
: [`Disallow: /`]),
];
@@ -5,17 +5,21 @@ import { NextRequest } from 'next/server';
import { getSpaceContentData } from '@/lib/api';
import { absoluteHref } from '@/lib/links';
import { getPagePath } from '@/lib/pages';
import { getSiteContentPointer } from '@/lib/pointer';
import { isPageIndexable } from '@/lib/seo';
import { getSiteContentPointer } from '../../fetch';
export const runtime = 'edge';
/**
* Generate a sitemap.xml for the current space.
*/
export async function GET(req: NextRequest) {
const pointer = getSiteContentPointer();
const { pages: rootPages } = await getSpaceContentData(pointer, pointer.siteShareKey);
const pointer = getContentPointer();
const { pages: rootPages } = await getSpaceContentData(
pointer,
'siteId' in pointer ? pointer.siteShareKey : undefined,
);
const pages = flattenPages(rootPages, (page) => !page.hidden && isPageIndexable([], page));
const urls = pages.map(({ page, depth }) => {
@@ -84,12 +88,10 @@ function flattenPages(
return [
...(page.type === 'document' ? [{ page, depth }] : []),
...page.pages.flatMap((child) =>
child.type === 'document' ? flattenPage(child, depth + 1) : [],
child.type === 'link' ? [] : flattenPage(child, depth + 1),
),
];
};
return rootPags.flatMap((page) =>
page.type === 'group' || page.type === 'document' ? flattenPage(page, 0) : [],
);
return rootPags.flatMap((page) => (page.type === 'link' ? [] : flattenPage(page, 0)));
}
@@ -1,11 +1,18 @@
import { ContentVisibility } from '@gitbook/api';
import { notFound } from 'next/navigation';
import { ImageResponse } from 'next/og';
import { NextRequest } from 'next/server';
import React from 'react';
import { getSite, getSiteData, getSpace } from '@/lib/api';
import { getSiteContentPointer } from '@/app/(space)/fetch';
import {
getCollection,
getCurrentSiteCustomization,
getSite,
getSpace,
getSpaceCustomization,
} from '@/lib/api';
import { getEmojiForCode } from '@/lib/emojis';
import { getSiteContentPointer } from '@/lib/pointer';
import { tcls } from '@/lib/tailwind';
import { getContentTitle } from '@/lib/utils';
@@ -38,12 +45,13 @@ export async function GET(req: NextRequest) {
const pointer = getSiteContentPointer();
const spaceId = pointer.spaceId;
const [space, { customization }] = await Promise.all([
const [space, customization] = await Promise.all([
getSpace(spaceId, pointer.siteShareKey),
getSiteData(pointer),
getCurrentSiteCustomization(pointer),
]);
const site = await getSite(pointer.organizationId, pointer.siteId);
const contentTitle = getContentTitle(space, customization, site);
const parent = await getSite(pointer.organizationId, pointer.siteId);
const contentTitle = getContentTitle(space, customization, parent);
return new ImageResponse(
(
@@ -0,0 +1,53 @@
import { redirect } from 'next/navigation';
import { ImageResponse } from 'next/og';
import { NextRequest } from 'next/server';
import React from 'react';
import { getContentTitle } from '@/lib/utils';
import { PageIdParams, fetchPageData } from '../../../../fetch';
export const runtime = 'edge';
/**
* Render the OpenGraph image for a space.
*/
export async function GET(req: NextRequest, { params }: { params: PageIdParams }) {
const { space, page, customization, parent } = await fetchPageData(params);
const url = new URL(space.urls.published ?? space.urls.app);
if (customization.socialPreview.url) {
// If user configured a custom social preview, we redirect to it.
redirect(customization.socialPreview.url);
}
return new ImageResponse(
(
<div
tw="bg-gray-50 py-16 px-14"
style={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
}}
>
<h2 tw="text-7xl font-bold tracking-tight text-left">
{getContentTitle(space, customization, parent)}
</h2>
<div tw="flex flex-1">
<p tw="text-4xl">{page ? page.title : 'Not found'}</p>
</div>
<div tw="flex">
<p tw="text-4xl">
{url.hostname + (url.pathname.length > 1 ? url.pathname : '')}
</p>
</div>
</div>
),
{
width: 1200,
height: 630,
},
);
}
@@ -11,7 +11,8 @@ import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import * as React from 'react';
import { DocumentView } from '@/components/DocumentView';
import { getSiteContentPointer } from '@/app/(space)/fetch';
import { DocumentView, createHighlightingContext } from '@/components/DocumentView';
import { TrademarkLink } from '@/components/TableOfContents/Trademark';
import { PolymorphicComponentProp } from '@/components/utils/types';
import { getSpaceLanguage } from '@/intl/server';
@@ -21,7 +22,7 @@ import {
getSpace,
getSpaceCustomization,
getSpaceContentData,
getSiteData,
getCurrentSiteCustomization,
} from '@/lib/api';
import { pagePDFContainerId, PageHrefContext, absoluteHref } from '@/lib/links';
import { resolvePageId } from '@/lib/pages';
@@ -31,7 +32,6 @@ import { PDFSearchParams, getPDFSearchParams } from '@/lib/urls';
import './pdf.css';
import { PageControlButtons } from './PageControlButtons';
import { getSiteOrSpacePointerForPDF } from './pointer';
import { PrintButton } from './PrintButton';
const DEFAULT_LIMIT = 100;
@@ -39,10 +39,10 @@ const DEFAULT_LIMIT = 100;
export const runtime = 'edge';
export async function generateMetadata(): Promise<Metadata> {
const pointer = getSiteOrSpacePointerForPDF();
const [space, { customization }] = await Promise.all([
getSpace(pointer.spaceId, 'siteId' in pointer ? pointer.siteShareKey : undefined),
'siteId' in pointer ? getSiteData(pointer) : getSpaceCustomization(pointer.spaceId),
const contentPointer = getSiteContentPointer();
const [space, customization] = await Promise.all([
getSpace(contentPointer.spaceId, contentPointer.siteShareKey),
getCurrentSiteCustomization(contentPointer),
]);
return {
@@ -55,7 +55,7 @@ export async function generateMetadata(): Promise<Metadata> {
* Render a space as a standalone HTML page that can be printed as a PDF.
*/
export default async function PDFHTMLOutput(props: { searchParams: { [key: string]: string } }) {
const pointer = getSiteOrSpacePointerForPDF();
const contentPointer = getSiteContentPointer();
const searchParams = new URLSearchParams(props.searchParams);
const pdfParams = getPDFSearchParams(new URLSearchParams(searchParams));
@@ -65,9 +65,9 @@ export default async function PDFHTMLOutput(props: { searchParams: { [key: strin
currentPDFUrl += '?' + searchParams.toString();
// Load the content,
const [{ customization }, { space, contentTarget, pages: rootPages }] = await Promise.all([
'siteId' in pointer ? getSiteData(pointer) : getSpaceCustomization(pointer.spaceId),
getSpaceContentData(pointer, 'siteId' in pointer ? pointer.siteShareKey : undefined),
const [customization, { space, contentTarget, pages: rootPages }] = await Promise.all([
getCurrentSiteCustomization(contentPointer),
getSpaceContentData(contentPointer),
]);
const language = getSpaceLanguage(customization);
@@ -165,7 +165,7 @@ export default async function PDFHTMLOutput(props: { searchParams: { [key: strin
space={space}
page={page}
refContext={{
siteContext: 'siteId' in pointer ? pointer : null,
siteContext: contentPointer,
space,
revisionId: contentTarget.revisionId,
pages: rootPages,
@@ -227,6 +227,7 @@ async function PDFPageDocument(props: {
const { space, page, refContext } = props;
const document = page.documentId ? await getDocument(space.id, page.documentId) : null;
const shouldHighlightCode = createHighlightingContext();
return (
<PrintPage id={pagePDFContainerId(page)}>
@@ -249,6 +250,7 @@ async function PDFPageDocument(props: {
contentRefContext: refContext,
resolveContentRef: (ref) => resolveContentRef(ref, refContext),
getId: (id) => pagePDFContainerId(page, id),
shouldHighlightCode,
}}
/>
) : null}
@@ -309,7 +311,7 @@ function selectPages(
return [
{ page, depth },
...page.pages.flatMap((child) => {
if (child.type !== 'document') {
if (child.type === 'link') {
return [];
}
@@ -344,7 +346,7 @@ function selectPages(
}
const allPages = rootPages.flatMap((page) => {
if (page.type !== 'document' && page.type !== 'group') {
if (page.type === 'link') {
return [];
}
+222
View File
@@ -0,0 +1,222 @@
import { ContentVisibility, RevisionPage, Space } from '@gitbook/api';
import { headers } from 'next/headers';
import {
getCollectionSpaces,
getCollection,
getRevisionPageByPath,
getDocument,
ContentTarget,
SiteContentPointer,
getCurrentSiteData,
getSite,
getSiteSpaces,
getCurrentSiteCustomization,
} from '@/lib/api';
import { resolvePagePath, resolvePageId } from '@/lib/pages';
export interface PagePathParams {
pathname?: string[];
}
export interface PageIdParams {
pageId: string;
}
/**
* Get the current site content pointer from the params.
*/
export function getSiteContentPointer(): SiteContentPointer {
const headerSet = headers();
const spaceId = headerSet.get('x-gitbook-content-space');
if (!spaceId) {
throw new Error(
'getSiteContentPointer is called outside the scope of a request processed by the middleware',
);
}
const siteId = headerSet.get('x-gitbook-content-site');
if (!siteId) {
throw new Error('Expected site content headers');
}
const organizationId = headerSet.get('x-gitbook-content-organization');
const siteSpaceId = headerSet.get('x-gitbook-content-site-space');
const siteShareKey = headerSet.get('x-gitbook-content-site-share-key');
if (!organizationId) {
throw new Error('Missing site content headers');
}
const siteContent: SiteContentPointer = {
siteId,
spaceId,
siteSpaceId: siteSpaceId ?? undefined,
siteShareKey: siteShareKey ?? undefined,
organizationId,
revisionId: headerSet.get('x-gitbook-content-revision') ?? undefined,
changeRequestId: headerSet.get('x-gitbook-content-changerequest') ?? undefined,
};
return siteContent;
}
/**
* Fetch all the data needed to render the space layout.
*/
export async function fetchSpaceData() {
const content = getSiteContentPointer();
const siteShareKey = content.siteShareKey;
const [{ space, contentTarget, pages, customization, scripts }, parentSite] = await Promise.all(
'siteId' in content
? [
getCurrentSiteData(content),
fetchParentSite({
organizationId: content.organizationId,
siteId: content.siteId,
siteShareKey,
}),
]
: [getSpaceData(content, siteShareKey)],
);
const parent = await (parentSite ?? fetchParentCollection(space));
// we grab the space attached to the parent as it contains overriden customizations
const spaceRelativeToParent = parent?.spaces.find((space) => space.id === content.spaceId);
return {
content,
contentTarget,
space: spaceRelativeToParent ?? space,
pages,
customization,
scripts,
ancestors: [],
...parent,
};
}
/**
* Fetch all the data needed to render the content.
* Optimized to fetch in parallel as much as possible.
*/
export async function fetchPageData(params: PagePathParams | PageIdParams) {
const content = getSiteContentPointer();
const { space, contentTarget, pages, customization, scripts } =
await getCurrentSiteData(content);
const page = await resolvePage(contentTarget, pages, params);
const [parent, document] = await Promise.all([
fetchParentSite({
organizationId: content.organizationId,
siteId: content.siteId,
siteShareKey: content.siteShareKey,
}),
page?.page.documentId ? getDocument(space.id, page.page.documentId) : null,
]);
return {
content,
contentTarget,
space,
pages,
customization,
scripts,
ancestors: [],
...page,
...parent,
document,
};
}
/**
* Resolve a page from the params.
* If the path can't be found, we try to resolve it from the API to handle redirects.
*/
async function resolvePage(
contentTarget: ContentTarget,
pages: RevisionPage[],
params: PagePathParams | PageIdParams,
) {
if ('pageId' in params) {
return resolvePageId(pages, params.pageId);
}
const rawPathname = getPathnameParam(params);
const pathname = normalizePathname(rawPathname);
// When resolving a page, we use the lowercased pathname
const page = resolvePagePath(pages, pathname);
if (page) {
return page;
}
// If page can't be found, we try with the API, in case we have a redirect
// We use the raw pathname to handle special/malformed redirects setup by users in the GitSync.
// The page rendering will take care of redirecting to a normalized pathname.
//
// We don't test path that are too long as GitBook doesn't support them and will return a 404 anyway.
if (rawPathname.length <= 512) {
const resolved = await getRevisionPageByPath(
contentTarget.spaceId,
contentTarget.revisionId,
rawPathname,
);
if (resolved) {
return resolvePageId(pages, resolved.id);
}
}
return undefined;
}
async function fetchParentSite(args: {
organizationId: string;
siteId: string;
siteShareKey: string | undefined;
}) {
const { organizationId, siteId, siteShareKey } = args;
const [site, siteSpaces, siteParentCustomizations] = await Promise.all([
getSite(organizationId, siteId),
getSiteSpaces({ organizationId, siteId, siteShareKey }),
getCurrentSiteCustomization({ organizationId, siteId, siteSpaceId: undefined }),
]);
const spaces: Record<string, Space> = {};
siteSpaces.forEach((siteSpace) => {
spaces[siteSpace.space.id] = {
...siteSpace.space,
title: siteSpace.title ?? siteSpace.space.title,
urls: {
...siteSpace.space.urls,
published: siteSpace.urls.published,
},
};
});
// override the title with the customization title
const parent = {
...site,
...(siteParentCustomizations?.title ? { title: siteParentCustomizations.title } : {}),
};
return {
parent,
spaces: Object.values(spaces),
};
}
/**
* Get the page path from the params.
*/
export function getPathnameParam(params: PagePathParams): string {
const { pathname } = params;
return pathname ? pathname.map((part) => decodeURIComponent(part)).join('/') : '';
}
/**
* Normalize the URL pathname into the format used in the revision page path.
*/
export function normalizePathname(pathname: string) {
return pathname.toLowerCase();
}
@@ -7,19 +7,27 @@
--scrollbar-width: calc(100vw - 100%);
--dark-base: 20 20 20;
--light-base: 251 251 251;
--light-base: 251 252 252;
--light-1: color-mix(in srgb, rgb(var(--tint-color-600)), rgb(var(--light-base)) 99%);
--light-DEFAULT: color-mix(in srgb, rgb(var(--tint-color-700)), rgb(var(--light-base)) 96%);
--light-2: color-mix(in srgb, rgb(var(--tint-color-700)), rgb(var(--light-base)) 94%);
--light-3: color-mix(in srgb, rgb(var(--tint-color-800)), rgb(var(--light-base)) 88%);
--light-4: color-mix(in srgb, rgb(var(--tint-color-800)), rgb(var(--light-base)) 72%);
--light-1: color-mix(in srgb, rgb(var(--primary-base-600)), rgb(var(--light-base)) 99%);
--light-DEFAULT: color-mix(
in srgb,
rgb(var(--primary-base-700)),
rgb(var(--light-base)) 96%
);
--light-2: color-mix(in srgb, rgb(var(--primary-base-800)), rgb(var(--light-base)) 92%);
--light-3: color-mix(in srgb, rgb(var(--primary-base-800)), rgb(var(--light-base)) 88%);
--light-4: color-mix(in srgb, rgb(var(--primary-base-800)), rgb(var(--light-base)) 72%);
--dark-1: color-mix(in srgb, rgb(var(--tint-color-400)), rgb(var(--dark-base)) 96%);
--dark-DEFAULT: color-mix(in srgb, rgb(var(--tint-color-300)), rgb(var(--dark-base)) 94%);
--dark-2: color-mix(in srgb, rgb(var(--tint-color-300)), rgb(var(--dark-base)) 92%);
--dark-3: color-mix(in srgb, rgb(var(--tint-color-200)), rgb(var(--dark-base)) 91%);
--dark-4: color-mix(in srgb, rgb(var(--tint-color-200)), rgb(var(--dark-base)) 64%);
--dark-1: color-mix(in srgb, rgb(var(--primary-base-100)), rgb(1 1 1) 96%);
--dark-DEFAULT: color-mix(
in srgb,
rgb(var(--primary-base-300)),
rgb(var(--dark-base)) 94.5%
);
--dark-2: color-mix(in srgb, rgb(var(--primary-base-200)), rgb(var(--dark-base)) 92%);
--dark-3: color-mix(in srgb, rgb(var(--primary-base-200)), rgb(25 25 25) 91%);
--dark-4: color-mix(in srgb, rgb(var(--primary-base-200)), rgb(var(--dark-base)) 64%);
@apply leading-relaxed;
}
body {
@@ -28,13 +36,29 @@
html {
@apply gutter-stable;
}
html:is(.no-tint) {
html:is(.plain-background) {
--dark-base: 22 22 22;
--light-base: 255 255 255;
/* reset primaries as greys */
--primary-base-50: 239 239 239;
--primary-base-100: 222 222 222;
--primary-base-200: 200 200 200;
--primary-base-300: 180 180 180;
--primary-base-400: 150 150 150;
--primary-base-500: 120 120 120;
--primary-base-600: 90 90 90;
--primary-base-700: 70 70 70;
--primary-base-800: 30 30 30;
--primary-base-900: 19 19 19;
--light-DEFAULT: color-mix(
in srgb,
rgb(var(--tint-color-500)),
rgb(var(--primary-base-700)),
rgb(var(--light-base)) 100%
);
--light-2: color-mix(in srgb, rgb(var(--primary-base-800)), rgb(var(--light-base)) 96%);
--light-3: color-mix(in srgb, rgb(var(--primary-base-800)), rgb(var(--light-base)) 90%);
--light-4: color-mix(in srgb, rgb(var(--primary-base-800)), rgb(var(--light-base)) 76%);
}
h1 {
@apply tracking-[-0.025em] text-dark dark:text-light text-balance;
@@ -102,6 +126,23 @@
@apply [&:is(h1,h2,h3,h4)>div:first-child]:[grid-area:1/2];
@apply [&:is(h1,h2,h3,h4)>div:first-child]:ml-1;
}
/* bullet icons */
.bullet-dash {
@apply [mask-image:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNyIgdmlld0JveD0iMCAwIDEwIDciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjEiIHk9IjIuNSIgd2lkdGg9IjgiIGhlaWdodD0iMS41IiByeD0iMC42NiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==")];
}
.bullet-circleFilled {
@apply [mask-image:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNyIgdmlld0JveD0iMCAwIDEwIDciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjIiIHdpZHRoPSI2IiBoZWlnaHQ9IjYiIHJ4PSIzIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K")];
}
.bullet-circle {
@apply [mask-image:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNyIgdmlld0JveD0iMCAwIDEwIDciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjIuNzUiIHk9IjAuNzUiIHdpZHRoPSI1LjUiIGhlaWdodD0iNS41IiByeD0iMi43NSIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIxLjUiLz4KPC9zdmc+Cg==")];
}
.bullet-squareFilled {
@apply [mask-image:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNyIgdmlld0JveD0iMCAwIDEwIDciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjIiIHdpZHRoPSI2IiBoZWlnaHQ9IjYiIHJ4PSIxIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K")];
}
.bullet-square {
@apply [mask-image:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNyIgdmlld0JveD0iMCAwIDEwIDciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxtYXNrIGlkPSJwYXRoLTEtaW5zaWRlLTFfOV80OCIgZmlsbD0id2hpdGUiPgo8cmVjdCB4PSIyIiB3aWR0aD0iNyIgaGVpZ2h0PSI3IiByeD0iMSIvPgo8L21hc2s+CjxyZWN0IHg9IjIiIHdpZHRoPSI3IiBoZWlnaHQ9IjciIHJ4PSIxIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjMiIG1hc2s9InVybCgjcGF0aC0xLWluc2lkZS0xXzlfNDgpIi8+Cjwvc3ZnPgo=")];
}
}
@layer utilities {
@@ -192,30 +233,3 @@ html {
html.dark {
color-scheme: dark light;
}
/* Emojis */
[class*='emoji'] {
font-family: var(--font-emojis-sbix);
}
/* Safari Only CSS here */
_::-webkit-full-page-media,
_:future,
:root [class*='emoji'] {
font-family: var(--font-emojis-sbix);
}
/* Chrome Only CSS here */
@media screen and (-webkit-min-device-pixel-ratio: 0) {
.emoji {
font-family: var(--font-emojis-colrv1);
}
}
/* Firefox Only CSS here */
@-moz-document url-prefix() {
.emoji {
font-family: var(--font-emojis-colrv1);
}
}
@@ -1,9 +1,9 @@
import {
CustomizationBackground,
CustomizationCorners,
CustomizationHeaderPreset,
CustomizationIconsStyle,
CustomizationHeaderPreset,
CustomizationSettings,
CustomizationTint,
SiteCustomizationSettings,
} from '@gitbook/api';
import { IconsProvider, IconStyle } from '@gitbook/icons';
@@ -11,48 +11,52 @@ import assertNever from 'assert-never';
import colorContrast from 'postcss-color-contrast/js';
import colors from 'tailwindcss/colors';
import { emojiFontClassName } from '@/components/primitives';
import { fonts, ibmPlexMono } from '@/fonts';
import { getSpaceLanguage } from '@/intl/server';
import { getCurrentSiteLayoutData } from '@/lib/api';
import { getStaticFileURL } from '@/lib/assets';
import { hexToRgb, shadesOfColor } from '@/lib/colors';
import { tcls } from '@/lib/tailwind';
import { emojiFontClassName } from '../primitives';
import { ClientContexts } from './ClientContexts';
import '@gitbook/icons/style.css';
import './globals.css';
const DEFAULT_TINT_COLOR = '#787878';
import '@gitbook/icons/style.css';
import { getSiteContentPointer } from './fetch';
/**
* Layout shared between the content and the PDF renderer.
* It takes care of setting the theme and the language.
*/
export async function CustomizationRootLayout(props: {
customization: SiteCustomizationSettings | CustomizationSettings;
children: React.ReactNode;
}) {
const { customization, children } = props;
export default async function SpaceRootLayout(props: { children: React.ReactNode }) {
const { children } = props;
const pointer = getSiteContentPointer();
const { customization } = await getCurrentSiteLayoutData(pointer);
const headerTheme = generateHeaderTheme(customization);
const language = getSpaceLanguage(customization);
const tintColor = getTintColor(customization);
return (
<html
suppressHydrationWarning
lang={customization.internationalization.locale}
className={tcls(
customization.header.preset === CustomizationHeaderPreset.None
? null
: 'scroll-pt-[76px]', // Take the sticky header in consideration for the scrolling
customization.styling.corners === CustomizationCorners.Straight
className={
tcls(
customization.header.preset === CustomizationHeaderPreset.None
? null
: [
// Take the sticky header in consideration for the scrolling
`scroll-pt-[76px]`,
],
) +
(customization.styling.corners === CustomizationCorners.Straight
? ' straight-corners'
: '',
tintColor ? ' tint' : 'no-tint',
)}
: '') +
(customization.styling.background === CustomizationBackground.Plain
? ' plain-background'
: '')
}
>
<head>
{customization.privacyPolicy.url ? (
@@ -83,23 +87,11 @@ export async function CustomizationRootLayout(props: {
),
)
}
${generateColorVariable('tint-color', tintColor?.light ?? DEFAULT_TINT_COLOR)}
${
// Generate the right contrast color for each shade of tint-color
generateColorVariable(
'contrast-tint',
Object.fromEntries(
Object.entries(
shadesOfColor(tintColor?.light || DEFAULT_TINT_COLOR),
).map(([index, color]) => [
index,
colorContrast(color, ['#000', '#fff']),
]),
),
)
}
${generateColorVariable(
'primary-base',
customization.styling.primaryColor.light,
)}
${generateColorVariable(
'header-background',
headerTheme.backgroundColor.light,
@@ -112,6 +104,10 @@ export async function CustomizationRootLayout(props: {
'primary-color',
customization.styling.primaryColor.dark,
)}
${generateColorVariable(
'primary-base',
customization.styling.primaryColor.dark,
)}
${
// Generate the right contrast color for each shade of primary-color
generateColorVariable(
@@ -126,23 +122,6 @@ export async function CustomizationRootLayout(props: {
),
)
}
${generateColorVariable('tint-color', tintColor?.dark ?? DEFAULT_TINT_COLOR)}
${
// Generate the right contrast color for each shade of tint-color
generateColorVariable(
'contrast-tint',
Object.fromEntries(
Object.entries(
shadesOfColor(tintColor?.dark || DEFAULT_TINT_COLOR),
).map(([index, color]) => [
index,
colorContrast(color, ['#000', '#fff']),
]),
),
)
}
${generateColorVariable(
'header-background',
headerTheme.backgroundColor.dark,
@@ -182,21 +161,6 @@ export async function CustomizationRootLayout(props: {
);
}
/**
* Get the tint color from the customization settings.
* If the tint color is not set or it is a space customization, it will return the default color.
*/
export function getTintColor(
customization: CustomizationSettings | SiteCustomizationSettings,
): CustomizationTint['color'] | undefined {
if ('tint' in customization.styling && customization.styling.tint) {
return {
light: customization.styling.tint?.color.light ?? DEFAULT_TINT_COLOR,
dark: customization.styling.tint?.color.dark ?? DEFAULT_TINT_COLOR,
};
}
}
type ColorInput = string | Record<string, string>;
function generateColorVariable(name: string, color: ColorInput) {
const shades: Record<string, string> = typeof color === 'string' ? shadesOfColor(color) : color;
@@ -214,8 +178,6 @@ function generateHeaderTheme(customization: CustomizationSettings | SiteCustomiz
backgroundColor: { light: ColorInput; dark: ColorInput };
linkColor: { light: ColorInput; dark: ColorInput };
} {
const tintColor = getTintColor(customization);
switch (customization.header.preset) {
case CustomizationHeaderPreset.None:
case CustomizationHeaderPreset.Default: {
@@ -233,20 +195,13 @@ function generateHeaderTheme(customization: CustomizationSettings | SiteCustomiz
case CustomizationHeaderPreset.Bold: {
return {
backgroundColor: {
light: tintColor?.light ?? customization.styling.primaryColor.light,
dark: tintColor?.dark ?? customization.styling.primaryColor.dark,
light: customization.styling.primaryColor.light,
dark: customization.styling.primaryColor.dark,
},
linkColor: {
light: colorContrast(
tintColor?.light ?? customization.styling.primaryColor.light,
[colors.white, colors.black],
'aaa',
),
dark: colorContrast(
tintColor?.dark ?? customization.styling.primaryColor.dark,
[colors.white, colors.black],
'aaa',
),
// TODO: should depend on the color of the background
light: colors.white,
dark: colors.black,
},
};
}
@@ -265,25 +220,15 @@ function generateHeaderTheme(customization: CustomizationSettings | SiteCustomiz
case CustomizationHeaderPreset.Custom: {
return {
backgroundColor: {
light:
customization.header.backgroundColor?.light ??
tintColor?.light ??
colors.white,
dark:
customization.header.backgroundColor?.dark ??
tintColor?.dark ??
colors.black,
light: customization.header.backgroundColor?.light ?? colors.white,
dark: customization.header.backgroundColor?.dark ?? colors.black,
},
linkColor: {
light:
customization.header.linkColor?.light ??
(tintColor?.light &&
colorContrast(tintColor.light, [colors.white, colors.black], 'aaa')) ??
customization.styling.primaryColor.light,
dark:
customization.header.linkColor?.dark ??
(tintColor?.dark &&
colorContrast(tintColor.dark, [colors.white, colors.black], 'aaa')) ??
customization.styling.primaryColor.dark,
},
};
@@ -1,38 +0,0 @@
import { SpaceIntegrationScript } from '@gitbook/api';
import { CustomizationRootLayout } from '@/components/RootLayout';
import { getSiteData, getSpaceCustomization } from '@/lib/api';
import { getSiteOrSpacePointerForPDF } from './pointer';
/**
* Layout to be used for the site root. It fetches the customization data for the
* site or space and initializes the CustomizationRootLayout with it.
*/
export default async function PDFRootLayout(props: { children: React.ReactNode }) {
const { children } = props;
const pointer = getSiteOrSpacePointerForPDF();
const { customization } = await ('siteId' in pointer
? getSiteData(pointer)
: getSpaceLayoutData(pointer.spaceId));
return (
<CustomizationRootLayout customization={customization}>{children}</CustomizationRootLayout>
);
}
/**
* Fetch all the layout data about a space at once.
*/
async function getSpaceLayoutData(spaceId: string) {
const [{ customization }, scripts] = await Promise.all([
getSpaceCustomization(spaceId),
[] as SpaceIntegrationScript[],
]);
return {
customization,
scripts,
};
}
@@ -1,17 +0,0 @@
import { SiteContentPointer, SpaceContentPointer } from '@/lib/api';
import { getSiteContentPointer, getSpacePointer } from '@/lib/pointer';
/**
* PDF generation can be done at the site level (e.g. docs.foo.com/~gitbook/pdf) or
* at the space level (e.g. open.gitbook.com/~space/:spaceId/~gitbook/pdf) which is
* for PDF export of in-app private spaces.
*
* This function returns the pointer depending on the context.
*/
export function getSiteOrSpacePointerForPDF(): SiteContentPointer | SpaceContentPointer {
try {
return getSiteContentPointer();
} catch (error) {
return getSpacePointer();
}
}
@@ -1,18 +0,0 @@
// @ts-ignore
import nextOnPagesHandler from '@cloudflare/next-on-pages/fetch-handler';
import { withMiddlewareHeadersStorage } from './lib/middleware';
/**
* We use a custom entrypoint until we can move to opennext (https://github.com/opennextjs/opennextjs-cloudflare/issues/92).
* There is a bug in next-on-pages where headers can't be set on the response in the middleware for RSC requests (https://github.com/cloudflare/next-on-pages/issues/897).
*/
export default {
async fetch(request, env, ctx) {
const response = await withMiddlewareHeadersStorage(() =>
nextOnPagesHandler.fetch(request, env, ctx),
);
return response;
},
} as ExportedHandler<{ ASSETS: Fetcher }>;
@@ -2,7 +2,7 @@ import { Space } from '@gitbook/api';
import { Icon } from '@gitbook/icons';
import React from 'react';
import { getChangeRequest, getRevision, SiteContentPointer } from '@/lib/api';
import { SiteContentPointer, getChangeRequest, getRevision } from '@/lib/api';
import { tcls } from '@/lib/tailwind';
import { RefreshChangeRequestButton } from './RefreshChangeRequestButton';

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