mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-13 14:19:25 +00:00
Compare commits
107 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fc45cccee2 | |||
| 4cbcc5bb20 | |||
| 51955dae3b | |||
| 51fa3ab2c8 | |||
| af9e66e87e | |||
| 8878dc7f80 | |||
| 4336bc20e0 | |||
| 4a7613a521 | |||
| 689f55340f | |||
| 160ae3d49d | |||
| c079c3c6d9 | |||
| cb782a7557 | |||
| 502d8fc087 | |||
| 6295881138 | |||
| 17f71ba680 | |||
| 5fe7adb87b | |||
| 41684d091b | |||
| 9f6c3347cb | |||
| aa321989c4 | |||
| 0f1565cb4b | |||
| 3996110ebd | |||
| 3422ad4c84 | |||
| 3c07e65b9a | |||
| 287df91661 | |||
| 776bc3135a | |||
| 4638958c9f | |||
| 1f24fe46c4 | |||
| ccbcab8822 | |||
| 094e9cdac2 | |||
| 5c35f36b3a | |||
| d7ec2b57a4 | |||
| 373a1e0312 | |||
| e9fe3975bb | |||
| 32a73b55d4 | |||
| 0426312d6c | |||
| ede2335fb3 | |||
| 709f1a1ccb | |||
| 24b785cbfb | |||
| acc3f2f418 | |||
| d4a653d6a9 | |||
| 7e74d25447 | |||
| 3445db41ce | |||
| d1bd02a6f5 | |||
| 24cd72e088 | |||
| 47f7558c38 | |||
| de747b7b33 | |||
| 8e8b73fbd9 | |||
| e966e33e8f | |||
| bd0ca5babd | |||
| f6bacea1ae | |||
| ba2a523722 | |||
| 57adb3e54a | |||
| 7377928c8a | |||
| 5f8a8fe9b1 | |||
| 29a0bf25c4 | |||
| 92bb016576 | |||
| c8810ea002 | |||
| aa8bbaddc4 | |||
| b8c136e9c0 | |||
| 46d54172b0 | |||
| 68dc2de190 | |||
| 81b372f372 | |||
| 966e520640 | |||
| 12702930ec | |||
| 6ce57bcda1 | |||
| d99b453bc4 | |||
| aaf5793cb7 | |||
| 9b1762d2ab | |||
| 8d38d000fd | |||
| 4cf9cdd56d | |||
| 39406bbf54 | |||
| 9c5c1e5b68 | |||
| 6929b0b98b | |||
| 7c26d491b1 | |||
| 97035a054a | |||
| db45c6d773 | |||
| e3f5f81e62 | |||
| f5df40e17d | |||
| e13c76efed | |||
| 4a11d8d9eb | |||
| 009ed8cc6e | |||
| 106cbbd987 | |||
| 56e5c3c3f8 | |||
| 1dc3234003 | |||
| 22dc0d4ff5 | |||
| 3a4caf062c | |||
| 8ad0657159 | |||
| d8a4ecd0c6 | |||
| 65c7968438 | |||
| 3914f11150 | |||
| d363908297 | |||
| 414556ac7a | |||
| c768e9a90a | |||
| 553f1f228f | |||
| 30986528a7 | |||
| c70c2ddad8 | |||
| cf74d00758 | |||
| c2256bfd14 | |||
| 5d73a0f062 | |||
| 8f5f6c61f8 | |||
| 9a86965ba1 | |||
| c8d5f825fc | |||
| 5db3cbc2ce | |||
| f8d148bec4 | |||
| 198dd9f158 | |||
| 2b63bc158a | |||
| 4a8097d968 |
@@ -0,0 +1,8 @@
|
||||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||
@@ -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': 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,11 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.0.2/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
@@ -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': minor
|
||||
---
|
||||
|
||||
Add support for page icons
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Only display spaces dropdown in compact header when site is multi-variants
|
||||
@@ -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
|
||||
---
|
||||
|
||||
Update lockfile
|
||||
@@ -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
|
||||
---
|
||||
|
||||
Fix inconsistent click area in table because of scroll indicator
|
||||
@@ -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/react-contentkit': patch
|
||||
---
|
||||
|
||||
Fix rendering of webframe with SSR causing wrong communication between frame and renderer
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Add optional env `GITBOOK_INTEGRATIONS_HOST` to configure the host serving the integrations
|
||||
@@ -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
|
||||
---
|
||||
|
||||
Update Scalar client to latest version
|
||||
@@ -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/icons': minor
|
||||
---
|
||||
|
||||
Fix bin gitbook-icons when installed without the Pro token
|
||||
@@ -7,7 +7,7 @@ runs:
|
||||
- name: 🏗 Prepare Playwright env
|
||||
shell: bash
|
||||
run: |
|
||||
PLAYWRIGHT_VERSION=$(npm ls --json @playwright/test | jq --raw-output '.dependencies["@playwright/test"].version')
|
||||
PLAYWRIGHT_VERSION=$(npm ls --json @playwright/test | jq --raw-output '.dependencies["gitbook"].dependencies["@playwright/test"].version')
|
||||
echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_ENV
|
||||
|
||||
# Cache browser binaries, cache key is based on Playwright version and OS
|
||||
@@ -24,6 +24,7 @@ runs:
|
||||
- name: 🏗 Install Playwright browser binaries & OS dependencies
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
working-directory: packages/gitbook
|
||||
run: |
|
||||
bun x playwright install --with-deps chromium
|
||||
|
||||
@@ -31,5 +32,6 @@ runs:
|
||||
- name: 🏗 Install Playwright OS dependencies
|
||||
if: steps.playwright-cache.outputs.cache-hit == 'true'
|
||||
shell: bash
|
||||
working-directory: packages/gitbook
|
||||
run: |
|
||||
bun x playwright install-deps
|
||||
|
||||
+35
-12
@@ -1,6 +1,11 @@
|
||||
on: [push]
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
env:
|
||||
NPMRC_FONT_AWESOME_TOKEN: ${{ secrets.NPMRC_FONT_AWESOME_TOKEN }}
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy to Cloudflare Pages
|
||||
@@ -16,7 +21,7 @@ jobs:
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.0.33
|
||||
bun-version: 1.1.18
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
@@ -54,7 +59,7 @@ jobs:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
projectName: ${{ vars.CLOUDFLARE_PROJECT_NAME }}
|
||||
directory: ./.vercel/output/static
|
||||
directory: ./packages/gitbook/.vercel/output/static
|
||||
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
workingDirectory: ./
|
||||
- name: Outputs
|
||||
@@ -78,7 +83,7 @@ jobs:
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.0.33
|
||||
bun-version: 1.1.18
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
- name: Setup Playwright
|
||||
@@ -98,13 +103,13 @@ jobs:
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.0.33
|
||||
bun-version: 1.1.18
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- name: Run pagespeed tests
|
||||
run: bun ./tests/pagespeed-testing.ts $DEPLOYMENT_URL
|
||||
run: bun ./packages/gitbook/tests/pagespeed-testing.ts $DEPLOYMENT_URL
|
||||
env:
|
||||
DEPLOYMENT_URL: ${{needs.deploy.outputs.deployment_url}}
|
||||
PAGESPEED_API_KEY: ${{ secrets.PAGESPEED_API_KEY }}
|
||||
@@ -117,12 +122,12 @@ jobs:
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.0.33
|
||||
bun-version: 1.1.18
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- run: bun format --check .
|
||||
- run: bun format:check
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
name: Lint
|
||||
@@ -132,7 +137,7 @@ jobs:
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.0.33
|
||||
bun-version: 1.1.18
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
@@ -147,12 +152,30 @@ jobs:
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.0.33
|
||||
bun-version: 1.1.18
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- run: bun unit
|
||||
build-oss:
|
||||
# CI to check that the repository builds correctly on a machine without the credentials
|
||||
runs-on: ubuntu-latest
|
||||
name: Build (Open Source)
|
||||
env:
|
||||
NPMRC_FONT_AWESOME_TOKEN: ''
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.1.18
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- run: bun run build
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
name: Typecheck
|
||||
@@ -162,7 +185,7 @@ jobs:
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.0.33
|
||||
bun-version: 1.1.18
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
name: Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
|
||||
fetch-depth: 0
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.1.18
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- name: Create Release Pull Request or Publish to npm
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
publish: npm run release
|
||||
env:
|
||||
# Using a PAT instead of GITHUB_TOKEN because we need to run workflows when releases are created
|
||||
# https://github.com/orgs/community/discussions/26875#discussioncomment-3253761
|
||||
GITHUB_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
+5
-29
@@ -5,16 +5,6 @@ node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
@@ -24,25 +14,11 @@ npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
# Turbo
|
||||
.turbo
|
||||
|
||||
# vercel
|
||||
# Vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# visual tests
|
||||
screenshots/
|
||||
|
||||
# Sentry Config File
|
||||
.sentryclirc
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
|
||||
# Generated public files
|
||||
/public/~gitbook/static/
|
||||
# Env files
|
||||
.env.local
|
||||
|
||||
+10
-1
@@ -1 +1,10 @@
|
||||
.next
|
||||
.next
|
||||
.vercel
|
||||
|
||||
# Generated
|
||||
packages/emoji-codepoints/index.ts
|
||||
packages/gitbook/public/~gitbook/static/
|
||||
packages/icons/src/data/*.json
|
||||
|
||||
# Build files
|
||||
dist/
|
||||
|
||||
@@ -75,7 +75,7 @@ GitBook's rendering engine is fully open-source and built on top of [Next.js](ht
|
||||
|
||||
### Types of contributions
|
||||
|
||||
We encourage you to contribute to GitBook to help us build the best tool for doucmenting techincal knowledge. If you're looking for some quick ways to contribute, continue reading to learn more about popular contributions.
|
||||
We encourage you to contribute to GitBook to help us build the best tool for documenting technical knowledge. If you're looking for some quick ways to contribute, continue reading to learn more about popular contributions.
|
||||
|
||||
#### Translations
|
||||
|
||||
@@ -92,7 +92,7 @@ Encounter a bug or find an issue you'd like to fix? Helping us fix issues relate
|
||||
>
|
||||
> _Looking to add a specific feature in GitBook? Head to our [contributing guide](/.github/CONTRIBUTING.md) to get started._
|
||||
>
|
||||
> Self-hosting this project puts the responsibility of maintaining and merging future updates on **you**. We cannot guarantee support, maintainance, or updates to forked and self-hosted instances of this project.
|
||||
> Self-hosting this project puts the responsibility of maintaining and merging future updates on **you**. We cannot guarantee support, maintenance, or updates to forked and self-hosted instances of this project.
|
||||
>
|
||||
> We want to make it as easy as possible for our community to collaborate and push the future of GitBook, which is why we encourage you to contribute to our product directly instead of creating your own version.
|
||||
|
||||
@@ -106,7 +106,7 @@ On the con side, you become responsible for the reliability of your published si
|
||||
|
||||
Distributed under the [GNU GPLv3 License](https://github.com/GitBookIO/gitbook/blob/main/LICENSE).
|
||||
|
||||
If you plan to distribute the code, you must the source code public to comply with GNU GPLv3. To clone in a private repository, acquire a [commercial license](https://www.gitbook.com/pricing).
|
||||
If you plan to distribute the code, you must make the source code public to comply with the GNU GPLv3. To clone in a private repository, acquire a [commercial license](https://www.gitbook.com/pricing).
|
||||
|
||||
See `LICENSE` for more information.
|
||||
|
||||
@@ -119,6 +119,12 @@ GitBook wouldn't be possible without these projects:
|
||||
- [Tailwind CSS](https://tailwindcss.com/)
|
||||
- [Framer Motion](https://www.npmjs.com/package/framer-motion)
|
||||
|
||||
## Contributors
|
||||
|
||||
<a href="https://github.com/gitbookIO/gitbook/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=gitbookIO/gitbook" />
|
||||
</a>
|
||||
|
||||
## Legacy GitBook (Deprecated)
|
||||
|
||||
Our previous version of GitBook and it's CLI tool are now deprecated. You can still view the old repository and it's commits on this [branch](https://github.com/GitbookIO/gitbook/tree/legacy).
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
[install.scopes]
|
||||
"awesome.me" = { token = "$NPMRC_FONT_AWESOME_TOKEN", url = "https://npm.fontawesome.com/" }
|
||||
+20
-77
@@ -2,91 +2,34 @@
|
||||
"name": "gitbook",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"packageManager": "bun@1.1.18",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"build:cloudflare": "next-on-pages",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"dev": "turbo run dev",
|
||||
"build": "turbo run build",
|
||||
"build:cloudflare": "turbo run build:cloudflare",
|
||||
"lint": "turbo run lint",
|
||||
"lint:fix": "turbo run lint -- --fix",
|
||||
"typecheck": "turbo run typecheck",
|
||||
"format": "prettier ./ --ignore-unknown --write",
|
||||
"format:check": "prettier ./ --ignore-unknown --list-different",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"unit": "bun test {src,packages}/**/*.test.ts",
|
||||
"e2e": "playwright test",
|
||||
"postinstall": "rm -rf ./public/~gitbook/static/mathjax@3.2.2 && mkdir -p ./public/~gitbook/static/ && cp -R node_modules/mathjax/es5 ./public/~gitbook/static/mathjax@3.2.2"
|
||||
"unit": "turbo run unit",
|
||||
"e2e": "turbo run e2e",
|
||||
"changeset": "changeset",
|
||||
"release": "turbo run release && changeset publish",
|
||||
"download:env": "op read op://gitbook-x-dev/gitbook-open/.env.local >> .env.local"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"@geist-ui/icons": "^1.0.2",
|
||||
"@gitbook/api": "^0.43.0",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@sentry/nextjs": "^7.94.1",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@upstash/redis": "^1.27.1",
|
||||
"ajv": "^8.12.0",
|
||||
"assert-never": "^1.2.1",
|
||||
"bun-types": "^1.0.7",
|
||||
"classnames": "^2.5.1",
|
||||
"content-security-policy-merger": "^1.0.0",
|
||||
"framer-motion": "^10.16.14",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jsontoxml": "^1.0.1",
|
||||
"katex": "^0.16.9",
|
||||
"mathjax": "^3.2.2",
|
||||
"memoizee": "^0.4.15",
|
||||
"next": "^14.1.3",
|
||||
"next-themes": "^0.2.1",
|
||||
"nuqs": "^1.15.4",
|
||||
"object-hash": "^3.0.0",
|
||||
"openapi-types": "^12.1.3",
|
||||
"p-map": "^7.0.0",
|
||||
"parse-cache-control": "^1.0.1",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-hotkeys-hook": "^4.4.1",
|
||||
"recoil": "^0.7.7",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"rehype-stringify": "^10.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.0",
|
||||
"rison": "^0.1.1",
|
||||
"server-only": "^0.0.1",
|
||||
"shiki": "^1.2.0",
|
||||
"tailwind-merge": "^2.2.0",
|
||||
"tailwind-shades": "^1.1.2",
|
||||
"unified": "^11.0.4",
|
||||
"url-join": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@argos-ci/playwright": "^2.0.0",
|
||||
"@cloudflare/next-on-pages": "^1.9.0",
|
||||
"@cloudflare/workers-types": "^4.20231218.0",
|
||||
"@playwright/test": "^1.42.1",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/jsontoxml": "^1.0.5",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/katex": "^0.16.5",
|
||||
"@types/node": "^20",
|
||||
"@types/object-hash": "^3.0.6",
|
||||
"@types/parse-cache-control": "^1.0.4",
|
||||
"@types/psi": "^4.1.6",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/rison": "^0.0.9",
|
||||
"autoprefixer": "^10",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "13.5.6",
|
||||
"eslint-plugin-import": "^2.29.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"postcss": "^8",
|
||||
"@changesets/cli": "^2.27.7",
|
||||
"prettier": "^3.0.3",
|
||||
"psi": "^4.1.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"typescript": "^5"
|
||||
"turbo": "^2.0.9"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"@vercel/next@4.3.6": "patches/@vercel%2Fnext@4.3.6.patch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@scalar/api-client-react": "0.3.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
index.ts
|
||||
dist/
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# @gitbook/emoji-codepoints
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 57adb3e: Second release to fix publishing with changeset
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 5f8a8fe: Initial release
|
||||
@@ -1,4 +1,5 @@
|
||||
import fs from 'fs';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import emojisRaws from 'emoji-assets/emoji.json';
|
||||
|
||||
interface EmojiData {
|
||||
@@ -20,7 +21,8 @@ Object.entries(emojis).forEach(([key, value]) => {
|
||||
}
|
||||
});
|
||||
|
||||
fs.mkdirSync(path.resolve(__dirname, 'dist'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
'index.ts',
|
||||
path.resolve(__dirname, 'dist/index.ts'),
|
||||
`export const emojiCodepoints: Record<string, string> = ${JSON.stringify(output, null, 4)};`,
|
||||
);
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"name": "@gitbook/emoji-codepoints",
|
||||
"description": "Optimized mapping of codepoints to the fully qualified emoji codepoints",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"exports": "./index.ts",
|
||||
"exports": "./dist/index.ts",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"emoji-assets": "^8.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "bun ./build.ts"
|
||||
"generate": "bun ./build.ts"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# visual tests
|
||||
screenshots/
|
||||
|
||||
# Sentry Config File
|
||||
.sentryclirc
|
||||
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
|
||||
# Generated public files
|
||||
/public/~gitbook/static/
|
||||
@@ -0,0 +1,47 @@
|
||||
# gitbook
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 24b785c: Update shiki for code block syntax highlighting, with support for more languages and fixes for diffs. It also patches the deployment on Cloudflare to support edge functions larger than 4MB.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- acc3f2f: Fix error with the "Try it" of OpenAPI block because of the Scalar proxy failing on Cloudflare with the `cache` option
|
||||
- Updated dependencies [709f1a1]
|
||||
- Updated dependencies [ede2335]
|
||||
- Updated dependencies [0426312]
|
||||
- @gitbook/react-openapi@0.6.0
|
||||
|
||||
## 0.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3445db4]
|
||||
- @gitbook/react-contentkit@0.5.0
|
||||
- @gitbook/react-openapi@0.5.0
|
||||
- @gitbook/react-math@0.5.0
|
||||
|
||||
## 0.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [24cd72e]
|
||||
- @gitbook/react-contentkit@0.4.0
|
||||
- @gitbook/react-math@0.4.0
|
||||
- @gitbook/react-openapi@0.4.0
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- de747b7: Refactor the repository to be a proper monorepo and publish JS files on NPM instead of TypeScript files.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [de747b7]
|
||||
- Updated dependencies [de747b7]
|
||||
- @gitbook/react-contentkit@0.3.0
|
||||
- @gitbook/react-openapi@0.3.0
|
||||
- @gitbook/react-math@0.3.0
|
||||
@@ -1,8 +1,10 @@
|
||||
import { DeepPartial } from 'ts-essentials';
|
||||
import { argosScreenshot } from '@argos-ci/playwright';
|
||||
import {
|
||||
CustomizationHeaderPreset,
|
||||
CustomizationIconsStyle,
|
||||
CustomizationLocale,
|
||||
CustomizationSettings,
|
||||
SiteCustomizationSettings,
|
||||
} from '@gitbook/api';
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
import jwt from 'jsonwebtoken';
|
||||
@@ -106,6 +108,93 @@ const testCases: TestsCase[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'GitBook Site (Navigation when switching variant)',
|
||||
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/',
|
||||
tests: [
|
||||
{
|
||||
name: 'Keep navigation path/route when switching variant (Public)',
|
||||
url: 'api-multi-versions/reference/api-reference/pets',
|
||||
screenshot: false,
|
||||
run: async (page) => {
|
||||
const spaceDrowpdown = await page.waitForSelector(
|
||||
'[data-testid="space-dropdown-button"]',
|
||||
);
|
||||
await spaceDrowpdown.click();
|
||||
|
||||
// Click the second variant in the dropdown
|
||||
await page
|
||||
.getByRole('link', {
|
||||
name: '2.0',
|
||||
})
|
||||
.click();
|
||||
|
||||
// It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant
|
||||
await page.waitForURL(
|
||||
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions/v/2.0/reference/api-reference/pets?fallback=true',
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Keep navigation path/route when switching variant (Share link)',
|
||||
url: 'api-multi-versions-share-links/bRfQbzwsK8rbN1GRxx7K/reference/api-reference/pets',
|
||||
screenshot: false,
|
||||
run: async (page) => {
|
||||
const spaceDrowpdown = await page.waitForSelector(
|
||||
'[data-testid="space-dropdown-button"]',
|
||||
);
|
||||
await spaceDrowpdown.click();
|
||||
|
||||
// Click the second variant in the dropdown
|
||||
await page
|
||||
.getByRole('link', {
|
||||
name: '2.0',
|
||||
})
|
||||
.click();
|
||||
|
||||
// It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant
|
||||
await page.waitForURL(
|
||||
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-share-links/bRfQbzwsK8rbN1GRxx7K/v/2.0/reference/api-reference/pets?fallback=true',
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Keep navigation path/route when switching variant (VA)',
|
||||
screenshot: false,
|
||||
url: (() => {
|
||||
const privateKey = 'c26190fc-74b2-4b54-9fc7-df9941104953';
|
||||
const token = jwt.sign(
|
||||
{
|
||||
name: 'gitbook-open-tests',
|
||||
},
|
||||
privateKey,
|
||||
{
|
||||
expiresIn: '24h',
|
||||
},
|
||||
);
|
||||
return `api-multi-versions-va/reference/api-reference/pets?jwt_token=${token}`;
|
||||
})(),
|
||||
run: async (page) => {
|
||||
const spaceDrowpdown = await page.waitForSelector(
|
||||
'[data-testid="space-dropdown-button"]',
|
||||
);
|
||||
await spaceDrowpdown.click();
|
||||
|
||||
// Click the second variant in the dropdown
|
||||
await page
|
||||
.getByRole('link', {
|
||||
name: '2.0',
|
||||
})
|
||||
.click();
|
||||
|
||||
// It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant
|
||||
await page.waitForURL(
|
||||
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-va/v/2.0/reference/api-reference/pets?fallback=true',
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'GitBook',
|
||||
baseUrl: 'https://docs.gitbook.com',
|
||||
@@ -143,7 +232,7 @@ const testCases: TestsCase[] = [
|
||||
},
|
||||
{
|
||||
name: 'Versioning',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-gitbook-open/',
|
||||
tests: [
|
||||
{
|
||||
name: 'Revision',
|
||||
@@ -154,7 +243,7 @@ const testCases: TestsCase[] = [
|
||||
},
|
||||
{
|
||||
name: 'PDF',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-gitbook-open/',
|
||||
tests: [
|
||||
{
|
||||
name: 'PDF',
|
||||
@@ -164,7 +253,7 @@ const testCases: TestsCase[] = [
|
||||
},
|
||||
{
|
||||
name: 'Content tests',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-gitbook-open/',
|
||||
tests: [
|
||||
{
|
||||
name: 'Text',
|
||||
@@ -262,6 +351,11 @@ const testCases: TestsCase[] = [
|
||||
url: 'blocks/embeds',
|
||||
fullPage: true,
|
||||
},
|
||||
{
|
||||
name: 'Page links',
|
||||
url: 'blocks/page-links',
|
||||
fullPage: true,
|
||||
},
|
||||
{
|
||||
name: 'Annotations',
|
||||
url: 'blocks/annotations',
|
||||
@@ -274,8 +368,13 @@ const testCases: TestsCase[] = [
|
||||
},
|
||||
{
|
||||
name: 'Page options',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-gitbook-open/',
|
||||
tests: [
|
||||
{
|
||||
name: 'Hidden',
|
||||
url: 'page-options/page-hidden',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'With cover',
|
||||
url: 'page-options/page-with-cover',
|
||||
@@ -291,11 +390,16 @@ const testCases: TestsCase[] = [
|
||||
url: 'page-options/page-with-cover-and-no-toc',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'With icon',
|
||||
url: 'page-options/page-with-icon',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Customization',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-gitbook-open/',
|
||||
tests: [
|
||||
{
|
||||
name: 'Without header',
|
||||
@@ -307,16 +411,37 @@ const testCases: TestsCase[] = [
|
||||
}),
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'With duotone icons',
|
||||
url:
|
||||
'page-options/page-with-icon' +
|
||||
getCustomizationURL({
|
||||
styling: {
|
||||
icons: CustomizationIconsStyle.Duotone,
|
||||
},
|
||||
}),
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Ads',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-gitbook-open/',
|
||||
tests: [
|
||||
{
|
||||
name: 'Without previewed ads',
|
||||
url: 'text-page?ads_preview=1',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Share links',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-share-links/',
|
||||
baseUrl: 'https://gitbook.gitbook.io/gbo-tests-share-links/',
|
||||
tests: [
|
||||
{
|
||||
name: 'Valid link',
|
||||
url: 'Fc6mMII9FKgnwm7qqynx/',
|
||||
run: waitForCookiesDialog,
|
||||
url: 'TGs8PkF4GWVtbmPnWhYL/',
|
||||
},
|
||||
{
|
||||
name: 'Invalid link',
|
||||
@@ -519,13 +644,12 @@ const testCases: TestsCase[] = [
|
||||
},
|
||||
{
|
||||
name: 'Languages',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-gitbook-open/',
|
||||
tests: allLocales.map((locale) => ({
|
||||
name: locale,
|
||||
url: getCustomizationURL({
|
||||
internationalization: {
|
||||
locale,
|
||||
inherit: false,
|
||||
},
|
||||
}),
|
||||
run: async (page) => {
|
||||
@@ -566,7 +690,7 @@ for (const testCase of testCases) {
|
||||
/**
|
||||
* Create a URL with customization settings.
|
||||
*/
|
||||
function getCustomizationURL(partial: Partial<CustomizationSettings>): string {
|
||||
function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettings>): string {
|
||||
const encoded = rison.encode_object(partial);
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
@@ -7,9 +7,11 @@ module.exports = withSentryConfig(
|
||||
SENTRY_DSN: process.env.SENTRY_DSN ?? '',
|
||||
SENTRY_ENVIRONMENT: process.env.SENTRY_ENVIRONMENT ?? 'development',
|
||||
GITBOOK_ASSETS_PREFIX: process.env.GITBOOK_ASSETS_PREFIX,
|
||||
GITBOOK_ICONS_URL: process.env.GITBOOK_ICONS_URL,
|
||||
GITBOOK_ICONS_TOKEN: process.env.GITBOOK_ICONS_TOKEN,
|
||||
},
|
||||
|
||||
webpack(config) {
|
||||
webpack(config, { dev, webpack }) {
|
||||
config.resolve.fallback = {
|
||||
...config.resolve.fallback,
|
||||
|
||||
@@ -19,6 +21,21 @@ module.exports = withSentryConfig(
|
||||
http: false,
|
||||
};
|
||||
|
||||
// Tree shake debug code for Sentry
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/tree-shaking/#tree-shaking-with-nextjs
|
||||
if (!dev) {
|
||||
config.plugins.push(
|
||||
new webpack.DefinePlugin({
|
||||
__SENTRY_DEBUG__: false,
|
||||
// We always init Sentry with enableTracing: false for now, so this is useless
|
||||
__SENTRY_TRACING__: false,
|
||||
__RRWEB_EXCLUDE_IFRAME__: true,
|
||||
__RRWEB_EXCLUDE_SHADOW_DOM__: true,
|
||||
__SENTRY_EXCLUDE_REPLAY_WORKER__: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
|
||||
@@ -39,6 +56,15 @@ module.exports = withSentryConfig(
|
||||
|
||||
assetPrefix: process.env.GITBOOK_ASSETS_PREFIX,
|
||||
poweredByHeader: false,
|
||||
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: '*.gitbook.io',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
silent: true,
|
||||
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"name": "gitbook",
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "env-cmd --silent -f ../../.env.local next dev",
|
||||
"build": "next build",
|
||||
"build:cloudflare": "next-on-pages",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"e2e": "playwright test",
|
||||
"unit": "bun test {src,packages}",
|
||||
"generate": "gitbook-icons ./public/~gitbook/static/icons custom-icons && gitbook-math ./public/~gitbook/static/math",
|
||||
"copy:icons": "gitbook-icons ./public/~gitbook/static/icons"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gitbook/api": "^0.60.0",
|
||||
"@gitbook/icons": "workspace:*",
|
||||
"@gitbook/react-math": "workspace:*",
|
||||
"@gitbook/react-openapi": "workspace:*",
|
||||
"@gitbook/react-contentkit": "workspace:*",
|
||||
"@gitbook/emoji-codepoints": "workspace:*",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@sentry/nextjs": "^7.94.1",
|
||||
"@sindresorhus/fnv1a": "^3.1.0",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@upstash/redis": "^1.27.1",
|
||||
"ajv": "^8.12.0",
|
||||
"assert-never": "^1.2.1",
|
||||
"bun-types": "^1.1.20",
|
||||
"classnames": "^2.5.1",
|
||||
"content-security-policy-merger": "^1.0.0",
|
||||
"framer-motion": "^10.16.14",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jsontoxml": "^1.0.1",
|
||||
"katex": "^0.16.9",
|
||||
"mathjax": "^3.2.2",
|
||||
"memoizee": "^0.4.15",
|
||||
"next": "^14.2.5",
|
||||
"next-themes": "^0.2.1",
|
||||
"nuqs": "^1.17.4",
|
||||
"object-hash": "^3.0.0",
|
||||
"openapi-types": "^12.1.3",
|
||||
"p-map": "^7.0.0",
|
||||
"parse-cache-control": "^1.0.1",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-hotkeys-hook": "^4.4.1",
|
||||
"recoil": "^0.7.7",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"rehype-stringify": "^10.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.0",
|
||||
"rison": "^0.1.1",
|
||||
"server-only": "^0.0.1",
|
||||
"shiki": "^1.11.1",
|
||||
"tailwind-merge": "^2.2.0",
|
||||
"tailwind-shades": "^1.1.2",
|
||||
"unified": "^11.0.4",
|
||||
"url-join": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@argos-ci/playwright": "^2.0.0",
|
||||
"@playwright/test": "^1.42.1",
|
||||
"@cloudflare/next-on-pages": "^1.12.1",
|
||||
"@cloudflare/workers-types": "^4.20240725.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/jsontoxml": "^1.0.5",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/node": "^20",
|
||||
"@types/object-hash": "^3.0.6",
|
||||
"@types/parse-cache-control": "^1.0.4",
|
||||
"@types/psi": "^4.1.6",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/rison": "^0.0.9",
|
||||
"autoprefixer": "^10",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "^14.2.5",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"postcss": "^8",
|
||||
"psi": "^4.1.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"typescript": "^5.5.3",
|
||||
"env-cmd": "^10.1.0",
|
||||
"ts-essentials": "^10.0.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import {
|
||||
BrowserClient,
|
||||
makeFetchTransport,
|
||||
defaultStackParser,
|
||||
getCurrentScope,
|
||||
} from '@sentry/nextjs';
|
||||
|
||||
const dsn = process.env.SENTRY_DSN;
|
||||
if (dsn) {
|
||||
// To tree shake default integrations that we don't use
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/tree-shaking/#tree-shaking-default-integrations
|
||||
const client = new BrowserClient({
|
||||
debug: false,
|
||||
dsn,
|
||||
integrations: [],
|
||||
sampleRate: 0.1,
|
||||
enableTracing: false,
|
||||
beforeSendTransaction: () => {
|
||||
return null;
|
||||
},
|
||||
transport: makeFetchTransport,
|
||||
stackParser: defaultStackParser,
|
||||
});
|
||||
|
||||
getCurrentScope().setClient(client);
|
||||
client.init();
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import { init } from '@sentry/nextjs';
|
||||
|
||||
const dsn = process.env.SENTRY_DSN;
|
||||
if (dsn) {
|
||||
Sentry.init({
|
||||
init({
|
||||
debug: false,
|
||||
dsn,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import { init } from '@sentry/nextjs';
|
||||
|
||||
const dsn = process.env.SENTRY_DSN;
|
||||
if (dsn) {
|
||||
Sentry.init({
|
||||
init({
|
||||
debug: false,
|
||||
dsn,
|
||||
|
||||
+21
-4
@@ -1,6 +1,12 @@
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
import { verifyImageSignature, resizeImage, CloudflareImageOptions } from '@/lib/images';
|
||||
import {
|
||||
verifyImageSignature,
|
||||
resizeImage,
|
||||
CloudflareImageOptions,
|
||||
checkIsSizableImageURL,
|
||||
} from '@/lib/images';
|
||||
import { parseImageAPIURL } from '@/lib/urls';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
@@ -10,19 +16,30 @@ export const runtime = 'edge';
|
||||
* Fetch and resize an image.
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
const url = request.nextUrl.searchParams.get('url');
|
||||
let urlParam = request.nextUrl.searchParams.get('url');
|
||||
const signature = request.nextUrl.searchParams.get('sign');
|
||||
if (!url || !signature) {
|
||||
// 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
|
||||
if (!checkIsSizableImageURL(url)) {
|
||||
return new Response('Invalid url parameter', { status: 400 });
|
||||
}
|
||||
|
||||
// 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 });
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
'use client';
|
||||
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
import React from 'react';
|
||||
|
||||
import { useScrollToHash } from '@/components/hooks';
|
||||
|
||||
/**
|
||||
* Client component to initialize interactivity for a page.
|
||||
*/
|
||||
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 the hash.
|
||||
useScrollToHash();
|
||||
|
||||
useStripFallbackQueryParam();
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip the fallback query parameter from current URL.
|
||||
*
|
||||
* When the user switches variants using the space dropdown, we pass a fallback=true parameter.
|
||||
* This parameter indicates that we should redirect to the root page if the path from the
|
||||
* previous variant doesn't exist in the new variant. If the path does exist, no redirect occurs,
|
||||
* so we need to remove the fallback parameter.
|
||||
*/
|
||||
function useStripFallbackQueryParam() {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (searchParams.has('fallback')) {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.delete('fallback');
|
||||
router.push(`${pathname}?${params.toString()}${window.location.hash ?? ''}`);
|
||||
}
|
||||
}, [router, pathname, searchParams]);
|
||||
}
|
||||
+70
-10
@@ -6,9 +6,10 @@ import React from 'react';
|
||||
import { PageAside } from '@/components/PageAside';
|
||||
import { PageBody, PageCover } from '@/components/PageBody';
|
||||
import { PageHrefContext, absoluteHref, pageHref } from '@/lib/links';
|
||||
import { getPagePath } from '@/lib/pages';
|
||||
import { getPagePath, resolveFirstDocument } from '@/lib/pages';
|
||||
import { ContentRefContext } from '@/lib/references';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { getContentTitle } from '@/lib/utils';
|
||||
|
||||
import { PageClientLayout } from './PageClientLayout';
|
||||
import { PagePathParams, fetchPageData, getPathnameParam, normalizePathname } from '../../fetch';
|
||||
@@ -18,14 +19,29 @@ export const runtime = 'edge';
|
||||
/**
|
||||
* Fetch and render a page.
|
||||
*/
|
||||
export default async function Page(props: { params: PagePathParams }) {
|
||||
const { params } = props;
|
||||
export default async function Page(props: {
|
||||
params: PagePathParams;
|
||||
searchParams: { fallback?: string };
|
||||
}) {
|
||||
const { params, searchParams } = props;
|
||||
|
||||
const {
|
||||
content: contentPointer,
|
||||
contentTarget,
|
||||
space,
|
||||
parent,
|
||||
customization,
|
||||
pages,
|
||||
page,
|
||||
document,
|
||||
} = await getPageDataWithFallback({
|
||||
pagePathParams: params,
|
||||
searchParams,
|
||||
redirectOnFallback: true,
|
||||
});
|
||||
|
||||
const rawPathname = getPathnameParam(params);
|
||||
const { contentTarget, space, customization, pages, page, document } =
|
||||
await fetchPageData(params);
|
||||
const linksContext: PageHrefContext = {};
|
||||
|
||||
const rawPathname = getPathnameParam(params);
|
||||
if (!page) {
|
||||
const pathname = normalizePathname(rawPathname);
|
||||
if (pathname !== rawPathname) {
|
||||
@@ -48,6 +64,7 @@ export default async function Page(props: { params: PagePathParams }) {
|
||||
const withPageFeedback = customization.feedback.enabled;
|
||||
|
||||
const contentRefContext: ContentRefContext = {
|
||||
siteContext: 'siteId' in contentPointer ? contentPointer : null,
|
||||
space,
|
||||
revisionId: contentTarget.revisionId,
|
||||
pages,
|
||||
@@ -62,6 +79,7 @@ export default async function Page(props: { params: PagePathParams }) {
|
||||
<div className={tcls('flex', 'flex-row')}>
|
||||
<PageBody
|
||||
space={space}
|
||||
contentPointer={contentPointer}
|
||||
contentTarget={contentTarget}
|
||||
customization={customization}
|
||||
context={contentRefContext}
|
||||
@@ -75,6 +93,7 @@ export default async function Page(props: { params: PagePathParams }) {
|
||||
{page.layout.outline ? (
|
||||
<PageAside
|
||||
space={space}
|
||||
site={parent?.object === 'site' ? parent : undefined}
|
||||
customization={customization}
|
||||
page={page}
|
||||
document={document}
|
||||
@@ -103,14 +122,24 @@ export async function generateViewport({ params }: { params: PagePathParams }):
|
||||
};
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: PagePathParams }): Promise<Metadata> {
|
||||
const { space, pages, page, customization, parent } = await fetchPageData(params);
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: PagePathParams;
|
||||
searchParams: { fallback?: string };
|
||||
}): Promise<Metadata> {
|
||||
const { space, pages, page, customization, parent } = await getPageDataWithFallback({
|
||||
pagePathParams: params,
|
||||
searchParams,
|
||||
});
|
||||
|
||||
if (!page) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return {
|
||||
title: [page.title, customization.title ?? space.title, parent?.title]
|
||||
title: [page.title, getContentTitle(space, customization, parent)]
|
||||
.filter(Boolean)
|
||||
.join(' | '),
|
||||
description: page.description ?? '',
|
||||
@@ -125,3 +154,34 @@ export async function generateMetadata({ params }: { params: PagePathParams }):
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the page data matching the requested pathname and fallback to root page when page is not found.
|
||||
*/
|
||||
async function getPageDataWithFallback(args: {
|
||||
pagePathParams: PagePathParams;
|
||||
searchParams: { fallback?: string };
|
||||
redirectOnFallback?: boolean;
|
||||
}) {
|
||||
const { pagePathParams, searchParams, redirectOnFallback = false } = args;
|
||||
|
||||
const { pages, page: targetPage, ...otherPageData } = await fetchPageData(pagePathParams);
|
||||
|
||||
let page = targetPage;
|
||||
const canFallback = !!searchParams.fallback;
|
||||
if (!page && canFallback) {
|
||||
const rootPage = resolveFirstDocument(pages, []);
|
||||
|
||||
if (redirectOnFallback && rootPage?.page) {
|
||||
redirect(pageHref(pages, rootPage?.page));
|
||||
}
|
||||
|
||||
page = rootPage?.page;
|
||||
}
|
||||
|
||||
return {
|
||||
...otherPageData,
|
||||
pages,
|
||||
page,
|
||||
};
|
||||
}
|
||||
+9
-1
@@ -8,10 +8,13 @@ import { AdminToolbar } from '@/components/AdminToolbar';
|
||||
import { CookiesToast } from '@/components/Cookies';
|
||||
import { LoadIntegrations } from '@/components/Integrations';
|
||||
import { SpaceLayout } from '@/components/SpaceLayout';
|
||||
import { api } from '@/lib/api';
|
||||
import { assetsDomain } from '@/lib/assets';
|
||||
import { buildVersion } from '@/lib/build';
|
||||
import { getContentSecurityPolicyNonce } from '@/lib/csp';
|
||||
import { absoluteHref, baseUrl } from '@/lib/links';
|
||||
import { shouldIndexSpace } from '@/lib/seo';
|
||||
import { getContentTitle } from '@/lib/utils';
|
||||
|
||||
import { ClientContexts } from './ClientContexts';
|
||||
import { RocketLoaderDetector } from './RocketLoaderDetector';
|
||||
@@ -38,6 +41,11 @@ export default async function ContentLayout(props: { children: React.ReactNode }
|
||||
scripts,
|
||||
} = await fetchSpaceData();
|
||||
|
||||
ReactDOM.preconnect(api().endpoint);
|
||||
if (assetsDomain) {
|
||||
ReactDOM.preconnect(assetsDomain);
|
||||
}
|
||||
|
||||
scripts.forEach(({ script }) => {
|
||||
ReactDOM.preload(script, {
|
||||
as: 'script',
|
||||
@@ -104,7 +112,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
||||
const customIcon = 'icon' in customization.favicon ? customization.favicon.icon : null;
|
||||
|
||||
return {
|
||||
title: `${parent ? parent.title : customization.title ?? space.title}`,
|
||||
title: getContentTitle(space, customization, parent),
|
||||
generator: `GitBook (${buildVersion()})`,
|
||||
metadataBase: new URL(baseUrl()),
|
||||
icons: {
|
||||
+4
-1
@@ -14,7 +14,10 @@ export const runtime = 'edge';
|
||||
*/
|
||||
export async function GET(req: NextRequest) {
|
||||
const pointer = getContentPointer();
|
||||
const space = await getSpace(pointer.spaceId);
|
||||
const space = await getSpace(
|
||||
pointer.spaceId,
|
||||
'siteId' in pointer ? pointer.siteShareKey : undefined,
|
||||
);
|
||||
const parent =
|
||||
'siteId' in pointer
|
||||
? await getSite(pointer.organizationId, pointer.siteId)
|
||||
+11
-4
@@ -14,8 +14,12 @@ export const runtime = 'edge';
|
||||
* Generate a sitemap.xml for the current space.
|
||||
*/
|
||||
export async function GET(req: NextRequest) {
|
||||
const { pages: rootPages } = await getSpaceContentData(getContentPointer());
|
||||
const pages = flattenPages(rootPages);
|
||||
const pointer = getContentPointer();
|
||||
const { pages: rootPages } = await getSpaceContentData(
|
||||
pointer,
|
||||
'siteId' in pointer ? pointer.siteShareKey : undefined,
|
||||
);
|
||||
const pages = flattenPages(rootPages, (page) => !page.hidden);
|
||||
const urls = pages.map(({ page, depth }) => {
|
||||
// Decay priority with depth
|
||||
const priority = Math.pow(2, -0.25 * depth);
|
||||
@@ -66,13 +70,16 @@ export async function GET(req: NextRequest) {
|
||||
|
||||
type FlatPageEntry = { page: RevisionPageDocument; depth: number };
|
||||
|
||||
function flattenPages(rootPags: RevisionPage[]): FlatPageEntry[] {
|
||||
function flattenPages(
|
||||
rootPags: RevisionPage[],
|
||||
filter: (page: RevisionPageDocument) => boolean,
|
||||
): FlatPageEntry[] {
|
||||
const flattenPage = (
|
||||
page: RevisionPageDocument | RevisionPageGroup,
|
||||
depth: number,
|
||||
): FlatPageEntry[] => {
|
||||
return [
|
||||
...(page.type === 'document' ? [{ page, depth }] : []),
|
||||
...(page.type === 'document' && filter(page) ? [{ page, depth }] : []),
|
||||
...page.pages.flatMap((child) =>
|
||||
child.type === 'link' ? [] : flattenPage(child, depth + 1),
|
||||
),
|
||||
+3
-2
@@ -14,6 +14,7 @@ import {
|
||||
} from '@/lib/api';
|
||||
import { getEmojiForCode } from '@/lib/emojis';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { getContentTitle } from '@/lib/utils';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
@@ -45,7 +46,7 @@ export async function GET(req: NextRequest) {
|
||||
const spaceId = pointer.spaceId;
|
||||
|
||||
const [space, customization] = await Promise.all([
|
||||
getSpace(spaceId),
|
||||
getSpace(spaceId, 'siteId' in pointer ? pointer.siteShareKey : undefined),
|
||||
'siteId' in pointer ? getCurrentSiteCustomization(pointer) : getSpaceCustomization(spaceId),
|
||||
]);
|
||||
const parent =
|
||||
@@ -54,7 +55,7 @@ export async function GET(req: NextRequest) {
|
||||
: space.visibility === ContentVisibility.InCollection && space.parent
|
||||
? await getCollection(space.parent)
|
||||
: null;
|
||||
const contentTitle = parent?.title ?? customization.title ?? space.title;
|
||||
const contentTitle = getContentTitle(space, customization, parent);
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
+3
-1
@@ -3,6 +3,8 @@ 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';
|
||||
@@ -31,7 +33,7 @@ export async function GET(req: NextRequest, { params }: { params: PageIdParams }
|
||||
}}
|
||||
>
|
||||
<h2 tw="text-7xl font-bold tracking-tight text-left">
|
||||
{parent?.title ?? customization.title ?? space.title}
|
||||
{getContentTitle(space, customization, parent)}
|
||||
</h2>
|
||||
<div tw="flex flex-1">
|
||||
<p tw="text-4xl">{page ? page.title : 'Not found'}</p>
|
||||
+5
-2
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { AlertTriangle } from '@geist-ui/icons';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import React from 'react';
|
||||
|
||||
import { useScrollActiveId } from '@/components/hooks';
|
||||
@@ -105,7 +105,10 @@ export function PageControlButtons(props: {
|
||||
'max-w-sm',
|
||||
)}
|
||||
>
|
||||
<AlertTriangle className={tcls('size-6', 'mr-3', 'mt-1')} />{' '}
|
||||
<Icon
|
||||
icon="triangle-exclamation"
|
||||
className={tcls('size-6', 'mr-3', 'mt-1')}
|
||||
/>{' '}
|
||||
<div>
|
||||
<div>{t(language, 'pdf_limit_reached', total, pageIds.length)}</div>
|
||||
<div>
|
||||
+34
-11
@@ -1,4 +1,3 @@
|
||||
import { ArrowLeft, Printer } from '@geist-ui/icons';
|
||||
import {
|
||||
CustomizationSettings,
|
||||
Revision,
|
||||
@@ -7,6 +6,7 @@ import {
|
||||
SiteCustomizationSettings,
|
||||
Space,
|
||||
} from '@gitbook/api';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
import * as React from 'react';
|
||||
@@ -41,7 +41,10 @@ export const runtime = 'edge';
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const contentPointer = getContentPointer();
|
||||
const [space, customization] = await Promise.all([
|
||||
getSpace(contentPointer.spaceId),
|
||||
getSpace(
|
||||
contentPointer.spaceId,
|
||||
'siteId' in contentPointer ? contentPointer.siteShareKey : undefined,
|
||||
),
|
||||
'siteId' in contentPointer
|
||||
? getCurrentSiteCustomization(contentPointer)
|
||||
: getSpaceCustomization(contentPointer.spaceId),
|
||||
@@ -71,7 +74,10 @@ export default async function PDFHTMLOutput(props: { searchParams: { [key: strin
|
||||
'siteId' in contentPointer
|
||||
? getCurrentSiteCustomization(contentPointer)
|
||||
: getSpaceCustomization(contentPointer.spaceId),
|
||||
getSpaceContentData(contentPointer),
|
||||
getSpaceContentData(
|
||||
contentPointer,
|
||||
'siteId' in contentPointer ? contentPointer.siteShareKey : undefined,
|
||||
),
|
||||
]);
|
||||
const language = getSpaceLanguage(customization);
|
||||
|
||||
@@ -109,7 +115,7 @@ export default async function PDFHTMLOutput(props: { searchParams: { [key: strin
|
||||
'border',
|
||||
)}
|
||||
>
|
||||
<ArrowLeft className={tcls('size-6')} />
|
||||
<Icon icon="arrow-left" className={tcls('size-6')} />
|
||||
</a>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -135,7 +141,7 @@ export default async function PDFHTMLOutput(props: { searchParams: { [key: strin
|
||||
'border',
|
||||
)}
|
||||
>
|
||||
<Printer className={tcls('size-6')} />
|
||||
<Icon icon="print" className={tcls('size-6')} />
|
||||
</PrintButton>
|
||||
</div>
|
||||
|
||||
@@ -169,6 +175,7 @@ export default async function PDFHTMLOutput(props: { searchParams: { [key: strin
|
||||
space={space}
|
||||
page={page}
|
||||
refContext={{
|
||||
siteContext: 'siteId' in contentPointer ? contentPointer : null,
|
||||
space,
|
||||
revisionId: contentTarget.revisionId,
|
||||
pages: rootPages,
|
||||
@@ -313,9 +320,17 @@ function selectPages(
|
||||
): FlatPageEntry[] => {
|
||||
return [
|
||||
{ page, depth },
|
||||
...page.pages.flatMap((child) =>
|
||||
child.type === 'link' ? [] : flattenPage(child, depth + 1),
|
||||
),
|
||||
...page.pages.flatMap((child) => {
|
||||
if (child.type === 'link') {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (child.hidden) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return flattenPage(child, depth + 1);
|
||||
}),
|
||||
];
|
||||
};
|
||||
|
||||
@@ -340,8 +355,16 @@ function selectPages(
|
||||
return limitTo(flattenPage(found.page, 0));
|
||||
}
|
||||
|
||||
const allPages = rootPages.flatMap((page) =>
|
||||
page.type === 'link' ? [] : flattenPage(page, 0),
|
||||
);
|
||||
const allPages = rootPages.flatMap((page) => {
|
||||
if (page.type === 'link') {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (page.hidden) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return flattenPage(page, 0);
|
||||
});
|
||||
return limitTo(allPages);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import { captureException } from '@sentry/nextjs';
|
||||
import React from 'react';
|
||||
|
||||
import { Button } from '@/components/primitives/Button';
|
||||
@@ -15,7 +15,7 @@ export default function ErrorPage(props: {
|
||||
const language = useLanguage();
|
||||
|
||||
React.useEffect(() => {
|
||||
Sentry.captureException(error);
|
||||
captureException(error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
@@ -40,6 +40,7 @@ export function getContentPointer(): ContentPointer | SiteContentPointer {
|
||||
if (siteId) {
|
||||
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');
|
||||
}
|
||||
@@ -48,6 +49,7 @@ export function getContentPointer(): ContentPointer | 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,
|
||||
@@ -69,10 +71,19 @@ export function getContentPointer(): ContentPointer | SiteContentPointer {
|
||||
export async function fetchSpaceData() {
|
||||
const content = getContentPointer();
|
||||
|
||||
const siteShareKey = 'siteId' in content ? content.siteShareKey : undefined;
|
||||
|
||||
const [{ space, contentTarget, pages, customization, scripts }, parentSite] = await Promise.all(
|
||||
'siteId' in content
|
||||
? [getCurrentSiteData(content), fetchParentSite(content.organizationId, content.siteId)]
|
||||
: [getSpaceData(content)],
|
||||
? [
|
||||
getCurrentSiteData(content),
|
||||
fetchParentSite({
|
||||
organizationId: content.organizationId,
|
||||
siteId: content.siteId,
|
||||
siteShareKey,
|
||||
}),
|
||||
]
|
||||
: [getSpaceData(content, siteShareKey)],
|
||||
);
|
||||
|
||||
const parent = await (parentSite ?? fetchParentCollection(space));
|
||||
@@ -95,14 +106,19 @@ export async function fetchSpaceData() {
|
||||
*/
|
||||
export async function fetchPageData(params: PagePathParams | PageIdParams) {
|
||||
const content = getContentPointer();
|
||||
const siteShareKey = 'siteId' in content ? content.siteShareKey : undefined;
|
||||
const { space, contentTarget, pages, customization, scripts } = await ('siteId' in content
|
||||
? getCurrentSiteData(content)
|
||||
: getSpaceData(content));
|
||||
: getSpaceData(content, siteShareKey));
|
||||
|
||||
const page = await resolvePage(contentTarget, pages, params);
|
||||
const [parent, document] = await Promise.all([
|
||||
'siteId' in content
|
||||
? fetchParentSite(content.organizationId, content.siteId)
|
||||
? fetchParentSite({
|
||||
organizationId: content.organizationId,
|
||||
siteId: content.siteId,
|
||||
siteShareKey: content.siteShareKey,
|
||||
})
|
||||
: fetchParentCollection(space),
|
||||
page?.page.documentId ? getDocument(space.id, page.page.documentId) : null,
|
||||
]);
|
||||
@@ -173,10 +189,15 @@ async function fetchParentCollection(space: Space) {
|
||||
return { parent: collection, spaces };
|
||||
}
|
||||
|
||||
async function fetchParentSite(organizationId: string, siteId: string) {
|
||||
async function fetchParentSite(args: {
|
||||
organizationId: string;
|
||||
siteId: string;
|
||||
siteShareKey: string | undefined;
|
||||
}) {
|
||||
const { organizationId, siteId, siteShareKey } = args;
|
||||
const [site, siteSpaces] = await Promise.all([
|
||||
getSite(organizationId, siteId),
|
||||
getSiteSpaces(organizationId, siteId),
|
||||
getSiteSpaces({ organizationId, siteId, siteShareKey }),
|
||||
]);
|
||||
|
||||
const spaces: Record<string, Space> = {};
|
||||
@@ -1,10 +1,12 @@
|
||||
import {
|
||||
CustomizationBackground,
|
||||
CustomizationCorners,
|
||||
CustomizationIconsStyle,
|
||||
CustomizationHeaderPreset,
|
||||
CustomizationSettings,
|
||||
SiteCustomizationSettings,
|
||||
} from '@gitbook/api';
|
||||
import { IconsProvider, IconStyle } from '@gitbook/icons';
|
||||
import assertNever from 'assert-never';
|
||||
import colors from 'tailwindcss/colors';
|
||||
|
||||
@@ -12,11 +14,13 @@ import { emojiFontClassName } from '@/components/primitives';
|
||||
import { fonts, ibmPlexMono } from '@/fonts';
|
||||
import { getSpaceLanguage } from '@/intl/server';
|
||||
import { getCurrentSiteLayoutData, getSpaceLayoutData } from '@/lib/api';
|
||||
import { getStaticFileURL } from '@/lib/assets';
|
||||
import { hexToRgb, shadesOfColor } from '@/lib/colors';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { ClientContexts } from './ClientContexts';
|
||||
import './globals.css';
|
||||
import '@gitbook/icons/style.css';
|
||||
import { getContentPointer } from './fetch';
|
||||
|
||||
/**
|
||||
@@ -105,7 +109,22 @@ export default async function SpaceRootLayout(props: { children: React.ReactNode
|
||||
'dark:bg-dark',
|
||||
)}
|
||||
>
|
||||
<ClientContexts language={language}>{children}</ClientContexts>
|
||||
<IconsProvider
|
||||
assetsURL={process.env.GITBOOK_ICONS_URL ?? getStaticFileURL('icons')}
|
||||
assetsURLToken={process.env.GITBOOK_ICONS_TOKEN}
|
||||
assetsByStyles={{
|
||||
'custom-icons': {
|
||||
assetsURL: getStaticFileURL('icons'),
|
||||
},
|
||||
}}
|
||||
iconStyle={
|
||||
('icons' in customization.styling
|
||||
? apiToIconsStyles[customization.styling.icons]
|
||||
: null) || IconStyle.Regular
|
||||
}
|
||||
>
|
||||
<ClientContexts language={language}>{children}</ClientContexts>
|
||||
</IconsProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
@@ -188,3 +207,13 @@ function generateHeaderTheme(customization: CustomizationSettings | SiteCustomiz
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const apiToIconsStyles: {
|
||||
[key in CustomizationIconsStyle]: IconStyle;
|
||||
} = {
|
||||
[CustomizationIconsStyle.Regular]: IconStyle.Regular,
|
||||
[CustomizationIconsStyle.Solid]: IconStyle.Solid,
|
||||
[CustomizationIconsStyle.Duotone]: IconStyle.Duotone,
|
||||
[CustomizationIconsStyle.Thin]: IconStyle.Thin,
|
||||
[CustomizationIconsStyle.Light]: IconStyle.Light,
|
||||
};
|
||||
@@ -1,12 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import { captureException } from '@sentry/nextjs';
|
||||
import Error from 'next/error';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function GlobalError({ error }: { error: Error }) {
|
||||
useEffect(() => {
|
||||
Sentry.captureException(error);
|
||||
captureException(error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
@@ -30,7 +30,7 @@ export async function POST(req: NextRequest) {
|
||||
// no-cors, *cors, same-origin
|
||||
// mode: 'cors', // Not supported on Cloudflare Workers
|
||||
// *default, no-cache, reload, force-cache, only-if-cached
|
||||
cache: 'no-cache',
|
||||
// cache: 'no-cache', // Not supported on Cloudflare Workers
|
||||
// include, *same-origin, omit
|
||||
// credentials: 'include', // Not supported on Cloudflare Workers
|
||||
headers: requestBody.headers,
|
||||
@@ -0,0 +1,112 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import { ClassValue, tcls } from '@/lib/tailwind';
|
||||
|
||||
import { renderAd } from './renderAd';
|
||||
|
||||
/**
|
||||
* Zone ID provided by BuySellAds for the preview.
|
||||
*/
|
||||
const PREVIEW_ZONE_ID = 'CVAIKKQM';
|
||||
|
||||
/**
|
||||
* Fetch and render the Ad placement.
|
||||
* https://docs.buysellads.com/ad-serving-api
|
||||
*/
|
||||
export function Ad({
|
||||
zoneId,
|
||||
spaceId,
|
||||
placement,
|
||||
ignore,
|
||||
style,
|
||||
mode = 'auto',
|
||||
}: {
|
||||
zoneId: string | null;
|
||||
spaceId: string;
|
||||
placement: string;
|
||||
ignore: boolean;
|
||||
style?: ClassValue;
|
||||
mode?: 'classic' | 'auto' | 'cover';
|
||||
}) {
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
const [ad, setAd] = React.useState<React.ReactNode | undefined>(undefined);
|
||||
|
||||
// Observe the container visibility
|
||||
React.useEffect(() => {
|
||||
if (!containerRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setVisible(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
root: null,
|
||||
rootMargin: '0px',
|
||||
threshold: 0.1,
|
||||
},
|
||||
);
|
||||
|
||||
observer.observe(containerRef.current);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// When the container is visible,
|
||||
// track an impression on the ad and fetch it
|
||||
React.useEffect(() => {
|
||||
if (!visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
const preview = new URL(window.location.href).searchParams.has('ads_preview');
|
||||
const realZoneId = preview ? PREVIEW_ZONE_ID : zoneId;
|
||||
|
||||
if (!realZoneId) {
|
||||
return;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const result = await renderAd({
|
||||
spaceId,
|
||||
placement,
|
||||
ignore: ignore || preview,
|
||||
zoneId: realZoneId,
|
||||
mode,
|
||||
});
|
||||
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result) {
|
||||
setAd(result);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [visible, spaceId, 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);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={tcls(style)}>
|
||||
{ad ? ad : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { getResizedImageURL } from '@/lib/images';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { AdItem } from './types';
|
||||
|
||||
/**
|
||||
* Classic rendering for an ad.
|
||||
*/
|
||||
export async function AdClassicRendering({ ad }: { ad: AdItem }) {
|
||||
return (
|
||||
<a
|
||||
className={tcls(
|
||||
'flex',
|
||||
'flex-col',
|
||||
'gap-4',
|
||||
'bg-light-2',
|
||||
'text-dark/7',
|
||||
'dark:bg-dark-2',
|
||||
'dark:text-light/7',
|
||||
'hover:text-dark/9',
|
||||
'dark:hover:text-light/9',
|
||||
'rounded-lg',
|
||||
'p-4',
|
||||
)}
|
||||
href={ad.statlink}
|
||||
rel="sponsored noopener"
|
||||
target="_blank"
|
||||
>
|
||||
{'smallImage' in ad ? (
|
||||
<div>
|
||||
<img
|
||||
alt="Ads logo"
|
||||
className={tcls('rounded-md')}
|
||||
src={await getResizedImageURL(ad.smallImage, { width: 192, dpr: 2 })}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={tcls('px-6', 'py-4', 'rounded-md')}
|
||||
style={{ backgroundColor: ad.backgroundColor }}
|
||||
>
|
||||
<img
|
||||
alt="Ads logo"
|
||||
src={await getResizedImageURL(ad.logo, { width: 192 - 48, dpr: 2 })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={tcls('flex', 'flex-col')}>
|
||||
<div className={tcls('text-xs')}>{ad.description}</div>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { hexToRgba } from '@/lib/colors';
|
||||
import { getResizedImageURL } from '@/lib/images';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
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 });
|
||||
|
||||
return (
|
||||
<a
|
||||
className={tcls(
|
||||
'group/ad',
|
||||
'relative',
|
||||
'flex',
|
||||
'flex-col',
|
||||
'gap-4',
|
||||
'bg-light-2',
|
||||
'text-dark/7',
|
||||
'dark:bg-dark-2',
|
||||
'dark:text-light/7',
|
||||
'hover:text-dark/9',
|
||||
'dark:hover:text-light/9',
|
||||
'rounded-lg',
|
||||
'p-4',
|
||||
'overflow-hidden',
|
||||
'shadow-sm',
|
||||
)}
|
||||
style={{ backgroundColor: ad.backgroundColor, color: ad.textColor ?? '#ffffff' }}
|
||||
href={ad.statlink}
|
||||
rel="sponsored noopener"
|
||||
target="_blank"
|
||||
>
|
||||
<div
|
||||
className={tcls(
|
||||
'absolute',
|
||||
'inset-0',
|
||||
'bg-center',
|
||||
'bg-cover',
|
||||
'bg-no-repeat',
|
||||
'z-0',
|
||||
)}
|
||||
style={{
|
||||
backgroundImage: `url(${largeImage})`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className={tcls('z-[2]')}>
|
||||
<img
|
||||
alt="Large image"
|
||||
src={largeImage}
|
||||
className={tcls(
|
||||
'rounded-md',
|
||||
'shadow-md',
|
||||
'max-h-32',
|
||||
'group-hover/ad:max-h-16',
|
||||
'transition-all',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className={tcls('z-[2]')}>
|
||||
<img alt={ad.company} src={ad.logo} className={tcls('max-w-36', 'max-h-12')} />
|
||||
</div>
|
||||
<div className={tcls('flex', 'flex-col', 'z-[2]')}>
|
||||
<div className={tcls('text-sm', 'font-semibold', 'mb-2')}>{ad.companyTagline}</div>
|
||||
<div
|
||||
className={tcls(
|
||||
'text-xs',
|
||||
'h-0',
|
||||
'opacity-0',
|
||||
'group-hover/ad:h-16',
|
||||
'group-hover/ad:opacity-10',
|
||||
'transition-all',
|
||||
)}
|
||||
>
|
||||
{ad.description}
|
||||
</div>
|
||||
</div>
|
||||
<div className={tcls('z-[2]')}>
|
||||
<span
|
||||
className={tcls(
|
||||
'text-sm',
|
||||
'font-semibold',
|
||||
'shadow-lg',
|
||||
'rounded-md',
|
||||
'bg-white',
|
||||
'py-2',
|
||||
'px-4',
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: ad.ctaBackgroundColor,
|
||||
color: ad.ctaTextColor ?? ad.backgroundColor,
|
||||
}}
|
||||
>
|
||||
{ad.callToAction}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={tcls('absolute', 'inset-0', 'backdrop-blur', 'z-[1]')}
|
||||
style={{
|
||||
backgroundColor: hexToRgba(ad.backgroundColor, 0.8),
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
/**
|
||||
* Render attribution or verification pixels.
|
||||
* https://docs.buysellads.com/ad-serving-api#pixels
|
||||
*/
|
||||
export function AdPixels({ rawPixel }: { rawPixel: string }) {
|
||||
const pixels = rawPixel.split('||');
|
||||
const time = String(Math.round(Date.now() / 1e4) | 0);
|
||||
|
||||
return (
|
||||
<div className={tcls('hidden')}>
|
||||
{pixels.map((pixel, index) => {
|
||||
return (
|
||||
<img
|
||||
key={index}
|
||||
src={pixel.replace('[timestamp]', time)}
|
||||
width="1"
|
||||
height="1"
|
||||
style={{ display: 'none' }}
|
||||
alt="Ads tracking pixel"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Ad';
|
||||
@@ -0,0 +1,104 @@
|
||||
'use server';
|
||||
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { AdClassicRendering } from './AdClassicRendering';
|
||||
import { AdCoverRendering } from './AdCoverRendering';
|
||||
import { AdPixels } from './AdPixels';
|
||||
import { AdItem, AdsResponse } from './types';
|
||||
|
||||
interface FetchAdOptions {
|
||||
/** ID of the zone to fetch Ads for */
|
||||
zoneId: string;
|
||||
/** ID of the space */
|
||||
spaceId: string;
|
||||
/** Mode to render the Ad */
|
||||
mode: 'classic' | 'auto' | 'cover';
|
||||
/** Name of the placement for the ad */
|
||||
placement: string;
|
||||
/** If true, we'll not track it as an impression */
|
||||
ignore: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 { spaceId, mode } = options;
|
||||
const result = await fetchAd(options);
|
||||
if (!result || !result.ad.description || !result.ad.statlink) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { ad, ip } = result;
|
||||
|
||||
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);
|
||||
|
||||
return (
|
||||
<>
|
||||
{mode === 'classic' || !('callToAction' in ad) ? (
|
||||
<AdClassicRendering ad={ad} />
|
||||
) : (
|
||||
<AdCoverRendering ad={ad} />
|
||||
)}
|
||||
{ad.pixel ? <AdPixels rawPixel={ad.pixel} /> : null}
|
||||
<p
|
||||
className={tcls(
|
||||
'mt-2',
|
||||
'mr-2',
|
||||
'text-xs',
|
||||
'text-right',
|
||||
'text-dark/5',
|
||||
'dark:text-light/5',
|
||||
)}
|
||||
data-debug-ip={ip}
|
||||
>
|
||||
<a target="_blank" href={viaUrl.toString()} className={tcls('hover:underline')}>
|
||||
Sponsored via GitBook
|
||||
</a>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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') ?? '';
|
||||
|
||||
const url = new URL(`https://srv.buysellads.com/ads/${zoneId}.json`);
|
||||
url.searchParams.set('segment', `placement:${placement}`);
|
||||
url.searchParams.set('v', 'true');
|
||||
url.searchParams.set('forwardedip', ip);
|
||||
url.searchParams.set('useragent', userAgent);
|
||||
if (ignore) {
|
||||
url.searchParams.set('ignore', 'true');
|
||||
}
|
||||
|
||||
const res = await fetch(url);
|
||||
const json: AdsResponse = await res.json();
|
||||
|
||||
const first = json.ads[0];
|
||||
if (first && 'active' in first) {
|
||||
return { ad: first, ip };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
export interface AdGeneric {
|
||||
active: string;
|
||||
ad_via_link: string;
|
||||
bannerid: string;
|
||||
creativeid: string;
|
||||
evenodd: string;
|
||||
external_id: string;
|
||||
height: string;
|
||||
i: string;
|
||||
identifier: string;
|
||||
longimp: string;
|
||||
longlink: string;
|
||||
num_slots: string;
|
||||
statimp: string;
|
||||
statlink: string;
|
||||
timestamp: string;
|
||||
width: string;
|
||||
zoneid: string;
|
||||
zonekey: string;
|
||||
rendering: 'carbon';
|
||||
pixel?: string;
|
||||
}
|
||||
|
||||
export interface AdClassic extends AdGeneric {
|
||||
description: string;
|
||||
smallImage: string;
|
||||
}
|
||||
|
||||
export interface AdCover extends AdGeneric {
|
||||
backgroundColor: string;
|
||||
backgroundHoverColor?: string;
|
||||
textColor?: string;
|
||||
textColorHover?: string;
|
||||
callToAction: string;
|
||||
company: string;
|
||||
companyTagline: string;
|
||||
description: string;
|
||||
largeImage: string;
|
||||
image?: string;
|
||||
logo: string;
|
||||
ctaBackgroundColor?: string;
|
||||
ctaBackgroundHoverColor?: string;
|
||||
ctaTextColor?: string;
|
||||
ctaTextColorHover?: string;
|
||||
}
|
||||
|
||||
export type AdItem = AdClassic | AdCover;
|
||||
|
||||
export interface AdsResponse {
|
||||
ads: Array<AdItem | {}>;
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import IconX from '@geist-ui/icons/x';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import * as React from 'react';
|
||||
|
||||
import { Button } from '@/components/primitives';
|
||||
@@ -92,7 +92,7 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
|
||||
)}
|
||||
title={tString(language, 'cookies_close')}
|
||||
>
|
||||
<IconX className={tcls('w-4')} />
|
||||
<Icon icon="xmark" className={tcls('size-4')} />
|
||||
</button>
|
||||
<div className={tcls('mt-3', 'flex', 'flex-row', 'gap-2')}>
|
||||
<Button
|
||||
+1
@@ -27,6 +27,7 @@ export async function Annotation(props: InlineProps<DocumentInlineAnnotation>) {
|
||||
ancestorBlocks={[]}
|
||||
context={context}
|
||||
nodes={fragment.nodes}
|
||||
style={['space-y-4']}
|
||||
/>
|
||||
}
|
||||
>
|
||||
@@ -0,0 +1,73 @@
|
||||
import { DocumentBlockContentRef } from '@gitbook/api';
|
||||
|
||||
import { Card, Emoji } from '@/components/primitives';
|
||||
import { getSpaceCustomization, ignoreAPIError } from '@/lib/api';
|
||||
import { ResolvedContentRef } from '@/lib/references';
|
||||
|
||||
import { BlockProps } from './Block';
|
||||
import { SpaceIcon } from '../Space/SpaceIcon';
|
||||
|
||||
export async function BlockContentRef(props: BlockProps<DocumentBlockContentRef>) {
|
||||
const { block, context, style } = props;
|
||||
|
||||
const resolved = await context.resolveContentRef(block.data.ref, {
|
||||
resolveAnchorText: true,
|
||||
iconStyle: ['text-xl', 'text-dark/6', 'dark:text-light/6'],
|
||||
});
|
||||
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isContentInOtherSpace =
|
||||
context.contentRefContext?.space &&
|
||||
'space' in block.data.ref &&
|
||||
context.contentRefContext.space.id !== block.data.ref.space;
|
||||
const kind = block?.data?.ref?.kind;
|
||||
if ((resolved.active && kind === 'space') || isContentInOtherSpace) {
|
||||
return <SpaceRefCard {...props} resolved={resolved} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
leadingIcon={resolved.icon ? resolved.icon : null}
|
||||
href={resolved.href}
|
||||
title={resolved.text}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
async function SpaceRefCard(
|
||||
props: { resolved: ResolvedContentRef } & BlockProps<DocumentBlockContentRef>,
|
||||
) {
|
||||
const { context, style, resolved } = props;
|
||||
const spaceId = context.contentRefContext?.space.id;
|
||||
|
||||
if (!spaceId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const spaceCustomization = await ignoreAPIError(getSpaceCustomization(spaceId));
|
||||
const customFavicon = spaceCustomization?.favicon;
|
||||
const customEmoji = customFavicon && 'emoji' in customFavicon ? customFavicon.emoji : undefined;
|
||||
const customIcon = customFavicon && 'icon' in customFavicon ? customFavicon.icon : undefined;
|
||||
|
||||
return (
|
||||
<Card
|
||||
leadingIcon={
|
||||
<SpaceIcon
|
||||
icon={customIcon}
|
||||
emoji={customEmoji}
|
||||
alt=""
|
||||
sizes={[{ width: 24 }]}
|
||||
style={['object-contain', 'size-6']}
|
||||
/>
|
||||
}
|
||||
href={resolved.href}
|
||||
title={resolved.text}
|
||||
postTitle={resolved.subText}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -15,7 +15,7 @@ import './theme.css';
|
||||
*/
|
||||
export async function CodeBlock(props: BlockProps<DocumentBlockCode>) {
|
||||
const { block, document, style, context } = props;
|
||||
const withHighlighting = context.shouldHighlightCode();
|
||||
const withHighlighting = context.shouldHighlightCode(context.content?.spaceId);
|
||||
const lines = withHighlighting ? await highlight(block) : plainHighlighting(block);
|
||||
|
||||
const id = block.key!;
|
||||
+15
-1
@@ -1,5 +1,15 @@
|
||||
const CODE_HIGHLIGHT_BLOCK_LIMIT = 50;
|
||||
|
||||
/**
|
||||
* Spaces where we'll always use plain highlighting.
|
||||
*/
|
||||
const PLAIN_HIGHLIGHTING_SPACES: string[] = [
|
||||
'V9geAO9ITPi8WOYK5o0r',
|
||||
'puRmcwVxGFtHph8IjXaf',
|
||||
'e3jwbMOrr4RhKtZ9C0XL',
|
||||
'ryjzVNizLfd6pYogN3dm',
|
||||
];
|
||||
|
||||
/**
|
||||
* Protect against memory issues when highlighting a large number of code blocks.
|
||||
* This context only allows 50 code blocks per render to be highlighted.
|
||||
@@ -9,7 +19,11 @@ const CODE_HIGHLIGHT_BLOCK_LIMIT = 50;
|
||||
*/
|
||||
export function createHighlightingContext() {
|
||||
let count = 0;
|
||||
return () => {
|
||||
return (spaceId?: string | undefined) => {
|
||||
if (!spaceId || PLAIN_HIGHLIGHTING_SPACES.includes(spaceId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
count += 1;
|
||||
return count < CODE_HIGHLIGHT_BLOCK_LIMIT;
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user