mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-14 06:35:17 +00:00
Compare commits
95 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 |
@@ -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
|
||||
|
||||
+30
-11
@@ -4,7 +4,8 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
NPMRC_FONT_AWESOME_TOKEN: ${{ secrets.NPMRC_FONT_AWESOME_TOKEN }}
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy to Cloudflare Pages
|
||||
@@ -20,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:
|
||||
@@ -58,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
|
||||
@@ -82,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
|
||||
@@ -102,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 }}
|
||||
@@ -121,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
|
||||
@@ -136,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:
|
||||
@@ -151,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
|
||||
@@ -166,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/
|
||||
|
||||
@@ -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.46.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.11.3",
|
||||
"@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;
|
||||
},
|
||||
|
||||
@@ -45,9 +62,9 @@ module.exports = withSentryConfig(
|
||||
{
|
||||
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,
|
||||
|
||||
+16
-2
@@ -1,6 +1,11 @@
|
||||
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';
|
||||
@@ -13,6 +18,9 @@ export const runtime = 'edge';
|
||||
export async function GET(request: NextRequest) {
|
||||
let urlParam = request.nextUrl.searchParams.get('url');
|
||||
const signature = request.nextUrl.searchParams.get('sign');
|
||||
// The current signature algorithm sets version as 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 });
|
||||
}
|
||||
@@ -24,8 +32,14 @@ export async function GET(request: NextRequest) {
|
||||
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
@@ -7,6 +7,7 @@ const PLAIN_HIGHLIGHTING_SPACES: string[] = [
|
||||
'V9geAO9ITPi8WOYK5o0r',
|
||||
'puRmcwVxGFtHph8IjXaf',
|
||||
'e3jwbMOrr4RhKtZ9C0XL',
|
||||
'ryjzVNizLfd6pYogN3dm',
|
||||
];
|
||||
|
||||
/**
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user