mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-13 06:09:21 +00:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3fe3809416 | |||
| 363d337a72 | |||
| f89b31c940 | |||
| 630c3e84dd | |||
| d43202f137 | |||
| 0964043489 | |||
| e9b31a545d | |||
| 7f28a88374 | |||
| 07ea45bf84 | |||
| a86d35f721 | |||
| d6c28a0ff6 | |||
| a7af3ca7bc | |||
| cda08a9843 | |||
| 7588774237 | |||
| 683373b263 | |||
| 673ec8fb5d | |||
| 63bacaa671 | |||
| e3a3d6a505 | |||
| 175171dda1 | |||
| a4b63b88c8 | |||
| c3675fdfe7 | |||
| 2fa08519b6 | |||
| 065627060b | |||
| 2982b6a808 | |||
| c3f56ecd40 | |||
| fe9e6c1afd | |||
| 73394ae929 | |||
| c754fc9ff4 | |||
| 1ca6208057 | |||
| cdfae4d4df | |||
| c3da5469ca | |||
| 79f2d8ba76 | |||
| 7b1bdb92a6 | |||
| f9a075eed1 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Avoid multiple <h1> in the page by using a <div> for the title in the header
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Adds content-visibility css property to OpenAPI Operation for better render performance
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@gitbook/cache-do': minor
|
||||
---
|
||||
|
||||
Experiment with optimizing billable duration in Cloudflare by using multiple RPC sessions instead of one
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Fix scroll position when navigating pages on mobile
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@gitbook/react-openapi': minor
|
||||
---
|
||||
|
||||
Add Python support in Code Samples
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
RND-3532: drop down menu for hidden links at small screen size
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Change dark mode shadow for multi-space search toolbar
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Use ellipsis and fix icon color for more links in the header on small screen
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Improve consistency of change request preview by removing cache-control on response
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@gitbook/icons': patch
|
||||
---
|
||||
|
||||
Update version for static assets to fix invalid caching
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Revalidate change request cached content when pressing refresh button
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@gitbook/icons': patch
|
||||
---
|
||||
|
||||
Fix first run of the package in development mode
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Improve performances of loading pages with embeds by caching them
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
bump: scalar from 1.0.5 to 1.0.7
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Replace all icons, previously imported from Geist, by new package `@gitbook/icons`
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@gitbook/cache-do': minor
|
||||
---
|
||||
|
||||
First version of a new cache backend powered by Cloudflare Durable Objects
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Add support for Norwegian language
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Optimize layout shift while transitioning between pages with full width blocks (ex: OpenAPI blocks)
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Add support for page icons
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Fix GitBook specific static assets not being served correctly when deployed on Cloudflare
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Only display spaces dropdown in compact header when site is multi-variants
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@gitbook/cache-do': patch
|
||||
---
|
||||
|
||||
Enable Workers observability with a sampling of 0.1
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@gitbook/icons': minor
|
||||
---
|
||||
|
||||
Initial release for `@gitbook/icons`, a package of content icons to be used for pages, documents, etc.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Automatically scroll to active item in TOC
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Persist state of tabs and dynamically sync them based on title
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Ensure "Sponsored via GitBook" can be translated in all languages
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Show the caption for file blocks
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Use url hash to open Expandable and scroll to anchor
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Fix margin for paragraphs in quote blocks
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Use new cache backend, powered by Durable Objects, alongside the existing ones (KV, etc).
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Include offset in calculations of whether scrollable element is in view
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@gitbook/react-openapi': minor
|
||||
---
|
||||
|
||||
Add required query parameters to the code sample
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@gitbook/react-openapi': minor
|
||||
---
|
||||
|
||||
Display pattern if available in parmas in OpenAPI block
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Fix inconsistent click area in table because of scroll indicator
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Stop using KV cache backend for now, but also improves it for higher performances
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
'@gitbook/react-openapi': minor
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Synchronize response and response example tabs
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Show definition title when visible in cards
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
'@gitbook/react-openapi': minor
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Rollback of scalar modal while fixing perf issue
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Optimize images rendered in community ads
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Update design of Checkbox to be more consistent and readable
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@gitbook/react-contentkit': patch
|
||||
---
|
||||
|
||||
Fix rendering of webframe with SSR causing wrong communication between frame and renderer
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Disable KV cache for docs.gitbook.com as a test, also disable it for change-request to improve consistency
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Add optional env `GITBOOK_INTEGRATIONS_HOST` to configure the host serving the integrations
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Fix crash when `example` is undefined for a response
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Improve the toolbar for change-requests and revisions to show more actions
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Add support for icons style customization for sites
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Adds tabs to Response Example section e.g. for status code examples
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Render mandatory headers in code sample
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Update Scalar client to latest version
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Prevent search indexation for pages where it's configured as disabled
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Fix table of contents being visible on mobile when disabled at the page level
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Fix accessibility of the table of contents by using `aria-current` instead of `aria-selected`
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Update rendering of community ads to match new API response, and make it possible to preview ads.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@gitbook/react-math': minor
|
||||
---
|
||||
|
||||
Export binary `gitbook-math` to copy assets to a directory
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Fix "ip" being passed to BSA for community ads
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Changed the alternative URL resolution criteria in order to support site URLs without /v/ prefix
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@gitbook/icons': minor
|
||||
---
|
||||
|
||||
Fix bin gitbook-icons when installed without the Pro token
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Don't show hidden pages in the empty state of a page
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Fix linking to a tab itself
|
||||
+43
-11
@@ -13,8 +13,12 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
checks: write
|
||||
statuses: write
|
||||
outputs:
|
||||
deployment_url: ${{ steps.cloudflare.outputs.url }}
|
||||
deployment_url: ${{ steps.deploy.outputs.url }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -52,27 +56,55 @@ jobs:
|
||||
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}
|
||||
SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
||||
- id: cloudflare
|
||||
name: Publish to Cloudflare Pages
|
||||
uses: cloudflare/pages-action@v1
|
||||
- id: deploy
|
||||
name: Deploy to Cloudflare
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
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 "ID: ${{ steps.cloudflare.outputs.id }}"
|
||||
echo "URL: ${{ steps.cloudflare.outputs.url }}"
|
||||
echo "Environment: ${{ steps.cloudflare.outputs.environment }}"
|
||||
echo "Alias: ${{ steps.cloudflare.outputs.alias }}"
|
||||
echo "URL: ${{ steps.deploy.outputs.url }}"
|
||||
echo "Alias URL: ${{ steps.deploy.outputs.deployment-alias-url }}"
|
||||
- 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.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.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.url }}](${{ steps.deploy.outputs.url }})
|
||||
PR: [${{ steps.deploy.outputs.alias }}](${{ steps.deploy.outputs.alias }})
|
||||
edit-mode: replace
|
||||
visual-testing:
|
||||
runs-on: ubuntu-latest
|
||||
name: Visual Testing
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
},
|
||||
"packageManager": "bun@1.1.18",
|
||||
"patchedDependencies": {
|
||||
"@vercel/next@4.3.6": "patches/@vercel%2Fnext@4.3.6.patch"
|
||||
"@vercel/next@4.3.15": "patches/@vercel%2Fnext@4.3.15.patch"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# @gitbook/cache-do
|
||||
|
||||
## 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.0.0",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@msgpack/msgpack": "^3.0.0-beta2",
|
||||
"lru_map": "^0.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.3",
|
||||
"wrangler": "^3.78.7"
|
||||
"wrangler": "3.82.0"
|
||||
},
|
||||
"scripts": {
|
||||
"generate": "wrangler types --experimental-include-runtime",
|
||||
|
||||
@@ -32,4 +32,5 @@ screenshots/
|
||||
/playwright/.cache/
|
||||
|
||||
# Generated public files
|
||||
/public/~gitbook/static/
|
||||
/public/~gitbook/static/*
|
||||
!/public/~gitbook/static/images
|
||||
|
||||
@@ -1,5 +1,100 @@
|
||||
# gitbook
|
||||
|
||||
## 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
|
||||
|
||||
@@ -395,6 +395,10 @@ const testCases: TestsCase[] = [
|
||||
await page.click('[data-testid="annotation-button"]');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Stepper',
|
||||
url: 'blocks/stepper',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -462,13 +466,11 @@ 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',
|
||||
},
|
||||
],
|
||||
@@ -495,7 +497,7 @@ const testCases: TestsCase[] = [
|
||||
tests: [
|
||||
{
|
||||
name: 'Valid link',
|
||||
url: 'TGs8PkF4GWVtbmPnWhYL/',
|
||||
url: 'thDznyWXCeEoT55WB7HC/',
|
||||
},
|
||||
{
|
||||
name: 'Invalid link',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gitbook",
|
||||
"version": "0.3.0",
|
||||
"version": "0.4.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "env-cmd --silent -f ../../.env.local next dev",
|
||||
@@ -16,7 +16,7 @@
|
||||
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gitbook/api": "0.64.1",
|
||||
"@gitbook/api": "^0.73.0",
|
||||
"@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.5",
|
||||
"next": "14.2.15",
|
||||
"next-themes": "^0.2.1",
|
||||
"nuqs": "^1.17.4",
|
||||
"object-hash": "^3.0.0",
|
||||
@@ -60,7 +60,7 @@
|
||||
"remark-rehype": "^11.1.0",
|
||||
"rison": "^0.1.1",
|
||||
"server-only": "^0.0.1",
|
||||
"shiki": "^1.11.1",
|
||||
"shiki": "^1.22.0",
|
||||
"tailwind-merge": "^2.2.0",
|
||||
"tailwind-shades": "^1.1.2",
|
||||
"unified": "^11.0.4",
|
||||
@@ -69,7 +69,7 @@
|
||||
"devDependencies": {
|
||||
"@argos-ci/playwright": "^2.0.0",
|
||||
"@playwright/test": "^1.42.1",
|
||||
"@cloudflare/next-on-pages": "^1.13.3",
|
||||
"@cloudflare/next-on-pages": "^1.13.5",
|
||||
"@cloudflare/workers-types": "^4.20240725.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/jsontoxml": "^1.0.5",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
+3
-4
@@ -1,12 +1,11 @@
|
||||
import { getSpaceLanguage, t } from '@/intl/server';
|
||||
import { getCurrentSiteLayoutData } from '@/lib/api';
|
||||
import { getSiteLayoutData } from '@/lib/api';
|
||||
import { getSiteContentPointer } from '@/lib/pointer';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { getSiteContentPointer } from '../../fetch';
|
||||
|
||||
export default async function NotFound() {
|
||||
const pointer = getSiteContentPointer();
|
||||
const { customization } = await getCurrentSiteLayoutData(pointer);
|
||||
const { customization } = await getSiteLayoutData(pointer);
|
||||
|
||||
const language = getSpaceLanguage(customization);
|
||||
|
||||
+24
-19
@@ -29,8 +29,9 @@ export default async function Page(props: {
|
||||
const {
|
||||
content: contentPointer,
|
||||
contentTarget,
|
||||
sections,
|
||||
space,
|
||||
parent,
|
||||
site,
|
||||
customization,
|
||||
pages,
|
||||
page,
|
||||
@@ -72,15 +73,32 @@ export default async function Page(props: {
|
||||
page,
|
||||
};
|
||||
|
||||
const withSections = Boolean(sections && sections.length > 0);
|
||||
const headerOffset = { sectionsHeader: withSections, topHeader: withTopHeader };
|
||||
|
||||
return (
|
||||
<>
|
||||
{withFullPageCover && page.cover ? (
|
||||
<PageCover as="full" page={page} cover={page.cover} context={contentRefContext} />
|
||||
) : null}
|
||||
<div className={tcls('flex', 'flex-row')}>
|
||||
{/* 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}
|
||||
<PageBody
|
||||
space={space}
|
||||
contentPointer={contentPointer}
|
||||
pointer={contentPointer}
|
||||
contentTarget={contentTarget}
|
||||
customization={customization}
|
||||
context={contentRefContext}
|
||||
@@ -91,19 +109,6 @@ export default async function Page(props: {
|
||||
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 />
|
||||
@@ -130,7 +135,7 @@ export async function generateMetadata({
|
||||
params: PagePathParams;
|
||||
searchParams: { fallback?: string };
|
||||
}): Promise<Metadata> {
|
||||
const { space, pages, page, customization, parent, ancestors } = await getPageDataWithFallback({
|
||||
const { space, pages, page, customization, site, ancestors } = await getPageDataWithFallback({
|
||||
pagePathParams: params,
|
||||
searchParams,
|
||||
});
|
||||
@@ -140,7 +145,7 @@ export async function generateMetadata({
|
||||
}
|
||||
|
||||
return {
|
||||
title: [page.title, getContentTitle(space, customization, parent)]
|
||||
title: [page.title, getContentTitle(space, customization, site ?? null)]
|
||||
.filter(Boolean)
|
||||
.join(' | '),
|
||||
description: page.description ?? '',
|
||||
@@ -154,7 +159,7 @@ export async function generateMetadata({
|
||||
],
|
||||
},
|
||||
robots:
|
||||
isSpaceIndexable({ space, parent }) && isPageIndexable(ancestors, page)
|
||||
isSpaceIndexable({ space, site: site ?? null }) && isPageIndexable(ancestors, page)
|
||||
? 'index, follow'
|
||||
: 'noindex, nofollow',
|
||||
};
|
||||
+10
-8
@@ -18,7 +18,7 @@ import { getContentTitle } from '@/lib/utils';
|
||||
|
||||
import { ClientContexts } from './ClientContexts';
|
||||
import { RocketLoaderDetector } from './RocketLoaderDetector';
|
||||
import { fetchSpaceData } from '../fetch';
|
||||
import { fetchContentData } from '../fetch';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
@@ -35,11 +35,12 @@ export default async function ContentLayout(props: { children: React.ReactNode }
|
||||
contentTarget,
|
||||
customization,
|
||||
pages,
|
||||
parent,
|
||||
site,
|
||||
spaces,
|
||||
ancestors,
|
||||
scripts,
|
||||
} = await fetchSpaceData();
|
||||
sections,
|
||||
} = await fetchContentData();
|
||||
|
||||
ReactDOM.preconnect(api().endpoint);
|
||||
if (assetsDomain) {
|
||||
@@ -64,8 +65,9 @@ export default async function ContentLayout(props: { children: React.ReactNode }
|
||||
<SpaceLayout
|
||||
space={space}
|
||||
contentTarget={contentTarget}
|
||||
parent={parent}
|
||||
site={site}
|
||||
spaces={spaces}
|
||||
sections={sections}
|
||||
customization={customization}
|
||||
pages={pages}
|
||||
ancestors={ancestors}
|
||||
@@ -97,7 +99,7 @@ export default async function ContentLayout(props: { children: React.ReactNode }
|
||||
}
|
||||
|
||||
export async function generateViewport(): Promise<Viewport> {
|
||||
const { customization } = await fetchSpaceData();
|
||||
const { customization } = await fetchContentData();
|
||||
return {
|
||||
colorScheme: customization.themes.toggeable
|
||||
? customization.themes.default === CustomizationThemeMode.Dark
|
||||
@@ -108,11 +110,11 @@ export async function generateViewport(): Promise<Viewport> {
|
||||
}
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const { space, parent, customization } = await fetchSpaceData();
|
||||
const { space, site, customization } = await fetchContentData();
|
||||
const customIcon = 'icon' in customization.favicon ? customization.favicon.icon : null;
|
||||
|
||||
return {
|
||||
title: getContentTitle(space, customization, parent),
|
||||
title: getContentTitle(space, customization, site),
|
||||
generator: `GitBook (${buildVersion()})`,
|
||||
metadataBase: new URL(baseUrl()),
|
||||
icons: {
|
||||
@@ -133,7 +135,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
||||
},
|
||||
],
|
||||
},
|
||||
robots: isSpaceIndexable({ space, parent }) ? 'index, follow' : 'noindex, nofollow',
|
||||
robots: isSpaceIndexable({ space, site }) ? 'index, follow' : 'noindex, nofollow',
|
||||
};
|
||||
}
|
||||
|
||||
+6
-5
@@ -3,10 +3,9 @@ import { NextRequest } from 'next/server';
|
||||
|
||||
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';
|
||||
|
||||
/**
|
||||
@@ -14,13 +13,15 @@ export const runtime = 'edge';
|
||||
*/
|
||||
export async function GET(req: NextRequest) {
|
||||
const pointer = getSiteContentPointer();
|
||||
const space = await getSpace(pointer.spaceId, pointer.siteShareKey);
|
||||
const parent = await getSite(pointer.organizationId, pointer.siteId);
|
||||
const [site, space] = await Promise.all([
|
||||
getSite(pointer.organizationId, pointer.siteId),
|
||||
getSpace(pointer.spaceId, pointer.siteShareKey),
|
||||
]);
|
||||
|
||||
const lines = [
|
||||
`User-agent: *`,
|
||||
'Disallow: /~gitbook/',
|
||||
...(isSpaceIndexable({ space, parent })
|
||||
...(isSpaceIndexable({ space, site })
|
||||
? [`Allow: /`, `Sitemap: ${absoluteHref(`/sitemap.xml`, true)}`]
|
||||
: [`Disallow: /`]),
|
||||
];
|
||||
+7
-9
@@ -5,21 +5,17 @@ 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 = getContentPointer();
|
||||
const { pages: rootPages } = await getSpaceContentData(
|
||||
pointer,
|
||||
'siteId' in pointer ? pointer.siteShareKey : undefined,
|
||||
);
|
||||
const pointer = getSiteContentPointer();
|
||||
const { pages: rootPages } = await getSpaceContentData(pointer, pointer.siteShareKey);
|
||||
|
||||
const pages = flattenPages(rootPages, (page) => !page.hidden && isPageIndexable([], page));
|
||||
const urls = pages.map(({ page, depth }) => {
|
||||
@@ -88,10 +84,12 @@ function flattenPages(
|
||||
return [
|
||||
...(page.type === 'document' ? [{ page, depth }] : []),
|
||||
...page.pages.flatMap((child) =>
|
||||
child.type === 'link' ? [] : flattenPage(child, depth + 1),
|
||||
child.type === 'document' ? flattenPage(child, depth + 1) : [],
|
||||
),
|
||||
];
|
||||
};
|
||||
|
||||
return rootPags.flatMap((page) => (page.type === 'link' ? [] : flattenPage(page, 0)));
|
||||
return rootPags.flatMap((page) =>
|
||||
page.type === 'group' || page.type === 'document' ? flattenPage(page, 0) : [],
|
||||
);
|
||||
}
|
||||
+4
-12
@@ -1,18 +1,11 @@
|
||||
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 { getSiteContentPointer } from '@/app/(space)/fetch';
|
||||
import {
|
||||
getCollection,
|
||||
getCurrentSiteCustomization,
|
||||
getSite,
|
||||
getSpace,
|
||||
getSpaceCustomization,
|
||||
} from '@/lib/api';
|
||||
import { getCurrentSiteCustomization, getSite, getSpace } from '@/lib/api';
|
||||
import { getEmojiForCode } from '@/lib/emojis';
|
||||
import { getSiteContentPointer } from '@/lib/pointer';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { getContentTitle } from '@/lib/utils';
|
||||
|
||||
@@ -49,9 +42,8 @@ export async function GET(req: NextRequest) {
|
||||
getSpace(spaceId, pointer.siteShareKey),
|
||||
getCurrentSiteCustomization(pointer),
|
||||
]);
|
||||
const parent = await getSite(pointer.organizationId, pointer.siteId);
|
||||
|
||||
const contentTitle = getContentTitle(space, customization, parent);
|
||||
const site = await getSite(pointer.organizationId, pointer.siteId);
|
||||
const contentTitle = getContentTitle(space, customization, site);
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
@@ -0,0 +1,223 @@
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
+76
-79
@@ -1,19 +1,17 @@
|
||||
import { ContentVisibility, RevisionPage, Space } from '@gitbook/api';
|
||||
import { headers } from 'next/headers';
|
||||
import { RevisionPage, SiteSection, SiteSpace, Space } from '@gitbook/api';
|
||||
import { assert } from 'ts-essentials';
|
||||
|
||||
import {
|
||||
getCollectionSpaces,
|
||||
getCollection,
|
||||
getRevisionPageByPath,
|
||||
getDocument,
|
||||
ContentTarget,
|
||||
SiteContentPointer,
|
||||
getCurrentSiteData,
|
||||
getSiteData,
|
||||
getSite,
|
||||
getSiteSpaces,
|
||||
getCurrentSiteCustomization,
|
||||
getSiteStructure,
|
||||
} from '@/lib/api';
|
||||
import { resolvePagePath, resolvePageId } from '@/lib/pages';
|
||||
import { getSiteContentPointer } from '@/lib/pointer';
|
||||
|
||||
export interface PagePathParams {
|
||||
pathname?: string[];
|
||||
@@ -23,91 +21,73 @@ 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;
|
||||
}
|
||||
type SectionsList = { list: SiteSection[]; section: SiteSection };
|
||||
|
||||
/**
|
||||
* Fetch all the data needed to render the space layout.
|
||||
* Fetch all the data needed to render the content layout.
|
||||
*/
|
||||
export async function fetchSpaceData() {
|
||||
export async function fetchContentData() {
|
||||
const content = getSiteContentPointer();
|
||||
|
||||
const siteShareKey = content.siteShareKey;
|
||||
const [{ space, contentTarget, pages, customization, scripts }, siteStructure] =
|
||||
await Promise.all([
|
||||
getSiteData(content),
|
||||
fetchSiteStructure({
|
||||
organizationId: content.organizationId,
|
||||
siteId: content.siteId,
|
||||
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 site = siteStructure.site;
|
||||
|
||||
const siteSections =
|
||||
content.siteSectionId && siteStructure.sections
|
||||
? parseSiteSectionsList(content.siteSectionId, siteStructure.sections)
|
||||
: null;
|
||||
|
||||
const spaces =
|
||||
siteStructure.spaces ??
|
||||
(siteSections ? parseSpacesFromSiteSpaces(siteSections.section.siteSpaces) : []);
|
||||
|
||||
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);
|
||||
const spaceRelativeToParent = spaces?.find((space) => space.id === content.spaceId);
|
||||
|
||||
return {
|
||||
content,
|
||||
contentTarget,
|
||||
space: spaceRelativeToParent ?? space,
|
||||
pages,
|
||||
sections: siteSections,
|
||||
site,
|
||||
spaces,
|
||||
customization,
|
||||
scripts,
|
||||
ancestors: [],
|
||||
...parent,
|
||||
};
|
||||
}
|
||||
|
||||
function parseSiteSectionsList(siteSectionId: string, sections: SiteSection[]) {
|
||||
const section = sections.find((section) => section.id === siteSectionId);
|
||||
assert(sectionIsDefined(section), 'A section must be defined when there are multiple sections');
|
||||
return { list: sections, section } satisfies SectionsList;
|
||||
}
|
||||
|
||||
function sectionIsDefined(section?: SiteSection): section is NonNullable<SiteSection> {
|
||||
return typeof section !== 'undefined' && section !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 { space, contentTarget, pages, customization, scripts } = await getSiteData(content);
|
||||
const page = await resolvePage(contentTarget, pages, params);
|
||||
const [parent, document] = await Promise.all([
|
||||
fetchParentSite({
|
||||
const [siteStructure, document] = await Promise.all([
|
||||
fetchSiteStructure({
|
||||
organizationId: content.organizationId,
|
||||
siteId: content.siteId,
|
||||
siteShareKey: content.siteShareKey,
|
||||
@@ -124,7 +104,7 @@ export async function fetchPageData(params: PagePathParams | PageIdParams) {
|
||||
scripts,
|
||||
ancestors: [],
|
||||
...page,
|
||||
...parent,
|
||||
...siteStructure,
|
||||
document,
|
||||
};
|
||||
}
|
||||
@@ -170,18 +150,45 @@ async function resolvePage(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function fetchParentSite(args: {
|
||||
/**
|
||||
* Fetch the structure of an organization site.
|
||||
* This includes the site and its sections or spaces.
|
||||
*/
|
||||
async function fetchSiteStructure(args: {
|
||||
organizationId: string;
|
||||
siteId: string;
|
||||
siteShareKey: string | undefined;
|
||||
}) {
|
||||
const { organizationId, siteId, siteShareKey } = args;
|
||||
const [site, siteSpaces, siteParentCustomizations] = await Promise.all([
|
||||
const [orgSite, siteStructure, siteParentCustomizations] = await Promise.all([
|
||||
getSite(organizationId, siteId),
|
||||
getSiteSpaces({ organizationId, siteId, siteShareKey }),
|
||||
getSiteStructure({ organizationId, siteId, siteShareKey }),
|
||||
getCurrentSiteCustomization({ organizationId, siteId, siteSpaceId: undefined }),
|
||||
]);
|
||||
|
||||
const siteSections =
|
||||
siteStructure.type === 'sections' && siteStructure.structure
|
||||
? siteStructure.structure
|
||||
: null;
|
||||
const siteSpaces =
|
||||
siteStructure.type === 'siteSpaces' && siteStructure.structure
|
||||
? parseSpacesFromSiteSpaces(siteStructure.structure)
|
||||
: null;
|
||||
|
||||
// override the title with the customization title
|
||||
const site = {
|
||||
...orgSite,
|
||||
...(siteParentCustomizations?.title ? { title: siteParentCustomizations.title } : {}),
|
||||
};
|
||||
|
||||
return {
|
||||
site,
|
||||
spaces: siteSpaces,
|
||||
sections: siteSections,
|
||||
};
|
||||
}
|
||||
|
||||
function parseSpacesFromSiteSpaces(siteSpaces: SiteSpace[]) {
|
||||
const spaces: Record<string, Space> = {};
|
||||
siteSpaces.forEach((siteSpace) => {
|
||||
spaces[siteSpace.space.id] = {
|
||||
@@ -193,17 +200,7 @@ async function fetchParentSite(args: {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// override the title with the customization title
|
||||
const parent = {
|
||||
...site,
|
||||
...(siteParentCustomizations?.title ? { title: siteParentCustomizations.title } : {}),
|
||||
};
|
||||
|
||||
return {
|
||||
parent,
|
||||
spaces: Object.values(spaces),
|
||||
};
|
||||
return Object.values(spaces);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -0,0 +1,18 @@
|
||||
import { CustomizationRootLayout } from '@/components/RootLayout';
|
||||
import { getSiteLayoutData } 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 getSiteLayoutData(pointer);
|
||||
|
||||
return (
|
||||
<CustomizationRootLayout customization={customization}>{children}</CustomizationRootLayout>
|
||||
);
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
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,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { SpaceIntegrationScript } from '@gitbook/api';
|
||||
|
||||
import { CustomizationRootLayout } from '@/components/RootLayout';
|
||||
import { getSiteLayoutData, 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
|
||||
? getSiteLayoutData(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,
|
||||
};
|
||||
}
|
||||
+14
-10
@@ -11,7 +11,6 @@ import { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
import * as React from 'react';
|
||||
|
||||
import { getSiteContentPointer } from '@/app/(space)/fetch';
|
||||
import { DocumentView, createHighlightingContext } from '@/components/DocumentView';
|
||||
import { TrademarkLink } from '@/components/TableOfContents/Trademark';
|
||||
import { PolymorphicComponentProp } from '@/components/utils/types';
|
||||
@@ -32,6 +31,7 @@ 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,12 @@ const DEFAULT_LIMIT = 100;
|
||||
export const runtime = 'edge';
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const contentPointer = getSiteContentPointer();
|
||||
const pointer = getSiteOrSpacePointerForPDF();
|
||||
const [space, customization] = await Promise.all([
|
||||
getSpace(contentPointer.spaceId, contentPointer.siteShareKey),
|
||||
getCurrentSiteCustomization(contentPointer),
|
||||
getSpace(pointer.spaceId, 'siteId' in pointer ? pointer.siteShareKey : undefined),
|
||||
'siteId' in pointer
|
||||
? getCurrentSiteCustomization(pointer)
|
||||
: getSpaceCustomization(pointer.spaceId),
|
||||
]);
|
||||
|
||||
return {
|
||||
@@ -55,7 +57,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 contentPointer = getSiteContentPointer();
|
||||
const pointer = getSiteOrSpacePointerForPDF();
|
||||
|
||||
const searchParams = new URLSearchParams(props.searchParams);
|
||||
const pdfParams = getPDFSearchParams(new URLSearchParams(searchParams));
|
||||
@@ -66,8 +68,10 @@ export default async function PDFHTMLOutput(props: { searchParams: { [key: strin
|
||||
|
||||
// Load the content,
|
||||
const [customization, { space, contentTarget, pages: rootPages }] = await Promise.all([
|
||||
getCurrentSiteCustomization(contentPointer),
|
||||
getSpaceContentData(contentPointer),
|
||||
'siteId' in pointer
|
||||
? getCurrentSiteCustomization(pointer)
|
||||
: getSpaceCustomization(pointer.spaceId),
|
||||
getSpaceContentData(pointer, 'siteId' in pointer ? pointer.siteShareKey : undefined),
|
||||
]);
|
||||
const language = getSpaceLanguage(customization);
|
||||
|
||||
@@ -165,7 +169,7 @@ export default async function PDFHTMLOutput(props: { searchParams: { [key: strin
|
||||
space={space}
|
||||
page={page}
|
||||
refContext={{
|
||||
siteContext: contentPointer,
|
||||
siteContext: 'siteId' in pointer ? pointer : null,
|
||||
space,
|
||||
revisionId: contentTarget.revisionId,
|
||||
pages: rootPages,
|
||||
@@ -311,7 +315,7 @@ function selectPages(
|
||||
return [
|
||||
{ page, depth },
|
||||
...page.pages.flatMap((child) => {
|
||||
if (child.type === 'link') {
|
||||
if (child.type !== 'document') {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -346,7 +350,7 @@ function selectPages(
|
||||
}
|
||||
|
||||
const allPages = rootPages.flatMap((page) => {
|
||||
if (page.type === 'link') {
|
||||
if (page.type !== 'document' && page.type !== 'group') {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { Space } from '@gitbook/api';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import React from 'react';
|
||||
|
||||
import { SiteContentPointer, getChangeRequest, getRevision } from '@/lib/api';
|
||||
import { getChangeRequest, getRevision, SiteContentPointer } from '@/lib/api';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { RefreshChangeRequestButton } from './RefreshChangeRequestButton';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { SiteAds, SiteAdsStatus } from '@gitbook/api';
|
||||
import * as React from 'react';
|
||||
|
||||
import { t, useLanguage } from '@/intl/client';
|
||||
@@ -21,6 +22,7 @@ export function Ad({
|
||||
spaceId,
|
||||
placement,
|
||||
ignore,
|
||||
siteAdsStatus,
|
||||
style,
|
||||
mode = 'auto',
|
||||
}: {
|
||||
@@ -29,6 +31,7 @@ export function Ad({
|
||||
placement: string;
|
||||
ignore: boolean;
|
||||
style?: ClassValue;
|
||||
siteAdsStatus?: SiteAds['status'];
|
||||
mode?: 'classic' | 'auto' | 'cover';
|
||||
}) {
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
@@ -70,20 +73,31 @@ export function Ad({
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
const preview = new URL(window.location.href).searchParams.has('ads_preview');
|
||||
const previewParam = new URL(window.location.href).searchParams.get('ads_preview');
|
||||
const preview = !!previewParam;
|
||||
const realZoneId = preview ? PREVIEW_ZONE_ID : zoneId;
|
||||
const showPlaceholderAd =
|
||||
previewParam === 'placeholder' ||
|
||||
(siteAdsStatus &&
|
||||
(siteAdsStatus === SiteAdsStatus.Pending ||
|
||||
siteAdsStatus === SiteAdsStatus.InReview));
|
||||
|
||||
if (!realZoneId) {
|
||||
if (!realZoneId && !showPlaceholderAd) {
|
||||
return;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const result = await renderAd({
|
||||
placement,
|
||||
ignore: ignore || preview,
|
||||
zoneId: realZoneId,
|
||||
mode,
|
||||
});
|
||||
const result = showPlaceholderAd
|
||||
? await renderAd({ source: 'placeholder' })
|
||||
: realZoneId
|
||||
? await renderAd({
|
||||
placement,
|
||||
ignore: ignore || preview,
|
||||
zoneId: realZoneId,
|
||||
mode,
|
||||
source: 'live',
|
||||
})
|
||||
: undefined;
|
||||
|
||||
if (cancelled) {
|
||||
return;
|
||||
@@ -97,15 +111,10 @@ export function Ad({
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [visible, zoneId, ignore, placement, mode]);
|
||||
|
||||
const viaUrl = new URL('https://www.gitbook.com');
|
||||
viaUrl.searchParams.set('utm_source', 'content');
|
||||
viaUrl.searchParams.set('utm_medium', 'ads');
|
||||
viaUrl.searchParams.set('utm_campaign', spaceId);
|
||||
}, [visible, zoneId, ignore, placement, mode, siteAdsStatus]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={tcls(style)}>
|
||||
<div ref={containerRef} className={tcls(style)} data-visual-test="removed">
|
||||
{ad ? (
|
||||
<>
|
||||
{ad}
|
||||
@@ -122,7 +131,7 @@ function AdSponsoredLink(props: { spaceId: string }) {
|
||||
|
||||
const viaUrl = new URL('https://www.gitbook.com');
|
||||
viaUrl.searchParams.set('utm_source', 'content');
|
||||
viaUrl.searchParams.set('utm_medium', 'sponsoring');
|
||||
viaUrl.searchParams.set('utm_medium', 'sponsored-by-gitbook');
|
||||
viaUrl.searchParams.set('utm_campaign', spaceId);
|
||||
|
||||
return (
|
||||
|
||||
@@ -8,7 +8,7 @@ import { AdItem } from './types';
|
||||
/**
|
||||
* Classic rendering for an ad.
|
||||
*/
|
||||
export async function AdClassicRendering({ ad }: { ad: AdItem }) {
|
||||
export function AdClassicRendering({ ad }: { ad: AdItem }) {
|
||||
return (
|
||||
<a
|
||||
className={tcls(
|
||||
@@ -33,7 +33,7 @@ export async function AdClassicRendering({ ad }: { ad: AdItem }) {
|
||||
<img
|
||||
alt="Ads logo"
|
||||
className={tcls('rounded-md')}
|
||||
src={await getResizedImageURL(ad.smallImage, { width: 192, dpr: 2 })}
|
||||
src={getResizedImageURL(ad.smallImage, { width: 192, dpr: 2 })}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
@@ -43,7 +43,7 @@ export async function AdClassicRendering({ ad }: { ad: AdItem }) {
|
||||
>
|
||||
<img
|
||||
alt="Ads logo"
|
||||
src={await getResizedImageURL(ad.logo, { width: 192 - 48, dpr: 2 })}
|
||||
src={getResizedImageURL(ad.logo, { width: 192 - 48, dpr: 2 })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -9,8 +9,8 @@ import { AdCover } from './types';
|
||||
/**
|
||||
* Cover rendering for an ad.
|
||||
*/
|
||||
export async function AdCoverRendering({ ad }: { ad: AdCover }) {
|
||||
const largeImage = await getResizedImageURL(ad.largeImage, { width: 128, dpr: 2 });
|
||||
export function AdCoverRendering({ ad }: { ad: AdCover }) {
|
||||
const largeImage = getResizedImageURL(ad.largeImage, { width: 128, dpr: 2 });
|
||||
|
||||
return (
|
||||
<a
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<svg width="193" height="120" viewBox="0 0 193 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M183.432 0.776855H10.2788C5.07442 0.776855 0.855469 4.14309 0.855469 8.29556V111.678C0.855469 115.831 5.07442 119.197 10.2788 119.197H183.432C188.637 119.197 192.855 115.831 192.855 111.678V8.29556C192.855 4.14309 188.637 0.776855 183.432 0.776855Z"
|
||||
fill="#14171C" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M96.8556 72.2159C84.724 72.2159 74.8894 82.0505 74.8894 94.1818C74.8894 95.9661 73.4431 97.4124 71.659 97.4124C69.875 97.4124 68.4287 95.9661 68.4287 94.1818C68.4287 78.4825 81.1558 65.7554 96.8556 65.7554C112.556 65.7554 125.282 78.4825 125.282 94.1818C125.282 95.9661 123.836 97.4124 122.053 97.4124C120.268 97.4124 118.822 95.9661 118.822 94.1818C118.822 82.0505 108.987 72.2159 96.8556 72.2159Z"
|
||||
fill="url(#paint0_radial_3089_1090)" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M129.621 94.1816C129.621 76.0865 114.951 61.4172 96.8558 61.4172C78.7603 61.4172 64.091 76.0865 64.091 94.1816C64.091 95.9658 62.6447 97.4122 60.8607 97.4122C59.0766 97.4122 57.6304 95.9658 57.6304 94.1816C57.6304 72.5183 75.1922 54.9565 96.8558 54.9565C118.52 54.9565 136.081 72.5183 136.081 94.1816C136.081 95.9658 134.635 97.4122 132.85 97.4122C131.067 97.4122 129.621 95.9658 129.621 94.1816Z"
|
||||
fill="url(#paint1_radial_3089_1090)" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M96.856 50.3419C72.6438 50.3419 53.0159 69.9698 53.0159 94.1818C53.0159 95.966 51.5695 97.4124 49.7855 97.4124C48.0014 97.4124 46.5552 95.966 46.5552 94.1818C46.5552 66.4018 69.0756 43.8813 96.856 43.8813C124.637 43.8813 147.157 66.4018 147.157 94.1818C147.157 95.966 145.711 97.4124 143.927 97.4124C142.142 97.4124 140.696 95.966 140.696 94.1818C140.696 69.9698 121.068 50.3419 96.856 50.3419Z"
|
||||
fill="url(#paint2_radial_3089_1090)" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M151.587 94.1815C151.587 63.9548 127.083 39.4509 96.8557 39.4509C66.6286 39.4509 42.1246 63.9548 42.1246 94.1815C42.1246 95.9658 40.6784 97.4121 38.8943 97.4121C37.1103 97.4121 35.6641 95.9658 35.6641 94.1815C35.6641 60.3866 63.0605 32.9902 96.8557 32.9902C130.651 32.9902 158.047 60.3866 158.047 94.1815C158.047 95.9658 156.601 97.4121 154.818 97.4121C153.033 97.4121 151.587 95.9658 151.587 94.1815Z"
|
||||
fill="url(#paint3_radial_3089_1090)" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M96.8554 29.0217C60.8683 29.0217 31.6951 58.195 31.6951 94.1817C31.6951 95.966 30.2488 97.4123 28.4647 97.4123C26.6807 97.4123 25.2344 95.966 25.2344 94.1817C25.2344 54.6269 57.3003 22.561 96.8554 22.561C136.411 22.561 168.477 54.6269 168.477 94.1817C168.477 95.966 167.03 97.4123 165.246 97.4123C163.462 97.4123 162.015 95.966 162.015 94.1817C162.015 58.195 132.842 29.0217 96.8554 29.0217Z"
|
||||
fill="url(#paint4_radial_3089_1090)" />
|
||||
<defs>
|
||||
<radialGradient id="paint0_radial_3089_1090" cx="0" cy="0" r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(88.9053 80.4777) rotate(64.8515) scale(18.7082 33.8612)">
|
||||
<stop stop-color="#3F89A1" />
|
||||
<stop offset="0.339725" stop-color="#F4E28D" />
|
||||
<stop offset="0.505624" stop-color="#BBDDE5" />
|
||||
<stop offset="0.848958" stop-color="#FDA599" />
|
||||
</radialGradient>
|
||||
<radialGradient id="paint1_radial_3089_1090" cx="0" cy="0" r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(143.478 58.0317) rotate(168.06) scale(87.7455 57.2588)">
|
||||
<stop stop-color="#3F89A1" />
|
||||
<stop offset="0.19" stop-color="#F4E28D" />
|
||||
<stop offset="0.614011" stop-color="#BBDDE5" />
|
||||
<stop offset="1" stop-color="#FDA599" />
|
||||
</radialGradient>
|
||||
<radialGradient id="paint2_radial_3089_1090" cx="0" cy="0" r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(82.7879 68.7762) rotate(63.8366) scale(31.9052 59.4091)">
|
||||
<stop stop-color="#3F89A1" />
|
||||
<stop offset="0.339725" stop-color="#F4E28D" />
|
||||
<stop offset="0.505624" stop-color="#BBDDE5" />
|
||||
<stop offset="0.848958" stop-color="#FDA599" />
|
||||
</radialGradient>
|
||||
<radialGradient id="paint3_radial_3089_1090" cx="0" cy="0" r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(101.132 56.1925) rotate(172.165) scale(66.0854 41.9909)">
|
||||
<stop stop-color="#3F89A1" />
|
||||
<stop offset="0.339725" stop-color="#F4E28D" />
|
||||
<stop offset="0.505624" stop-color="#BBDDE5" />
|
||||
<stop offset="0.848958" stop-color="#FDA599" />
|
||||
</radialGradient>
|
||||
<radialGradient id="paint4_radial_3089_1090" cx="0" cy="0" r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(76.8246 57.371) rotate(63.4233) scale(44.7721 84.288)">
|
||||
<stop stop-color="#3F89A1" />
|
||||
<stop offset="0.339725" stop-color="#F4E28D" />
|
||||
<stop offset="0.505624" stop-color="#BBDDE5" />
|
||||
<stop offset="0.848958" stop-color="#FDA599" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.1 KiB |
@@ -5,9 +5,16 @@ import { headers } from 'next/headers';
|
||||
import { AdClassicRendering } from './AdClassicRendering';
|
||||
import { AdCoverRendering } from './AdCoverRendering';
|
||||
import { AdPixels } from './AdPixels';
|
||||
import adRainbow from './assets/ad-rainbow.svg';
|
||||
import { AdItem, AdsResponse } from './types';
|
||||
|
||||
interface FetchAdOptions {
|
||||
type FetchAdOptions = FetchLiveAdOptions | FetchPlaceholderAdOptions;
|
||||
|
||||
interface FetchLiveAdOptions {
|
||||
/**
|
||||
* Source of the ad (live: from the platform)
|
||||
*/
|
||||
source: 'live';
|
||||
/** ID of the zone to fetch Ads for */
|
||||
zoneId: string;
|
||||
/** Mode to render the Ad */
|
||||
@@ -18,14 +25,22 @@ interface FetchAdOptions {
|
||||
ignore: boolean;
|
||||
}
|
||||
|
||||
interface FetchPlaceholderAdOptions {
|
||||
/**
|
||||
* Source of the ad (placeholder: static placeholder ad)
|
||||
*/
|
||||
source: 'placeholder';
|
||||
}
|
||||
|
||||
/**
|
||||
* Server action to render the Ad placement.
|
||||
* We use a server-action to avoid caching issues with server-side components,
|
||||
* and properly access user-agent and IP.
|
||||
*/
|
||||
export async function renderAd(options: FetchAdOptions) {
|
||||
const { mode } = options;
|
||||
const result = await fetchAd(options);
|
||||
const mode = options.source === 'live' ? options.mode : 'classic';
|
||||
|
||||
const result = options.source === 'live' ? await fetchAd(options) : getPlaceholderAd();
|
||||
if (!result || !result.ad.description || !result.ad.statlink) {
|
||||
return null;
|
||||
}
|
||||
@@ -48,16 +63,8 @@ async function fetchAd({
|
||||
zoneId,
|
||||
placement,
|
||||
ignore,
|
||||
}: FetchAdOptions): Promise<{ ad: AdItem; ip: string } | null> {
|
||||
const headersSet = headers();
|
||||
const ip =
|
||||
headersSet.get('x-gitbook-ipv4') ??
|
||||
headersSet.get('x-gitbook-ip') ??
|
||||
headersSet.get('cf-pseudo-ipv4') ??
|
||||
headersSet.get('cf-connecting-ip') ??
|
||||
headersSet.get('x-forwarded-for') ??
|
||||
'';
|
||||
const userAgent = headersSet.get('user-agent') ?? '';
|
||||
}: FetchLiveAdOptions): Promise<{ ad: AdItem; ip: string } | null> {
|
||||
const { ip, userAgent } = getUserAgentAndIp();
|
||||
|
||||
const url = new URL(`https://srv.buysellads.com/ads/${zoneId}.json`);
|
||||
url.searchParams.set('segment', `placement:${placement}`);
|
||||
@@ -78,3 +85,50 @@ async function fetchAd({
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getPlaceholderAd(): { ad: AdItem; ip: string } {
|
||||
const { ip } = getUserAgentAndIp();
|
||||
|
||||
return {
|
||||
ad: {
|
||||
active: '1',
|
||||
ad_via_link: '',
|
||||
bannerid: '',
|
||||
creativeid: '',
|
||||
description:
|
||||
'Your docs could be this good.\nPublish incredible open source docs for free with GitBook',
|
||||
evenodd: '0',
|
||||
external_id: '',
|
||||
height: '0',
|
||||
i: '0',
|
||||
identifier: '',
|
||||
longimp: '',
|
||||
longlink: '',
|
||||
num_slots: '1',
|
||||
rendering: 'carbon',
|
||||
smallImage: adRainbow.src,
|
||||
statimp: '',
|
||||
statlink:
|
||||
'https://www.gitbook.com/solutions/open-source?utm_campaign=sponsored-content&utm_medium=ad&utm_source=content',
|
||||
timestamp: Date.now().toString(),
|
||||
width: '0',
|
||||
zoneid: '',
|
||||
zonekey: '',
|
||||
},
|
||||
ip,
|
||||
};
|
||||
}
|
||||
|
||||
function getUserAgentAndIp() {
|
||||
const headersSet = headers();
|
||||
const ip =
|
||||
headersSet.get('x-gitbook-ipv4') ??
|
||||
headersSet.get('x-gitbook-ip') ??
|
||||
headersSet.get('cf-pseudo-ipv4') ??
|
||||
headersSet.get('cf-connecting-ip') ??
|
||||
headersSet.get('x-forwarded-for') ??
|
||||
'';
|
||||
const userAgent = headersSet.get('user-agent') ?? '';
|
||||
|
||||
return { ip, userAgent };
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
|
||||
|
||||
const onUpdateState = (enabled: boolean) => {
|
||||
setCookiesTracking(enabled);
|
||||
|
||||
// Reload the page to take the change in consideration
|
||||
window.location.reload();
|
||||
};
|
||||
@@ -75,6 +74,7 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
|
||||
)}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShow(false)}
|
||||
aria-label={tString(language, 'cookies_close')}
|
||||
className={tcls(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { DocumentBlock, JSONDocument } from '@gitbook/api';
|
||||
import assertNever from 'assert-never';
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
@@ -12,7 +11,6 @@ import {
|
||||
import { ClassValue } from '@/lib/tailwind';
|
||||
|
||||
import { BlockContentRef } from './BlockContentRef';
|
||||
import { BlockSyncedBlock } from './BlockSyncedBlock';
|
||||
import { CodeBlock } from './CodeBlock';
|
||||
import { Divider } from './Divider';
|
||||
import { DocumentContextProps } from './DocumentView';
|
||||
@@ -32,6 +30,9 @@ import { BlockMath } from './Math';
|
||||
import { OpenAPI } from './OpenAPI';
|
||||
import { Paragraph } from './Paragraph';
|
||||
import { Quote } from './Quote';
|
||||
import { ReusableContent } from './ReusableContent';
|
||||
import { Stepper } from './Stepper';
|
||||
import { StepperStep } from './StepperStep';
|
||||
import { Table } from './Table';
|
||||
import { Tabs } from './Tabs';
|
||||
|
||||
@@ -39,77 +40,100 @@ export interface BlockProps<Block extends DocumentBlock> extends DocumentContext
|
||||
block: Block;
|
||||
document: JSONDocument;
|
||||
ancestorBlocks: DocumentBlock[];
|
||||
/** If true, we estimate that the block will be outside the initial viewport */
|
||||
isEstimatedOffscreen: boolean;
|
||||
/** Class names to be passed to the underlying DOM element */
|
||||
style?: ClassValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative to `assertNever` that returns `null` instead of throwing an error.
|
||||
*/
|
||||
function nullIfNever(value: never): null {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function Block<T extends DocumentBlock>(props: BlockProps<T>) {
|
||||
const { block, style, ...contextProps } = props;
|
||||
const { block, style, isEstimatedOffscreen, context } = props;
|
||||
|
||||
const content = (() => {
|
||||
switch (block.type) {
|
||||
case 'paragraph':
|
||||
return <Paragraph {...props} {...contextProps} block={block} />;
|
||||
return <Paragraph {...props} block={block} />;
|
||||
case 'heading-1':
|
||||
case 'heading-2':
|
||||
case 'heading-3':
|
||||
return <Heading {...props} {...contextProps} block={block} />;
|
||||
return <Heading {...props} block={block} />;
|
||||
case 'list-ordered':
|
||||
return <ListOrdered {...props} {...contextProps} block={block} />;
|
||||
return <ListOrdered {...props} block={block} />;
|
||||
case 'list-unordered':
|
||||
return <ListUnordered {...props} {...contextProps} block={block} />;
|
||||
return <ListUnordered {...props} block={block} />;
|
||||
case 'list-tasks':
|
||||
return <ListTasks {...props} {...contextProps} block={block} />;
|
||||
return <ListTasks {...props} block={block} />;
|
||||
case 'list-item':
|
||||
return <ListItem {...props} {...contextProps} block={block} />;
|
||||
return <ListItem {...props} block={block} />;
|
||||
case 'code':
|
||||
return <CodeBlock {...props} {...contextProps} block={block} />;
|
||||
return <CodeBlock {...props} block={block} />;
|
||||
case 'hint':
|
||||
return <Hint {...props} {...contextProps} block={block} />;
|
||||
return <Hint {...props} block={block} />;
|
||||
case 'images':
|
||||
return <Images {...props} {...contextProps} block={block} />;
|
||||
return <Images {...props} block={block} />;
|
||||
case 'tabs':
|
||||
return <Tabs {...props} {...contextProps} block={block} />;
|
||||
return <Tabs {...props} block={block} />;
|
||||
case 'expandable':
|
||||
return <Expandable {...props} {...contextProps} block={block} />;
|
||||
return <Expandable {...props} block={block} />;
|
||||
case 'table':
|
||||
return <Table {...props} {...contextProps} block={block} />;
|
||||
return <Table {...props} block={block} />;
|
||||
case 'swagger':
|
||||
return <OpenAPI {...props} {...contextProps} block={block} />;
|
||||
return <OpenAPI {...props} block={block} />;
|
||||
case 'embed':
|
||||
return <Embed {...props} {...contextProps} block={block} />;
|
||||
return <Embed {...props} block={block} />;
|
||||
case 'blockquote':
|
||||
return <Quote {...props} {...contextProps} block={block} />;
|
||||
return <Quote {...props} block={block} />;
|
||||
case 'math':
|
||||
return <BlockMath {...props} {...contextProps} block={block} />;
|
||||
return <BlockMath {...props} block={block} />;
|
||||
case 'file':
|
||||
return <File {...props} {...contextProps} block={block} />;
|
||||
return <File {...props} block={block} />;
|
||||
case 'divider':
|
||||
return <Divider {...props} {...contextProps} block={block} />;
|
||||
return <Divider {...props} block={block} />;
|
||||
case 'drawing':
|
||||
return <Drawing {...props} {...contextProps} block={block} />;
|
||||
return <Drawing {...props} block={block} />;
|
||||
case 'content-ref':
|
||||
return <BlockContentRef {...props} {...contextProps} block={block} />;
|
||||
return <BlockContentRef {...props} block={block} />;
|
||||
case 'image':
|
||||
case 'code-line':
|
||||
case 'tabs-item':
|
||||
throw new Error('Blocks should be directly rendered by parent');
|
||||
case 'integration':
|
||||
return <IntegrationBlock {...props} {...contextProps} block={block} />;
|
||||
case 'synced-block':
|
||||
return <BlockSyncedBlock {...props} {...contextProps} block={block} />;
|
||||
return <IntegrationBlock {...props} block={block} />;
|
||||
case 'reusable-content':
|
||||
return <ReusableContent {...props} block={block} />;
|
||||
case 'stepper':
|
||||
return <Stepper {...props} block={block} />;
|
||||
case 'stepper-step':
|
||||
return <StepperStep {...props} block={block} />;
|
||||
default:
|
||||
assertNever(block);
|
||||
return nullIfNever(block);
|
||||
}
|
||||
})();
|
||||
|
||||
if (!isEstimatedOffscreen || context.wrapBlocksInSuspense === false) {
|
||||
// When blocks are estimated to be on the initial viewport, we render them immediately
|
||||
// to avoid a flash of a loading skeleton.
|
||||
return content;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Suspense fallback={<BlockPlaceholder block={block} style={style} />}>
|
||||
<React.Suspense fallback={<BlockSkeleton block={block} style={style} />}>
|
||||
{content}
|
||||
</React.Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
function BlockPlaceholder(props: { block: DocumentBlock; style: ClassValue }) {
|
||||
/**
|
||||
* Skeleton for a block while it is being loaded.
|
||||
*/
|
||||
export function BlockSkeleton(props: { block: DocumentBlock; style: ClassValue }) {
|
||||
const { block, style } = props;
|
||||
const id = 'meta' in block && block.meta && 'id' in block.meta ? block.meta.id : undefined;
|
||||
|
||||
@@ -129,7 +153,6 @@ function BlockPlaceholder(props: { block: DocumentBlock; style: ClassValue }) {
|
||||
case 'code':
|
||||
case 'hint':
|
||||
case 'tabs':
|
||||
case 'synced-block':
|
||||
return <SkeletonParagraph id={id} style={style} />;
|
||||
case 'expandable':
|
||||
case 'table':
|
||||
@@ -138,6 +161,8 @@ function BlockPlaceholder(props: { block: DocumentBlock; style: ClassValue }) {
|
||||
case 'divider':
|
||||
case 'content-ref':
|
||||
case 'integration':
|
||||
case 'stepper':
|
||||
case 'reusable-content':
|
||||
return <SkeletonCard id={id} style={style} />;
|
||||
case 'embed':
|
||||
case 'images':
|
||||
@@ -146,8 +171,9 @@ function BlockPlaceholder(props: { block: DocumentBlock; style: ClassValue }) {
|
||||
case 'image':
|
||||
case 'code-line':
|
||||
case 'tabs-item':
|
||||
case 'stepper-step':
|
||||
throw new Error('Blocks should be directly rendered by parent');
|
||||
default:
|
||||
assertNever(block);
|
||||
return nullIfNever(block);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { DocumentBlockSyncedBlock } from '@gitbook/api';
|
||||
|
||||
import { getSyncedBlockContent } from '@/lib/api';
|
||||
import { resolveContentRefWithFiles } from '@/lib/references';
|
||||
|
||||
import { BlockProps } from './Block';
|
||||
import { Blocks, UnwrappedBlocks } from './Blocks';
|
||||
|
||||
export async function BlockSyncedBlock(props: BlockProps<DocumentBlockSyncedBlock>) {
|
||||
const { block, ancestorBlocks, context, style } = props;
|
||||
|
||||
const apiToken = block.meta?.apiToken;
|
||||
if (!apiToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We can't resolve the synced block without an organization context.
|
||||
if (!context.contentRefContext) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const syncedBlock = await getSyncedBlockContent(
|
||||
apiToken,
|
||||
context.contentRefContext.space.organization,
|
||||
block.data.ref.syncedBlock,
|
||||
);
|
||||
|
||||
if (!syncedBlock) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<UnwrappedBlocks
|
||||
nodes={syncedBlock.document.nodes}
|
||||
document={syncedBlock.document}
|
||||
ancestorBlocks={[...ancestorBlocks, block]}
|
||||
context={{
|
||||
...context,
|
||||
resolveContentRef: async (ref, options) => {
|
||||
if (!syncedBlock?.files) {
|
||||
return context.resolveContentRef(ref, options);
|
||||
}
|
||||
const result = resolveContentRefWithFiles(syncedBlock.files, ref);
|
||||
if (result !== undefined) {
|
||||
return result;
|
||||
}
|
||||
return context.resolveContentRef(ref, options);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user