mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-15 15:15:55 +00:00
Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b1de0adb6a | |||
| 408540ae74 | |||
| 73892f4fc2 | |||
| 636b868bf1 | |||
| b144368e3f | |||
| 683b4193d1 | |||
| f4c953665a | |||
| 0f990c772a | |||
| 0bf985af61 | |||
| 42473611d3 | |||
| 2c437f7d94 | |||
| aa8c49eb0c | |||
| b32e40c72a | |||
| 2ff7ed1b52 | |||
| bfbed1a69b | |||
| ef9d012be9 | |||
| f12a2155c0 | |||
| 43f696c4a8 | |||
| 4c19014dcd | |||
| 0e7f9d6778 | |||
| 00a0bf3c1b | |||
| e9149036f6 | |||
| b6a9967959 | |||
| 8bbc3caf3e | |||
| 237b703a6c | |||
| cf3045a718 | |||
| a679e7283f | |||
| 13c75341e2 | |||
| 4cbcc5bb20 | |||
| 51955dae3b | |||
| 51fa3ab2c8 | |||
| af9e66e87e | |||
| 8878dc7f80 | |||
| 4336bc20e0 | |||
| 4a7613a521 | |||
| 689f55340f | |||
| cb9a15339d | |||
| 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 |
@@ -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/react-openapi': minor
|
||||
---
|
||||
|
||||
Add Python support in Code Samples
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
RND-3532: drop down menu for hidden links at small screen size
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Change dark mode shadow for multi-space search toolbar
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Use ellipsis and fix icon color for more links in the header on small screen
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/icons': patch
|
||||
---
|
||||
|
||||
Fix first run of the package in development mode
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
bump: scalar from 1.0.5 to 1.0.7
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Replace all icons, previously imported from Geist, by new package `@gitbook/icons`
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/cache-do': minor
|
||||
---
|
||||
|
||||
First version of a new cache backend powered by Cloudflare Durable Objects
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Add support for Norwegian language
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Optimize layout shift while transitioning between pages with full width blocks (ex: OpenAPI blocks)
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Add support for page icons
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
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': minor
|
||||
---
|
||||
|
||||
Persist state of tabs and dynamically sync them based on title
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Ensure "Sponsored via GitBook" can be translated in all languages
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Use url hash to open Expandable and scroll to anchor
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Fix margin for paragraphs in quote blocks
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Use new cache backend, powered by Durable Objects, alongside the existing ones (KV, etc).
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': minor
|
||||
---
|
||||
|
||||
Add required query parameters to the code sample
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': minor
|
||||
---
|
||||
|
||||
Display pattern if available in parmas in OpenAPI block
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Fix inconsistent click area in table because of scroll indicator
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@gitbook/react-openapi': minor
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Synchronize response and response example tabs
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Show definition title when visible in cards
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@gitbook/react-openapi': minor
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Rollback of scalar modal while fixing perf issue
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Optimize images rendered in community ads
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/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/react-openapi': patch
|
||||
---
|
||||
|
||||
Fix crash when `example` is undefined for a response
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Add support for icons style customization for sites
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Adds tabs to Response Example section e.g. for status code examples
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Render mandatory headers in code sample
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Update Scalar client to latest version
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Prevent search indexation for pages where it's configured as disabled
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Fix table of contents being visible on mobile when disabled at the page level
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': 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
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Don't show hidden pages in the empty state of a page
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Fix linking to a tab itself
|
||||
+21
-7
@@ -1,6 +1,8 @@
|
||||
# Welcome to GitBook's contributing guide!
|
||||
|
||||
Thank you for investing your time in contributing to GitBook. Any contribution you make will be reviewed by our team.In this guide, you'll learn the different ways you can contribute.
|
||||
> _For help, support, feature requests, and product questions - head to our [GitHub Community](https://github.com/orgs/GitbookIO/discussions) 🤖_
|
||||
|
||||
Thank you for investing your time in contributing to GitBook. Any contribution you make will be reviewed by our team. In this guide, you'll learn the different ways you can contribute.
|
||||
|
||||
## Types of Contributions
|
||||
|
||||
@@ -45,13 +47,25 @@ Any contribution you make can be made to the code located in this repository. In
|
||||
|
||||
- [Fork the repo](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#fork-an-example-repository) so that you can make your changes without affecting the original project until you're ready to merge them.
|
||||
|
||||
##### GitHub Codespaces:
|
||||
|
||||
- [Fork, edit, and preview](https://docs.github.com/en/free-pro-team@latest/github/developing-online-with-codespaces/creating-a-codespace) using [GitHub Codespaces](https://github.com/features/codespaces) without having to install and run the project locally.
|
||||
|
||||
#### 2. Create a working branch and start with your changes
|
||||
|
||||
After forking this repository, you'll want to [create a branch](https://docs.github.com/en/issues/tracking-your-work-with-issues/creating-a-branch-for-an-issue) to work off of. After creating the branch, you can start making changes!
|
||||
After forking this repository, you'll want to [create a branch](https://docs.github.com/en/issues/tracking-your-work-with-issues/creating-a-branch-for-an-issue) to work off of.
|
||||
|
||||
#### 3. Install dependencies and run the project locally
|
||||
|
||||
GitBook uses [Bun](https://bun.sh/) to run the project. Make sure you're using the specified version of `node` before running any of the development commands to ensure a smooth development experience.
|
||||
|
||||
You can easily do this by running the command `nvm use`.
|
||||
|
||||
To start your local version of GitBook, run the command `bun dev`.
|
||||
|
||||
#### 4. Preview your changes
|
||||
|
||||
When running the development server, published GitBook sites can be rendered through your local version at `http://localhost:3000/`.
|
||||
|
||||
For example, our published docs can be viewed using the local version by visiting `http://localhost:3000/docs.gitbook.com` after running the development server.
|
||||
|
||||
You can visit any published GitBook site behind your development server. Please make sure your site is [published publicly](https://docs.gitbook.com/published-documentation/publish-your-content-as-a-docs-site) to ensure you can view the site correctly in your development version.
|
||||
|
||||
### Commit your update
|
||||
|
||||
@@ -71,4 +85,4 @@ When you're finished with the changes, [create a pull request](https://docs.gith
|
||||
|
||||
### Your PR is merged
|
||||
|
||||
Congratulations 🎉Thank you for your contribution! Once your PR is merged, your contributions will be publicly visible on the relevant repository.
|
||||
Congratulations 🎉 Thank you for your contribution! Once your PR is merged, your contributions will be publicly visible on the relevant repository.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -92,6 +93,12 @@ jobs:
|
||||
env:
|
||||
BASE_URL: ${{needs.deploy.outputs.deployment_url}}
|
||||
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-test-results
|
||||
path: packages/gitbook/test-results/
|
||||
retention-days: 3
|
||||
pagespeed-testing:
|
||||
runs-on: ubuntu-latest
|
||||
name: PageSpeed Testing
|
||||
@@ -157,6 +164,24 @@ jobs:
|
||||
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
|
||||
|
||||
@@ -35,3 +35,26 @@ jobs:
|
||||
# https://github.com/orgs/community/discussions/26875#discussioncomment-3253761
|
||||
GITHUB_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
release-preview:
|
||||
# For now it releases the cache-do to both preview and production
|
||||
# Once we changed to deploy the app only on release, we should change `release:preview` in `cache-do`
|
||||
name: Release Preview
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
- 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: Release preview packages
|
||||
run: bun run release:preview
|
||||
env:
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
|
||||
@@ -20,3 +20,5 @@ yarn-error.log*
|
||||
# Vercel
|
||||
.vercel
|
||||
|
||||
# Env files
|
||||
.env.local
|
||||
|
||||
@@ -4,3 +4,7 @@
|
||||
# Generated
|
||||
packages/emoji-codepoints/index.ts
|
||||
packages/gitbook/public/~gitbook/static/
|
||||
packages/icons/src/data/*.json
|
||||
|
||||
# Build files
|
||||
dist/
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<p align="center">Welcome to GitBook, the platform for managing technical knowledge for teams.</p>
|
||||
|
||||
<p align="center">This repository contains the open-source code used to render GitBook's published content.</p>
|
||||
<p align="center">This repository contains the open source code used to render GitBook's published content.</p>
|
||||
|
||||
<p align="center">
|
||||
<img alt="GitBook Open Published Site" src="./assets/published-site.png">
|
||||
@@ -46,19 +46,28 @@ To run a local version of this project, please follow these simple steps.
|
||||
git clone https://github.com/gitbookIO/gitbook.git
|
||||
```
|
||||
|
||||
2. Install the project's dependencies through Bun.
|
||||
2. Ensure you are using the project's version of `node`. Running `nvm use` will change your local version to the correct one.
|
||||
|
||||
3. Install the project's dependencies through Bun.
|
||||
|
||||
```
|
||||
bun install
|
||||
```
|
||||
|
||||
3. Start your local development server.
|
||||
4. Start your local development server.
|
||||
|
||||
```
|
||||
bun dev
|
||||
```
|
||||
|
||||
Then open the space in your web browser, using http://localhost:3000/<host>/<path> (example: http://localhost:3000/docs.gitbook.com).
|
||||
5. Open a published GitBook space in your web browser, prefixing it with `http://localhost:3000/`.
|
||||
|
||||
examples:
|
||||
|
||||
- http://localhost:3000/docs.gitbook.com
|
||||
- http://localhost:3000/open-source.gitbook.io/midjourney
|
||||
|
||||
Any published GitBook site can be accessed through your local development instance, and any updates you make to the codebase will be reflected in your browser.
|
||||
|
||||
### Other development commands
|
||||
|
||||
@@ -71,7 +80,7 @@ All pull-requests will be tested against both visual and performances testing to
|
||||
|
||||
## Contributing
|
||||
|
||||
GitBook's rendering engine is fully open-source and built on top of [Next.js](https://nextjs.org/). Head to our [contributing guide](https://github.com/GitbookIO/gitbook/.github/CONTRIBUTING.md) to learn more about the workflow on adding your first Pull Request.
|
||||
GitBook's rendering engine is fully open source and built on top of [Next.js](https://nextjs.org/). Head to our [contributing guide](https://github.com/GitbookIO/gitbook/blob/main/.github/CONTRIBUTING.md) to learn more about the workflow on adding your first Pull Request.
|
||||
|
||||
### Types of contributions
|
||||
|
||||
@@ -79,7 +88,7 @@ We encourage you to contribute to GitBook to help us build the best tool for doc
|
||||
|
||||
#### Translations
|
||||
|
||||
The GitBook UI is rendered using a set of translation files found in [`src/intl/translations`](/src/intl/translations/). We welcome all additional translations for the UI.
|
||||
The GitBook UI is rendered using a set of translation files found in [`packages/gitbook/src/intl/translations`](/packages/gitbook/src/intl/translations/). We welcome all additional translations for the UI.
|
||||
|
||||
#### Bugs
|
||||
|
||||
@@ -90,7 +99,7 @@ Encounter a bug or find an issue you'd like to fix? Helping us fix issues relate
|
||||
> [!WARNING]
|
||||
> While it is possible to self-host this project, we do not recommend this unless you are certain this option fits your need.
|
||||
>
|
||||
> _Looking to add a specific feature in GitBook? Head to our [contributing guide](/.github/CONTRIBUTING.md) to get started._
|
||||
> _Looking to add a specific feature in GitBook? Head to our [contributing guide](https://github.com/GitbookIO/gitbook/blob/main/.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, maintenance, or updates to forked and self-hosted instances of this project.
|
||||
>
|
||||
@@ -106,7 +115,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.
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
[install.scopes]
|
||||
"awesome.me" = { token = "$NPMRC_FONT_AWESOME_TOKEN", url = "https://npm.fontawesome.com/" }
|
||||
+9
-2
@@ -8,13 +8,17 @@
|
||||
"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",
|
||||
"unit": "turbo run unit",
|
||||
"e2e": "turbo run e2e",
|
||||
"changeset": "changeset",
|
||||
"release": "turbo run build && changeset publish"
|
||||
"release": "turbo run release && changeset publish",
|
||||
"release:preview": "turbo run release:preview",
|
||||
"download:env": "op read op://gitbook-x-dev/gitbook-open/.env.local >> .env.local",
|
||||
"clean": "turbo run clean"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
@@ -22,6 +26,9 @@
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.27.7",
|
||||
"prettier": "^3.0.3",
|
||||
"turbo": "^2.0.9"
|
||||
"turbo": "^2.1.2"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"@vercel/next@4.3.6": "patches/@vercel%2Fnext@4.3.6.patch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.wrangler
|
||||
worker-configuration.d.ts
|
||||
dist/
|
||||
@@ -0,0 +1,22 @@
|
||||
# `@gitbook/cache-do`
|
||||
|
||||
Cache backend, powered by Cloudflare Durable Objects. The cache is optimized for GitBook use-cases.
|
||||
|
||||
### Performances
|
||||
|
||||
The cache backend is optimized for performances by being distributed and accessible close to the worker locations that are reading it.
|
||||
|
||||
### Geo-distribution
|
||||
|
||||
To achieve a good balance between **performances** and **consistency**, cache objects are distributed over 7 locations, representing continents.
|
||||
|
||||
It makes it possible to purge all 7 locations in one go and achieve fast consistency.
|
||||
|
||||
### Concepts
|
||||
|
||||
**Cache tag**: unique tag in the cache environment. A cache tag groups multiple keys that should be purged together in one operation.
|
||||
Cache tags should not contain a large set of unique keys. Exceeding thousands could lead to performances or reliability issues.
|
||||
|
||||
**Cache key**: unique key in the cache environment. Each key should be assigned to a `tag`.
|
||||
|
||||
**Location**: cache is distributed over 7 unique locations, one for each continent.
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "@gitbook/cache-do",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"development": "./src/index.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./api": {
|
||||
"types": "./dist/api.d.ts",
|
||||
"development": "./src/api.ts",
|
||||
"default": "./dist/api.js"
|
||||
}
|
||||
},
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@msgpack/msgpack": "^3.0.0-beta2",
|
||||
"lru_map": "^0.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.3",
|
||||
"wrangler": "^3.78.7"
|
||||
},
|
||||
"scripts": {
|
||||
"generate": "wrangler types --experimental-include-runtime",
|
||||
"build": "tsc",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "tsc -w",
|
||||
"release": "wrangler deploy",
|
||||
"release:preview": "wrangler deploy && wrangler deploy --env preview"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
"bin",
|
||||
"data",
|
||||
"README.md",
|
||||
"CHANGELOG.md"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
import { encode, decode } from '@msgpack/msgpack';
|
||||
import { DurableObject } from 'cloudflare:workers';
|
||||
import { LRUMap } from 'lru_map';
|
||||
|
||||
export interface CacheObjectDescriptor {
|
||||
get: <Value = unknown>(key: string) => Promise<Value | undefined>;
|
||||
set: <Value = unknown>(key: string, value: Value, expiresAt: number) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Value stored in a chunked binary msgpack format.
|
||||
* Stored under the key `prop.${key}.${index}`.
|
||||
*/
|
||||
interface CacheObjectProp<Value = unknown> {
|
||||
value: Value;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expiration clock stored under the key `exp.${expiresAt}.${key}`.
|
||||
*/
|
||||
interface CacheObjectExp {
|
||||
/** Key of the property */
|
||||
k: string;
|
||||
/** Number of chunks */
|
||||
c: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Durable Object class being deployed as a distributed cache.
|
||||
*/
|
||||
export class CacheObject extends DurableObject {
|
||||
private lru = new LRUMap<string, { match: CacheObjectProp | undefined }>(500);
|
||||
|
||||
/**
|
||||
* Open a descriptor to access the cache object.
|
||||
* The goal is to minimize the amount of RPC sessions between the client and the cache object.
|
||||
* One session is opened per request on the client side and used to perform multiple operations.
|
||||
* https://developers.cloudflare.com/workers/runtime-apis/rpc/#return-functions-from-rpc-methods
|
||||
*/
|
||||
public open(): CacheObjectDescriptor {
|
||||
return {
|
||||
get: async <Value = unknown>(key: string) => {
|
||||
return this.get<Value>(key);
|
||||
},
|
||||
set: async <Value = unknown>(key: string, value: Value, expiresAt: number) => {
|
||||
await this.set(key, value, expiresAt);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of a property.
|
||||
*/
|
||||
public async get<Value = unknown>(key: string) {
|
||||
return timeFn(`get: ${key}`, async () => {
|
||||
// Try the memory state first.
|
||||
const memoryEntry = this.lru.get(key);
|
||||
if (memoryEntry) {
|
||||
console.log(`get: (memory, ${!!memoryEntry.match}) ${key}`);
|
||||
if (!memoryEntry.match) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (memoryEntry.match.expiresAt > Date.now()) {
|
||||
return memoryEntry.match.value as Value;
|
||||
}
|
||||
}
|
||||
|
||||
return await this.getFromStorage<Value>(key);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of a property from the DO storage.
|
||||
*/
|
||||
public async getFromStorage<Value = unknown>(key: string) {
|
||||
const entries = await this.ctx.storage.list<Uint8Array>({
|
||||
prefix: getStoragePropKey(key),
|
||||
noCache: true,
|
||||
});
|
||||
if (entries.size) {
|
||||
const entry = decodeChunks<CacheObjectProp<Value>>(entries);
|
||||
if (entry && entry.expiresAt > Date.now()) {
|
||||
// Found
|
||||
this.lru.set(key, { match: entry });
|
||||
return entry.value;
|
||||
}
|
||||
}
|
||||
|
||||
// Not found
|
||||
this.lru.set(key, { match: undefined });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a value in the cache object.
|
||||
*/
|
||||
public async set<Value = unknown>(key: string, value: Value, expiresAt: number) {
|
||||
return timeFn(`set: ${key}`, async () => {
|
||||
const prop: CacheObjectProp<Value> = {
|
||||
value,
|
||||
expiresAt,
|
||||
};
|
||||
|
||||
this.lru.set(key, { match: prop });
|
||||
await this.ctx.storage.transaction(async (tx) => {
|
||||
const entries = encodeChunks(key, prop);
|
||||
|
||||
const clockValue: CacheObjectExp = {
|
||||
k: key,
|
||||
c: Object.keys(entries).length,
|
||||
};
|
||||
|
||||
await tx.put(getGCClockKey(key, expiresAt), clockValue);
|
||||
await tx.put(entries);
|
||||
|
||||
const currentAlarm = await tx.getAlarm();
|
||||
if (!currentAlarm) {
|
||||
// Set an alarm to garbage collect all entries that have expired in 12h.
|
||||
await tx.setAlarm(Date.now() + 12 * 60 * 60 * 1000);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge all keys in the cache object.
|
||||
*/
|
||||
public async purge() {
|
||||
let result = new Set<string>();
|
||||
|
||||
try {
|
||||
// List all the keys in the cache object.
|
||||
const entries = await this.ctx.storage.list<CacheObjectExp>({
|
||||
prefix: 'exp.',
|
||||
noCache: true,
|
||||
});
|
||||
console.log(`purge: ${entries.size} entries`);
|
||||
entries.forEach((exp) => {
|
||||
result.add(exp.k);
|
||||
});
|
||||
} catch (error) {
|
||||
// If an error occurs, reset the cache object.
|
||||
// This is a safety mechanism to prevent the cache object from being stuck in a bad state.
|
||||
console.error('Error during purge, resetting the cache object', error);
|
||||
}
|
||||
|
||||
await this.reset();
|
||||
|
||||
console.log(`purge returns`, Array.from(result));
|
||||
return Array.from(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alarm to garbage collect all entries that have expired.
|
||||
*/
|
||||
async alarm() {
|
||||
try {
|
||||
const entries = await this.ctx.storage.list<CacheObjectExp>({
|
||||
prefix: 'exp.',
|
||||
noCache: true,
|
||||
});
|
||||
const toDeleteSet = new Set<string>();
|
||||
|
||||
for (const [key, exp] of entries) {
|
||||
const timestamp = parseInt(key.split('.')[1]);
|
||||
if (timestamp < Date.now()) {
|
||||
toDeleteSet.add(key);
|
||||
for (let i = 0; i < exp.c; i++) {
|
||||
toDeleteSet.add(getStoragePropChunkKey(exp.k, i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the keys by batch of 128.
|
||||
const toDelete = Array.from(toDeleteSet);
|
||||
for (let i = 0; i < toDelete.length; i += 128) {
|
||||
await this.ctx.storage.delete(toDelete.slice(i, i + 128));
|
||||
}
|
||||
|
||||
// If there are still keys to delete, set an alarm to continue the deletion in 12h.
|
||||
if (toDelete.length) {
|
||||
await this.ctx.storage.setAlarm(Date.now() + 12 * 60 * 60 * 1000);
|
||||
}
|
||||
} catch (error) {
|
||||
// If an error occurs, reset the cache object.
|
||||
// This is a safety mechanism to prevent the cache object from being stuck in a bad state.
|
||||
console.error('Error during alarm, reset the cache object', error);
|
||||
await this.reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the cache object.
|
||||
*/
|
||||
async reset() {
|
||||
console.log('reset: clear all entries');
|
||||
this.lru.clear();
|
||||
await this.ctx.storage.deleteAll();
|
||||
}
|
||||
}
|
||||
|
||||
function getStoragePropKey(key: string): string {
|
||||
return `prop.${key}.`;
|
||||
}
|
||||
|
||||
function getStoragePropChunkKey(key: string, index: number): string {
|
||||
return `${getStoragePropKey(key)}${index}`;
|
||||
}
|
||||
|
||||
function getGCClockRootKey(timestamp: number): string {
|
||||
return `exp.${timestamp}.`;
|
||||
}
|
||||
|
||||
function getGCClockKey(key: string, expiresAt: number): string {
|
||||
return `${getGCClockRootKey(expiresAt)}${key}`;
|
||||
}
|
||||
|
||||
function encodeChunks<T>(key: string, value: T): Record<string, Uint8Array> {
|
||||
const buf = encode(value);
|
||||
const entries: Record<string, Uint8Array> = {};
|
||||
const chunks = chunkUint8Array(buf, 128 * 1024);
|
||||
|
||||
for (let index = 0; index < chunks.length; index++) {
|
||||
entries[getStoragePropChunkKey(key, index)] = chunks[index];
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
function decodeChunks<T>(entries: Map<string, Uint8Array>): T | undefined {
|
||||
const chunks = Array.from(entries.entries())
|
||||
.map(([key, value]) => {
|
||||
const index = parseInt(key.split('.').pop()!);
|
||||
return [index, value] as const;
|
||||
})
|
||||
.sort(([a], [b]) => a - b)
|
||||
.map(([, value]) => value);
|
||||
|
||||
if (chunks.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const buf = mergeUint8Array(chunks);
|
||||
return decode(buf) as T;
|
||||
}
|
||||
|
||||
function chunkUint8Array(input: Uint8Array, chunkSize: number): Uint8Array[] {
|
||||
const chunks: Uint8Array[] = [];
|
||||
for (let i = 0; i < input.length; i += chunkSize) {
|
||||
chunks.push(input.slice(i, i + chunkSize));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
function mergeUint8Array(chunks: Uint8Array[]): Uint8Array {
|
||||
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
||||
const result = new Uint8Array(totalLength);
|
||||
let offset = 0;
|
||||
for (const chunk of chunks) {
|
||||
result.set(chunk, offset);
|
||||
offset += chunk.length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function timeFn<T>(message: string, fn: () => Promise<T>): Promise<T> {
|
||||
const start = performance.now();
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
console.log(`${message} (${performance.now() - start}ms)`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import type { CacheObject, CacheObjectDescriptor } from './CacheObject';
|
||||
|
||||
export type CacheLocationId = ContinentCode;
|
||||
const allLocations: CacheLocationId[] = ['AF', 'AS', 'NA', 'SA', 'AN', 'EU', 'OC'];
|
||||
|
||||
/**
|
||||
* Location hint for the CacheObject durable object.
|
||||
*/
|
||||
const doLocationHints: {
|
||||
[key in CacheLocationId]: DurableObjectLocationHint;
|
||||
} = {
|
||||
AF: 'afr',
|
||||
AS: 'apac',
|
||||
NA: 'wnam',
|
||||
SA: 'sam',
|
||||
AN: 'oc',
|
||||
EU: 'weur',
|
||||
OC: 'oc',
|
||||
};
|
||||
|
||||
/**
|
||||
* Client to access a cache tag.
|
||||
*/
|
||||
export class CacheObjectStub {
|
||||
private opened: CacheObjectDescriptor | null = null;
|
||||
|
||||
constructor(
|
||||
/** Binding to the CacheObject durable object */
|
||||
private doNamespace: DurableObjectNamespace<CacheObject>,
|
||||
/** ID of the location to target */
|
||||
private locationId: CacheLocationId,
|
||||
/** Name of the tag */
|
||||
private tag: string,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Open the cache object.
|
||||
*/
|
||||
async open() {
|
||||
if (!this.opened) {
|
||||
const groupId = getCacheObjectIdName(this.locationId, this.tag);
|
||||
const cacheGroup = this.doNamespace.get(this.doNamespace.idFromName(groupId), {
|
||||
// Initialize the object with a locaiton hint,
|
||||
// as we might want to purge all locations before the object is created.
|
||||
// https://developers.cloudflare.com/durable-objects/reference/data-location/
|
||||
locationHint: doLocationHints[this.locationId],
|
||||
});
|
||||
this.opened = await cacheGroup.open();
|
||||
}
|
||||
|
||||
return this.opened;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a value from the cache.
|
||||
*/
|
||||
async get<Value = unknown>(key: string) {
|
||||
const desc = await this.open();
|
||||
return await desc.get<Value>(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a value in the cache.
|
||||
*/
|
||||
async set<Value = unknown>(key: string, value: Value, expiresAt: number) {
|
||||
// TODO: Should we write on all locations instead of just the current one?
|
||||
const desc = await this.open();
|
||||
return await desc.set<Value>(key, value, expiresAt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge all keys in the cache tag.
|
||||
*/
|
||||
async purge() {
|
||||
const keys = new Set<string>();
|
||||
await Promise.all(
|
||||
allLocations.map(async (locationId) => {
|
||||
const groupId = getCacheObjectIdName(locationId, this.tag);
|
||||
const cacheGroup = this.doNamespace.get(this.doNamespace.idFromName(groupId));
|
||||
const locationkeys = await cacheGroup.purge();
|
||||
locationkeys.forEach((key) => keys.add(key));
|
||||
}),
|
||||
);
|
||||
|
||||
return keys;
|
||||
}
|
||||
}
|
||||
|
||||
function getCacheObjectIdName(locationId: CacheLocationId, tag: string): string {
|
||||
return `${locationId}:${tag}`;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './CacheObjectStub';
|
||||
@@ -0,0 +1,9 @@
|
||||
import { WorkerEntrypoint } from 'cloudflare:workers';
|
||||
|
||||
export * from './CacheObject';
|
||||
|
||||
export default class Worker extends WorkerEntrypoint {
|
||||
fetch() {
|
||||
return new Response('Hello, world!');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"incremental": true,
|
||||
"types": ["./.wrangler/types/runtime.d.ts"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
main = "./src/index.ts"
|
||||
name = "gitbook-open-cache"
|
||||
compatibility_date = "2024-09-02"
|
||||
|
||||
durable_objects.bindings = [
|
||||
{name = "CACHE", class_name = "CacheObject"}
|
||||
]
|
||||
|
||||
migrations = [
|
||||
{tag = "v1", new_classes = ["CacheObject"]}
|
||||
]
|
||||
|
||||
[env.preview]
|
||||
name = "gitbook-open-cache-preview"
|
||||
durable_objects.bindings = [
|
||||
{name = "CACHE", class_name = "CacheObject"}
|
||||
]
|
||||
@@ -1 +1 @@
|
||||
index.ts
|
||||
dist/
|
||||
|
||||
@@ -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)};`,
|
||||
);
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
"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",
|
||||
"clean": "rm -rf ./dist"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# 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
|
||||
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
import type { CacheObject } from '@gitbook/cache-do';
|
||||
|
||||
declare global {
|
||||
interface CloudflareEnv {
|
||||
CACHE?: DurableObjectNamespace<CacheObject>;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -12,10 +14,10 @@ import { getContentTestURL } from '../tests/utils';
|
||||
|
||||
interface Test {
|
||||
name: string;
|
||||
url: string;
|
||||
run?: (page: Page) => Promise<unknown>;
|
||||
fullPage?: boolean;
|
||||
screenshot?: false;
|
||||
url: string; // URL to visit for testing
|
||||
run?: (page: Page) => Promise<unknown>; // The test to run
|
||||
fullPage?: boolean; // Whether the test should be fullscreened during testing
|
||||
screenshot?: false; // Should a screenshot be stored
|
||||
}
|
||||
|
||||
interface TestsCase {
|
||||
@@ -104,6 +106,31 @@ const testCases: TestsCase[] = [
|
||||
name: 'RFC variant',
|
||||
url: 'v/rfcs',
|
||||
},
|
||||
{
|
||||
name: 'Customized variant titles are displayed',
|
||||
url: '',
|
||||
run: async (page) => {
|
||||
const spaceDrowpdown = page.locator('[data-testid="space-dropdown-button"]');
|
||||
await spaceDrowpdown.click();
|
||||
|
||||
const variantSelectionDropdown = page.locator(
|
||||
'css=[data-testid="space-dropdown-button"] + div',
|
||||
);
|
||||
// the customized space title
|
||||
await expect(
|
||||
variantSelectionDropdown.getByRole('link', {
|
||||
name: 'Multi-Variants',
|
||||
}),
|
||||
).toBeVisible();
|
||||
|
||||
// the NON-customized space title
|
||||
await expect(
|
||||
variantSelectionDropdown.getByRole('link', {
|
||||
name: 'RFCs',
|
||||
}),
|
||||
).toBeVisible();
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -230,7 +257,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',
|
||||
@@ -241,7 +268,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',
|
||||
@@ -251,7 +278,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',
|
||||
@@ -349,6 +376,11 @@ const testCases: TestsCase[] = [
|
||||
url: 'blocks/embeds',
|
||||
fullPage: true,
|
||||
},
|
||||
{
|
||||
name: 'Page links',
|
||||
url: 'blocks/page-links',
|
||||
fullPage: true,
|
||||
},
|
||||
{
|
||||
name: 'Annotations',
|
||||
url: 'blocks/annotations',
|
||||
@@ -361,7 +393,7 @@ 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',
|
||||
@@ -383,11 +415,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',
|
||||
@@ -399,16 +436,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',
|
||||
@@ -611,13 +669,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) => {
|
||||
@@ -626,6 +683,57 @@ const testCases: TestsCase[] = [
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
name: 'SEO',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-gitbook-open/',
|
||||
tests: [
|
||||
{
|
||||
name: `Index by default`,
|
||||
url: '?x-gitbook-search-indexation=true',
|
||||
screenshot: false,
|
||||
run: async (page) => {
|
||||
const metaRobots = page.locator('meta[name="robots"]');
|
||||
await expect(metaRobots).toHaveAttribute('content', 'index, follow');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: `Don't index noIndex`,
|
||||
url: 'page-options/page-no-index?x-gitbook-search-indexation=true',
|
||||
screenshot: false,
|
||||
run: async (page) => {
|
||||
const metaRobots = page.locator('meta[name="robots"]');
|
||||
await expect(metaRobots).toHaveAttribute('content', 'noindex, nofollow');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: `Don't index descendant of noIndex`,
|
||||
url: 'page-options/page-no-index/descendant-of-page-no-index?x-gitbook-search-indexation=true',
|
||||
screenshot: false,
|
||||
run: async (page) => {
|
||||
const metaRobots = page.locator('meta[name="robots"]');
|
||||
await expect(metaRobots).toHaveAttribute('content', 'noindex, nofollow');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: `Don't index noRobotsIndex`,
|
||||
url: 'page-options/page-no-robots-index?x-gitbook-search-indexation=true',
|
||||
screenshot: false,
|
||||
run: async (page) => {
|
||||
const metaRobots = page.locator('meta[name="robots"]');
|
||||
await expect(metaRobots).toHaveAttribute('content', 'noindex, nofollow');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: `Don't index descendant of noRobotsIndex`,
|
||||
url: 'page-options/page-no-robots-index/descendant-of-page-no-robots-index?x-gitbook-search-indexation=true',
|
||||
screenshot: false,
|
||||
run: async (page) => {
|
||||
const metaRobots = page.locator('meta[name="robots"]');
|
||||
await expect(metaRobots).toHaveAttribute('content', 'noindex, nofollow');
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
@@ -658,7 +766,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,6 +7,8 @@ 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, { dev, webpack }) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "gitbook",
|
||||
"version": "0.2.1",
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"dev": "env-cmd --silent -f ../../.env.local next dev",
|
||||
"build": "next build",
|
||||
"build:cloudflare": "next-on-pages",
|
||||
"start": "next start",
|
||||
@@ -11,14 +11,18 @@
|
||||
"typecheck": "tsc --noEmit",
|
||||
"e2e": "playwright test",
|
||||
"unit": "bun test {src,packages}",
|
||||
"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"
|
||||
"generate": "gitbook-icons ./public/~gitbook/static/icons custom-icons && gitbook-math ./public/~gitbook/static/math",
|
||||
"copy:icons": "gitbook-icons ./public/~gitbook/static/icons",
|
||||
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static"
|
||||
},
|
||||
"dependencies": {
|
||||
"@geist-ui/icons": "^1.0.2",
|
||||
"@gitbook/api": "^0.56.0",
|
||||
"@gitbook/api": "0.60.0",
|
||||
"@gitbook/icons": "workspace:*",
|
||||
"@gitbook/react-math": "workspace:*",
|
||||
"@gitbook/react-openapi": "workspace:*",
|
||||
"@gitbook/react-contentkit": "workspace:*",
|
||||
"@gitbook/emoji-codepoints": "workspace:*",
|
||||
"@gitbook/cache-do": "workspace:*",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@sentry/nextjs": "^7.94.1",
|
||||
@@ -37,7 +41,7 @@
|
||||
"katex": "^0.16.9",
|
||||
"mathjax": "^3.2.2",
|
||||
"memoizee": "^0.4.15",
|
||||
"next": "^14.1.3",
|
||||
"next": "^14.2.5",
|
||||
"next-themes": "^0.2.1",
|
||||
"nuqs": "^1.17.4",
|
||||
"object-hash": "^3.0.0",
|
||||
@@ -55,7 +59,7 @@
|
||||
"remark-rehype": "^11.1.0",
|
||||
"rison": "^0.1.1",
|
||||
"server-only": "^0.0.1",
|
||||
"shiki": "^1.2.0",
|
||||
"shiki": "^1.11.1",
|
||||
"tailwind-merge": "^2.2.0",
|
||||
"tailwind-shades": "^1.1.2",
|
||||
"unified": "^11.0.4",
|
||||
@@ -64,8 +68,8 @@
|
||||
"devDependencies": {
|
||||
"@argos-ci/playwright": "^2.0.0",
|
||||
"@playwright/test": "^1.42.1",
|
||||
"@cloudflare/next-on-pages": "^1.11.3",
|
||||
"@cloudflare/workers-types": "^4.20231218.0",
|
||||
"@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",
|
||||
@@ -84,6 +88,8 @@
|
||||
"postcss": "^8",
|
||||
"psi": "^4.1.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"typescript": "^5.5.3"
|
||||
"typescript": "^5.5.3",
|
||||
"env-cmd": "^10.1.0",
|
||||
"ts-essentials": "^10.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
keys: result.keys,
|
||||
stats: result.stats,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function PageSkeleton() {
|
||||
'lg:items-start',
|
||||
)}
|
||||
>
|
||||
<div className={tcls('flex-1', 'max-w-3xl', 'mx-auto')}>
|
||||
<div className={tcls('flex-1', 'max-w-3xl', 'mx-auto', 'page-full-width:mx-0')}>
|
||||
<SkeletonHeading style={tcls('mb-8')} />
|
||||
<SkeletonParagraph style={tcls('mb-4')} />
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { PageBody, PageCover } from '@/components/PageBody';
|
||||
import { PageHrefContext, absoluteHref, pageHref } from '@/lib/links';
|
||||
import { getPagePath, resolveFirstDocument } from '@/lib/pages';
|
||||
import { ContentRefContext } from '@/lib/references';
|
||||
import { isSpaceIndexable, isPageIndexable } from '@/lib/seo';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { getContentTitle } from '@/lib/utils';
|
||||
|
||||
@@ -64,6 +65,7 @@ export default async function Page(props: {
|
||||
const withPageFeedback = customization.feedback.enabled;
|
||||
|
||||
const contentRefContext: ContentRefContext = {
|
||||
siteContext: 'siteId' in contentPointer ? contentPointer : null,
|
||||
space,
|
||||
revisionId: contentTarget.revisionId,
|
||||
pages,
|
||||
@@ -128,7 +130,7 @@ export async function generateMetadata({
|
||||
params: PagePathParams;
|
||||
searchParams: { fallback?: string };
|
||||
}): Promise<Metadata> {
|
||||
const { space, pages, page, customization, parent } = await getPageDataWithFallback({
|
||||
const { space, pages, page, customization, parent, ancestors } = await getPageDataWithFallback({
|
||||
pagePathParams: params,
|
||||
searchParams,
|
||||
});
|
||||
@@ -151,6 +153,10 @@ export async function generateMetadata({
|
||||
absoluteHref(`~gitbook/ogimage/${page.id}`, true),
|
||||
],
|
||||
},
|
||||
robots:
|
||||
isSpaceIndexable({ space, parent }) && isPageIndexable(ancestors, page)
|
||||
? 'index, follow'
|
||||
: 'noindex, nofollow',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ 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 { isSpaceIndexable } from '@/lib/seo';
|
||||
import { getContentTitle } from '@/lib/utils';
|
||||
|
||||
import { ClientContexts } from './ClientContexts';
|
||||
@@ -133,7 +133,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
||||
},
|
||||
],
|
||||
},
|
||||
robots: shouldIndexSpace({ space, parent }) ? 'index, follow' : 'noindex, nofollow',
|
||||
robots: isSpaceIndexable({ space, parent }) ? 'index, follow' : 'noindex, nofollow',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { NextRequest } from 'next/server';
|
||||
|
||||
import { getCollection, getSite, getSpace } from '@/lib/api';
|
||||
import { absoluteHref } from '@/lib/links';
|
||||
import { shouldIndexSpace } from '@/lib/seo';
|
||||
import { isSpaceIndexable } from '@/lib/seo';
|
||||
|
||||
import { getContentPointer } from '../../fetch';
|
||||
|
||||
@@ -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)
|
||||
@@ -25,7 +28,7 @@ export async function GET(req: NextRequest) {
|
||||
const lines = [
|
||||
`User-agent: *`,
|
||||
'Disallow: /~gitbook/',
|
||||
...(shouldIndexSpace({ space, parent })
|
||||
...(isSpaceIndexable({ space, parent })
|
||||
? [`Allow: /`, `Sitemap: ${absoluteHref(`/sitemap.xml`, true)}`]
|
||||
: [`Disallow: /`]),
|
||||
];
|
||||
|
||||
@@ -5,6 +5,7 @@ import { NextRequest } from 'next/server';
|
||||
import { getSpaceContentData } from '@/lib/api';
|
||||
import { absoluteHref } from '@/lib/links';
|
||||
import { getPagePath } from '@/lib/pages';
|
||||
import { isPageIndexable } from '@/lib/seo';
|
||||
|
||||
import { getContentPointer } from '../../fetch';
|
||||
|
||||
@@ -14,8 +15,13 @@ 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, (page) => !page.hidden);
|
||||
const pointer = getContentPointer();
|
||||
const { pages: rootPages } = await getSpaceContentData(
|
||||
pointer,
|
||||
'siteId' in pointer ? pointer.siteShareKey : undefined,
|
||||
);
|
||||
|
||||
const pages = flattenPages(rootPages, (page) => !page.hidden && isPageIndexable([], page));
|
||||
const urls = pages.map(({ page, depth }) => {
|
||||
// Decay priority with depth
|
||||
const priority = Math.pow(2, -0.25 * depth);
|
||||
@@ -68,14 +74,19 @@ type FlatPageEntry = { page: RevisionPageDocument; depth: number };
|
||||
|
||||
function flattenPages(
|
||||
rootPags: RevisionPage[],
|
||||
filter: (page: RevisionPageDocument) => boolean,
|
||||
filter: (page: RevisionPageDocument | RevisionPageGroup) => boolean,
|
||||
): FlatPageEntry[] {
|
||||
const flattenPage = (
|
||||
page: RevisionPageDocument | RevisionPageGroup,
|
||||
depth: number,
|
||||
): FlatPageEntry[] => {
|
||||
const allowed = filter(page);
|
||||
if (!allowed) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
...(page.type === 'document' && filter(page) ? [{ page, depth }] : []),
|
||||
...(page.type === 'document' ? [{ page, depth }] : []),
|
||||
...page.pages.flatMap((child) =>
|
||||
child.type === 'link' ? [] : flattenPage(child, depth + 1),
|
||||
),
|
||||
|
||||
@@ -46,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 =
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { ArrowLeft, Printer } from '@geist-ui/icons';
|
||||
import {
|
||||
CustomizationSettings,
|
||||
Revision,
|
||||
RevisionPageDocument,
|
||||
RevisionPageGroup,
|
||||
RevisionPageLink,
|
||||
SiteCustomizationSettings,
|
||||
Space,
|
||||
} from '@gitbook/api';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
import * as React from 'react';
|
||||
@@ -42,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),
|
||||
@@ -72,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);
|
||||
|
||||
@@ -110,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}
|
||||
@@ -136,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>
|
||||
|
||||
@@ -170,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,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
getCurrentSiteData,
|
||||
getSite,
|
||||
getSiteSpaces,
|
||||
getCurrentSiteCustomization,
|
||||
} from '@/lib/api';
|
||||
import { resolvePagePath, resolvePageId } from '@/lib/pages';
|
||||
|
||||
@@ -71,6 +72,8 @@ 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
|
||||
? [
|
||||
@@ -78,18 +81,20 @@ export async function fetchSpaceData() {
|
||||
fetchParentSite({
|
||||
organizationId: content.organizationId,
|
||||
siteId: content.siteId,
|
||||
siteShareKey: content.siteShareKey,
|
||||
siteShareKey,
|
||||
}),
|
||||
]
|
||||
: [getSpaceData(content)],
|
||||
: [getSpaceData(content, siteShareKey)],
|
||||
);
|
||||
|
||||
const parent = await (parentSite ?? fetchParentCollection(space));
|
||||
// we grab the space attached to the parent as it contains overriden customizations
|
||||
const spaceRelativeToParent = parent?.spaces.find((space) => space.id === content.spaceId);
|
||||
|
||||
return {
|
||||
content,
|
||||
contentTarget,
|
||||
space,
|
||||
space: spaceRelativeToParent ?? space,
|
||||
pages,
|
||||
customization,
|
||||
scripts,
|
||||
@@ -104,9 +109,10 @@ 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([
|
||||
@@ -192,23 +198,34 @@ async function fetchParentSite(args: {
|
||||
siteShareKey: string | undefined;
|
||||
}) {
|
||||
const { organizationId, siteId, siteShareKey } = args;
|
||||
const [site, siteSpaces] = await Promise.all([
|
||||
const [site, siteSpaces, siteParentCustomizations] = await Promise.all([
|
||||
getSite(organizationId, siteId),
|
||||
getSiteSpaces({ organizationId, siteId, siteShareKey }),
|
||||
getCurrentSiteCustomization({ organizationId, siteId, siteSpaceId: undefined }),
|
||||
]);
|
||||
|
||||
const spaces: Record<string, Space> = {};
|
||||
const spaces: Array<Space> = [];
|
||||
siteSpaces.forEach((siteSpace) => {
|
||||
spaces[siteSpace.space.id] = {
|
||||
spaces.push({
|
||||
...siteSpace.space,
|
||||
title: siteSpace.title ?? siteSpace.space.title,
|
||||
urls: {
|
||||
...siteSpace.space.urls,
|
||||
published: siteSpace.urls.published,
|
||||
},
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return { parent: site, spaces: Object.values(spaces) };
|
||||
// override the title with the customization title
|
||||
const parent = {
|
||||
...site,
|
||||
...(siteParentCustomizations?.title ? { title: siteParentCustomizations.title } : {}),
|
||||
};
|
||||
|
||||
return {
|
||||
parent,
|
||||
spaces,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import IconHeart from '@geist-ui/icons/heart';
|
||||
import * as React from 'react';
|
||||
|
||||
import { t, useLanguage } from '@/intl/client';
|
||||
import { ClassValue, tcls } from '@/lib/tailwind';
|
||||
|
||||
import { AdClassicRendering } from './AdClassicRendering';
|
||||
import { AdCoverRendering } from './AdCoverRendering';
|
||||
import { AdItem, AdsResponse } from './types';
|
||||
import { renderAd } from './renderAd';
|
||||
|
||||
/**
|
||||
* Zone ID provided by BuySellAds for the preview.
|
||||
*/
|
||||
const PREVIEW_ZONE_ID = 'CVAIKKQM';
|
||||
|
||||
/**
|
||||
* Fetch and render the Ad placement.
|
||||
@@ -21,7 +24,7 @@ export function Ad({
|
||||
style,
|
||||
mode = 'auto',
|
||||
}: {
|
||||
zoneId: string;
|
||||
zoneId: string | null;
|
||||
spaceId: string;
|
||||
placement: string;
|
||||
ignore: boolean;
|
||||
@@ -30,8 +33,7 @@ export function Ad({
|
||||
}) {
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
const [failed, setFailed] = React.useState(false);
|
||||
const [ad, setAd] = React.useState<AdItem | undefined>(undefined);
|
||||
const [ad, setAd] = React.useState<React.ReactNode | undefined>(undefined);
|
||||
|
||||
// Observe the container visibility
|
||||
React.useEffect(() => {
|
||||
@@ -68,39 +70,34 @@ export function Ad({
|
||||
|
||||
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 url = new URL(`https://srv.buysellads.com/ads/${zoneId}.json`);
|
||||
url.searchParams.set('segment', `placement:${placement}`);
|
||||
url.searchParams.set('v', 'true');
|
||||
if (ignore) {
|
||||
url.searchParams.set('ignore', 'true');
|
||||
const result = await renderAd({
|
||||
placement,
|
||||
ignore: ignore || preview,
|
||||
zoneId: realZoneId,
|
||||
mode,
|
||||
});
|
||||
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
const json: AdsResponse = await res.json();
|
||||
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const first = json.ads[0];
|
||||
if (first && 'active' in first) {
|
||||
setAd(first);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Failed to fetch ad, it might have been blocked by a ad-blocker',
|
||||
error,
|
||||
);
|
||||
setFailed(true);
|
||||
if (result) {
|
||||
setAd(result);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [visible, zoneId, ignore, placement]);
|
||||
}, [visible, zoneId, ignore, placement, mode]);
|
||||
|
||||
const viaUrl = new URL('https://www.gitbook.com');
|
||||
viaUrl.searchParams.set('utm_source', 'content');
|
||||
@@ -109,92 +106,39 @@ export function Ad({
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={tcls(style)}>
|
||||
{ad && ad.description && ad.statlink ? (
|
||||
{ad ? (
|
||||
<>
|
||||
{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',
|
||||
)}
|
||||
>
|
||||
<a
|
||||
target="_blank"
|
||||
href={viaUrl.toString()}
|
||||
className={tcls('hover:underline')}
|
||||
>
|
||||
Ads via GitBook
|
||||
</a>
|
||||
</p>
|
||||
{ad}
|
||||
<AdSponsoredLink spaceId={spaceId} />
|
||||
</>
|
||||
) : failed ? (
|
||||
<AdBlockerPlaceholder />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render attribution or verification pixels.
|
||||
* https://docs.buysellads.com/ad-serving-api#pixels
|
||||
*/
|
||||
function AdPixels({ rawPixel }: { rawPixel: string }) {
|
||||
const pixels = rawPixel.split('||');
|
||||
const time = String(Math.round(Date.now() / 1e4) | 0);
|
||||
function AdSponsoredLink(props: { spaceId: string }) {
|
||||
const { spaceId } = props;
|
||||
const language = useLanguage();
|
||||
|
||||
const viaUrl = new URL('https://www.gitbook.com');
|
||||
viaUrl.searchParams.set('utm_source', 'content');
|
||||
viaUrl.searchParams.set('utm_medium', 'sponsoring');
|
||||
viaUrl.searchParams.set('utm_campaign', spaceId);
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder when visitor has an ad-blocker.
|
||||
*/
|
||||
function AdBlockerPlaceholder() {
|
||||
return (
|
||||
<div
|
||||
<p
|
||||
className={tcls(
|
||||
'flex',
|
||||
'flex-col',
|
||||
'gap-3',
|
||||
'bg-light-2',
|
||||
'text-dark/7',
|
||||
'dark:bg-dark-2',
|
||||
'dark:text-light/7',
|
||||
'rounded-lg',
|
||||
'p-4',
|
||||
'mt-2',
|
||||
'mr-2',
|
||||
'text-xs',
|
||||
'text-right',
|
||||
'text-dark/5',
|
||||
'dark:text-light/5',
|
||||
)}
|
||||
>
|
||||
<div className={tcls('flex', 'flex-row', 'gap-2', 'items-center')}>
|
||||
<IconHeart className={tcls('w-4', 'h-4', 'text-primary-500')} />
|
||||
<p className={tcls('text-xs', 'font-semibold')}>Ad disabled</p>
|
||||
</div>
|
||||
<p className={tcls('text-xs')}>
|
||||
{`It looks like you're using an adblocker. Whitelist this site to help support this
|
||||
project.`}
|
||||
</p>
|
||||
</div>
|
||||
<a target="_blank" href={viaUrl.toString()} className={tcls('hover:underline')}>
|
||||
{t(language, 'sponsored_via_gitbook')}
|
||||
</a>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { getResizedImageURL } from '@/lib/images';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { AdItem } from './types';
|
||||
@@ -7,7 +8,7 @@ import { AdItem } from './types';
|
||||
/**
|
||||
* Classic rendering for an ad.
|
||||
*/
|
||||
export function AdClassicRendering({ ad }: { ad: AdItem }) {
|
||||
export async function AdClassicRendering({ ad }: { ad: AdItem }) {
|
||||
return (
|
||||
<a
|
||||
className={tcls(
|
||||
@@ -29,14 +30,21 @@ export function AdClassicRendering({ ad }: { ad: AdItem }) {
|
||||
>
|
||||
{'smallImage' in ad ? (
|
||||
<div>
|
||||
<img alt="Ads logo" className={tcls('rounded-md')} src={ad.smallImage} />
|
||||
<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={ad.logo} />
|
||||
<img
|
||||
alt="Ads logo"
|
||||
src={await getResizedImageURL(ad.logo, { width: 192 - 48, dpr: 2 })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={tcls('flex', 'flex-col')}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { hexToRgba } from '@/lib/colors';
|
||||
import { getResizedImageURL } from '@/lib/images';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { AdCover } from './types';
|
||||
@@ -8,7 +9,9 @@ import { AdCover } from './types';
|
||||
/**
|
||||
* Cover rendering for an ad.
|
||||
*/
|
||||
export function AdCoverRendering({ ad }: { ad: AdCover }) {
|
||||
export async function AdCoverRendering({ ad }: { ad: AdCover }) {
|
||||
const largeImage = await getResizedImageURL(ad.largeImage, { width: 128, dpr: 2 });
|
||||
|
||||
return (
|
||||
<a
|
||||
className={tcls(
|
||||
@@ -43,14 +46,14 @@ export function AdCoverRendering({ ad }: { ad: AdCover }) {
|
||||
'z-0',
|
||||
)}
|
||||
style={{
|
||||
backgroundImage: `url(${ad.largeImage})`,
|
||||
backgroundImage: `url(${largeImage})`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className={tcls('z-[2]')}>
|
||||
<img
|
||||
alt="Large image"
|
||||
src={ad.largeImage}
|
||||
src={largeImage}
|
||||
className={tcls(
|
||||
'rounded-md',
|
||||
'shadow-md',
|
||||
|
||||
@@ -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,80 @@
|
||||
'use server';
|
||||
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
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;
|
||||
/** 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 { mode } = options;
|
||||
const result = await fetchAd(options);
|
||||
if (!result || !result.ad.description || !result.ad.statlink) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { ad } = result;
|
||||
|
||||
return (
|
||||
<>
|
||||
{mode === 'classic' || !('callToAction' in ad) ? (
|
||||
<AdClassicRendering ad={ad} />
|
||||
) : (
|
||||
<AdCoverRendering ad={ad} />
|
||||
)}
|
||||
{ad.pixel ? <AdPixels rawPixel={ad.pixel} /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -27,6 +27,7 @@ export async function Annotation(props: InlineProps<DocumentInlineAnnotation>) {
|
||||
ancestorBlocks={[]}
|
||||
context={context}
|
||||
nodes={fragment.nodes}
|
||||
style={['space-y-4']}
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { DocumentBlockContentRef } from '@gitbook/api';
|
||||
|
||||
import { LogoIcon } from '@/components/icons';
|
||||
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) {
|
||||
@@ -29,7 +30,7 @@ export async function BlockContentRef(props: BlockProps<DocumentBlockContentRef>
|
||||
|
||||
return (
|
||||
<Card
|
||||
leadingIcon={resolved.emoji ? <Emoji code={resolved.emoji} style="text-xl" /> : null}
|
||||
leadingIcon={resolved.icon ? resolved.icon : null}
|
||||
href={resolved.href}
|
||||
title={resolved.text}
|
||||
style={style}
|
||||
@@ -55,7 +56,7 @@ async function SpaceRefCard(
|
||||
return (
|
||||
<Card
|
||||
leadingIcon={
|
||||
<LogoIcon
|
||||
<SpaceIcon
|
||||
icon={customIcon}
|
||||
emoji={customEmoji}
|
||||
alt=""
|
||||
|
||||
@@ -16,7 +16,7 @@ export function Blocks<T extends DocumentBlock, Tag extends React.ElementType =
|
||||
/** Ancestors of the blocks */
|
||||
ancestorBlocks: DocumentBlock[];
|
||||
|
||||
/** HTML tag to use */
|
||||
/** HTML tag to use for the wrapper */
|
||||
tag?: Tag;
|
||||
|
||||
/** Style passed to the wrapper */
|
||||
@@ -24,12 +24,15 @@ export function Blocks<T extends DocumentBlock, Tag extends React.ElementType =
|
||||
|
||||
/** Style passed to all blocks */
|
||||
blockStyle?: ClassValue;
|
||||
|
||||
/** Props to pass to the wrapper element */
|
||||
wrapperProps?: React.ComponentProps<Tag>;
|
||||
},
|
||||
) {
|
||||
const { nodes, tag: Tag = 'div', style, blockStyle, ...contextProps } = props;
|
||||
const { nodes, tag: Tag = 'div', style, blockStyle, wrapperProps, ...contextProps } = props;
|
||||
|
||||
return (
|
||||
<Tag className={tcls(style)}>
|
||||
<Tag {...wrapperProps} className={tcls(style)}>
|
||||
{nodes.map((node, index) => (
|
||||
<Block
|
||||
key={node.key}
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
--shiki-token-string-expression: #6a4906;
|
||||
--shiki-token-punctuation: theme('colors.pomegranate.700/.92');
|
||||
--shiki-token-link: theme('colors.dark.DEFAULT');
|
||||
--shiki-token-inserted: #22863a;
|
||||
--shiki-token-deleted: #b31d28;
|
||||
--shiki-token-changed: #8250df;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
@@ -22,4 +25,7 @@ html.dark {
|
||||
--shiki-token-string-expression: theme('colors.light.4');
|
||||
--shiki-token-punctuation: #acc6ee;
|
||||
--shiki-token-link: theme('colors.pomegranate.400');
|
||||
--shiki-token-inserted: #85e89d;
|
||||
--shiki-token-deleted: #fdaeb7;
|
||||
--shiki-token-changed: #56b6c2;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { useHash } from '@/components/hooks';
|
||||
import { ClassValue, tcls } from '@/lib/tailwind';
|
||||
|
||||
/**
|
||||
* Details component rendered on client so it can expand dependent on url hash changes.
|
||||
*/
|
||||
export function Details(props: {
|
||||
children: React.ReactNode;
|
||||
id: string;
|
||||
contentIds?: string[];
|
||||
open?: boolean;
|
||||
className?: ClassValue;
|
||||
}) {
|
||||
const { children, id, className } = props;
|
||||
|
||||
const detailsRef = React.useRef<HTMLDetailsElement>(null);
|
||||
|
||||
const [openFromHash, setOpenFromHash] = React.useState(false);
|
||||
|
||||
const hash = useHash();
|
||||
/**
|
||||
* Open the details element if the url hash refers to the id of the details element
|
||||
* or the id of some element contained within the details element.
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
if (!hash || !detailsRef.current) {
|
||||
return;
|
||||
}
|
||||
if (hash === id) {
|
||||
setOpenFromHash(true);
|
||||
}
|
||||
const activeElement = document.getElementById(hash);
|
||||
setOpenFromHash(Boolean(activeElement && detailsRef.current?.contains(activeElement)));
|
||||
}, [hash, id]);
|
||||
|
||||
return (
|
||||
<details
|
||||
ref={detailsRef}
|
||||
id={id}
|
||||
open={props.open || openFromHash}
|
||||
className={tcls(
|
||||
className,
|
||||
'group/expandable',
|
||||
'shadow-dark/1',
|
||||
'bg-gradient-to-t',
|
||||
'from-light-1',
|
||||
'to-light-1',
|
||||
'border',
|
||||
'border-b-0',
|
||||
'border-dark-3/3',
|
||||
//all
|
||||
'[&]:mt-[0px]',
|
||||
//select first child
|
||||
'[&:first-child]:mt-5',
|
||||
'[&:first-child]:rounded-t-lg',
|
||||
//select first in group
|
||||
'[:not(&)_+&]:mt-5',
|
||||
'[:not(&)_+&]:rounded-t-lg',
|
||||
//select last in group
|
||||
'[&:not(:has(+_&))]:mb-5',
|
||||
'[&:not(:has(+_&))]:rounded-b-lg',
|
||||
'[&:not(:has(+_&))]:border-b',
|
||||
/* '[&:not(:has(+_&))]:shadow-1xs', */
|
||||
|
||||
'dark:border-light-2/[0.06]',
|
||||
'dark:from-dark-2',
|
||||
'dark:to-dark-2',
|
||||
'dark:shadow-none',
|
||||
|
||||
'group open:dark:to-dark-2/8',
|
||||
'group open:to-light-1/6',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</details>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import ChevronRight from '@geist-ui/icons/chevronRight';
|
||||
import IconHash from '@geist-ui/icons/hash';
|
||||
import { DocumentBlockExpandable } from '@gitbook/api';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
|
||||
import { getNodeFragmentByType } from '@/lib/document';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { Details } from './Details';
|
||||
import { BlockProps } from '../Block';
|
||||
import { Blocks } from '../Blocks';
|
||||
import { Inlines } from '../Inlines';
|
||||
@@ -25,42 +25,7 @@ export function Expandable(props: BlockProps<DocumentBlockExpandable>) {
|
||||
id = context.getId ? context.getId(id) : id;
|
||||
|
||||
return (
|
||||
<details
|
||||
id={id}
|
||||
open={context.mode === 'print'}
|
||||
className={tcls(
|
||||
style,
|
||||
'group/expandable',
|
||||
'shadow-dark/1',
|
||||
'bg-gradient-to-t',
|
||||
'from-light-1',
|
||||
'to-light-1',
|
||||
'border',
|
||||
'border-b-0',
|
||||
'border-dark-3/3',
|
||||
//all
|
||||
'[&]:mt-[0px]',
|
||||
//select first child
|
||||
'[&:first-child]:mt-5',
|
||||
'[&:first-child]:rounded-t-lg',
|
||||
//select first in group
|
||||
'[:not(&)_+&]:mt-5',
|
||||
'[:not(&)_+&]:rounded-t-lg',
|
||||
//select last in group
|
||||
'[&:not(:has(+_&))]:mb-5',
|
||||
'[&:not(:has(+_&))]:rounded-b-lg',
|
||||
'[&:not(:has(+_&))]:border-b',
|
||||
/* '[&:not(:has(+_&))]:shadow-1xs', */
|
||||
|
||||
'dark:border-light-2/[0.06]',
|
||||
'dark:from-dark-2',
|
||||
'dark:to-dark-2',
|
||||
'dark:shadow-none',
|
||||
|
||||
'group open:dark:to-dark-2/8',
|
||||
'group open:to-light-1/6',
|
||||
)}
|
||||
>
|
||||
<Details id={id} open={context.mode === 'print'} className={style}>
|
||||
<summary
|
||||
className={tcls(
|
||||
'cursor-pointer',
|
||||
@@ -78,11 +43,11 @@ export function Expandable(props: BlockProps<DocumentBlockExpandable>) {
|
||||
'[&::-webkit-details-marker]:hidden',
|
||||
)}
|
||||
>
|
||||
<ChevronRight
|
||||
<Icon
|
||||
icon="chevron-right"
|
||||
className={tcls(
|
||||
'inline-block',
|
||||
'w-4',
|
||||
'h-4',
|
||||
'size-3',
|
||||
'mr-2',
|
||||
'mb-1',
|
||||
'transition-transform',
|
||||
@@ -111,15 +76,15 @@ export function Expandable(props: BlockProps<DocumentBlockExpandable>) {
|
||||
'dark:ring-0',
|
||||
)}
|
||||
>
|
||||
<IconHash
|
||||
<Icon
|
||||
icon="hashtag"
|
||||
className={tcls(
|
||||
'inline-block',
|
||||
'w-4',
|
||||
'h-4',
|
||||
'size-3',
|
||||
'transition-colors',
|
||||
'stroke-transparent',
|
||||
'group-hover:stroke-dark/6',
|
||||
'dark:group-hover:stroke-light/5',
|
||||
'text-transparent',
|
||||
'group-hover:text-dark/6',
|
||||
'dark:group-hover:text-light/5',
|
||||
)}
|
||||
/>
|
||||
</a>
|
||||
@@ -129,8 +94,8 @@ export function Expandable(props: BlockProps<DocumentBlockExpandable>) {
|
||||
document={document}
|
||||
ancestorBlocks={[...ancestorBlocks, block]}
|
||||
context={context}
|
||||
style={['px-10', 'pb-5', 'space-y-3']}
|
||||
style={['px-10', 'pb-5', 'space-y-4']}
|
||||
/>
|
||||
</details>
|
||||
</Details>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,8 +66,11 @@ export async function File(props: BlockProps<DocumentBlockFile>) {
|
||||
'dark:border-light/2',
|
||||
)}
|
||||
>
|
||||
<div className={tcls('*:w-5', '*:h-5', '*:stroke-primary')}>
|
||||
<FileIcon contentType={contentType} />
|
||||
<div>
|
||||
<FileIcon
|
||||
contentType={contentType}
|
||||
className={tcls('size-5', 'text-primary')}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={tcls(
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
import IconDownload from '@geist-ui/icons/download';
|
||||
import IconFileText from '@geist-ui/icons/fileText';
|
||||
import IconImage from '@geist-ui/icons/image';
|
||||
import IconPaperClip from '@geist-ui/icons/paperclip';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
|
||||
import { SimplifiedFileType } from '@/lib/files';
|
||||
|
||||
/**
|
||||
* Render an appropriate icon for a file.
|
||||
*/
|
||||
export function FileIcon(props: { contentType: SimplifiedFileType | null }) {
|
||||
const { contentType } = props;
|
||||
export function FileIcon(props: { contentType: SimplifiedFileType | null; className: string }) {
|
||||
const { contentType, className } = props;
|
||||
|
||||
switch (contentType) {
|
||||
case 'pdf':
|
||||
return <IconFileText />;
|
||||
return <Icon icon="file-pdf" className={className} />;
|
||||
case 'image':
|
||||
return <IconImage />;
|
||||
return <Icon icon="file-image" className={className} />;
|
||||
case 'archive':
|
||||
return <IconPaperClip />;
|
||||
return <Icon icon="file-archive" className={className} />;
|
||||
default:
|
||||
return <IconDownload />;
|
||||
return <Icon icon="file-download" className={className} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import IconHash from '@geist-ui/icons/hash';
|
||||
import { DocumentBlockHeading } from '@gitbook/api';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
@@ -48,14 +48,15 @@ export function Heading(props: BlockProps<DocumentBlockHeading>) {
|
||||
textStyle.lineHeight,
|
||||
)}
|
||||
>
|
||||
<IconHash
|
||||
<Icon
|
||||
icon="hashtag"
|
||||
className={tcls(
|
||||
'w-3.5',
|
||||
'h-[1lh]',
|
||||
'transition-colors',
|
||||
'stroke-transparent',
|
||||
'group-hover:stroke-dark/6',
|
||||
'dark:group-hover:stroke-light/5',
|
||||
'text-transparent',
|
||||
'group-hover:text-dark/6',
|
||||
'dark:group-hover:text-light/5',
|
||||
'lg:w-4',
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DocumentBlockHint } from '@gitbook/api';
|
||||
import { Icon, IconName } from '@gitbook/icons';
|
||||
import React from 'react';
|
||||
|
||||
import { ClassValue, tcls } from '@/lib/tailwind';
|
||||
@@ -9,9 +10,8 @@ import { getBlockTextStyle } from './spacing';
|
||||
|
||||
export function Hint(props: BlockProps<DocumentBlockHint>) {
|
||||
const { block, style, ancestorBlocks, ...contextProps } = props;
|
||||
const hintStyle = HINT_STYLES[block.data.style];
|
||||
const hintStyle = HINT_STYLES[block.data.style] ?? HINT_STYLES.info;
|
||||
const firstLine = getBlockTextStyle(block.nodes[0]);
|
||||
const RenderIcon = HINT_STYLES[block.data.style].icon;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -34,17 +34,17 @@ export function Hint(props: BlockProps<DocumentBlockHint>) {
|
||||
'pr-3',
|
||||
'mt-0.5',
|
||||
firstLine.lineHeight,
|
||||
hintStyle.anchorColor,
|
||||
hintStyle.iconColor,
|
||||
)}
|
||||
>
|
||||
<RenderIcon />
|
||||
<Icon icon={hintStyle.icon} className={tcls('size-4')} />
|
||||
</div>
|
||||
<Blocks
|
||||
{...contextProps}
|
||||
ancestorBlocks={[...ancestorBlocks, block]}
|
||||
nodes={block.nodes}
|
||||
blockStyle={tcls(
|
||||
hintStyle.anchorColor,
|
||||
hintStyle.bodyColor,
|
||||
// render hash icon on the other side of the heading
|
||||
'flip-heading-hash',
|
||||
)}
|
||||
@@ -57,14 +57,16 @@ export function Hint(props: BlockProps<DocumentBlockHint>) {
|
||||
|
||||
const HINT_STYLES: {
|
||||
[style in DocumentBlockHint['data']['style']]: {
|
||||
icon: () => React.ReactNode;
|
||||
anchorColor: ClassValue;
|
||||
icon: IconName;
|
||||
iconColor: ClassValue;
|
||||
bodyColor: ClassValue;
|
||||
style: ClassValue;
|
||||
};
|
||||
} = {
|
||||
info: {
|
||||
icon: InfoIcon,
|
||||
anchorColor: [
|
||||
icon: 'circle-info',
|
||||
iconColor: ['text-periwinkle-700', 'dark:text-periwinkle-400'],
|
||||
bodyColor: [
|
||||
'[&>a]:text-periwinkle-700',
|
||||
'[&>a:hover]:text-periwinkle-800',
|
||||
'[&>code]:bg-periwinkle-700/4',
|
||||
@@ -72,8 +74,6 @@ const HINT_STYLES: {
|
||||
'[&>code]:shadow-none',
|
||||
|
||||
'text-periwinkle-900',
|
||||
'fill-periwinkle-700',
|
||||
'stroke-periwinkle-700',
|
||||
|
||||
'dark:text-periwinkle-200',
|
||||
'dark:[&>a]:text-periwinkle',
|
||||
@@ -82,8 +82,6 @@ const HINT_STYLES: {
|
||||
'dark:[&>code]:text-inherit',
|
||||
'decoration-periwinkle-700/6',
|
||||
'dark:decoration-periwinkle/6',
|
||||
'dark:fill-periwinkle-400',
|
||||
'dark:stroke-periwinkle-400',
|
||||
],
|
||||
style: [
|
||||
'bg-gradient-to-b',
|
||||
@@ -98,8 +96,9 @@ const HINT_STYLES: {
|
||||
],
|
||||
},
|
||||
warning: {
|
||||
icon: WarningIcon,
|
||||
anchorColor: [
|
||||
icon: 'circle-exclamation',
|
||||
iconColor: ['text-yellow-800', 'dark:text-yellow'],
|
||||
bodyColor: [
|
||||
'[&>a]:text-yellow-700',
|
||||
'[&>a:hover]:text-yellow-800',
|
||||
'[&>code]:bg-yellow-600/5',
|
||||
@@ -108,7 +107,6 @@ const HINT_STYLES: {
|
||||
|
||||
'text-yellow-900',
|
||||
|
||||
'fill-yellow-800',
|
||||
'dark:text-yellow-200',
|
||||
'dark:[&>a]:text-yellow',
|
||||
'dark:[&>a:hover]:text-yellow-600',
|
||||
@@ -116,7 +114,6 @@ const HINT_STYLES: {
|
||||
'dark:[&>code]:text-inherit',
|
||||
'decoration-yellow-700/6',
|
||||
'dark:decoration-yellow/6',
|
||||
'dark:fill-yellow',
|
||||
],
|
||||
style: [
|
||||
'bg-gradient-to-b',
|
||||
@@ -131,8 +128,9 @@ const HINT_STYLES: {
|
||||
],
|
||||
},
|
||||
danger: {
|
||||
icon: DangerIcon,
|
||||
anchorColor: [
|
||||
icon: 'triangle-exclamation',
|
||||
iconColor: ['text-pomegranate-700', 'dark:text-pomegranate-400'],
|
||||
bodyColor: [
|
||||
'[&>a]:text-pomegranate-600/9',
|
||||
'[&>a:hover]:text-pomegranate-800',
|
||||
'[&>code]:bg-pomegranate-600/4',
|
||||
@@ -141,7 +139,6 @@ const HINT_STYLES: {
|
||||
|
||||
'text-pomegranate-900',
|
||||
|
||||
'fill-pomegranate-700',
|
||||
'dark:text-pomegranate-100',
|
||||
'dark:[&>a]:text-pomegranate',
|
||||
'dark:[&>a:hover]:text-pomegranate-600',
|
||||
@@ -149,7 +146,6 @@ const HINT_STYLES: {
|
||||
'dark:[&>code]:text-inherit',
|
||||
'decoration-pomegranate-600/6',
|
||||
'dark:decoration-pomegranate/6',
|
||||
'dark:fill-pomegranate-400',
|
||||
],
|
||||
style: [
|
||||
'bg-gradient-to-b',
|
||||
@@ -164,8 +160,9 @@ const HINT_STYLES: {
|
||||
],
|
||||
},
|
||||
success: {
|
||||
icon: SuccessIcon,
|
||||
anchorColor: [
|
||||
icon: 'circle-check',
|
||||
iconColor: ['text-teal-700', 'dark:text-teal-400'],
|
||||
bodyColor: [
|
||||
'[&>a]:text-teal-600',
|
||||
'[&>a:hover]:text-teal-800',
|
||||
'[&>code]:bg-teal-600/4',
|
||||
@@ -173,8 +170,6 @@ const HINT_STYLES: {
|
||||
'[&>code]:shadow-none',
|
||||
|
||||
'text-teal-800',
|
||||
'fill-teal-700',
|
||||
'stroke-teal-700',
|
||||
|
||||
'dark:text-teal-100',
|
||||
'dark:[&>a]:text-teal-400',
|
||||
@@ -183,8 +178,6 @@ const HINT_STYLES: {
|
||||
'dark:[&>code]:text-inherit',
|
||||
'decoration-teal/6',
|
||||
'dark:decoration-teal/6',
|
||||
'dark:fill-teal-400',
|
||||
'dark:stroke-teal-300',
|
||||
],
|
||||
style: [
|
||||
'bg-gradient-to-b',
|
||||
@@ -199,131 +192,3 @@ const HINT_STYLES: {
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
function InfoIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={tcls('overflow-visible')}
|
||||
>
|
||||
<circle cx="8" cy="4.5" r="1.5" fill="inherit" fillOpacity="1" stroke="none" />
|
||||
<circle
|
||||
cx="8"
|
||||
cy="4.5"
|
||||
r="1.5"
|
||||
fill="inherit"
|
||||
fillOpacity="0.2"
|
||||
stroke="none"
|
||||
className={tcls(
|
||||
'animate-[pingAlt_8s_ease_infinite_forwards]',
|
||||
'origin-[8px_4.5px]',
|
||||
'w-[4px]',
|
||||
'h-[4px]',
|
||||
)}
|
||||
/>
|
||||
<rect
|
||||
x="7"
|
||||
y="7"
|
||||
width="2"
|
||||
rx="1"
|
||||
height="6"
|
||||
fill="inherit"
|
||||
fillOpacity="1"
|
||||
stroke="none"
|
||||
/>
|
||||
<circle cx="8" cy="8" r="8" fill="inherit" fillOpacity="0.16" stroke="none" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function DangerIcon() {
|
||||
return (
|
||||
<svg width="18" height="16" viewBox="0 0 18 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M7.2546 1C8.03034 -0.333333 9.96967 -0.333333 10.7454 1L17.727 13C18.5027 14.3333 17.533 16 15.9816 16H2.01842C0.466953 16 -0.502711 14.3333 0.27302 13L7.2546 1Z"
|
||||
fill="inherit"
|
||||
fillOpacity="0.16"
|
||||
stroke="none"
|
||||
/>
|
||||
<rect
|
||||
x="8"
|
||||
y="4"
|
||||
width="2"
|
||||
height="6"
|
||||
rx="1"
|
||||
fill="inherit"
|
||||
fillOpacity="0.92"
|
||||
stroke="none"
|
||||
className={tcls(
|
||||
'animate-[wag_4s_ease_infinite_forwards]',
|
||||
'[animation-composition:accumulate]',
|
||||
'origin-[9px_12.5px]',
|
||||
'will-change-transform',
|
||||
)}
|
||||
/>
|
||||
<circle cx="9" cy="12.5" r="1.5" fill="inherit" fillOpacity="0.92" stroke="none" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function WarningIcon() {
|
||||
return (
|
||||
<svg width="16" height="18" viewBox="0 0 16 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M6.9736 0.277489C7.60874 -0.0924963 8.39126 -0.0924962 9.0264 0.277489L14.9736 3.74189C15.6087 4.11187 16 4.79563 16 5.5356V12.4644C16 13.2044 15.6087 13.8881 14.9736 14.2581L9.0264 17.7225C8.39126 18.0925 7.60874 18.0925 6.9736 17.7225L1.0264 14.2581C0.391261 13.8881 0 13.2044 0 12.4644V5.5356C0 4.79563 0.391262 4.11187 1.0264 3.74189L6.9736 0.277489Z"
|
||||
fill="inherit"
|
||||
fillOpacity="0.16"
|
||||
stroke="none"
|
||||
/>
|
||||
<rect
|
||||
x="7"
|
||||
y="4"
|
||||
width="2"
|
||||
height="6"
|
||||
rx="1"
|
||||
fill="inherit"
|
||||
fillOpacity="0.8"
|
||||
stroke="none"
|
||||
/>
|
||||
<circle
|
||||
cx="8"
|
||||
cy="12.5"
|
||||
r="1.5"
|
||||
fill="inherit"
|
||||
fillOpacity="1"
|
||||
stroke="none"
|
||||
className={tcls('animate-[pulse_1s_ease_infinite_forwards]')}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function SuccessIcon() {
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="3" fill="inherit" fillOpacity="0.16" stroke="none" />
|
||||
<path
|
||||
className={tcls('animate-[stroke_8s_ease_infinite_forwards]')}
|
||||
d="M4 8.5L6.5 11L12 5.5"
|
||||
stroke="inherit"
|
||||
fill="none"
|
||||
strokeOpacity="0.8"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
pathLength="100"
|
||||
/>
|
||||
<path
|
||||
d="M4 8.5L6.5 11L12 5.5"
|
||||
stroke="inherit"
|
||||
fill="none"
|
||||
strokeOpacity="0.16"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
pathLength="100"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,9 @@
|
||||
import {
|
||||
Edit,
|
||||
Github,
|
||||
Gitlab,
|
||||
Maximize,
|
||||
X,
|
||||
Mail,
|
||||
Settings,
|
||||
Search,
|
||||
Delete,
|
||||
Star,
|
||||
AlertTriangle,
|
||||
Link,
|
||||
Eye,
|
||||
Lock,
|
||||
} from '@geist-ui/icons';
|
||||
import { ContentKitContext, DocumentBlockIntegration } from '@gitbook/api';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import { ContentKit, ContentKitOutput, ContentKitServerContext } from '@gitbook/react-contentkit';
|
||||
|
||||
import { ignoreAPIError, renderIntegrationUi } from '@/lib/api';
|
||||
import { INTEGRATIONS_HOST } from '@/lib/csp';
|
||||
import { parseMarkdown } from '@/lib/markdown';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
@@ -27,21 +13,21 @@ import { PlainCodeBlock } from '../CodeBlock';
|
||||
|
||||
const outputContext: ContentKitServerContext = {
|
||||
icons: {
|
||||
maximize: Maximize,
|
||||
edit: Edit,
|
||||
github: Github,
|
||||
gitlab: Gitlab,
|
||||
close: X,
|
||||
email: Mail,
|
||||
settings: Settings,
|
||||
search: Search,
|
||||
delete: Delete,
|
||||
star: Star,
|
||||
warning: AlertTriangle,
|
||||
link: Link,
|
||||
'link-external': Link,
|
||||
eye: Eye,
|
||||
lock: Lock,
|
||||
maximize: (props) => <Icon icon="maximize" {...props} />,
|
||||
edit: (props) => <Icon icon="edit" {...props} />,
|
||||
github: (props) => <Icon icon="github" {...props} />,
|
||||
gitlab: (props) => <Icon icon="gitlab" {...props} />,
|
||||
close: (props) => <Icon icon="x" {...props} />,
|
||||
email: (props) => <Icon icon="envelope" {...props} />,
|
||||
settings: (props) => <Icon icon="gear" {...props} />,
|
||||
search: (props) => <Icon icon="magnifying-glass" {...props} />,
|
||||
delete: (props) => <Icon icon="trash" {...props} />,
|
||||
star: (props) => <Icon icon="star" {...props} />,
|
||||
warning: (props) => <Icon icon="triangle-exclamation" {...props} />,
|
||||
link: (props) => <Icon icon="link" {...props} />,
|
||||
'link-external': (props) => <Icon icon="arrow-up-right-from-square" {...props} />,
|
||||
eye: (props) => <Icon icon="eye" {...props} />,
|
||||
lock: (props) => <Icon icon="lock" {...props} />,
|
||||
},
|
||||
codeBlock: (props) => {
|
||||
return <PlainCodeBlock code={props.code} syntax={props.syntax} />;
|
||||
@@ -84,7 +70,7 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio
|
||||
return (
|
||||
<div className={tcls(style)}>
|
||||
<ContentKit
|
||||
security={{ firstPartyDomains: ['integrations.gitbook.com'] }}
|
||||
security={{ firstPartyDomains: [INTEGRATIONS_HOST] }}
|
||||
initialInput={initialInput}
|
||||
initialOutput={initialOutput}
|
||||
render={async (request) => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { tcls } from '@/lib/tailwind';
|
||||
import { BlockProps } from './Block';
|
||||
import { InlineProps } from './Inline';
|
||||
|
||||
const mathJaxUrl = getStaticFileURL('mathjax@3.2.2/tex-chtml.js');
|
||||
const assetsUrl = getStaticFileURL('math');
|
||||
|
||||
export async function BlockMath(props: BlockProps<DocumentBlockMath>) {
|
||||
const { block, style } = props;
|
||||
@@ -17,7 +17,7 @@ export async function BlockMath(props: BlockProps<DocumentBlockMath>) {
|
||||
formula={block.data.formula}
|
||||
inline={false}
|
||||
className={tcls(style, 'overflow-x-auto')}
|
||||
mathJaxUrl={mathJaxUrl}
|
||||
assetsUrl={assetsUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -25,5 +25,5 @@ export async function BlockMath(props: BlockProps<DocumentBlockMath>) {
|
||||
export async function InlineMath(props: InlineProps<DocumentInlineMath>) {
|
||||
const { inline } = props;
|
||||
|
||||
return <MathFormula formula={inline.data.formula} inline={true} mathJaxUrl={mathJaxUrl} />;
|
||||
return <MathFormula formula={inline.data.formula} inline={true} assetsUrl={assetsUrl} />;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user