mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-15 07:05:18 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d6b9fe6643 | |||
| 766c97a100 | |||
| 29fca1e835 | |||
| 48b5c0707b | |||
| 434cfd7a08 | |||
| e3587d59aa |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Avoid multiple <h1> in the page by using a <div> for the title in the header
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Adds content-visibility css property to OpenAPI Operation for better render performance
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/cache-do': minor
|
||||
---
|
||||
|
||||
Experiment with optimizing billable duration in Cloudflare by using multiple RPC sessions instead of one
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Fix scroll position when navigating pages on mobile
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': minor
|
||||
---
|
||||
|
||||
Add Python support in Code Samples
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
RND-3532: drop down menu for hidden links at small screen size
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Change dark mode shadow for multi-space search toolbar
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Use ellipsis and fix icon color for more links in the header on small screen
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Improve consistency of change request preview by removing cache-control on response
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/icons': patch
|
||||
---
|
||||
|
||||
Update version for static assets to fix invalid caching
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Revalidate change request cached content when pressing refresh button
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/icons': patch
|
||||
---
|
||||
|
||||
Fix first run of the package in development mode
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Improve performances of loading pages with embeds by caching them
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
bump: scalar from 1.0.5 to 1.0.7
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Replace all icons, previously imported from Geist, by new package `@gitbook/icons`
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/cache-do': minor
|
||||
---
|
||||
|
||||
First version of a new cache backend powered by Cloudflare Durable Objects
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Add support for Norwegian language
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Optimize layout shift while transitioning between pages with full width blocks (ex: OpenAPI blocks)
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Add support for page icons
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Fix GitBook specific static assets not being served correctly when deployed on Cloudflare
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Only display spaces dropdown in compact header when site is multi-variants
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/cache-do': patch
|
||||
---
|
||||
|
||||
Enable Workers observability with a sampling of 0.1
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/icons': minor
|
||||
---
|
||||
|
||||
Initial release for `@gitbook/icons`, a package of content icons to be used for pages, documents, etc.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Automatically scroll to active item in TOC
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Persist state of tabs and dynamically sync them based on title
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Ensure "Sponsored via GitBook" can be translated in all languages
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Show the caption for file blocks
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Use url hash to open Expandable and scroll to anchor
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Fix margin for paragraphs in quote blocks
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Use new cache backend, powered by Durable Objects, alongside the existing ones (KV, etc).
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Include offset in calculations of whether scrollable element is in view
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': minor
|
||||
---
|
||||
|
||||
Add required query parameters to the code sample
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': minor
|
||||
---
|
||||
|
||||
Display pattern if available in parmas in OpenAPI block
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Fix inconsistent click area in table because of scroll indicator
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Stop using KV cache backend for now, but also improves it for higher performances
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@gitbook/react-openapi': minor
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Synchronize response and response example tabs
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Show definition title when visible in cards
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@gitbook/react-openapi': minor
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Rollback of scalar modal while fixing perf issue
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Optimize images rendered in community ads
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Update design of Checkbox to be more consistent and readable
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-contentkit': patch
|
||||
---
|
||||
|
||||
Fix rendering of webframe with SSR causing wrong communication between frame and renderer
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Add optional env `GITBOOK_INTEGRATIONS_HOST` to configure the host serving the integrations
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Fix crash when `example` is undefined for a response
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Improve the toolbar for change-requests and revisions to show more actions
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Add support for icons style customization for sites
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Adds tabs to Response Example section e.g. for status code examples
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Render mandatory headers in code sample
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Update Scalar client to latest version
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Prevent search indexation for pages where it's configured as disabled
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Fix table of contents being visible on mobile when disabled at the page level
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Fix accessibility of the table of contents by using `aria-current` instead of `aria-selected`
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Update rendering of community ads to match new API response, and make it possible to preview ads.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-math': minor
|
||||
---
|
||||
|
||||
Export binary `gitbook-math` to copy assets to a directory
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Fix "ip" being passed to BSA for community ads
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Changed the alternative URL resolution criteria in order to support site URLs without /v/ prefix
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/icons': minor
|
||||
---
|
||||
|
||||
Fix bin gitbook-icons when installed without the Pro token
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Don't show hidden pages in the empty state of a page
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Fix linking to a tab itself
|
||||
@@ -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
@@ -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
|
||||
|
||||
+1
-2
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -32,5 +32,4 @@ screenshots/
|
||||
/playwright/.cache/
|
||||
|
||||
# Generated public files
|
||||
/public/~gitbook/static/*
|
||||
!/public/~gitbook/static/images
|
||||
/public/~gitbook/static/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
+2
-2
@@ -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;
|
||||
+4
-3
@@ -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);
|
||||
|
||||
+21
-29
@@ -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',
|
||||
};
|
||||
+9
-11
@@ -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',
|
||||
};
|
||||
}
|
||||
|
||||
+7
-7
@@ -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: /`]),
|
||||
];
|
||||
+9
-7
@@ -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)));
|
||||
}
|
||||
+14
-6
@@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
+16
-14
@@ -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 [];
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
+54
-40
@@ -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);
|
||||
}
|
||||
}
|
||||
+42
-97
@@ -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
Reference in New Issue
Block a user