Compare commits

..

65 Commits

Author SHA1 Message Date
Samy Pessé d1bd02a6f5 Version Packages (#2395)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-22 15:43:27 +02:00
Samy Pessé 24cd72e088 Use a personal token for the changeset GitHub workflow (#2394) 2024-07-22 15:30:11 +02:00
github-actions[bot] 47f7558c38 Version Packages (#2393)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-22 15:05:57 +02:00
Samy Pessé de747b7b33 Better structure monorepo and publish JS files for react-openapi and react-math (#2392) 2024-07-22 15:02:04 +02:00
Samy Pessé 8e8b73fbd9 Publish packages as public with changeset (#2391) 2024-07-21 21:25:43 +02:00
github-actions[bot] e966e33e8f Version Packages (#2390)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-21 19:45:18 +02:00
Samy Pessé bd0ca5babd Fix @gitbook/react-openapi when react import are mandatory (#2389) 2024-07-21 19:43:50 +02:00
Samy Pessé f6bacea1ae Skip hidden pages in sitemap.xml (#2388) 2024-07-19 09:20:16 +02:00
github-actions[bot] ba2a523722 Version Packages (#2387)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-19 08:28:14 +02:00
Samy Pessé 57adb3e54a Fix publish with changeset (#2386) 2024-07-19 08:23:03 +02:00
github-actions[bot] 7377928c8a Version Packages (#2385)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-18 21:05:22 +02:00
Samy Pessé 5f8a8fe9b1 Setup Changeset to publish packages to NPM (#2384) 2024-07-18 21:00:50 +02:00
Scott Cazan 29a0bf25c4 Update Site search endpoint (#2382)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2024-07-18 10:36:12 +02:00
spastorelli 92bb016576 Show additional context when linking to a page group & a page in another space (#2381) 2024-07-18 10:22:15 +02:00
Samy Pessé c8810ea002 Hide pages from table of contents when set as hidden (#2383)
Co-authored-by: Scott Cazan <scottcazan@gmail.com>
2024-07-18 09:50:46 +02:00
spastorelli aa8bbaddc4 Fallback redirect to root when switching variants (#2376) 2024-07-16 09:24:31 +02:00
Samy Pessé b8c136e9c0 Fix render of one of when alternatives are primitives (#2378) 2024-07-15 10:15:42 +02:00
Samy Pessé 46d54172b0 Fix CI step to verify formatting (#2379) 2024-07-14 23:53:55 +02:00
Brett Jephson 68dc2de190 Remove Page/Space label from BlockContentRef (#2374) 2024-07-12 15:02:05 +01:00
Samy Pessé 81b372f372 Fix response example when response example is a string (#2377) 2024-07-12 10:35:22 +02:00
Scott Cazan 966e520640 Fix Source links in Ask AI not navigating to the page (#2375) 2024-07-11 15:39:28 +02:00
spastorelli 12702930ec Optimize SpacesDropdownMenuItem client side props (#2373) 2024-07-11 10:42:53 +02:00
spastorelli 6ce57bcda1 Fix issue where nav route/path is not retained when switching variants (#2372) 2024-07-10 17:10:19 +02:00
spastorelli d99b453bc4 Fix issue where published content crashes when localstorage is disabled (#2371) 2024-07-10 14:56:21 +02:00
Samy Pessé aaf5793cb7 Use schema.example if defined, even if value is falsy (#2370) 2024-07-05 16:34:48 +02:00
Samy Pessé 9b1762d2ab Fix invalid api response from carbon (#2369) 2024-07-05 15:55:35 +02:00
Samy Pessé 8d38d000fd Fix RND-4344 (#2368) 2024-07-04 17:04:35 +02:00
Scott Cazan 4cf9cdd56d Fix ToC not re-rendering when linking to a variant from SearchResults (#2366) 2024-07-03 12:22:08 +02:00
Scott Cazan 39406bbf54 Use published URL for site when displaying results for a Site space (#2364) 2024-07-02 16:14:39 +02:00
Scott Cazan 9c5c1e5b68 Show sources with no answer (#2363)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2024-06-28 15:25:01 +02:00
Johan Preynat 6929b0b98b Prevent SVG images to be resized and rendered on the /~gitbook/image endpoint (#2362) 2024-06-27 12:24:46 +02:00
Johan Preynat 7c26d491b1 Fix passing the correct nonce for iframely script and preload it (#2360) 2024-06-25 17:11:19 +02:00
Johan Preynat 97035a054a Tree shake unused code for Sentry (#2359) 2024-06-25 15:55:48 +02:00
Johan Preynat db45c6d773 Rework context to actually share memory cache between middleware and handlers for all requests (#2358) 2024-06-25 14:30:38 +02:00
Johan Preynat e3f5f81e62 Use fnv1a instead of SHA256 to create images signatures (#2353) 2024-06-24 13:31:18 +02:00
Johan Preynat f5df40e17d Use a singleton to store the memory cache in an internal WeakMap for the current request (#2356) 2024-06-24 11:13:03 +02:00
Johan Preynat e13c76efed Set preconnect links to main domains on render (#2354) 2024-06-21 17:02:02 +02:00
Johan Preynat 4a11d8d9eb Share memory cache between middleware and request handler (#2347) 2024-06-21 11:13:06 +02:00
spastorelli 009ed8cc6e Call trackViewInSiteById API endpoint when tracking view for sites (#2350) 2024-06-21 10:01:35 +02:00
vibhanshub 106cbbd987 Add support for examples in OpenAPI block (#2314) 2024-06-20 16:02:58 +05:30
Johan Preynat 56e5c3c3f8 Parse cache tags returned from the API set them in response (#2349) 2024-06-20 10:09:02 +02:00
Taran Vohra 1dc3234003 Fix page metadata title (#2348) 2024-06-18 20:52:12 +05:30
Johan Preynat 22dc0d4ff5 Fix passing revalidateBefore option when calling cacheResponse to ensure these go through background revalidation (#2342) 2024-06-17 17:16:12 +02:00
Taran Vohra 3a4caf062c Add getContentTitle to properly render the title for sites & legacy published content (#2343) 2024-06-17 20:28:21 +05:30
Taran Vohra 8ad0657159 Use site's share key as context while fetching site spaces (#2341) 2024-06-14 20:59:05 +05:30
Taran Vohra d8a4ecd0c6 Pass site url as context to listSiteSpaces API (#2339) 2024-06-14 10:40:18 +05:30
David Burghoff 65c7968438 Add German Locale support (#2317)
Co-authored-by: David Burghoff <david.burghoff@swot.de>
Co-authored-by: taranvohra <taranvohra@outlook.com>
2024-06-13 15:04:49 +02:00
Rodrigo Castro 3914f11150 Add Brazilian Portuguese translation (#2285)
Co-authored-by: taranvohra <taranvohra@outlook.com>
2024-06-13 14:54:47 +02:00
Addison d363908297 Add contributors to readme, bump API (#2333) 2024-06-12 15:24:18 +02:00
Greg Bergé 414556ac7a Remove cache client-side (#2337) 2024-06-10 11:17:48 +02:00
Steven H c768e9a90a Add another space to ignore list whilst we debug issues. (#2334) 2024-05-28 19:02:07 +01:00
Taran Vohra 553f1f228f Bump @gitbook/api to 0.48.0 (#2328) 2024-05-24 20:10:25 +05:30
Taran Vohra 30986528a7 bump @gitbook/api to 0.47 (#2327) 2024-05-24 19:07:54 +05:30
Steven H c70c2ddad8 Bump next-on-pages version to fix SSRF attack. (#2319) 2024-05-16 17:05:58 +01:00
Marc Laventure cf74d00758 chore: update scalar client (#2316) 2024-05-14 11:52:09 -07:00
Steven H c2256bfd14 Fix an issue where attackers could use the Nextjs image endpoint to redirect to any URL. (#2312) 2024-05-14 11:34:33 +01:00
fuyangpengqi 5d73a0f062 chore: fix some typos in README.md (#2290)
Signed-off-by: fuyangpengqi <995764973@qq.com>
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2024-05-13 16:22:07 +02:00
Samy Pessé 8f5f6c61f8 Setup the pull_request workflow on actions (#2311) 2024-05-13 16:10:15 +02:00
Taran Vohra 9a86965ba1 Fix VA cookie to be only set if basePaths match (#2300) 2024-05-06 21:02:48 +05:30
Scott Cazan c8d5f825fc Update GitBook trademark (#2305) 2024-05-02 12:18:02 +02:00
Steven H 5db3cbc2ce Render Embed blocks that can be handled by an integration in the Space. (#2302) 2024-04-25 15:25:53 +01:00
Steven H f8d148bec4 Use plain code highlighting on some spaces that are currently crashing. (#2301) 2024-04-25 09:50:08 +01:00
Steven H 198dd9f158 Investigate an issue where images with 127.0.0.1 in the URL are not correctly resolved. (#2298) 2024-04-19 17:21:22 +01:00
Steven H 2b63bc158a Improve rendering of File column in tables. (#2297) 2024-04-19 16:58:46 +01:00
spastorelli 4a8097d968 Use site space context from token in multi-id mode when provided (#2295) 2024-04-19 16:51:12 +02:00
370 changed files with 2751 additions and 833 deletions
+8
View File
@@ -0,0 +1,8 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
+11
View File
@@ -0,0 +1,11 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.0.2/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
+15 -11
View File
@@ -1,5 +1,9 @@
on: [push]
name: CI
on:
pull_request:
push:
branches:
- main
jobs:
deploy:
@@ -16,7 +20,7 @@ jobs:
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.0.33
bun-version: 1.1.18
- name: Install dependencies
run: bun install --frozen-lockfile
env:
@@ -54,7 +58,7 @@ jobs:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: ${{ vars.CLOUDFLARE_PROJECT_NAME }}
directory: ./.vercel/output/static
directory: ./packages/gitbook/.vercel/output/static
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
workingDirectory: ./
- name: Outputs
@@ -78,7 +82,7 @@ jobs:
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.0.33
bun-version: 1.1.18
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Setup Playwright
@@ -98,13 +102,13 @@ jobs:
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.0.33
bun-version: 1.1.18
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Run pagespeed tests
run: bun ./tests/pagespeed-testing.ts $DEPLOYMENT_URL
run: bun ./packages/gitbook/tests/pagespeed-testing.ts $DEPLOYMENT_URL
env:
DEPLOYMENT_URL: ${{needs.deploy.outputs.deployment_url}}
PAGESPEED_API_KEY: ${{ secrets.PAGESPEED_API_KEY }}
@@ -117,12 +121,12 @@ jobs:
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.0.33
bun-version: 1.1.18
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- run: bun format --check .
- run: bun format:check
lint:
runs-on: ubuntu-latest
name: Lint
@@ -132,7 +136,7 @@ jobs:
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.0.33
bun-version: 1.1.18
- name: Install dependencies
run: bun install --frozen-lockfile
env:
@@ -147,7 +151,7 @@ jobs:
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.0.33
bun-version: 1.1.18
- name: Install dependencies
run: bun install --frozen-lockfile
env:
@@ -162,7 +166,7 @@ jobs:
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.0.33
bun-version: 1.1.18
- name: Install dependencies
run: bun install --frozen-lockfile
env:
+37
View File
@@ -0,0 +1,37 @@
name: Publish
on:
push:
branches:
- main
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
publish:
name: Publish
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v3
with:
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
fetch-depth: 0
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.18
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Create Release Pull Request or Publish to npm
id: changesets
uses: changesets/action@v1
with:
publish: npm run release
env:
# Using a PAT instead of GITHUB_TOKEN because we need to run workflows when releases are created
# https://github.com/orgs/community/discussions/26875#discussioncomment-3253761
GITHUB_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+3 -29
View File
@@ -5,16 +5,6 @@ node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
@@ -24,25 +14,9 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# Turbo
.turbo
# vercel
# Vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# visual tests
screenshots/
# Sentry Config File
.sentryclirc
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
# Generated public files
/public/~gitbook/static/
+6 -1
View File
@@ -1 +1,6 @@
.next
.next
.vercel
# Generated
packages/emoji-codepoints/index.ts
packages/gitbook/public/~gitbook/static/
+8 -2
View File
@@ -75,7 +75,7 @@ GitBook's rendering engine is fully open-source and built on top of [Next.js](ht
### Types of contributions
We encourage you to contribute to GitBook to help us build the best tool for doucmenting techincal knowledge. If you're looking for some quick ways to contribute, continue reading to learn more about popular contributions.
We encourage you to contribute to GitBook to help us build the best tool for documenting technical knowledge. If you're looking for some quick ways to contribute, continue reading to learn more about popular contributions.
#### Translations
@@ -92,7 +92,7 @@ Encounter a bug or find an issue you'd like to fix? Helping us fix issues relate
>
> _Looking to add a specific feature in GitBook? Head to our [contributing guide](/.github/CONTRIBUTING.md) to get started._
>
> Self-hosting this project puts the responsibility of maintaining and merging future updates on **you**. We cannot guarantee support, maintainance, or updates to forked and self-hosted instances of this project.
> Self-hosting this project puts the responsibility of maintaining and merging future updates on **you**. We cannot guarantee support, maintenance, or updates to forked and self-hosted instances of this project.
>
> We want to make it as easy as possible for our community to collaborate and push the future of GitBook, which is why we encourage you to contribute to our product directly instead of creating your own version.
@@ -119,6 +119,12 @@ GitBook wouldn't be possible without these projects:
- [Tailwind CSS](https://tailwindcss.com/)
- [Framer Motion](https://www.npmjs.com/package/framer-motion)
## Contributors
<a href="https://github.com/gitbookIO/gitbook/graphs/contributors">
<img src="https://contrib.rocks/image?repo=gitbookIO/gitbook" />
</a>
## Legacy GitBook (Deprecated)
Our previous version of GitBook and it's CLI tool are now deprecated. You can still view the old repository and it's commits on this [branch](https://github.com/GitbookIO/gitbook/tree/legacy).
BIN
View File
Binary file not shown.
+12 -77
View File
@@ -2,91 +2,26 @@
"name": "gitbook",
"version": "0.1.0",
"private": true,
"packageManager": "bun@1.1.18",
"scripts": {
"dev": "next dev",
"build": "next build",
"build:cloudflare": "next-on-pages",
"start": "next start",
"lint": "next lint",
"dev": "turbo run dev",
"build": "turbo run build",
"build:cloudflare": "turbo run build:cloudflare",
"lint": "turbo run lint",
"typecheck": "turbo run typecheck",
"format": "prettier ./ --ignore-unknown --write",
"format:check": "prettier ./ --ignore-unknown --list-different",
"typecheck": "tsc --noEmit",
"unit": "bun test {src,packages}/**/*.test.ts",
"e2e": "playwright test",
"postinstall": "rm -rf ./public/~gitbook/static/mathjax@3.2.2 && mkdir -p ./public/~gitbook/static/ && cp -R node_modules/mathjax/es5 ./public/~gitbook/static/mathjax@3.2.2"
"unit": "turbo run unit",
"e2e": "turbo run e2e",
"changeset": "changeset",
"release": "turbo run build && changeset publish"
},
"workspaces": [
"packages/*"
],
"dependencies": {
"@geist-ui/icons": "^1.0.2",
"@gitbook/api": "^0.43.0",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-popover": "^1.0.7",
"@sentry/nextjs": "^7.94.1",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/typography": "^0.5.10",
"@upstash/redis": "^1.27.1",
"ajv": "^8.12.0",
"assert-never": "^1.2.1",
"bun-types": "^1.0.7",
"classnames": "^2.5.1",
"content-security-policy-merger": "^1.0.0",
"framer-motion": "^10.16.14",
"js-cookie": "^3.0.5",
"jsontoxml": "^1.0.1",
"katex": "^0.16.9",
"mathjax": "^3.2.2",
"memoizee": "^0.4.15",
"next": "^14.1.3",
"next-themes": "^0.2.1",
"nuqs": "^1.15.4",
"object-hash": "^3.0.0",
"openapi-types": "^12.1.3",
"p-map": "^7.0.0",
"parse-cache-control": "^1.0.1",
"react": "^18",
"react-dom": "^18",
"react-hotkeys-hook": "^4.4.1",
"recoil": "^0.7.7",
"rehype-sanitize": "^6.0.0",
"rehype-stringify": "^10.0.0",
"remark-gfm": "^4.0.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.0",
"rison": "^0.1.1",
"server-only": "^0.0.1",
"shiki": "^1.2.0",
"tailwind-merge": "^2.2.0",
"tailwind-shades": "^1.1.2",
"unified": "^11.0.4",
"url-join": "^5.0.0"
},
"devDependencies": {
"@argos-ci/playwright": "^2.0.0",
"@cloudflare/next-on-pages": "^1.9.0",
"@cloudflare/workers-types": "^4.20231218.0",
"@playwright/test": "^1.42.1",
"@types/js-cookie": "^3.0.6",
"@types/jsontoxml": "^1.0.5",
"@types/jsonwebtoken": "^9.0.6",
"@types/katex": "^0.16.5",
"@types/node": "^20",
"@types/object-hash": "^3.0.6",
"@types/parse-cache-control": "^1.0.4",
"@types/psi": "^4.1.6",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/rison": "^0.0.9",
"autoprefixer": "^10",
"eslint": "^8",
"eslint-config-next": "13.5.6",
"eslint-plugin-import": "^2.29.0",
"jsonwebtoken": "^9.0.2",
"postcss": "^8",
"@changesets/cli": "^2.27.7",
"prettier": "^3.0.3",
"psi": "^4.1.0",
"tailwindcss": "^3.4.0",
"typescript": "^5"
"turbo": "^2.0.9"
}
}
+13
View File
@@ -0,0 +1,13 @@
# @gitbook/emoji-codepoints
## 0.2.0
### Minor Changes
- 57adb3e: Second release to fix publishing with changeset
## 0.1.0
### Minor Changes
- 5f8a8fe: Initial release
+1
View File
@@ -1,6 +1,7 @@
{
"name": "@gitbook/emoji-codepoints",
"description": "Optimized mapping of codepoints to the fully qualified emoji codepoints",
"version": "0.2.0",
"private": true,
"exports": "./index.ts",
"dependencies": {},
+35
View File
@@ -0,0 +1,35 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# visual tests
screenshots/
# Sentry Config File
.sentryclirc
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
# Generated public files
/public/~gitbook/static/
+24
View File
@@ -0,0 +1,24 @@
# gitbook
## 0.2.1
### Patch Changes
- Updated dependencies [24cd72e]
- @gitbook/react-contentkit@0.4.0
- @gitbook/react-math@0.4.0
- @gitbook/react-openapi@0.4.0
## 0.2.0
### Minor Changes
- de747b7: Refactor the repository to be a proper monorepo and publish JS files on NPM instead of TypeScript files.
### Patch Changes
- Updated dependencies [de747b7]
- Updated dependencies [de747b7]
- @gitbook/react-contentkit@0.3.0
- @gitbook/react-openapi@0.3.0
- @gitbook/react-math@0.3.0
@@ -106,6 +106,93 @@ const testCases: TestsCase[] = [
},
],
},
{
name: 'GitBook Site (Navigation when switching variant)',
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/',
tests: [
{
name: 'Keep navigation path/route when switching variant (Public)',
url: 'api-multi-versions/reference/api-reference/pets',
screenshot: false,
run: async (page) => {
const spaceDrowpdown = await page.waitForSelector(
'[data-testid="space-dropdown-button"]',
);
await spaceDrowpdown.click();
// Click the second variant in the dropdown
await page
.getByRole('link', {
name: '2.0',
})
.click();
// It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant
await page.waitForURL(
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions/v/2.0/reference/api-reference/pets?fallback=true',
);
},
},
{
name: 'Keep navigation path/route when switching variant (Share link)',
url: 'api-multi-versions-share-links/bRfQbzwsK8rbN1GRxx7K/reference/api-reference/pets',
screenshot: false,
run: async (page) => {
const spaceDrowpdown = await page.waitForSelector(
'[data-testid="space-dropdown-button"]',
);
await spaceDrowpdown.click();
// Click the second variant in the dropdown
await page
.getByRole('link', {
name: '2.0',
})
.click();
// It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant
await page.waitForURL(
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-share-links/bRfQbzwsK8rbN1GRxx7K/v/2.0/reference/api-reference/pets?fallback=true',
);
},
},
{
name: 'Keep navigation path/route when switching variant (VA)',
screenshot: false,
url: (() => {
const privateKey = 'c26190fc-74b2-4b54-9fc7-df9941104953';
const token = jwt.sign(
{
name: 'gitbook-open-tests',
},
privateKey,
{
expiresIn: '24h',
},
);
return `api-multi-versions-va/reference/api-reference/pets?jwt_token=${token}`;
})(),
run: async (page) => {
const spaceDrowpdown = await page.waitForSelector(
'[data-testid="space-dropdown-button"]',
);
await spaceDrowpdown.click();
// Click the second variant in the dropdown
await page
.getByRole('link', {
name: '2.0',
})
.click();
// It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant
await page.waitForURL(
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-va/v/2.0/reference/api-reference/pets?fallback=true',
);
},
},
],
},
{
name: 'GitBook',
baseUrl: 'https://docs.gitbook.com',
@@ -276,6 +363,11 @@ const testCases: TestsCase[] = [
name: 'Page options',
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
tests: [
{
name: 'Hidden',
url: 'page-options/page-hidden',
run: waitForCookiesDialog,
},
{
name: 'With cover',
url: 'page-options/page-with-cover',
@@ -9,7 +9,7 @@ module.exports = withSentryConfig(
GITBOOK_ASSETS_PREFIX: process.env.GITBOOK_ASSETS_PREFIX,
},
webpack(config) {
webpack(config, { dev, webpack }) {
config.resolve.fallback = {
...config.resolve.fallback,
@@ -19,6 +19,21 @@ module.exports = withSentryConfig(
http: false,
};
// Tree shake debug code for Sentry
// https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/tree-shaking/#tree-shaking-with-nextjs
if (!dev) {
config.plugins.push(
new webpack.DefinePlugin({
__SENTRY_DEBUG__: false,
// We always init Sentry with enableTracing: false for now, so this is useless
__SENTRY_TRACING__: false,
__RRWEB_EXCLUDE_IFRAME__: true,
__RRWEB_EXCLUDE_SHADOW_DOM__: true,
__SENTRY_EXCLUDE_REPLAY_WORKER__: true,
}),
);
}
return config;
},
@@ -39,6 +54,15 @@ module.exports = withSentryConfig(
assetPrefix: process.env.GITBOOK_ASSETS_PREFIX,
poweredByHeader: false,
images: {
remotePatterns: [
{
protocol: 'https',
hostname: '*.gitbook.io',
},
],
},
},
{
silent: true,
+89
View File
@@ -0,0 +1,89 @@
{
"name": "gitbook",
"version": "0.2.1",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"build:cloudflare": "next-on-pages",
"start": "next start",
"lint": "next lint",
"typecheck": "tsc --noEmit",
"e2e": "playwright test",
"unit": "bun test {src,packages}",
"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"
},
"dependencies": {
"@geist-ui/icons": "^1.0.2",
"@gitbook/api": "^0.56.0",
"@gitbook/react-math": "workspace:*",
"@gitbook/react-openapi": "workspace:*",
"@gitbook/react-contentkit": "workspace:*",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-popover": "^1.0.7",
"@sentry/nextjs": "^7.94.1",
"@sindresorhus/fnv1a": "^3.1.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/typography": "^0.5.10",
"@upstash/redis": "^1.27.1",
"ajv": "^8.12.0",
"assert-never": "^1.2.1",
"bun-types": "^1.1.20",
"classnames": "^2.5.1",
"content-security-policy-merger": "^1.0.0",
"framer-motion": "^10.16.14",
"js-cookie": "^3.0.5",
"jsontoxml": "^1.0.1",
"katex": "^0.16.9",
"mathjax": "^3.2.2",
"memoizee": "^0.4.15",
"next": "^14.1.3",
"next-themes": "^0.2.1",
"nuqs": "^1.17.4",
"object-hash": "^3.0.0",
"openapi-types": "^12.1.3",
"p-map": "^7.0.0",
"parse-cache-control": "^1.0.1",
"react": "^18",
"react-dom": "^18",
"react-hotkeys-hook": "^4.4.1",
"recoil": "^0.7.7",
"rehype-sanitize": "^6.0.0",
"rehype-stringify": "^10.0.0",
"remark-gfm": "^4.0.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.0",
"rison": "^0.1.1",
"server-only": "^0.0.1",
"shiki": "^1.2.0",
"tailwind-merge": "^2.2.0",
"tailwind-shades": "^1.1.2",
"unified": "^11.0.4",
"url-join": "^5.0.0"
},
"devDependencies": {
"@argos-ci/playwright": "^2.0.0",
"@playwright/test": "^1.42.1",
"@cloudflare/next-on-pages": "^1.11.3",
"@cloudflare/workers-types": "^4.20231218.0",
"@types/js-cookie": "^3.0.6",
"@types/jsontoxml": "^1.0.5",
"@types/jsonwebtoken": "^9.0.6",
"@types/node": "^20",
"@types/object-hash": "^3.0.6",
"@types/parse-cache-control": "^1.0.4",
"@types/psi": "^4.1.6",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/rison": "^0.0.9",
"autoprefixer": "^10",
"eslint": "^8",
"eslint-config-next": "^14.2.5",
"eslint-plugin-import": "^2.29.1",
"jsonwebtoken": "^9.0.2",
"postcss": "^8",
"psi": "^4.1.0",
"tailwindcss": "^3.4.0",
"typescript": "^5.5.3"
}
}
+27
View File
@@ -0,0 +1,27 @@
import {
BrowserClient,
makeFetchTransport,
defaultStackParser,
getCurrentScope,
} from '@sentry/nextjs';
const dsn = process.env.SENTRY_DSN;
if (dsn) {
// To tree shake default integrations that we don't use
// https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/tree-shaking/#tree-shaking-default-integrations
const client = new BrowserClient({
debug: false,
dsn,
integrations: [],
sampleRate: 0.1,
enableTracing: false,
beforeSendTransaction: () => {
return null;
},
transport: makeFetchTransport,
stackParser: defaultStackParser,
});
getCurrentScope().setClient(client);
client.init();
}
@@ -1,8 +1,8 @@
import * as Sentry from '@sentry/nextjs';
import { init } from '@sentry/nextjs';
const dsn = process.env.SENTRY_DSN;
if (dsn) {
Sentry.init({
init({
debug: false,
dsn,
@@ -1,8 +1,8 @@
import * as Sentry from '@sentry/nextjs';
import { init } from '@sentry/nextjs';
const dsn = process.env.SENTRY_DSN;
if (dsn) {
Sentry.init({
init({
debug: false,
dsn,
@@ -1,6 +1,12 @@
import { NextRequest } from 'next/server';
import { verifyImageSignature, resizeImage, CloudflareImageOptions } from '@/lib/images';
import {
verifyImageSignature,
resizeImage,
CloudflareImageOptions,
checkIsSizableImageURL,
} from '@/lib/images';
import { parseImageAPIURL } from '@/lib/urls';
export const runtime = 'edge';
@@ -10,19 +16,30 @@ export const runtime = 'edge';
* Fetch and resize an image.
*/
export async function GET(request: NextRequest) {
const url = request.nextUrl.searchParams.get('url');
let urlParam = request.nextUrl.searchParams.get('url');
const signature = request.nextUrl.searchParams.get('sign');
if (!url || !signature) {
// The current signature algorithm sets version as 1, but we need to support the older version as well
// for previously generated content. In this case, we default to version 0.
const signatureVersion = (request.nextUrl.searchParams.get('sv') as '1') || '0';
if (!urlParam || !signature) {
return new Response('Missing url/sign parameters', { status: 400 });
}
const url = parseImageAPIURL(urlParam);
// Prevent infinite loops
if (url.includes('/~gitbook/image')) {
return new Response('Invalid url parameter', { status: 400 });
}
// Check again if the image can be sized, even though we checked when rendering the Image component
// Otherwise, it's possible to pass just any link to this endpoint and trigger HTML injection on the domain
if (!checkIsSizableImageURL(url)) {
return new Response('Invalid url parameter', { status: 400 });
}
// Verify the signature
const verified = await verifyImageSignature(url, signature);
const verified = await verifyImageSignature(url, { signature, version: signatureVersion });
if (!verified) {
return new Response(`Invalid signature "${signature ?? ''}" for "${url}"`, { status: 400 });
}
@@ -0,0 +1,40 @@
'use client';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import React from 'react';
import { useScrollToHash } from '@/components/hooks';
/**
* Client component to initialize interactivity for a page.
*/
export function PageClientLayout(props: {}) {
// We use this hook in the page layout to ensure the elements for the blocks
// are rendered before we scroll to the hash.
useScrollToHash();
useStripFallbackQueryParam();
return null;
}
/**
* Strip the fallback query parameter from current URL.
*
* When the user switches variants using the space dropdown, we pass a fallback=true parameter.
* This parameter indicates that we should redirect to the root page if the path from the
* previous variant doesn't exist in the new variant. If the path does exist, no redirect occurs,
* so we need to remove the fallback parameter.
*/
function useStripFallbackQueryParam() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
React.useEffect(() => {
if (searchParams.has('fallback')) {
const params = new URLSearchParams(searchParams.toString());
params.delete('fallback');
router.push(`${pathname}?${params.toString()}${window.location.hash ?? ''}`);
}
}, [router, pathname, searchParams]);
}
@@ -6,9 +6,10 @@ import React from 'react';
import { PageAside } from '@/components/PageAside';
import { PageBody, PageCover } from '@/components/PageBody';
import { PageHrefContext, absoluteHref, pageHref } from '@/lib/links';
import { getPagePath } from '@/lib/pages';
import { getPagePath, resolveFirstDocument } from '@/lib/pages';
import { ContentRefContext } from '@/lib/references';
import { tcls } from '@/lib/tailwind';
import { getContentTitle } from '@/lib/utils';
import { PageClientLayout } from './PageClientLayout';
import { PagePathParams, fetchPageData, getPathnameParam, normalizePathname } from '../../fetch';
@@ -18,14 +19,29 @@ export const runtime = 'edge';
/**
* Fetch and render a page.
*/
export default async function Page(props: { params: PagePathParams }) {
const { params } = props;
export default async function Page(props: {
params: PagePathParams;
searchParams: { fallback?: string };
}) {
const { params, searchParams } = props;
const {
content: contentPointer,
contentTarget,
space,
parent,
customization,
pages,
page,
document,
} = await getPageDataWithFallback({
pagePathParams: params,
searchParams,
redirectOnFallback: true,
});
const rawPathname = getPathnameParam(params);
const { contentTarget, space, customization, pages, page, document } =
await fetchPageData(params);
const linksContext: PageHrefContext = {};
const rawPathname = getPathnameParam(params);
if (!page) {
const pathname = normalizePathname(rawPathname);
if (pathname !== rawPathname) {
@@ -62,6 +78,7 @@ export default async function Page(props: { params: PagePathParams }) {
<div className={tcls('flex', 'flex-row')}>
<PageBody
space={space}
contentPointer={contentPointer}
contentTarget={contentTarget}
customization={customization}
context={contentRefContext}
@@ -75,6 +92,7 @@ export default async function Page(props: { params: PagePathParams }) {
{page.layout.outline ? (
<PageAside
space={space}
site={parent?.object === 'site' ? parent : undefined}
customization={customization}
page={page}
document={document}
@@ -103,14 +121,24 @@ export async function generateViewport({ params }: { params: PagePathParams }):
};
}
export async function generateMetadata({ params }: { params: PagePathParams }): Promise<Metadata> {
const { space, pages, page, customization, parent } = await fetchPageData(params);
export async function generateMetadata({
params,
searchParams,
}: {
params: PagePathParams;
searchParams: { fallback?: string };
}): Promise<Metadata> {
const { space, pages, page, customization, parent } = await getPageDataWithFallback({
pagePathParams: params,
searchParams,
});
if (!page) {
notFound();
}
return {
title: [page.title, customization.title ?? space.title, parent?.title]
title: [page.title, getContentTitle(space, customization, parent)]
.filter(Boolean)
.join(' | '),
description: page.description ?? '',
@@ -125,3 +153,34 @@ export async function generateMetadata({ params }: { params: PagePathParams }):
},
};
}
/**
* Fetches the page data matching the requested pathname and fallback to root page when page is not found.
*/
async function getPageDataWithFallback(args: {
pagePathParams: PagePathParams;
searchParams: { fallback?: string };
redirectOnFallback?: boolean;
}) {
const { pagePathParams, searchParams, redirectOnFallback = false } = args;
const { pages, page: targetPage, ...otherPageData } = await fetchPageData(pagePathParams);
let page = targetPage;
const canFallback = !!searchParams.fallback;
if (!page && canFallback) {
const rootPage = resolveFirstDocument(pages, []);
if (redirectOnFallback && rootPage?.page) {
redirect(pageHref(pages, rootPage?.page));
}
page = rootPage?.page;
}
return {
...otherPageData,
pages,
page,
};
}
@@ -8,10 +8,13 @@ import { AdminToolbar } from '@/components/AdminToolbar';
import { CookiesToast } from '@/components/Cookies';
import { LoadIntegrations } from '@/components/Integrations';
import { SpaceLayout } from '@/components/SpaceLayout';
import { api } from '@/lib/api';
import { assetsDomain } from '@/lib/assets';
import { buildVersion } from '@/lib/build';
import { getContentSecurityPolicyNonce } from '@/lib/csp';
import { absoluteHref, baseUrl } from '@/lib/links';
import { shouldIndexSpace } from '@/lib/seo';
import { getContentTitle } from '@/lib/utils';
import { ClientContexts } from './ClientContexts';
import { RocketLoaderDetector } from './RocketLoaderDetector';
@@ -38,6 +41,11 @@ export default async function ContentLayout(props: { children: React.ReactNode }
scripts,
} = await fetchSpaceData();
ReactDOM.preconnect(api().endpoint);
if (assetsDomain) {
ReactDOM.preconnect(assetsDomain);
}
scripts.forEach(({ script }) => {
ReactDOM.preload(script, {
as: 'script',
@@ -104,7 +112,7 @@ export async function generateMetadata(): Promise<Metadata> {
const customIcon = 'icon' in customization.favicon ? customization.favicon.icon : null;
return {
title: `${parent ? parent.title : customization.title ?? space.title}`,
title: getContentTitle(space, customization, parent),
generator: `GitBook (${buildVersion()})`,
metadataBase: new URL(baseUrl()),
icons: {
@@ -15,7 +15,7 @@ export const runtime = 'edge';
*/
export async function GET(req: NextRequest) {
const { pages: rootPages } = await getSpaceContentData(getContentPointer());
const pages = flattenPages(rootPages);
const pages = flattenPages(rootPages, (page) => !page.hidden);
const urls = pages.map(({ page, depth }) => {
// Decay priority with depth
const priority = Math.pow(2, -0.25 * depth);
@@ -66,13 +66,16 @@ export async function GET(req: NextRequest) {
type FlatPageEntry = { page: RevisionPageDocument; depth: number };
function flattenPages(rootPags: RevisionPage[]): FlatPageEntry[] {
function flattenPages(
rootPags: RevisionPage[],
filter: (page: RevisionPageDocument) => boolean,
): FlatPageEntry[] {
const flattenPage = (
page: RevisionPageDocument | RevisionPageGroup,
depth: number,
): FlatPageEntry[] => {
return [
...(page.type === 'document' ? [{ page, depth }] : []),
...(page.type === 'document' && filter(page) ? [{ page, depth }] : []),
...page.pages.flatMap((child) =>
child.type === 'link' ? [] : flattenPage(child, depth + 1),
),
@@ -14,6 +14,7 @@ import {
} from '@/lib/api';
import { getEmojiForCode } from '@/lib/emojis';
import { tcls } from '@/lib/tailwind';
import { getContentTitle } from '@/lib/utils';
export const runtime = 'edge';
@@ -54,7 +55,7 @@ export async function GET(req: NextRequest) {
: space.visibility === ContentVisibility.InCollection && space.parent
? await getCollection(space.parent)
: null;
const contentTitle = parent?.title ?? customization.title ?? space.title;
const contentTitle = getContentTitle(space, customization, parent);
return new ImageResponse(
(
@@ -3,6 +3,8 @@ import { ImageResponse } from 'next/og';
import { NextRequest } from 'next/server';
import React from 'react';
import { getContentTitle } from '@/lib/utils';
import { PageIdParams, fetchPageData } from '../../../../fetch';
export const runtime = 'edge';
@@ -31,7 +33,7 @@ export async function GET(req: NextRequest, { params }: { params: PageIdParams }
}}
>
<h2 tw="text-7xl font-bold tracking-tight text-left">
{parent?.title ?? customization.title ?? space.title}
{getContentTitle(space, customization, parent)}
</h2>
<div tw="flex flex-1">
<p tw="text-4xl">{page ? page.title : 'Not found'}</p>
@@ -4,6 +4,7 @@ import {
Revision,
RevisionPageDocument,
RevisionPageGroup,
RevisionPageLink,
SiteCustomizationSettings,
Space,
} from '@gitbook/api';
@@ -313,9 +314,17 @@ function selectPages(
): FlatPageEntry[] => {
return [
{ page, depth },
...page.pages.flatMap((child) =>
child.type === 'link' ? [] : flattenPage(child, depth + 1),
),
...page.pages.flatMap((child) => {
if (child.type === 'link') {
return [];
}
if (child.hidden) {
return [];
}
return flattenPage(child, depth + 1);
}),
];
};
@@ -340,8 +349,16 @@ function selectPages(
return limitTo(flattenPage(found.page, 0));
}
const allPages = rootPages.flatMap((page) =>
page.type === 'link' ? [] : flattenPage(page, 0),
);
const allPages = rootPages.flatMap((page) => {
if (page.type === 'link') {
return [];
}
if (page.hidden) {
return [];
}
return flattenPage(page, 0);
});
return limitTo(allPages);
}
@@ -1,6 +1,6 @@
'use client';
import * as Sentry from '@sentry/nextjs';
import { captureException } from '@sentry/nextjs';
import React from 'react';
import { Button } from '@/components/primitives/Button';
@@ -15,7 +15,7 @@ export default function ErrorPage(props: {
const language = useLanguage();
React.useEffect(() => {
Sentry.captureException(error);
captureException(error);
}, [error]);
return (
@@ -40,6 +40,7 @@ export function getContentPointer(): ContentPointer | SiteContentPointer {
if (siteId) {
const organizationId = headerSet.get('x-gitbook-content-organization');
const siteSpaceId = headerSet.get('x-gitbook-content-site-space');
const siteShareKey = headerSet.get('x-gitbook-content-site-share-key');
if (!organizationId) {
throw new Error('Missing site content headers');
}
@@ -48,6 +49,7 @@ export function getContentPointer(): ContentPointer | SiteContentPointer {
siteId,
spaceId,
siteSpaceId: siteSpaceId ?? undefined,
siteShareKey: siteShareKey ?? undefined,
organizationId,
revisionId: headerSet.get('x-gitbook-content-revision') ?? undefined,
changeRequestId: headerSet.get('x-gitbook-content-changerequest') ?? undefined,
@@ -71,7 +73,14 @@ export async function fetchSpaceData() {
const [{ space, contentTarget, pages, customization, scripts }, parentSite] = await Promise.all(
'siteId' in content
? [getCurrentSiteData(content), fetchParentSite(content.organizationId, content.siteId)]
? [
getCurrentSiteData(content),
fetchParentSite({
organizationId: content.organizationId,
siteId: content.siteId,
siteShareKey: content.siteShareKey,
}),
]
: [getSpaceData(content)],
);
@@ -102,7 +111,11 @@ export async function fetchPageData(params: PagePathParams | PageIdParams) {
const page = await resolvePage(contentTarget, pages, params);
const [parent, document] = await Promise.all([
'siteId' in content
? fetchParentSite(content.organizationId, content.siteId)
? fetchParentSite({
organizationId: content.organizationId,
siteId: content.siteId,
siteShareKey: content.siteShareKey,
})
: fetchParentCollection(space),
page?.page.documentId ? getDocument(space.id, page.page.documentId) : null,
]);
@@ -173,10 +186,15 @@ async function fetchParentCollection(space: Space) {
return { parent: collection, spaces };
}
async function fetchParentSite(organizationId: string, siteId: string) {
async function fetchParentSite(args: {
organizationId: string;
siteId: string;
siteShareKey: string | undefined;
}) {
const { organizationId, siteId, siteShareKey } = args;
const [site, siteSpaces] = await Promise.all([
getSite(organizationId, siteId),
getSiteSpaces(organizationId, siteId),
getSiteSpaces({ organizationId, siteId, siteShareKey }),
]);
const spaces: Record<string, Space> = {};
@@ -1,12 +1,12 @@
'use client';
import * as Sentry from '@sentry/nextjs';
import { captureException } from '@sentry/nextjs';
import Error from 'next/error';
import { useEffect } from 'react';
export default function GlobalError({ error }: { error: Error }) {
useEffect(() => {
Sentry.captureException(error);
captureException(error);
}, [error]);
return (
+200
View File
@@ -0,0 +1,200 @@
'use client';
import IconHeart from '@geist-ui/icons/heart';
import * as React from 'react';
import { ClassValue, tcls } from '@/lib/tailwind';
import { AdClassicRendering } from './AdClassicRendering';
import { AdCoverRendering } from './AdCoverRendering';
import { AdItem, AdsResponse } from './types';
/**
* Fetch and render the Ad placement.
* https://docs.buysellads.com/ad-serving-api
*/
export function Ad({
zoneId,
spaceId,
placement,
ignore,
style,
mode = 'auto',
}: {
zoneId: string;
spaceId: string;
placement: string;
ignore: boolean;
style?: ClassValue;
mode?: 'classic' | 'auto' | 'cover';
}) {
const containerRef = React.useRef<HTMLDivElement>(null);
const [visible, setVisible] = React.useState(false);
const [failed, setFailed] = React.useState(false);
const [ad, setAd] = React.useState<AdItem | undefined>(undefined);
// Observe the container visibility
React.useEffect(() => {
if (!containerRef.current) {
return;
}
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setVisible(true);
}
},
{
root: null,
rootMargin: '0px',
threshold: 0.1,
},
);
observer.observe(containerRef.current);
return () => {
observer.disconnect();
};
}, []);
// When the container is visible,
// track an impression on the ad and fetch it
React.useEffect(() => {
if (!visible) {
return;
}
let cancelled = false;
(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');
}
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);
}
})();
return () => {
cancelled = true;
};
}, [visible, zoneId, ignore, placement]);
const viaUrl = new URL('https://www.gitbook.com');
viaUrl.searchParams.set('utm_source', 'content');
viaUrl.searchParams.set('utm_medium', 'ads');
viaUrl.searchParams.set('utm_campaign', spaceId);
return (
<div ref={containerRef} className={tcls(style)}>
{ad && ad.description && ad.statlink ? (
<>
{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>
</>
) : 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);
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
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',
)}
>
<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>
);
}
@@ -0,0 +1,47 @@
import * as React from 'react';
import { tcls } from '@/lib/tailwind';
import { AdItem } from './types';
/**
* Classic rendering for an ad.
*/
export function AdClassicRendering({ ad }: { ad: AdItem }) {
return (
<a
className={tcls(
'flex',
'flex-col',
'gap-4',
'bg-light-2',
'text-dark/7',
'dark:bg-dark-2',
'dark:text-light/7',
'hover:text-dark/9',
'dark:hover:text-light/9',
'rounded-lg',
'p-4',
)}
href={ad.statlink}
rel="sponsored noopener"
target="_blank"
>
{'smallImage' in ad ? (
<div>
<img alt="Ads logo" className={tcls('rounded-md')} src={ad.smallImage} />
</div>
) : (
<div
className={tcls('px-6', 'py-4', 'rounded-md')}
style={{ backgroundColor: ad.backgroundColor }}
>
<img alt="Ads logo" src={ad.logo} />
</div>
)}
<div className={tcls('flex', 'flex-col')}>
<div className={tcls('text-xs')}>{ad.description}</div>
</div>
</a>
);
}
@@ -0,0 +1,108 @@
import * as React from 'react';
import { hexToRgba } from '@/lib/colors';
import { tcls } from '@/lib/tailwind';
import { AdCover } from './types';
/**
* Cover rendering for an ad.
*/
export function AdCoverRendering({ ad }: { ad: AdCover }) {
return (
<a
className={tcls(
'group/ad',
'relative',
'flex',
'flex-col',
'gap-4',
'bg-light-2',
'text-dark/7',
'dark:bg-dark-2',
'dark:text-light/7',
'hover:text-dark/9',
'dark:hover:text-light/9',
'rounded-lg',
'p-4',
'overflow-hidden',
'shadow-sm',
)}
style={{ backgroundColor: ad.backgroundColor, color: ad.textColor ?? '#ffffff' }}
href={ad.statlink}
rel="sponsored noopener"
target="_blank"
>
<div
className={tcls(
'absolute',
'inset-0',
'bg-center',
'bg-cover',
'bg-no-repeat',
'z-0',
)}
style={{
backgroundImage: `url(${ad.largeImage})`,
}}
/>
<div className={tcls('z-[2]')}>
<img
alt="Large image"
src={ad.largeImage}
className={tcls(
'rounded-md',
'shadow-md',
'max-h-32',
'group-hover/ad:max-h-16',
'transition-all',
)}
/>
</div>
<div className={tcls('z-[2]')}>
<img alt={ad.company} src={ad.logo} className={tcls('max-w-36', 'max-h-12')} />
</div>
<div className={tcls('flex', 'flex-col', 'z-[2]')}>
<div className={tcls('text-sm', 'font-semibold', 'mb-2')}>{ad.companyTagline}</div>
<div
className={tcls(
'text-xs',
'h-0',
'opacity-0',
'group-hover/ad:h-16',
'group-hover/ad:opacity-10',
'transition-all',
)}
>
{ad.description}
</div>
</div>
<div className={tcls('z-[2]')}>
<span
className={tcls(
'text-sm',
'font-semibold',
'shadow-lg',
'rounded-md',
'bg-white',
'py-2',
'px-4',
)}
style={{
backgroundColor: ad.ctaBackgroundColor,
color: ad.ctaTextColor ?? ad.backgroundColor,
}}
>
{ad.callToAction}
</span>
</div>
<div
className={tcls('absolute', 'inset-0', 'backdrop-blur', 'z-[1]')}
style={{
backgroundColor: hexToRgba(ad.backgroundColor, 0.8),
}}
/>
</a>
);
}
@@ -0,0 +1 @@
export * from './Ad';
@@ -0,0 +1,51 @@
export interface AdGeneric {
active: string;
ad_via_link: string;
bannerid: string;
creativeid: string;
evenodd: string;
external_id: string;
height: string;
i: string;
identifier: string;
longimp: string;
longlink: string;
num_slots: string;
statimp: string;
statlink: string;
timestamp: string;
width: string;
zoneid: string;
zonekey: string;
rendering: 'carbon';
pixel?: string;
}
export interface AdClassic extends AdGeneric {
description: string;
smallImage: string;
}
export interface AdCover extends AdGeneric {
backgroundColor: string;
backgroundHoverColor?: string;
textColor?: string;
textColorHover?: string;
callToAction: string;
company: string;
companyTagline: string;
description: string;
largeImage: string;
image?: string;
logo: string;
ctaBackgroundColor?: string;
ctaBackgroundHoverColor?: string;
ctaTextColor?: string;
ctaTextColorHover?: string;
}
export type AdItem = AdClassic | AdCover;
export interface AdsResponse {
ads: Array<AdItem | {}>;
}
@@ -0,0 +1,72 @@
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';
export async function BlockContentRef(props: BlockProps<DocumentBlockContentRef>) {
const { block, context, style } = props;
const resolved = await context.resolveContentRef(block.data.ref, {
resolveAnchorText: true,
});
if (!resolved) {
return null;
}
const isContentInOtherSpace =
context.contentRefContext?.space &&
'space' in block.data.ref &&
context.contentRefContext.space.id !== block.data.ref.space;
const kind = block?.data?.ref?.kind;
if ((resolved.active && kind === 'space') || isContentInOtherSpace) {
return <SpaceRefCard {...props} resolved={resolved} />;
}
return (
<Card
leadingIcon={resolved.emoji ? <Emoji code={resolved.emoji} style="text-xl" /> : null}
href={resolved.href}
title={resolved.text}
style={style}
/>
);
}
async function SpaceRefCard(
props: { resolved: ResolvedContentRef } & BlockProps<DocumentBlockContentRef>,
) {
const { context, style, resolved } = props;
const spaceId = context.contentRefContext?.space.id;
if (!spaceId) {
return null;
}
const spaceCustomization = await ignoreAPIError(getSpaceCustomization(spaceId));
const customFavicon = spaceCustomization?.favicon;
const customEmoji = customFavicon && 'emoji' in customFavicon ? customFavicon.emoji : undefined;
const customIcon = customFavicon && 'icon' in customFavicon ? customFavicon.icon : undefined;
return (
<Card
leadingIcon={
<LogoIcon
icon={customIcon}
emoji={customEmoji}
alt=""
sizes={[{ width: 24 }]}
style={['object-contain', 'size-6']}
/>
}
href={resolved.href}
title={resolved.text}
postTitle={resolved.subText}
style={style}
/>
);
}
@@ -15,7 +15,7 @@ import './theme.css';
*/
export async function CodeBlock(props: BlockProps<DocumentBlockCode>) {
const { block, document, style, context } = props;
const withHighlighting = context.shouldHighlightCode();
const withHighlighting = context.shouldHighlightCode(context.content?.spaceId);
const lines = withHighlighting ? await highlight(block) : plainHighlighting(block);
const id = block.key!;
@@ -1,5 +1,15 @@
const CODE_HIGHLIGHT_BLOCK_LIMIT = 50;
/**
* Spaces where we'll always use plain highlighting.
*/
const PLAIN_HIGHLIGHTING_SPACES: string[] = [
'V9geAO9ITPi8WOYK5o0r',
'puRmcwVxGFtHph8IjXaf',
'e3jwbMOrr4RhKtZ9C0XL',
'ryjzVNizLfd6pYogN3dm',
];
/**
* Protect against memory issues when highlighting a large number of code blocks.
* This context only allows 50 code blocks per render to be highlighted.
@@ -9,7 +19,11 @@ const CODE_HIGHLIGHT_BLOCK_LIMIT = 50;
*/
export function createHighlightingContext() {
let count = 0;
return () => {
return (spaceId?: string | undefined) => {
if (!spaceId || PLAIN_HIGHLIGHTING_SPACES.includes(spaceId)) {
return false;
}
count += 1;
return count < CODE_HIGHLIGHT_BLOCK_LIMIT;
};
@@ -45,7 +45,7 @@ export interface DocumentContext {
*
* https://linear.app/gitbook-x/issue/RND-3588/gitbook-open-code-syntax-highlighting-runs-out-of-memory-after-a
*/
shouldHighlightCode: () => boolean;
shouldHighlightCode: (spaceId: string | undefined) => boolean;
}
export interface DocumentContextProps {
@@ -0,0 +1,80 @@
import * as gitbookAPI from '@gitbook/api';
import { headers } from 'next/headers';
import Script from 'next/script';
import ReactDOM from 'react-dom';
import { Card } from '@/components/primitives';
import { api } from '@/lib/api';
import { tcls } from '@/lib/tailwind';
import { BlockProps } from './Block';
import { Caption } from './Caption';
import { IntegrationBlock } from './Integration';
export async function Embed(props: BlockProps<gitbookAPI.DocumentBlockEmbed>) {
const { block, context, ...otherProps } = props;
const nonce = headers().get('x-nonce') || undefined;
ReactDOM.preload('https://cdn.iframe.ly/embed.js', { as: 'script', nonce });
const { data: embed } = await (context.content
? api().spaces.getEmbedByUrlInSpace(context.content.spaceId, { url: block.data.url })
: api().urls.getEmbedByUrl({ url: block.data.url }));
return (
<Caption {...props}>
{embed.type === 'rich' ? (
<>
<div
dangerouslySetInnerHTML={{
__html: embed.html,
}}
/>
<Script src="https://cdn.iframe.ly/embed.js" nonce={nonce} />
</>
) : embed.type === 'integration' ? (
<IntegrationBlock
{...otherProps}
context={context}
block={createIntegrationBlock(block.data.url, embed.integration, embed.block)}
/>
) : (
<Card
leadingIcon={
embed.icon ? (
<img src={embed.icon} className={tcls('w-5', 'h-5')} alt="Logo" />
) : null
}
href={block.data.url}
title={embed.title}
postTitle={embed.site}
/>
)}
</Caption>
);
}
/**
* Create an integration block with an unfurl action from the GitBook Embed response.
*/
function createIntegrationBlock(
url: string,
integration: string,
block: gitbookAPI.IntegrationBlock,
): gitbookAPI.DocumentBlockIntegration {
return {
object: 'block',
type: 'integration',
isVoid: true,
data: {
integration,
block: block.id,
props: {},
action: {
action: '@link.unfurl',
url,
},
url,
},
};
}
@@ -1,12 +1,10 @@
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 { DocumentBlockFile } from '@gitbook/api';
import { getSimplifiedContentType } from '@/lib/files';
import { tcls } from '@/lib/tailwind';
import { BlockProps } from './Block';
import { FileIcon } from './FileIcon';
export async function File(props: BlockProps<DocumentBlockFile>) {
const { block, context, style } = props;
@@ -18,42 +16,7 @@ export async function File(props: BlockProps<DocumentBlockFile>) {
return null;
}
const contentType = (() => {
switch (file.contentType) {
case 'application/pdf':
case 'application/x-pdf':
return 'pdf';
case 'image/png':
case 'image/jpeg':
case 'image/gif':
case 'image/webp':
case 'image/tiff':
case 'image/svg+xml':
return 'image';
case 'application/zip':
case 'application/x-7z-compressed':
case 'application/x-zip-compressed':
case 'application/x-tar':
case 'application/x-rar-compressed':
case 'application/vnd.rar':
return 'archive';
default:
return null;
}
})();
const icon = (() => {
switch (contentType) {
case 'pdf':
return <IconFileText />;
case 'image':
return <IconImage />;
case 'archive':
return <IconPaperClip />;
default:
return <IconDownload />;
}
})();
const contentType = getSimplifiedContentType(file.contentType);
return (
<a
@@ -103,7 +66,9 @@ export async function File(props: BlockProps<DocumentBlockFile>) {
'dark:border-light/2',
)}
>
<div className={tcls('*:w-5', '*:h-5', '*:stroke-primary')}>{icon}</div>
<div className={tcls('*:w-5', '*:h-5', '*:stroke-primary')}>
<FileIcon contentType={contentType} />
</div>
<div
className={tcls(
'text-xs',
@@ -0,0 +1,24 @@
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 { SimplifiedFileType } from '@/lib/files';
/**
* Render an appropriate icon for a file.
*/
export function FileIcon(props: { contentType: SimplifiedFileType | null }) {
const { contentType } = props;
switch (contentType) {
case 'pdf':
return <IconFileText />;
case 'image':
return <IconImage />;
case 'archive':
return <IconPaperClip />;
default:
return <IconDownload />;
}
}
@@ -15,13 +15,12 @@ import {
Lock,
} from '@geist-ui/icons';
import { ContentKitContext, DocumentBlockIntegration } from '@gitbook/api';
import { ContentKit, ContentKitOutput, ContentKitServerContext } from '@gitbook/react-contentkit';
import { ignoreAPIError, renderIntegrationUi } from '@/lib/api';
import { parseMarkdown } from '@/lib/markdown';
import { tcls } from '@/lib/tailwind';
import { ContentKit, ContentKitOutput, ContentKitServerContext } from '@gitbook/react-contentkit';
import type { BlockProps } from '../Block';
import './contentkit.css';
import { PlainCodeBlock } from '../CodeBlock';
@@ -70,6 +69,7 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio
const initialInput = {
componentId: block.data.block,
props: block.data.props,
action: block.data.action,
context: contentKitContext,
};
@@ -1,10 +1,9 @@
import { DocumentBlockMath, DocumentInlineMath } from '@gitbook/api';
import { MathFormula } from '@gitbook/react-math';
import { getStaticFileURL } from '@/lib/assets';
import { tcls } from '@/lib/tailwind';
import { MathFormula } from '@gitbook/react-math';
import { BlockProps } from './Block';
import { InlineProps } from './Inline';
@@ -1,13 +1,12 @@
import { ChevronDown, ChevronRight } from '@geist-ui/icons';
import { DocumentBlockSwagger } from '@gitbook/api';
import { OpenAPIOperation } from '@gitbook/react-openapi';
import React from 'react';
import { LoadingPane } from '@/components/primitives';
import { fetchOpenAPIBlock } from '@/lib/openapi';
import { tcls } from '@/lib/tailwind';
import { OpenAPIOperation } from '@gitbook/react-openapi';
import { BlockProps } from '../Block';
import { PlainCodeBlock } from '../CodeBlock';

Some files were not shown because too many files have changed in this diff Show More