Compare commits

..

150 Commits

Author SHA1 Message Date
taranvohra b1de0adb6a get rid of Object.values 2024-09-24 19:17:05 +05:30
taranvohra 408540ae74 Merge remote-tracking branch 'origin/main' into fix-spaces-list 2024-09-24 19:15:39 +05:30
Samy Pessé 73892f4fc2 Fix release preview CI workflow (#2481) 2024-09-24 13:13:45 +02:00
Samy Pessé 636b868bf1 Experimental new cache backend powered by DO (#2479)
Co-authored-by: Johan Preynat <johan.preynat@gmail.com>
2024-09-24 12:55:49 +02:00
Addison b144368e3f Update Dutch "Ask or Search" terminology to fit on one line (#2480) 2024-09-24 16:30:30 +09:00
Scott Cazan 683b4193d1 Don't use customized site title for space in search selector (#2367) 2024-09-23 12:43:03 +02:00
Samy Pessé f4c953665a Optimize layout shift while transitioning between pages with full width blocks (#2477) 2024-09-23 06:19:49 +02:00
Samy Pessé 0f990c772a Show definition title when visible in cards (#2476) 2024-09-22 19:04:23 +02:00
Samy Pessé 0bf985af61 Don't show hidden pages in the empty state of a page (#2475) 2024-09-19 15:17:13 +02:00
Samy Pessé 42473611d3 Add required query parameters to the code sample (#2474) 2024-09-19 14:31:22 +02:00
Samy Pessé 2c437f7d94 Fix linking to tabs itself (#2473) 2024-09-19 10:52:34 +02:00
vibhanshub aa8c49eb0c Display pattern if available in OpenAPI params (#2461)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2024-09-19 09:59:10 +02:00
Samy Pessé b32e40c72a Persist state of tabs and dynamically sync them based on title (#2472) 2024-09-19 09:40:54 +02:00
Samy Pessé 2ff7ed1b52 Hide button on mobile to open toc if disabled at the page level (#2471) 2024-09-18 19:49:02 +02:00
Samy Pessé bfbed1a69b Use translation for "Sponsored via GitBook" (#2469) 2024-09-17 13:41:53 +02:00
Samy Pessé ef9d012be9 Fix generation of icons package and add task "clean" (#2470) 2024-09-17 13:41:18 +02:00
Nichlas f12a2155c0 Added the Norwegian language (#2467)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2024-09-17 12:15:21 +02:00
Taran Vohra 43f696c4a8 use samesite none for tracking cookie (#2299) 2024-09-17 11:57:28 +05:30
Samy Pessé 4c19014dcd Prevent robots indexation for pages where it's disabled (#2464) 2024-09-13 13:52:15 +02:00
Samy Pessé 0e7f9d6778 Fix TS error when adding support for a locale not yet supported by API (#2465) 2024-09-13 10:30:20 +02:00
Rens Reinders 00a0bf3c1b Add Dutch Locale support (#2450) 2024-09-13 10:12:44 +02:00
Brett Jephson e9149036f6 RND-3387: synchronize response tabs (#2457)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2024-09-12 11:01:29 +01:00
Addison b6a9967959 Update search/ask wording for Japanese localization to fit UI better (#2462) 2024-09-12 09:58:58 +02:00
Addison 8bbc3caf3e Update Readme & Contributing Guide (#2459) 2024-09-09 15:28:19 +02:00
Samy Pessé 237b703a6c Fix crash in OpenAPI block because of undefined examples (#2458) 2024-09-06 16:55:04 +02:00
vibhanshub cf3045a718 Add Python support in code samples (#2453) 2024-09-06 14:29:50 +01:00
vibhanshub a679e7283f Render mandatory headers in the code sample (#2445) 2024-09-06 13:24:52 +01:00
Samy Pessé 13c75341e2 Use ellipsis and fix icon color for more links in the header on small screen (#2455) 2024-09-06 11:57:42 +02:00
Marc Laventure 4cbcc5bb20 fix: rollback scalar (#2454)
Co-authored-by: Brett Jephson <brett@gitbook.io>
2024-09-06 10:32:33 +01:00
Brett Jephson 51955dae3b Response examples with status code tabs (#2448) 2024-09-06 09:35:43 +01:00
Brett Jephson 51fa3ab2c8 Use content-visibility on open api operations (#2451) 2024-09-05 17:21:42 +01:00
Samy Pessé af9e66e87e Only display spaces dropdown in compact header when site is multi-variants (#2447) 2024-09-03 19:49:05 +02:00
Samy Pessé 8878dc7f80 Accept special ip headers from gitbook backend (#2446) 2024-09-03 16:47:51 +02:00
Scott Cazan 4336bc20e0 Filter out short searches less then 2 chars, bigger debounce (#2440) 2024-09-03 14:49:35 +02:00
Samy Pessé 4a7613a521 Fix community ads fetching (#2444) 2024-09-03 13:54:31 +02:00
Samy Pessé 689f55340f Fix inconsistent click area in table because of scroll indicator (#2443) 2024-09-02 14:58:56 +02:00
taranvohra cb9a15339d Fix spaces list of a site 2024-08-28 16:05:44 +05:30
Taran Vohra 160ae3d49d Add spaces dropdown to CompactHeader component (#2439) 2024-08-28 10:08:25 +05:30
Marc Laventure c079c3c6d9 chore: bump scalar package (#2421)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2024-08-21 08:27:22 +02:00
Samy Pessé cb782a7557 Fix running GitBook locally and fix community ad rendering (#2437) 2024-08-20 19:37:33 +02:00
Abhishek 502d8fc087 Fix typo in README.md (#2430) 2024-08-20 14:55:46 +02:00
Brett Jephson 6295881138 RND-3373: update the shadow for dark mode in multi-space search (#2436) 2024-08-20 13:54:28 +01:00
Brett Jephson 17f71ba680 RND-3113: expand when expandable anchor (#2435) 2024-08-20 11:04:00 +01:00
Brett Jephson 5fe7adb87b RND-3532: drop down menu for hidden links at small screen size (#2434) 2024-08-19 11:51:48 +01:00
spastorelli 41684d091b Revert "Automatically scroll to the active page in table of contents (#2425) (#2429) 2024-08-15 16:52:27 +02:00
spastorelli 9f6c3347cb Automatically scroll to the active page in table of contents (#2425) 2024-08-15 11:42:48 +02:00
Samy Pessé aa321989c4 Use a <div> instead of <h1> in the top header (#2428) 2024-08-14 10:02:42 +02:00
Samy Pessé 0f1565cb4b Fix mermaid / contentkit webframe not updating properly (#2427) 2024-08-13 15:26:11 +02:00
Samy Pessé 3996110ebd Optimize and resize images in community ads (#2426) 2024-08-09 15:41:35 +02:00
Samy Pessé 3422ad4c84 Iteration on community ads (#2422) 2024-08-09 12:31:36 +02:00
spastorelli 3c07e65b9a Fix paragraph top margin in quote blocks (#2423) 2024-08-09 10:58:20 +02:00
Steven H 287df91661 Add an nvmrc file (#2405) 2024-08-07 15:32:08 +01:00
Samy Pessé 776bc3135a Fix running repository without access to Pro token for @gitbook/icons (#2417)
Co-authored-by: Valentino Hudhra <2587839+valentin0h@users.noreply.github.com>
2024-08-02 18:40:44 +02:00
Samy Pessé 4638958c9f Fix preview of customization (#2418) 2024-08-02 18:24:41 +02:00
Samy Pessé 1f24fe46c4 Add support for page icons and icons style customization (#2416) 2024-08-02 17:28:32 +02:00
Valentino Hudhra ccbcab8822 Control tracking of page views with env/header variables (#2380) 2024-08-02 16:10:28 +02:00
Marc Laventure 094e9cdac2 chore: bump scalar (#2410)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2024-08-02 10:24:39 +02:00
Samy Pessé 5c35f36b3a Prepare @gitbook/icons package and use it as a replacement of geist/icons (#2404) 2024-08-02 09:59:44 +02:00
Steven H d7ec2b57a4 Bump getPublishedContentByUrl cache to fix a recent issue with the GitBook API (#2414) 2024-08-01 06:54:09 +01:00
Steven H 373a1e0312 Bump URL resolution caches after a recent hotfix. (#2412) 2024-07-29 14:22:20 +01:00
Taran Vohra e9fe3975bb Pass shareKey as context while fetching space (#2409) 2024-07-26 16:14:20 +05:30
Samy Pessé 32a73b55d4 Version Packages (#2403)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-25 15:12:18 +02:00
Samy Pessé 0426312d6c Reset to first tab when tabs change in an InteractiveSection (#2407) 2024-07-25 15:09:21 +02:00
Samy Pessé ede2335fb3 Fix x-codeSamples: false not working at the single operation level (#2408) 2024-07-25 14:59:30 +02:00
Amrit Kahlon 709f1a1ccb Update Scalar Api Client to new version (#2406)
Co-authored-by: marclave <marc@scalar.com>
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2024-07-25 10:25:50 +02:00
Samy Pessé 24b785cbfb Update shiki to fix diff code highlighting (#2338) 2024-07-25 09:33:42 +02:00
Samy Pessé acc3f2f418 Fix Scalar proxy failing on Cloudflare (#2402) 2024-07-24 11:41:35 +02:00
Scott Cazan d4a653d6a9 Use SpaceURL in getURL function to for cross-space searches (#2399) 2024-07-23 12:07:32 +02:00
Samy Pessé 7e74d25447 Version Packages (#2397)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-22 16:26:53 +02:00
Samy Pessé 3445db41ce Fix packages not including all files (#2396) 2024-07-22 16:20:06 +02:00
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
spastorelli f1440ca7ef Fix preview crashing when token do not include site data (#2292) 2024-04-18 17:36:16 +02:00
Taran Vohra 7ef2eeed13 fix site cache tag to exclude org id (#2291) 2024-04-18 20:28:39 +05:30
Taran Vohra 4005895337 Fix previews for sites in multi-id mode (#2289) 2024-04-18 18:19:52 +05:30
Steven H bf450e5cc7 Support GitHub-flavoured Markdown when parsing OpenAPI descriptions. (#2288) 2024-04-17 14:21:07 +01:00
Taran Vohra 4c12722c0a Use appropriate customization settings for sites (#2279) 2024-04-17 17:28:14 +05:30
Taran Vohra ac124a7ea5 Add e2e tests for site variants (#2286) 2024-04-16 20:59:58 +05:30
spastorelli 1de25056fa Fix space dropdown showing for single variant sites and update E2E tests (#2284) 2024-04-16 15:29:15 +02:00
Steven H bf3d2032ab Avoid crashing Cloudflare Worker when syntax highlighting too many lines of code. (#2283) 2024-04-15 11:57:22 +01:00
spastorelli bfb2342ab3 Add support for integration inject script for sites (#2277) 2024-04-11 10:28:24 +02:00
Samy Pessé 76cbc3c10f Print signature and url when having error with image resizing (#2275) 2024-04-09 11:22:36 +02:00
Taran Vohra c80c76d50d Override site's spaces published URLs (#2274) 2024-04-09 09:59:25 +05:30
Johan Preynat 263c89391e Add HSTS and other security headers (#2271) 2024-04-08 19:44:57 +02:00
Samy Pessé ebf742e81a Use a sampleRate of 0.1 for edge/node runtimes (#2273) 2024-04-08 15:05:01 +02:00
Samy Pessé 685317f508 Fix full-width for code-blocks and embeds (#2272) 2024-04-08 14:45:42 +02:00
Taran Vohra 0c224861e3 Sites support (#2262) 2024-04-08 17:06:22 +05:30
474 changed files with 7726 additions and 2868 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)
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Avoid multiple <h1> in the page by using a <div> for the title in the header
+6
View File
@@ -0,0 +1,6 @@
---
'@gitbook/react-openapi': patch
'gitbook': patch
---
Adds content-visibility css property to OpenAPI Operation for better render performance
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': minor
---
Add Python support in Code Samples
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
RND-3532: drop down menu for hidden links at small screen size
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Change dark mode shadow for multi-space search toolbar
+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": []
}
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Use ellipsis and fix icon color for more links in the header on small screen
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/icons': patch
---
Fix first run of the package in development mode
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': patch
---
bump: scalar from 1.0.5 to 1.0.7
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': minor
---
Replace all icons, previously imported from Geist, by new package `@gitbook/icons`
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/cache-do': minor
---
First version of a new cache backend powered by Cloudflare Durable Objects
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': minor
---
Add support for Norwegian language
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': minor
---
Optimize layout shift while transitioning between pages with full width blocks (ex: OpenAPI blocks)
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': minor
---
Add support for page icons
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Only display spaces dropdown in compact header when site is multi-variants
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/icons': minor
---
Initial release for `@gitbook/icons`, a package of content icons to be used for pages, documents, etc.
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': minor
---
Persist state of tabs and dynamically sync them based on title
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Ensure "Sponsored via GitBook" can be translated in all languages
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Use url hash to open Expandable and scroll to anchor
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Fix margin for paragraphs in quote blocks
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Use new cache backend, powered by Durable Objects, alongside the existing ones (KV, etc).
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': minor
---
Add required query parameters to the code sample
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': minor
---
Display pattern if available in parmas in OpenAPI block
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Fix inconsistent click area in table because of scroll indicator
+6
View File
@@ -0,0 +1,6 @@
---
'@gitbook/react-openapi': minor
'gitbook': patch
---
Synchronize response and response example tabs
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Show definition title when visible in cards
+6
View File
@@ -0,0 +1,6 @@
---
'@gitbook/react-openapi': minor
'gitbook': patch
---
Rollback of scalar modal while fixing perf issue
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Optimize images rendered in community ads
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-contentkit': patch
---
Fix rendering of webframe with SSR causing wrong communication between frame and renderer
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Add optional env `GITBOOK_INTEGRATIONS_HOST` to configure the host serving the integrations
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': patch
---
Fix crash when `example` is undefined for a response
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': minor
---
Add support for icons style customization for sites
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': patch
---
Adds tabs to Response Example section e.g. for status code examples
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': patch
---
Render mandatory headers in code sample
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': patch
---
Update Scalar client to latest version
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': minor
---
Prevent search indexation for pages where it's configured as disabled
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Fix table of contents being visible on mobile when disabled at the page level
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': minor
---
Update rendering of community ads to match new API response, and make it possible to preview ads.
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-math': minor
---
Export binary `gitbook-math` to copy assets to a directory
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Fix "ip" being passed to BSA for community ads
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/icons': minor
---
Fix bin gitbook-icons when installed without the Pro token
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Don't show hidden pages in the empty state of a page
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': minor
---
Fix linking to a tab itself
+21 -7
View File
@@ -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.
+3 -1
View File
@@ -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
+41 -12
View File
@@ -1,6 +1,11 @@
on: [push]
name: CI
on:
pull_request:
push:
branches:
- main
env:
NPMRC_FONT_AWESOME_TOKEN: ${{ secrets.NPMRC_FONT_AWESOME_TOKEN }}
jobs:
deploy:
name: Deploy to Cloudflare Pages
@@ -16,7 +21,7 @@ jobs:
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.0.33
bun-version: 1.1.18
- name: Install dependencies
run: bun install --frozen-lockfile
env:
@@ -54,7 +59,7 @@ jobs:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: ${{ vars.CLOUDFLARE_PROJECT_NAME }}
directory: ./.vercel/output/static
directory: ./packages/gitbook/.vercel/output/static
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
workingDirectory: ./
- name: Outputs
@@ -78,7 +83,7 @@ jobs:
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.0.33
bun-version: 1.1.18
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Setup Playwright
@@ -88,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
@@ -98,13 +109,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 +128,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 +143,7 @@ jobs:
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.0.33
bun-version: 1.1.18
- name: Install dependencies
run: bun install --frozen-lockfile
env:
@@ -147,12 +158,30 @@ jobs:
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.0.33
bun-version: 1.1.18
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- run: bun unit
build-oss:
# CI to check that the repository builds correctly on a machine without the credentials
runs-on: ubuntu-latest
name: Build (Open Source)
env:
NPMRC_FONT_AWESOME_TOKEN: ''
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.18
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- run: bun run build
typecheck:
runs-on: ubuntu-latest
name: Typecheck
@@ -162,7 +191,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:
+60
View File
@@ -0,0 +1,60 @@
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 }}
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 }}
+5 -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,11 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# Turbo
.turbo
# vercel
# Vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# visual tests
screenshots/
# Sentry Config File
.sentryclirc
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
# Generated public files
/public/~gitbook/static/
# Env files
.env.local
+1
View File
@@ -0,0 +1 @@
v20.6
+10 -1
View File
@@ -1 +1,10 @@
.next
.next
.vercel
# Generated
packages/emoji-codepoints/index.ts
packages/gitbook/public/~gitbook/static/
packages/icons/src/data/*.json
# Build files
dist/
+25 -10
View File
@@ -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,15 +80,15 @@ 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
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
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,9 +99,9 @@ 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, maintainance, or updates to forked and self-hosted instances of this project.
> Self-hosting this project puts the responsibility of maintaining and merging future updates on **you**. We cannot guarantee support, maintenance, or updates to forked and self-hosted instances of this project.
>
> We want to make it as easy as possible for our community to collaborate and push the future of GitBook, which is why we encourage you to contribute to our product directly instead of creating your own version.
@@ -106,7 +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.
@@ -119,6 +128,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.
+2
View File
@@ -0,0 +1,2 @@
[install.scopes]
"awesome.me" = { token = "$NPMRC_FONT_AWESOME_TOKEN", url = "https://npm.fontawesome.com/" }
+19 -76
View File
@@ -2,90 +2,33 @@
"name": "gitbook",
"version": "0.1.0",
"private": true,
"packageManager": "bun@1.1.18",
"scripts": {
"dev": "next dev",
"build": "next build",
"build:cloudflare": "next-on-pages",
"start": "next start",
"lint": "next lint",
"dev": "turbo run dev",
"build": "turbo run build",
"build:cloudflare": "turbo run build:cloudflare",
"lint": "turbo run lint",
"lint:fix": "turbo run lint -- --fix",
"typecheck": "turbo run typecheck",
"format": "prettier ./ --ignore-unknown --write",
"format:check": "prettier ./ --ignore-unknown --list-different",
"typecheck": "tsc --noEmit",
"unit": "bun test {src,packages}/**/*.test.ts",
"e2e": "playwright test",
"postinstall": "rm -rf ./public/~gitbook/static/mathjax@3.2.2 && mkdir -p ./public/~gitbook/static/ && cp -R node_modules/mathjax/es5 ./public/~gitbook/static/mathjax@3.2.2"
"unit": "turbo run unit",
"e2e": "turbo run e2e",
"changeset": "changeset",
"release": "turbo run release && changeset publish",
"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/*"
],
"dependencies": {
"@geist-ui/icons": "^1.0.2",
"@gitbook/api": "^0.39.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-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.1.2"
},
"patchedDependencies": {
"@vercel/next@4.3.6": "patches/@vercel%2Fnext@4.3.6.patch"
}
}
+3
View File
@@ -0,0 +1,3 @@
.wrangler
worker-configuration.d.ts
dist/
+22
View File
@@ -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.
+42
View File
@@ -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"
]
}
+274
View File
@@ -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)`);
}
}
+91
View File
@@ -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}`;
}
+1
View File
@@ -0,0 +1 @@
export * from './CacheObjectStub';
+9
View File
@@ -0,0 +1,9 @@
import { WorkerEntrypoint } from 'cloudflare:workers';
export * from './CacheObject';
export default class Worker extends WorkerEntrypoint {
fetch() {
return new Response('Hello, world!');
}
}
+21
View File
@@ -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"]
}
+17
View File
@@ -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
View File
@@ -1 +1 @@
index.ts
dist/
+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
+4 -2
View File
@@ -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)};`,
);
+4 -2
View File
@@ -1,13 +1,15 @@
{
"name": "@gitbook/emoji-codepoints",
"description": "Optimized mapping of codepoints to the fully qualified emoji codepoints",
"version": "0.2.0",
"private": true,
"exports": "./index.ts",
"exports": "./dist/index.ts",
"dependencies": {},
"devDependencies": {
"emoji-assets": "^8.0.0"
},
"scripts": {
"postinstall": "bun ./build.ts"
"generate": "bun ./build.ts",
"clean": "rm -rf ./dist"
}
}
+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/
+47
View File
@@ -0,0 +1,47 @@
# gitbook
## 0.3.0
### Minor Changes
- 24b785c: Update shiki for code block syntax highlighting, with support for more languages and fixes for diffs. It also patches the deployment on Cloudflare to support edge functions larger than 4MB.
### Patch Changes
- acc3f2f: Fix error with the "Try it" of OpenAPI block because of the Scalar proxy failing on Cloudflare with the `cache` option
- Updated dependencies [709f1a1]
- Updated dependencies [ede2335]
- Updated dependencies [0426312]
- @gitbook/react-openapi@0.6.0
## 0.2.2
### Patch Changes
- Updated dependencies [3445db4]
- @gitbook/react-contentkit@0.5.0
- @gitbook/react-openapi@0.5.0
- @gitbook/react-math@0.5.0
## 0.2.1
### Patch Changes
- Updated dependencies [24cd72e]
- @gitbook/react-contentkit@0.4.0
- @gitbook/react-math@0.4.0
- @gitbook/react-openapi@0.4.0
## 0.2.0
### Minor Changes
- de747b7: Refactor the repository to be a proper monorepo and publish JS files on NPM instead of TypeScript files.
### Patch Changes
- Updated dependencies [de747b7]
- Updated dependencies [de747b7]
- @gitbook/react-contentkit@0.3.0
- @gitbook/react-openapi@0.3.0
- @gitbook/react-math@0.3.0
+7
View File
@@ -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 {
@@ -40,6 +42,184 @@ async function waitForCookiesDialog(page: Page) {
}
const testCases: TestsCase[] = [
{
name: 'GitBook Site (Single Variant)',
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/gitbook-doc/',
tests: [
{
name: 'Home',
url: '',
run: waitForCookiesDialog,
},
{
name: 'No variants dropdown',
url: '',
run: async (page) => {
await expect(page.locator('[data-testid="space-dropdown-button"]')).toHaveCount(
0,
);
},
},
{
name: 'Search',
url: '?q=',
},
{
name: 'Search Results',
url: '?q=gitbook',
run: async (page) => {
await page.waitForSelector('[data-test="search-results"]');
},
},
{
name: 'AI Search',
url: '?q=What+is+GitBook%3F&ask=true',
run: async (page) => {
await page.waitForSelector('[data-test="search-ask-answer"]');
},
screenshot: false,
},
{
name: 'Not found',
url: 'content-not-found',
run: waitForCookiesDialog,
},
],
},
{
name: 'GitBook Site (Multi Variants)',
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/multi-variants/',
tests: [
{
name: 'Variants dropdown',
url: '',
run: async (page) => {
const spaceDrowpdown = page.locator('[data-testid="space-dropdown-button"]');
await spaceDrowpdown.waitFor();
},
},
{
name: 'Default variant',
url: '',
},
{
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();
},
},
],
},
{
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',
@@ -77,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',
@@ -88,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',
@@ -98,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',
@@ -177,6 +357,11 @@ const testCases: TestsCase[] = [
url: 'blocks/lists',
fullPage: true,
},
{
name: 'Code',
url: 'blocks/code',
fullPage: true,
},
{
name: 'Cards',
url: 'blocks/cards',
@@ -189,6 +374,12 @@ const testCases: TestsCase[] = [
{
name: 'Embeds',
url: 'blocks/embeds',
fullPage: true,
},
{
name: 'Page links',
url: 'blocks/page-links',
fullPage: true,
},
{
name: 'Annotations',
@@ -202,8 +393,13 @@ const testCases: TestsCase[] = [
},
{
name: 'Page options',
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
baseUrl: 'https://gitbook.gitbook.io/test-gitbook-open/',
tests: [
{
name: 'Hidden',
url: 'page-options/page-hidden',
run: waitForCookiesDialog,
},
{
name: 'With cover',
url: 'page-options/page-with-cover',
@@ -219,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',
@@ -235,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',
@@ -447,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) => {
@@ -462,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) {
@@ -476,7 +748,7 @@ for (const testCase of testCases) {
}
if (testEntry.screenshot !== false) {
await argosScreenshot(page, `${testCase.name} - ${testEntry.name}`, {
viewports: ['macbook-13', 'iphone-x', 'ipad-2'],
viewports: ['macbook-16', 'macbook-13', 'iphone-x', 'ipad-2'],
argosCSS: `
/* Hide Intercom */
.intercom-lightweight-app {
@@ -494,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,9 +7,11 @@ module.exports = withSentryConfig(
SENTRY_DSN: process.env.SENTRY_DSN ?? '',
SENTRY_ENVIRONMENT: process.env.SENTRY_ENVIRONMENT ?? 'development',
GITBOOK_ASSETS_PREFIX: process.env.GITBOOK_ASSETS_PREFIX,
GITBOOK_ICONS_URL: process.env.GITBOOK_ICONS_URL,
GITBOOK_ICONS_TOKEN: process.env.GITBOOK_ICONS_TOKEN,
},
webpack(config) {
webpack(config, { dev, webpack }) {
config.resolve.fallback = {
...config.resolve.fallback,
@@ -19,6 +21,21 @@ module.exports = withSentryConfig(
http: false,
};
// Tree shake debug code for Sentry
// https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/tree-shaking/#tree-shaking-with-nextjs
if (!dev) {
config.plugins.push(
new webpack.DefinePlugin({
__SENTRY_DEBUG__: false,
// We always init Sentry with enableTracing: false for now, so this is useless
__SENTRY_TRACING__: false,
__RRWEB_EXCLUDE_IFRAME__: true,
__RRWEB_EXCLUDE_SHADOW_DOM__: true,
__SENTRY_EXCLUDE_REPLAY_WORKER__: true,
}),
);
}
return config;
},
@@ -39,6 +56,15 @@ module.exports = withSentryConfig(
assetPrefix: process.env.GITBOOK_ASSETS_PREFIX,
poweredByHeader: false,
images: {
remotePatterns: [
{
protocol: 'https',
hostname: '*.gitbook.io',
},
],
},
},
{
silent: true,
+95
View File
@@ -0,0 +1,95 @@
{
"name": "gitbook",
"version": "0.3.0",
"private": true,
"scripts": {
"dev": "env-cmd --silent -f ../../.env.local next dev",
"build": "next build",
"build:cloudflare": "next-on-pages",
"start": "next start",
"lint": "next lint",
"typecheck": "tsc --noEmit",
"e2e": "playwright test",
"unit": "bun test {src,packages}",
"generate": "gitbook-icons ./public/~gitbook/static/icons custom-icons && gitbook-math ./public/~gitbook/static/math",
"copy:icons": "gitbook-icons ./public/~gitbook/static/icons",
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static"
},
"dependencies": {
"@gitbook/api": "0.60.0",
"@gitbook/icons": "workspace:*",
"@gitbook/react-math": "workspace:*",
"@gitbook/react-openapi": "workspace:*",
"@gitbook/react-contentkit": "workspace:*",
"@gitbook/emoji-codepoints": "workspace:*",
"@gitbook/cache-do": "workspace:*",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-popover": "^1.0.7",
"@sentry/nextjs": "^7.94.1",
"@sindresorhus/fnv1a": "^3.1.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/typography": "^0.5.10",
"@upstash/redis": "^1.27.1",
"ajv": "^8.12.0",
"assert-never": "^1.2.1",
"bun-types": "^1.1.20",
"classnames": "^2.5.1",
"content-security-policy-merger": "^1.0.0",
"framer-motion": "^10.16.14",
"js-cookie": "^3.0.5",
"jsontoxml": "^1.0.1",
"katex": "^0.16.9",
"mathjax": "^3.2.2",
"memoizee": "^0.4.15",
"next": "^14.2.5",
"next-themes": "^0.2.1",
"nuqs": "^1.17.4",
"object-hash": "^3.0.0",
"openapi-types": "^12.1.3",
"p-map": "^7.0.0",
"parse-cache-control": "^1.0.1",
"react": "^18",
"react-dom": "^18",
"react-hotkeys-hook": "^4.4.1",
"recoil": "^0.7.7",
"rehype-sanitize": "^6.0.0",
"rehype-stringify": "^10.0.0",
"remark-gfm": "^4.0.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.0",
"rison": "^0.1.1",
"server-only": "^0.0.1",
"shiki": "^1.11.1",
"tailwind-merge": "^2.2.0",
"tailwind-shades": "^1.1.2",
"unified": "^11.0.4",
"url-join": "^5.0.0"
},
"devDependencies": {
"@argos-ci/playwright": "^2.0.0",
"@playwright/test": "^1.42.1",
"@cloudflare/next-on-pages": "^1.12.1",
"@cloudflare/workers-types": "^4.20240725.0",
"@types/js-cookie": "^3.0.6",
"@types/jsontoxml": "^1.0.5",
"@types/jsonwebtoken": "^9.0.6",
"@types/node": "^20",
"@types/object-hash": "^3.0.6",
"@types/parse-cache-control": "^1.0.4",
"@types/psi": "^4.1.6",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/rison": "^0.0.9",
"autoprefixer": "^10",
"eslint": "^8",
"eslint-config-next": "^14.2.5",
"eslint-plugin-import": "^2.29.1",
"jsonwebtoken": "^9.0.2",
"postcss": "^8",
"psi": "^4.1.0",
"tailwindcss": "^3.4.0",
"typescript": "^5.5.3",
"env-cmd": "^10.1.0",
"ts-essentials": "^10.0.1"
}
}
+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,11 +1,13 @@
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,
sampleRate: 0.1,
// Disable tracing as it creates additional requests in an env where subrequests are limited.
enableTracing: false,
@@ -1,11 +1,13 @@
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,
sampleRate: 0.1,
// Disable tracing as it creates additional requests in an env where subrequests are limited.
enableTracing: false,
@@ -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,21 +16,32 @@ 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', { status: 400 });
return new Response(`Invalid signature "${signature ?? ''}" for "${url}"`, { status: 400 });
}
// Cloudflare-specific options are in the cf object.
@@ -29,7 +29,6 @@ export async function POST(req: NextRequest) {
return NextResponse.json({
success: true,
keys: result.keys,
stats: result.stats,
});
}
@@ -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]);
}
@@ -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>
@@ -1,11 +1,14 @@
import { getSpaceLanguage, t } from '@/intl/server';
import { getSpaceLayoutData } from '@/lib/api';
import { getCurrentSiteLayoutData, getSpaceLayoutData } from '@/lib/api';
import { tcls } from '@/lib/tailwind';
import { getContentPointer } from '../../fetch';
export default async function NotFound() {
const { customization } = await getSpaceLayoutData(getContentPointer().spaceId);
const pointer = getContentPointer();
const { customization } = await ('siteId' in pointer
? getCurrentSiteLayoutData(pointer)
: getSpaceLayoutData(pointer.spaceId));
const language = getSpaceLanguage(customization);
@@ -6,9 +6,11 @@ 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 { isSpaceIndexable, isPageIndexable } from '@/lib/seo';
import { tcls } from '@/lib/tailwind';
import { getContentTitle } from '@/lib/utils';
import { PageClientLayout } from './PageClientLayout';
import { PagePathParams, fetchPageData, getPathnameParam, normalizePathname } from '../../fetch';
@@ -18,14 +20,29 @@ export const runtime = 'edge';
/**
* Fetch and render a page.
*/
export default async function Page(props: { params: PagePathParams }) {
const { params } = props;
export default async function Page(props: {
params: PagePathParams;
searchParams: { fallback?: string };
}) {
const { params, searchParams } = props;
const {
content: contentPointer,
contentTarget,
space,
parent,
customization,
pages,
page,
document,
} = await getPageDataWithFallback({
pagePathParams: params,
searchParams,
redirectOnFallback: true,
});
const rawPathname = getPathnameParam(params);
const { contentTarget, space, customization, pages, page, document } =
await fetchPageData(params);
const linksContext: PageHrefContext = {};
const rawPathname = getPathnameParam(params);
if (!page) {
const pathname = normalizePathname(rawPathname);
if (pathname !== rawPathname) {
@@ -48,6 +65,7 @@ export default async function Page(props: { params: PagePathParams }) {
const withPageFeedback = customization.feedback.enabled;
const contentRefContext: ContentRefContext = {
siteContext: 'siteId' in contentPointer ? contentPointer : null,
space,
revisionId: contentTarget.revisionId,
pages,
@@ -62,6 +80,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 +94,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 +123,24 @@ export async function generateViewport({ params }: { params: PagePathParams }):
};
}
export async function generateMetadata({ params }: { params: PagePathParams }): Promise<Metadata> {
const { space, pages, page, customization, collection } = await fetchPageData(params);
export async function generateMetadata({
params,
searchParams,
}: {
params: PagePathParams;
searchParams: { fallback?: string };
}): Promise<Metadata> {
const { space, pages, page, customization, parent, ancestors } = await getPageDataWithFallback({
pagePathParams: params,
searchParams,
});
if (!page) {
notFound();
}
return {
title: [page.title, customization.title ?? space.title, collection?.title]
title: [page.title, getContentTitle(space, customization, parent)]
.filter(Boolean)
.join(' | '),
description: page.description ?? '',
@@ -123,5 +153,40 @@ export async function generateMetadata({ params }: { params: PagePathParams }):
absoluteHref(`~gitbook/ogimage/${page.id}`, true),
],
},
robots:
isSpaceIndexable({ space, parent }) && isPageIndexable(ancestors, page)
? 'index, follow'
: 'noindex, nofollow',
};
}
/**
* 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 { isSpaceIndexable } from '@/lib/seo';
import { getContentTitle } from '@/lib/utils';
import { ClientContexts } from './ClientContexts';
import { RocketLoaderDetector } from './RocketLoaderDetector';
@@ -32,12 +35,17 @@ export default async function ContentLayout(props: { children: React.ReactNode }
contentTarget,
customization,
pages,
collection,
collectionSpaces,
parent,
spaces,
ancestors,
scripts,
} = await fetchSpaceData();
ReactDOM.preconnect(api().endpoint);
if (assetsDomain) {
ReactDOM.preconnect(assetsDomain);
}
scripts.forEach(({ script }) => {
ReactDOM.preload(script, {
as: 'script',
@@ -56,8 +64,8 @@ export default async function ContentLayout(props: { children: React.ReactNode }
<SpaceLayout
space={space}
contentTarget={contentTarget}
collection={collection}
collectionSpaces={collectionSpaces}
parent={parent}
spaces={spaces}
customization={customization}
pages={pages}
ancestors={ancestors}
@@ -100,11 +108,11 @@ export async function generateViewport(): Promise<Viewport> {
}
export async function generateMetadata(): Promise<Metadata> {
const { space, collection, customization } = await fetchSpaceData();
const { space, parent, customization } = await fetchSpaceData();
const customIcon = 'icon' in customization.favicon ? customization.favicon.icon : null;
return {
title: `${collection ? collection.title : customization.title ?? space.title}`,
title: getContentTitle(space, customization, parent),
generator: `GitBook (${buildVersion()})`,
metadataBase: new URL(baseUrl()),
icons: {
@@ -125,7 +133,7 @@ export async function generateMetadata(): Promise<Metadata> {
},
],
},
robots: shouldIndexSpace({ space, collection }) ? 'index, follow' : 'noindex, nofollow',
robots: isSpaceIndexable({ space, parent }) ? 'index, follow' : 'noindex, nofollow',
};
}
@@ -1,9 +1,9 @@
import { ContentVisibility } from '@gitbook/api';
import { NextRequest } from 'next/server';
import { getCollection, getSpace } from '@/lib/api';
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';
@@ -13,16 +13,22 @@ export const runtime = 'edge';
* Generate a robots.txt for the current space.
*/
export async function GET(req: NextRequest) {
const space = await getSpace(getContentPointer().spaceId);
const collection =
space.visibility === ContentVisibility.InCollection && space.parent
? await getCollection(space.parent)
: null;
const pointer = getContentPointer();
const space = await getSpace(
pointer.spaceId,
'siteId' in pointer ? pointer.siteShareKey : undefined,
);
const parent =
'siteId' in pointer
? await getSite(pointer.organizationId, pointer.siteId)
: space.visibility === ContentVisibility.InCollection && space.parent
? await getCollection(space.parent)
: null;
const lines = [
`User-agent: *`,
'Disallow: /~gitbook/',
...(shouldIndexSpace({ space, collection })
...(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);
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);
@@ -66,11 +72,19 @@ export async function GET(req: NextRequest) {
type FlatPageEntry = { page: RevisionPageDocument; depth: number };
function flattenPages(rootPags: RevisionPage[]): FlatPageEntry[] {
function flattenPages(
rootPags: RevisionPage[],
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' ? [{ page, depth }] : []),
...page.pages.flatMap((child) =>
@@ -5,9 +5,16 @@ import { NextRequest } from 'next/server';
import React from 'react';
import { getContentPointer } from '@/app/(space)/fetch';
import { getCollection, getSpace, getSpaceCustomization } from '@/lib/api';
import {
getCollection,
getCurrentSiteCustomization,
getSite,
getSpace,
getSpaceCustomization,
} from '@/lib/api';
import { getEmojiForCode } from '@/lib/emojis';
import { tcls } from '@/lib/tailwind';
import { getContentTitle } from '@/lib/utils';
export const runtime = 'edge';
@@ -35,17 +42,20 @@ export async function GET(req: NextRequest) {
const options = getOptions(req.url);
const size = SIZES[options.size];
const spaceId = getContentPointer().spaceId;
const pointer = getContentPointer();
const spaceId = pointer.spaceId;
const [space, customization] = await Promise.all([
getSpace(spaceId),
getSpaceCustomization(spaceId),
getSpace(spaceId, 'siteId' in pointer ? pointer.siteShareKey : undefined),
'siteId' in pointer ? getCurrentSiteCustomization(pointer) : getSpaceCustomization(spaceId),
]);
const collection =
space.visibility === ContentVisibility.InCollection && space.parent
? await getCollection(space.parent)
: null;
const contentTitle = collection?.title ?? customization.title ?? space.title;
const parent =
'siteId' in pointer
? await getSite(pointer.organizationId, pointer.siteId)
: space.visibility === ContentVisibility.InCollection && space.parent
? await getCollection(space.parent)
: null;
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';
@@ -11,7 +13,7 @@ export const runtime = 'edge';
* Render the OpenGraph image for a space.
*/
export async function GET(req: NextRequest, { params }: { params: PageIdParams }) {
const { space, page, customization, collection } = await fetchPageData(params);
const { space, page, customization, parent } = await fetchPageData(params);
const url = new URL(space.urls.published ?? space.urls.app);
if (customization.socialPreview.url) {
@@ -31,7 +33,7 @@ export async function GET(req: NextRequest, { params }: { params: PageIdParams }
}}
>
<h2 tw="text-7xl font-bold tracking-tight text-left">
{collection?.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>
@@ -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,22 +1,29 @@
import { ArrowLeft, Printer } from '@geist-ui/icons';
import {
CustomizationSettings,
Revision,
RevisionPageDocument,
RevisionPageGroup,
SiteCustomizationSettings,
Space,
} from '@gitbook/api';
import { Icon } from '@gitbook/icons';
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import * as React from 'react';
import { getContentPointer } from '@/app/(space)/fetch';
import { DocumentView } from '@/components/DocumentView';
import { DocumentView, createHighlightingContext } from '@/components/DocumentView';
import { TrademarkLink } from '@/components/TableOfContents/Trademark';
import { PolymorphicComponentProp } from '@/components/utils/types';
import { getSpaceLanguage } from '@/intl/server';
import { tString } from '@/intl/translate';
import { getDocument, getSpace, getSpaceCustomization, getSpaceContentData } from '@/lib/api';
import {
getDocument,
getSpace,
getSpaceCustomization,
getSpaceContentData,
getCurrentSiteCustomization,
} from '@/lib/api';
import { pagePDFContainerId, PageHrefContext, absoluteHref } from '@/lib/links';
import { resolvePageId } from '@/lib/pages';
import { ContentRefContext, resolveContentRef } from '@/lib/references';
@@ -34,8 +41,13 @@ export const runtime = 'edge';
export async function generateMetadata(): Promise<Metadata> {
const contentPointer = getContentPointer();
const [space, customization] = await Promise.all([
getSpace(contentPointer.spaceId),
getSpaceCustomization(contentPointer.spaceId),
getSpace(
contentPointer.spaceId,
'siteId' in contentPointer ? contentPointer.siteShareKey : undefined,
),
'siteId' in contentPointer
? getCurrentSiteCustomization(contentPointer)
: getSpaceCustomization(contentPointer.spaceId),
]);
return {
@@ -59,8 +71,13 @@ export default async function PDFHTMLOutput(props: { searchParams: { [key: strin
// Load the content,
const [customization, { space, contentTarget, pages: rootPages }] = await Promise.all([
getSpaceCustomization(contentPointer.spaceId),
getSpaceContentData(contentPointer),
'siteId' in contentPointer
? getCurrentSiteCustomization(contentPointer)
: getSpaceCustomization(contentPointer.spaceId),
getSpaceContentData(
contentPointer,
'siteId' in contentPointer ? contentPointer.siteShareKey : undefined,
),
]);
const language = getSpaceLanguage(customization);
@@ -98,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}
@@ -124,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>
@@ -158,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,
@@ -172,7 +190,10 @@ export default async function PDFHTMLOutput(props: { searchParams: { [key: strin
);
}
async function PDFSpaceIntro(props: { space: Space; customization: CustomizationSettings }) {
async function PDFSpaceIntro(props: {
space: Space;
customization: CustomizationSettings | SiteCustomizationSettings;
}) {
const { space, customization } = props;
return (
@@ -216,6 +237,7 @@ async function PDFPageDocument(props: {
const { space, page, refContext } = props;
const document = page.documentId ? await getDocument(space.id, page.documentId) : null;
const shouldHighlightCode = createHighlightingContext();
return (
<PrintPage id={pagePDFContainerId(page)}>
@@ -238,6 +260,7 @@ async function PDFPageDocument(props: {
contentRefContext: refContext,
resolveContentRef: (ref) => resolveContentRef(ref, refContext),
getId: (id) => pagePDFContainerId(page, id),
shouldHighlightCode,
}}
/>
) : null}
@@ -297,9 +320,17 @@ function selectPages(
): FlatPageEntry[] => {
return [
{ page, depth },
...page.pages.flatMap((child) =>
child.type === 'link' ? [] : flattenPage(child, depth + 1),
),
...page.pages.flatMap((child) => {
if (child.type === 'link') {
return [];
}
if (child.hidden) {
return [];
}
return flattenPage(child, depth + 1);
}),
];
};
@@ -324,8 +355,16 @@ function selectPages(
return limitTo(flattenPage(found.page, 0));
}
const allPages = rootPages.flatMap((page) =>
page.type === 'link' ? [] : flattenPage(page, 0),
);
const allPages = rootPages.flatMap((page) => {
if (page.type === 'link') {
return [];
}
if (page.hidden) {
return [];
}
return flattenPage(page, 0);
});
return limitTo(allPages);
}
@@ -1,6 +1,6 @@
'use client';
import * as Sentry from '@sentry/nextjs';
import { captureException } from '@sentry/nextjs';
import React from 'react';
import { Button } from '@/components/primitives/Button';
@@ -15,7 +15,7 @@ export default function ErrorPage(props: {
const language = useLanguage();
React.useEffect(() => {
Sentry.captureException(error);
captureException(error);
}, [error]);
return (

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