Compare commits

...

153 Commits

Author SHA1 Message Date
Vib Bhardwaj 7ec64dad58 rm public 2024-12-26 20:16:02 +00:00
Vib Bhardwaj 326d674254 remove next assets 2024-12-26 20:14:00 +00:00
Vib Bhardwaj a6a78ce5ac ignore shiki 2024-12-26 14:18:38 +00:00
Vib Bhardwaj 56cb2f4c9a wip 2024-12-23 15:29:31 +00:00
Amrit Kahlon f92e90603f Prevent multiple versions of codemirror from loading (#2660)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2024-12-21 23:46:16 +01:00
Samy Pessé 14172791e9 Track click on links for site insights (#2659) 2024-12-21 18:18:30 +01:00
Samy Pessé e4e2f524d4 Track site insight event when opening scalar client (#2658) 2024-12-21 15:57:10 +01:00
Amrit Kahlon fc7b16f6a7 Update scalar dependency (#2652) 2024-12-20 23:16:11 +01:00
Samy Pessé 6ba3ae74d5 Workaround until referrer is accepted by API as an empty string (#2656) 2024-12-20 19:23:52 +01:00
Samy Pessé c71d1598d8 Insights site events support (#2655) 2024-12-20 19:06:50 +01:00
Greg Bergé 5950657b0e Fix emojis display (#2648) 2024-12-20 11:30:56 +01:00
Samy Pessé 0e1a48cd06 Upgrade bun and setup-bun, switch to text lockfile (#2651)
Co-authored-by: Greg Bergé <berge.greg@gmail.com>
2024-12-20 11:19:54 +01:00
spastorelli 98245e5f20 Adapt code to pull token from customer backend generated custom cookies (#2650)
Co-authored-by: Steven Hall <steven@gitbook.io>
2024-12-19 18:29:32 +01:00
Zeno Kapitein 0b6ddca981 Fix variant selector for non-default themes (#2649) 2024-12-19 12:28:13 +01:00
spastorelli ea1468c892 Send redirectOnError param to getPublishedContent when token is pulled from cookie (#2626) 2024-12-19 11:16:06 +01:00
Greg Bergé e8e64bf510 Fix bullet list display on full size blocks (#2646) 2024-12-18 13:55:18 +01:00
Greg Bergé aaf8daf0d7 Improve search UI (#2643) 2024-12-18 13:42:47 +01:00
Zeno Kapitein 8af1abc4e1 Improve contrast of search box placeholder (#2639) 2024-12-18 12:14:06 +01:00
Zeno Kapitein 48ab59feb3 Improve contrast of list decoration (#2638) 2024-12-18 11:49:01 +01:00
Samy Pessé f64d5073d4 Version Packages (#2557)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-18 10:25:08 +01:00
Samy Pessé 53b9f10ac8 Prepare @gitbook/proxy to support serving under sub-directory (#2641) 2024-12-18 10:05:31 +01:00
Samy Pessé 75606e4df7 Fix missing env GITBOOK_ASSETS_PREFIX passed to build (#2642) 2024-12-17 21:46:04 +01:00
Claire Chabas 0643b7f351 Improve ask/search Enter key press UX (#2624)
Co-authored-by: Zeno Kapitein <admin@zeno.zone>
2024-12-17 18:08:29 +01:00
Claire Chabas 5b3b4e0614 Fix closing search modal on Escape key press (#2637) 2024-12-17 15:33:36 +01:00
Samy Pessé 8d039d1348 Pass down ancestors instead of computing them (#2635) 2024-12-16 21:11:46 +01:00
Zeno Kapitein 87eea73081 Fix margin and image resolution of header logo (#2633) 2024-12-16 14:34:00 +01:00
Zeno Kapitein 67a6fb4c87 Fix localised search string from wrapping search box (#2630) 2024-12-13 16:48:06 +01:00
Zeno Kapitein 23584c9862 Update header styling (#2613)
Co-authored-by: Valentino Hudhra <v.hudhra@gmail.com>
Co-authored-by: Valentino Hudhra <2587839+valentin0h@users.noreply.github.com>
2024-12-13 15:51:23 +01:00
Zeno Kapitein 2f767125c4 Add breadcrumbs to page header (#2628) 2024-12-13 15:20:07 +01:00
Greg Bergé c73e07d42d Increase token max length to fix code not highlighted (#2629) 2024-12-13 13:16:55 +01:00
Viktor Renkema 6b5d7e6057 Improve styles of Tables (#2623) 2024-12-13 08:15:52 +01:00
Greg Bergé 076dc48e89 Fix expandable block anchor resolution (#2625) 2024-12-12 23:25:29 +01:00
Greg Bergé bb208ab36a Update JoyPixels (#2619) 2024-12-12 09:05:12 +01:00
Viktor Renkema 60e35001b6 Refactor Table to use flex layouts (#2622) 2024-12-11 21:34:37 +01:00
Greg Bergé 29a0d7ea02 Fix sublinks display on mobile (#2620) 2024-12-11 18:57:55 +01:00
Greg Bergé 1f2b7f7acf Prevent resizing AVIF (not supported by Cloudflare) (#2621) 2024-12-11 15:17:25 +01:00
spastorelli 39e5cb63ef Fix context Id not returned by lookupSpaceByAPI (#2618) 2024-12-11 10:54:17 +01:00
spastorelli 5b5928fb8d Fix VA e2e tests when ran locally (#2617) 2024-12-11 08:34:05 +01:00
Greg Bergé 08068743a5 Remove Next.js cache on CI (#2616) 2024-12-10 18:53:47 +01:00
Greg Bergé 35eae1adcf Fix canonical in a safe way (#2589) 2024-12-10 17:47:57 +01:00
Greg Bergé e85d357abd Fix React DOM warning (#2615) 2024-12-10 17:39:22 +01:00
spastorelli 31396d8109 Adapt GBO cache logic to support users sessions info (#2606) 2024-12-10 14:11:59 +01:00
Zeno Kapitein 664debc0bc Add tint color (#2605)
Co-authored-by: Valentino Hudhra <v.hudhra@gmail.com>
Co-authored-by: Valentino Hudhra <2587839+valentin0h@users.noreply.github.com>
2024-12-09 17:19:44 +01:00
Viktor Renkema 56c31c76b3 Center-align inline images (#2609) 2024-12-09 13:21:43 +01:00
Viktor Renkema 2d1a71f3d1 Allow cells in tables to use more available space (#2611) 2024-12-09 13:11:05 +01:00
Valentino Hudhra c67cd73058 Add visual tests for tint (#2601) 2024-12-09 12:29:03 +01:00
Greg Bergé 2572cb4352 Pin React versions (#2610) 2024-12-09 11:14:02 +01:00
Steven H c9f6c7ccac Fix an issue where Ask AI answers with too many sources could crash the Worker. (#2608) 2024-12-07 15:23:10 +00:00
Steven Hall b6520b0dbc hotfix: Fix an issue where Ask AI was crashing on some requests. 2024-12-06 22:26:41 +00:00
Steven H 9fe8142117 Fix an issue where Ask AI was erroring due to an object being passed as a param. (#2607) 2024-12-06 21:55:59 +00:00
Steven H 57cdd25b5f Ask AI now goes through the site API instead of spaces. (#2598) 2024-12-06 20:06:49 +00:00
Samy Pessé 1005ee5d52 Prevent resizing the url of an image already resized (#2600) 2024-12-05 16:34:31 +01:00
Valentino Hudhra d9bb9f9a07 Fix cookie banner button being non responsive (#2599) 2024-12-05 14:04:03 +01:00
Greg Bergé 75a6fabe61 Support dropdown without link (#2596) 2024-12-04 12:05:17 +01:00
Greg Bergé 75e29b69cc Allow to work locally (#2597) 2024-12-03 14:49:13 +01:00
Samy Pessé ffd3937538 Fix security issue with images resizing (#2595) 2024-12-03 08:51:46 +01:00
Valentino Hudhra 575587e4c5 Improve footer link styling (#2591) 2024-12-01 07:54:45 +01:00
Brett Jephson 2ce59d7c92 Fix whitespace added with image (#2594) 2024-11-29 17:10:29 +00:00
Brett Jephson 3b3d6e2a73 Add an icon to site sections (#2593) 2024-11-29 16:09:06 +00:00
Zeno Kapitein f2a467fdbf Fix hint block decoration color (#2590) 2024-11-29 16:40:10 +01:00
vibhanshub 867481c66d Support Integers in Response sample (#2592) 2024-11-28 14:19:37 +00:00
spastorelli fc8065b7a8 Revert "Fix canonical URL ending with "/" (#2586)" (#2588) 2024-11-27 13:05:34 +01:00
Greg Bergé 28f1ed9205 Fix canonical URL ending with "/" (#2586) 2024-11-26 21:57:15 +01:00
Viktor Renkema 568266bc9a Add transitions to Sections bg and line (#2491) 2024-11-26 19:59:07 +01:00
Samy Pessé 7739f341b8 Revert "Serve the correct status code for not found and redirects 👌" (#2587) 2024-11-26 18:36:00 +01:00
Greg Bergé 2b501f191c Serve the correct status code for not found and redirects 👌 (#2582) 2024-11-26 18:05:47 +01:00
Zeno Kapitein aa2ed0fda1 Restyle hint block (#2579) 2024-11-26 12:41:47 +01:00
Greg Bergé 51cba074d7 Update shiki (#2583) 2024-11-26 11:50:08 +01:00
Taran Vohra 2c945c663c Get the best possible target space for content refs (#2580) 2024-11-26 10:53:37 +05:30
Viktor Renkema d9a18ecabb Improve contrast ratio for text color on image captions (#2584) 2024-11-25 14:01:09 +01:00
Greg Bergé eab7931afb Parallelize and remove useless Promise.all (#2581) 2024-11-22 15:54:56 +01:00
Taran Vohra 99b6c92377 Resolve site level redirects (#2577) 2024-11-22 12:04:15 +05:30
Zeno Kapitein 4d56f1163b Improve styling of search & ask AI box (#2576) 2024-11-20 16:59:15 +01:00
Brett Jephson 07cf835551 Add a scroll margin to the top when layout has sections (#2574) 2024-11-19 16:34:27 +00:00
Steven H 061c0c1a90 Fix a regression in variant drop-down caused by missing z-index. (#2575) 2024-11-19 17:23:44 +01:00
Anthony Lio 1ed18c05ef Adds missing css variables (#2558)
Co-authored-by: Steven H <steven@gitbook.io>
2024-11-19 16:08:07 +01:00
Steven H ca134c8a60 Fix an issue where the active site section indicator appeared above any dropdowns. (#2568) 2024-11-19 15:16:47 +01:00
Amrit Kahlon 7ba67fd852 chore: scalar bump (#2571)
Co-authored-by: Steven Hall <steven@gitbook.io>
2024-11-19 13:54:35 +01:00
Taran Vohra 7675c2c411 use getPublishedContentSite API (#2570)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2024-11-15 09:21:08 +01:00
Brett Jephson 7c71363af8 Set adjustFallbackFont to false for mono font (#2569) 2024-11-12 13:03:25 +00:00
Steven H d48926eb2a Fix an issue where the space dropdown was shown under the site sections in Safari. (#2567) 2024-11-05 11:49:33 +00:00
Steven H d843e5e06f Fix an issue where the space dropdown could appear behind the header. (#2566) 2024-11-05 09:47:32 +00:00
Steven H a2e564738e Fix the styling of site section tabs on smaller screens. (#2565) 2024-11-04 17:36:10 +00:00
Greg Bergé 54a797a0b1 Nested list support (#2562) 2024-10-31 14:02:42 +01:00
Amrit Kahlon a78c1ecb4d Bump scalar api client package (#2563) 2024-10-30 11:45:56 -07:00
Amrit Kahlon 4771c78001 Fix Scalar ApiClient routing (#2561) 2024-10-28 12:38:18 -07:00
Samy Pessé b7a5106f36 Disable logs for cache-do (#2560) 2024-10-27 08:16:56 -04:00
Samy Pessé 4bcbdc50ff Output caching headers on RSC/prefetch responses (#2559) 2024-10-26 10:57:52 +02:00
Brett Jephson 5d72b35c7f Improve animation of section tabs (#2553) 2024-10-25 14:00:27 +01:00
Amrit Kahlon ff50ac215c Bump @scalar/api-client-react version (#2555)
Co-authored-by: taranvohra <taranvohra@outlook.com>
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2024-10-25 12:06:49 +05:30
Samy Pessé 3fe3809416 Version Packages (#2415)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-24 15:54:23 +02:00
Samy Pessé 363d337a72 Cleanup GitHub workflows (#2554) 2024-10-24 14:50:39 +02:00
Amrit Kahlon f89b31c940 Upgrade to the new scalar client (#2539)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2024-10-24 14:40:33 +02:00
Samy Pessé 630c3e84dd Migrate to cloudflare/wrangler-action for deployment (#2549) 2024-10-24 14:30:13 +02:00
Samy Pessé d43202f137 Reduce bundle size of shiki (#2551) 2024-10-24 10:16:56 +02:00
Scott Cazan 0964043489 Use updated endpoint for page feedbacks (#2548) 2024-10-24 10:16:46 +02:00
Taran Vohra e9b31a545d Unify section tab styles with page item styles (#2552) 2024-10-24 13:28:15 +05:30
Greg Bergé 7f28a88374 Use transparent bg for secondary button (#2545) 2024-10-24 06:51:39 +02:00
Steven H 07ea45bf84 Remove deprecated synced block from GitBook Open (#2546) 2024-10-23 15:45:31 +01:00
Zeno Kapitein a86d35f721 Fixes variant selector for sites without variants (#2544) 2024-10-22 17:22:07 +02:00
Zeno Kapitein d6c28a0ff6 Update header styling of sections, variant selector, and button links (#2543) 2024-10-22 16:58:27 +02:00
Brett Jephson a7af3ca7bc Improving the new tabs (#2537) 2024-10-21 19:15:22 +01:00
Taran Vohra cda08a9843 Add support for search in a sections site (#2541) 2024-10-21 15:54:29 +05:30
Taran Vohra 7588774237 add all mode while searching site content (#2540) 2024-10-21 12:13:00 +05:30
Greg Bergé 683373b263 Fix CLS and others (#2536) 2024-10-20 12:55:37 +02:00
spastorelli 673ec8fb5d Update Placeholder ad & Sponsored by GitBook links (#2538) 2024-10-19 08:51:22 +02:00
spastorelli 63bacaa671 Ensure pending or in-review sites still get placeholder ads even without preview query param (#2535) 2024-10-18 17:08:54 +02:00
Samy Pessé e3a3d6a505 Improve perf perception and fix flickering in Ask answer (#2534) 2024-10-18 15:53:57 +02:00
Taran Vohra 175171dda1 Add siteSectionId to lookup result (#2533) 2024-10-18 16:53:15 +05:30
Taran Vohra a4b63b88c8 generate upto URL with 4 segments to support sites with sections (#2532) 2024-10-18 16:34:25 +05:30
Steven H c3675fdfe7 Add support for reusable content (#2531) 2024-10-18 10:59:46 +01:00
Brett Jephson 2fa08519b6 RND-5227: Site section tabs (#2530) 2024-10-18 09:47:53 +01:00
spastorelli 065627060b Show placeholder ad when site ads status is pending or in-review (#2529) 2024-10-17 19:20:34 +02:00
Greg Bergé 2982b6a808 Implement Stepper (#2528) 2024-10-17 09:52:45 +02:00
Greg Bergé c3f56ecd40 Return null if block not handled (#2527) 2024-10-16 22:12:54 +02:00
Zeno Kapitein fe9e6c1afd Update ogimage with new design (#2517)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2024-10-15 17:28:37 +02:00
Taran Vohra 73394ae929 Mock data for getSpaceCustomization and getSpaceIntegrationScripts (#2522) 2024-10-15 15:40:05 +05:30
Zeno Kapitein c754fc9ff4 Add automatic color contrast in site header, restyle search button (#2512) 2024-10-15 11:05:25 +02:00
Zeno Kapitein 1ca6208057 Update Playwright test for share links (#2523) 2024-10-15 10:53:58 +02:00
Greg Bergé cdfae4d4df Remove custom types for HeaderLink (#2521) 2024-10-14 13:33:01 +02:00
Samy Pessé c3da5469ca Add basic support for computed pages types (#2520) 2024-10-14 10:04:12 +02:00
Taran Vohra 79f2d8ba76 Refactor getContentPointer usage into getSiteContentPointer (#2519) 2024-10-14 12:07:14 +05:30
Taran Vohra 7b1bdb92a6 Remove getContentPointer usage from search (#2516) 2024-10-14 10:26:14 +05:30
Taran Vohra f9a075eed1 add support for sites in multi id mode (#2514) 2024-10-11 15:23:38 +05:30
Greg Bergé 32e4193557 Implement header link style (#2513) 2024-10-09 15:25:47 +02:00
Zeno Kapitein 133c3e7c0a Update Checkbox component (#2511) 2024-10-08 11:02:51 +02:00
Samy Pessé 6ce3cea682 Stop using KV cache backend for now, but also improves it for higher performances (#2510) 2024-10-03 15:46:58 +02:00
Greg Bergé 250e77d18b Fix synced blocks display (#2508) 2024-10-03 10:30:21 +02:00
Addison e3bfe006e9 Add badges section to readme (#2507) 2024-10-02 18:15:37 +09:00
Taran Vohra 76aeecdc68 Remove /v/ from e2e tests (#2505) 2024-10-01 12:04:08 +05:30
Samy Pessé 15d2ee3e76 Show captions for file blocks (#2506) 2024-09-30 17:54:25 +02:00
Taran Vohra b066db479f Bump the cache key for api.getPublishedContentByUrl (#2502) 2024-09-30 15:04:30 +02:00
Samy Pessé a69ff2789a Fix locationHint not being passed when purging the cache (#2504) 2024-09-30 14:45:17 +02:00
Taran Vohra 4df235d64a Add more visitor auth cookie combinations with /v/ for backwards compatibility (#2503) 2024-09-30 17:33:33 +05:30
Taran Vohra 11524457e3 URL alternative resolution criteria (#2497) 2024-09-30 14:18:44 +05:30
Addison 8f7364293e Update Documented on GB badge from encoded to uploaded icon (#2501) 2024-09-30 15:44:45 +08:00
Samy Pessé 56f5fa1fbe Lower usage of KV cache to 30% of tags (#2500) 2024-09-29 10:31:27 +02:00
Samy Pessé b075f0f7e9 Switch to aria-current="page" for the TOC items (#2499) 2024-09-27 12:54:11 +02:00
Samy Pessé 0ca32afe8d Lower usage of KV cache to 60% of tags (#2498) 2024-09-27 11:46:15 +02:00
Samy Pessé 32c2f257a9 Lower usage of KV cache to 80% of tags (#2496) 2024-09-26 19:28:25 +02:00
Brett Jephson a7066ccbe1 Fix: mobile navigation scroll (#2486)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2024-09-26 14:42:09 +01:00
Taran Vohra 09248e0efe Revert "generate upto URL with 3 segments to support the new site url format" (#2494) 2024-09-26 18:29:01 +05:30
Taran Vohra e2ef22fc5f generate upto URL with 3 segments to support the new site url format (#2493)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2024-09-26 17:16:22 +05:30
Samy Pessé 75f825e4f8 Disable KV cache for 5% of the cache tags (#2490) 2024-09-26 10:23:25 +02:00
Samy Pessé 9b8d5198e4 Experiment with lower DO billable duration by using direct RPC methods (#2492) 2024-09-25 23:57:43 +02:00
Samy Pessé e09f747727 Refresh change request cache when click refresh button (#2489) 2024-09-25 16:15:42 +02:00
Samy Pessé 16e6171750 Cache embeds for faster rendering (#2488) 2024-09-25 13:33:40 +02:00
Samy Pessé f885e887ea Improve change request previews: consistency and toolbar (#2487) 2024-09-25 12:54:15 +02:00
Samy Pessé 5096f7fbc5 Disable KV cache for docs.gitbook.com and change requests (#2485) 2024-09-25 11:53:53 +02:00
Brett Jephson f16560c1ee Scroll offset (#2484) 2024-09-25 08:52:44 +01:00
Samy Pessé d0f4860759 Refresh icons urls to purge cache of 500 (#2483) 2024-09-24 18:16:35 +02:00
Samy Pessé 34d36c6de1 Fix next-on-pages configuration for ~gitbook/static (#2482) 2024-09-24 17:25:58 +02:00
Brett Jephson 042b850b5d RND-2147: automatically scroll TOC (#2478) 2024-09-24 16:24:55 +01:00
295 changed files with 9694 additions and 4512 deletions
+11
View File
@@ -0,0 +1,11 @@
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
],
"ignore": [
"node_modules",
"dist",
"public"
]
}
+1
View File
@@ -0,0 +1 @@
1.1.40
-5
View File
@@ -1,5 +0,0 @@
---
'gitbook': patch
---
Avoid multiple <h1> in the page by using a <div> for the title in the header
-6
View File
@@ -1,6 +0,0 @@
---
'@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': minor
---
Adapt code to pull token from customer backend generated custom cookies
-5
View File
@@ -1,5 +0,0 @@
---
'@gitbook/react-openapi': minor
---
Add Python support in Code Samples
-5
View File
@@ -1,5 +0,0 @@
---
'gitbook': patch
---
RND-3532: drop down menu for hidden links at small screen size
-5
View File
@@ -1,5 +0,0 @@
---
'gitbook': patch
---
Change dark mode shadow for multi-space search toolbar
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': minor
---
Add an optional client context to get a callback called when the Scalar client is opened for a block.
-5
View File
@@ -1,5 +0,0 @@
---
'gitbook': patch
---
Use ellipsis and fix icon color for more links in the header on small screen
-5
View File
@@ -1,5 +0,0 @@
---
'@gitbook/icons': patch
---
Fix first run of the package in development mode
-5
View File
@@ -1,5 +0,0 @@
---
'gitbook': minor
---
Replace all icons, previously imported from Geist, by new package `@gitbook/icons`
-5
View File
@@ -1,5 +0,0 @@
---
'@gitbook/cache-do': minor
---
First version of a new cache backend powered by Cloudflare Durable Objects
-5
View File
@@ -1,5 +0,0 @@
---
'gitbook': minor
---
Add support for Norwegian language
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Improve contrast of search box placeholder
-5
View File
@@ -1,5 +0,0 @@
---
'gitbook': minor
---
Optimize layout shift while transitioning between pages with full width blocks (ex: OpenAPI blocks)
-5
View File
@@ -1,5 +0,0 @@
---
'gitbook': minor
---
Add support for page icons
-5
View File
@@ -1,5 +0,0 @@
---
'gitbook': patch
---
Only display spaces dropdown in compact header when site is multi-variants
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': minor
---
Track events for site insights using the new dedicated API.
-5
View File
@@ -1,5 +0,0 @@
---
'@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': patch
---
Improve colour contrast of list item decoration
-5
View File
@@ -1,5 +0,0 @@
---
'gitbook': minor
---
Persist state of tabs and dynamically sync them based on title
+6
View File
@@ -0,0 +1,6 @@
---
'@gitbook/react-contentkit': minor
'gitbook': minor
---
Send redirectOnError param to getPublishedContent when token is pulled from cookie
-5
View File
@@ -1,5 +0,0 @@
---
'gitbook': patch
---
Ensure "Sponsored via GitBook" can be translated in all languages
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': minor
---
Track clicks on links (header, footer, content) for site insights.
-5
View File
@@ -1,5 +0,0 @@
---
'gitbook': patch
---
Use url hash to open Expandable and scroll to anchor
-5
View File
@@ -1,5 +0,0 @@
---
'gitbook': patch
---
Fix margin for paragraphs in quote blocks
-5
View File
@@ -1,5 +0,0 @@
---
'gitbook': patch
---
Use new cache backend, powered by Durable Objects, alongside the existing ones (KV, etc).
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Fix emojis display
-5
View File
@@ -1,5 +0,0 @@
---
'@gitbook/react-openapi': minor
---
Add required query parameters to the code sample
-5
View File
@@ -1,5 +0,0 @@
---
'@gitbook/react-openapi': minor
---
Display pattern if available in parmas in OpenAPI block
-5
View File
@@ -1,5 +0,0 @@
---
'gitbook': patch
---
Fix inconsistent click area in table because of scroll indicator
-6
View File
@@ -1,6 +0,0 @@
---
'@gitbook/react-openapi': minor
'gitbook': patch
---
Synchronize response and response example tabs
-5
View File
@@ -1,5 +0,0 @@
---
'gitbook': patch
---
Show definition title when visible in cards
-6
View File
@@ -1,6 +0,0 @@
---
'@gitbook/react-openapi': minor
'gitbook': patch
---
Rollback of scalar modal while fixing perf issue
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': patch
---
Prevent codemirror from loading multiple versions in scalar
-5
View File
@@ -1,5 +0,0 @@
---
'gitbook': patch
---
Optimize images rendered in community ads
-5
View File
@@ -1,5 +0,0 @@
---
'@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
---
Fix variant selector contrast for non-default themes
@@ -2,4 +2,4 @@
'@gitbook/react-openapi': patch
---
bump: scalar from 1.0.5 to 1.0.7
Updated scalar depdenency
-5
View File
@@ -1,5 +0,0 @@
---
'gitbook': patch
---
Add optional env `GITBOOK_INTEGRATIONS_HOST` to configure the host serving the integrations
-5
View File
@@ -1,5 +0,0 @@
---
'@gitbook/react-openapi': patch
---
Fix crash when `example` is undefined for a response
-5
View File
@@ -1,5 +0,0 @@
---
'gitbook': minor
---
Add support for icons style customization for sites
-5
View File
@@ -1,5 +0,0 @@
---
'@gitbook/react-openapi': patch
---
Adds tabs to Response Example section e.g. for status code examples
-5
View File
@@ -1,5 +0,0 @@
---
'@gitbook/react-openapi': patch
---
Render mandatory headers in code sample
-5
View File
@@ -1,5 +0,0 @@
---
'@gitbook/react-openapi': patch
---
Update Scalar client to latest version
-5
View File
@@ -1,5 +0,0 @@
---
'gitbook': minor
---
Prevent search indexation for pages where it's configured as disabled
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Fix bullet list display on full size blocks
-5
View File
@@ -1,5 +0,0 @@
---
'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
---
Track an event into site insights when visitor is opening the Scalar API client.
-5
View File
@@ -1,5 +0,0 @@
---
'gitbook': minor
---
Update rendering of community ads to match new API response, and make it possible to preview ads.
-5
View File
@@ -1,5 +0,0 @@
---
'@gitbook/react-math': minor
---
Export binary `gitbook-math` to copy assets to a directory
-5
View File
@@ -1,5 +0,0 @@
---
'gitbook': patch
---
Fix "ip" being passed to BSA for community ads
-5
View File
@@ -1,5 +0,0 @@
---
'@gitbook/icons': minor
---
Fix bin gitbook-icons when installed without the Pro token
-5
View File
@@ -1,5 +0,0 @@
---
'gitbook': patch
---
Don't show hidden pages in the empty state of a page
-5
View File
@@ -1,5 +0,0 @@
---
'gitbook': minor
---
Fix linking to a tab itself
+1 -1
View File
@@ -12,7 +12,7 @@ runs:
# Cache browser binaries, cache key is based on Playwright version and OS
- name: 🧰 Cache Playwright browser binaries
uses: actions/cache@v3
uses: actions/cache@v4
id: playwright-cache
with:
path: '~/.cache/ms-playwright'
+18
View File
@@ -0,0 +1,18 @@
name: 'Setup Bun'
description: 'Install Bun and cache dependencies'
runs:
using: 'composite'
steps:
# We cache dependencies even if Bun is fast to avoid downloading fontawesome too many times
# and impacting bandwidth there.
- name: Cache bun
uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}-${{ hashFiles('**/bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Setup bun
uses: oven-sh/setup-bun@v2
with:
bun-version-file: '.bun-version'
+62 -56
View File
@@ -5,7 +5,7 @@ on:
branches:
- main
env:
NPMRC_FONT_AWESOME_TOKEN: ${{ secrets.NPMRC_FONT_AWESOME_TOKEN }}
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
jobs:
deploy:
name: Deploy to Cloudflare Pages
@@ -13,29 +13,21 @@ jobs:
permissions:
contents: read
deployments: write
issues: write
pull-requests: write
checks: write
statuses: write
outputs:
deployment_url: ${{ steps.cloudflare.outputs.url }}
deployment_url: ${{ steps.deploy.outputs.deployment-url }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.18
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Cache Next.js build
uses: actions/cache@v3
with:
path: |
${{ github.workspace }}/.next/cache
# Generate a new cache whenever packages or source files change.
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/bun.lockb') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
# If source files changed but packages didn't, rebuild from a prior cache.
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/bun.lockb') }}-
- name: Sets env vars for production
run: |
echo "SENTRY_ENVIRONMENT=production" >> $GITHUB_ENV
@@ -52,27 +44,55 @@ jobs:
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}
SENTRY_DSN: ${{ vars.SENTRY_DSN }}
- id: cloudflare
name: Publish to Cloudflare Pages
uses: cloudflare/pages-action@v1
- id: deploy
name: Deploy to Cloudflare
uses: cloudflare/wrangler-action@v3.11.0
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: ${{ vars.CLOUDFLARE_PROJECT_NAME }}
directory: ./packages/gitbook/.vercel/output/static
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
workingDirectory: ./
wranglerVersion: '3.82.0'
command: pages deploy ./packages/gitbook/.vercel/output/static --project-name=${{ vars.CLOUDFLARE_PROJECT_NAME }} --branch=${{ github.ref == 'refs/heads/main' && 'main' || format('pr{0}', github.event.pull_request.number) }}
- name: Outputs
run: |
echo "ID: ${{ steps.cloudflare.outputs.id }}"
echo "URL: ${{ steps.cloudflare.outputs.url }}"
echo "Environment: ${{ steps.cloudflare.outputs.environment }}"
echo "Alias: ${{ steps.cloudflare.outputs.alias }}"
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
echo "Alias URL: ${{ steps.deploy.outputs.deployment-alias-url }}"
- name: Archive build output
uses: actions/upload-artifact@v4
with:
name: build-output
path: .vercel/
# Until https://github.com/cloudflare/wrangler-action/issues/301 is done
- name: Update Deployment Status to Success
env:
DEPLOYMENT_URL: ${{ steps.deploy.outputs.deployment-url }}
run: |
curl -X POST \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github.v3+json" \
-d '{"state": "success", "target_url": "${{ steps.deploy.outputs.deployment-url }}", "description": "Deployed Preview URL for commit", "context": "cloudflare/preview"}' \
https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.sha }}
- name: Find GitHub Comment
uses: peter-evans/find-comment@v3
id: fc
if: 1 && !startsWith(github.ref, 'refs/heads/main')
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: GitBook Preview
- name: Create or update GitHub comment
uses: peter-evans/create-or-update-comment@v4
if: 1 && !startsWith(github.ref, 'refs/heads/main')
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body: |
**GitBook Preview**
Latest commit: [${{ steps.deploy.outputs.deployment-url }}](${{ steps.deploy.outputs.deployment-url }})
PR: [${{ steps.deploy.outputs.deployment-alias-url }}](${{ steps.deploy.outputs.deployment-alias-url }})
edit-mode: replace
visual-testing:
runs-on: ubuntu-latest
name: Visual Testing
@@ -80,10 +100,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.18
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Setup Playwright
@@ -106,10 +124,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.18
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
@@ -125,10 +141,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.18
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
@@ -140,10 +154,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.18
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
@@ -155,10 +167,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.18
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
@@ -169,16 +179,14 @@ jobs:
runs-on: ubuntu-latest
name: Build (Open Source)
env:
NPMRC_FONT_AWESOME_TOKEN: ''
NPM_TOKEN_READONLY: ''
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.18
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
run: bun install
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- run: bun run build
@@ -188,10 +196,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.18
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
+5 -9
View File
@@ -17,10 +17,8 @@ jobs:
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: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
@@ -34,7 +32,7 @@ jobs:
# 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 }}
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
release-preview:
@@ -45,10 +43,8 @@ jobs:
steps:
- name: Checkout Repo
uses: actions/checkout@v3
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.18
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
+21 -1
View File
@@ -5,7 +5,7 @@
</p>
<p align="center">
<a href="https://gitbook.com"><img src="https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=data:image/svg%2bxml;base64,PHN2ZyB3aWR0aD0iNjUiIGhlaWdodD0iNjUiIHZpZXdCb3g9IjAgMCA2NSA2NSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTI3LjM5NjQgMzMuNjc2NEMzMC41MjU1IDM1LjQ4MjQgMzIuMDkgMzYuMzg1NCAzMy44MDgzIDM2LjM4NjlDMzUuNTI2NSAzNi4zODg0IDM3LjA5MjYgMzUuNDg4MiA0MC4yMjQ5IDMzLjY4NzdMNjAuMTkxNCAyMi4yMTA0QzYxLjA5MjcgMjEuNjkyMiA2MS42NDg0IDIwLjczMTggNjEuNjQ4NCAxOS42OTIxQzYxLjY0ODQgMTguNjUyNCA2MS4wOTI3IDE3LjY5MiA2MC4xOTE0IDE3LjE3MzlMNDAuMjE3NyA1LjY5MjQ1QzM3LjA4ODggMy44OTM4NiAzNS41MjQzIDIuOTk0NTcgMzMuODA3OCAyLjk5NTI0QzMyLjA5MTIgMi45OTU5MSAzMC41Mjc1IDMuODk2NDIgMjcuNCA1LjY5NzQ0TDEwLjIyOTMgMTUuNTg1NUMxMC4xMDIgMTUuNjU4OCAxMC4wMzg0IDE1LjY5NTQgOS45NzkwOCAxNS43MzAxQzQuMTEzNzEgMTkuMTYzNyAwLjQ4OTg5MiAyNS40MzIzIDAuNDQxNDM4IDMyLjIyODZDMC40NDA5NDggMzIuMjk3MyAwLjQ0MDk0OCAzMi4zNzA4IDAuNDQwOTQ4IDMyLjUxNzZDMC40NDA5NDggMzIuNjY0MyAwLjQ0MDk0OCAzMi43Mzc2IDAuNDQxNDM3IDMyLjgwNjNDMC40ODk3ODUgMzkuNTk0OSA0LjEwNTUyIDQ1Ljg1NzcgOS45NjA0NCA0OS4yOTRDMTAuMDE5NiA0OS4zMjg3IDEwLjA4MzIgNDkuMzY1NCAxMC4yMTAyIDQ5LjQzODdMMjAuOTY1OSA1NS42NDg3QzI3LjIzMzIgNTkuMjY3MyAzMC4zNjY4IDYxLjA3NjYgMzMuODA4MSA2MS4wNzc3QzM3LjI0OTMgNjEuMDc4OSA0MC4zODQyIDU5LjI3MTcgNDYuNjUzOSA1NS42NTc0TDU4LjAwOCA0OS4xMTIxQzYxLjE0NzQgNDcuMzAyMyA2Mi43MTcxIDQ2LjM5NzQgNjMuNTc5IDQ0LjkwNTZDNjQuNDQwOSA0My40MTM5IDY0LjQ0MDkgNDEuNjAyIDY0LjQ0MDkgMzcuOTc4NFYzMC45NzgxQzY0LjQ0MDkgMjkuOTcyOCA2My44OTY1IDI5LjA0NjQgNjMuMDE4MiAyOC41NTczQzYyLjE2ODQgMjguMDgzOSA2MS4xMzI1IDI4LjA5MSA2MC4yODkxIDI4LjU3NThMMzcuMDA3NCA0MS45NTg4QzM1LjQ0NTQgNDIuODU2NyAzNC42NjQzIDQzLjMwNTYgMzMuODA3MyA0My4zMDU5QzMyLjk1MDIgNDMuMzA2MiAzMi4xNjg5IDQyLjg1NzcgMzAuNjA2MyA0MS45NjA3TDE0Ljg0ODcgMzIuOTE1NUMxNC4wNTk0IDMyLjQ2MjQgMTMuNjY0NyAzMi4yMzU5IDEzLjM0NzcgMzIuMTk1QzEyLjYyNSAzMi4xMDE3IDExLjkzMDEgMzIuNTA2NiAxMS42NTQ4IDMzLjE4MTNDMTEuNTM0MSAzMy40NzcyIDExLjUzNjUgMzMuOTMyMiAxMS41NDE0IDM0Ljg0MjRDMTEuNTQ1IDM1LjUxMjQgMTEuNTQ2OCAzNS44NDc0IDExLjYwOTQgMzYuMTU1NkMxMS43NDk3IDM2Ljg0NTYgMTIuMTEyNyAzNy40NzA1IDEyLjY0MjggMzcuOTM0MUMxMi44Nzk1IDM4LjE0MTEgMTMuMTY5NiAzOC4zMDg1IDEzLjc1IDM4LjY0MzVMMzAuNTk3NCA0OC4zNjcyQzMyLjE2NDEgNDkuMjcxNCAzMi45NDc0IDQ5LjcyMzUgMzMuODA3NSA0OS43MjM3QzM0LjY2NzcgNDkuNzIzOSAzNS40NTEzIDQ5LjI3MjMgMzcuMDE4NCA0OC4zNjg5TDU3LjY2ODQgMzYuNDY1NEM1OC4yMDM3IDM2LjE1NjkgNTguNDcxNCAzNi4wMDI2IDU4LjY3MjEgMzYuMTE4NUM1OC44NzI3IDM2LjIzNDUgNTguODcyNyAzNi41NDM0IDU4Ljg3MjcgMzcuMTYxM1Y0MC4zMzY1QzU4Ljg3MjcgNDEuMjQyNCA1OC44NzI3IDQxLjY5NTQgNTguNjU3MiA0Mi4wNjgzQzU4LjQ0MTggNDIuNDQxMyA1OC4wNDkzIDQyLjY2NzUgNTcuMjY0NCA0My4xMTk5TDQwLjIzMjIgNTIuOTM4QzM3LjA5NjYgNTQuNzQ1NCAzNS41Mjg4IDU1LjY0OTIgMzMuODA3OSA1NS42NDg0QzMyLjA4NjkgNTUuNjQ3NiAzMC41MTk5IDU0Ljc0MjQgMjcuMzg2IDUyLjkzMjFMMTEuNDUwOSA0My43MjdDMTEuNDAwMyA0My42OTc4IDExLjM3NSA0My42ODMyIDExLjM1MTQgNDMuNjY5NEM4LjAxMDIzIDQxLjcxNyA1Ljk0ODU5IDM4LjE0NTEgNS45MjkyNSAzNC4yNzU0QzUuOTI5MTIgMzQuMjQ4IDUuOTI5MTIgMzQuMjE4OCA1LjkyOTEyIDM0LjE2MDRWMzEuMjQ1OEM1LjkyOTEyIDI5LjEwOTUgNy4wNjY4OSAyNy4xMzQ5IDguOTE1MTMgMjYuMDYzNkMxMC41NDgzIDI1LjExNjkgMTIuNTYyOCAyNS4xMTUxIDE0LjE5NzcgMjYuMDU4N0wyNy4zOTY0IDMzLjY3NjRaIiBmaWxsPSIjRjJGN0Y3Ii8+Cjwvc3ZnPgo=&labelColor=5c5c5c&color=3F89A1&label=%20" alt="Documented on GitBook"></a>
<a href="https://gitbook.com"><img src="https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=3F89A1"></a>
<a href="#"><img src="https://img.shields.io/badge/Open_Source-❤️-FDA599?"/></a>
<a href="/LICENSE"><img src="https://img.shields.io/badge/License-GNU_GPLv3-F4E28D"/></a>
<a href="/.github/CONTRIBUTING.md"><img src="https://img.shields.io/github/contributors/gitbookIO/gitbook"/></a>
@@ -119,6 +119,26 @@ If you plan to distribute the code, you must make the source code public to comp
See `LICENSE` for more information.
## Badges
<p align="left">
<a href="https://gitbook.com"><img src="https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=3F89A1"></a>
<a href="https://gitbook.com"><img src="https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=F4E28D"></a>
<a href="https://gitbook.com"><img src="https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=FDA599"></a>
</p>
```md
[![GitBook](https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=3F89A1)](https://gitbook.com/)
```
```html
<a href="https://gitbook.com">
<img
src="https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=3F89A1"
/>
</a>
```
## Acknowledgements
GitBook wouldn't be possible without these projects:
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -1,2 +1,2 @@
[install.scopes]
"awesome.me" = { token = "$NPMRC_FONT_AWESOME_TOKEN", url = "https://npm.fontawesome.com/" }
"gitbook" = { token = "$NPM_TOKEN_READONLY", url = "https://registry.npmjs.org" }
+5
View File
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
+43 -32
View File
@@ -1,34 +1,45 @@
{
"name": "gitbook",
"version": "0.1.0",
"private": true,
"packageManager": "bun@1.1.18",
"scripts": {
"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",
"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/*"
],
"devDependencies": {
"@changesets/cli": "^2.27.7",
"prettier": "^3.0.3",
"turbo": "^2.1.2"
},
"patchedDependencies": {
"@vercel/next@4.3.6": "patches/@vercel%2Fnext@4.3.6.patch"
}
"name": "gitbook",
"version": "0.1.0",
"type": "module",
"devDependencies": {
"@changesets/cli": "^2.27.7",
"prettier": "^3.0.3",
"turbo": "^2.1.2"
},
"packageManager": "bun@1.1.18",
"patchedDependencies": {
"@vercel/next@4.3.15": "patches/@vercel%2Fnext@4.3.15.patch",
"@cloudflare/next-on-pages@1.13.5": "patches/@cloudflare%2Fnext-on-pages@1.13.5.patch"
},
"overrides": {
"@codemirror/state": "6.4.1"
},
"private": true,
"scripts": {
"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",
"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": {
"@babel/cli": "^7.26.4",
"@babel/preset-env": "^7.26.0",
"@babel/preset-react": "^7.26.3",
"tsconfig-paths": "^4.2.0"
}
}
+18
View File
@@ -0,0 +1,18 @@
# @gitbook/cache-do
## 0.1.1
### Patch Changes
- b7a5106: Disable cloudflare observability in production
## 0.1.0
### Minor Changes
- 9b8d519: Experiment with optimizing billable duration in Cloudflare by using multiple RPC sessions instead of one
- 636b868: First version of a new cache backend powered by Cloudflare Durable Objects
### Patch Changes
- 56f5fa1: Enable Workers observability with a sampling of 0.1
+2 -2
View File
@@ -14,14 +14,14 @@
"default": "./dist/api.js"
}
},
"version": "0.0.0",
"version": "0.1.1",
"dependencies": {
"@msgpack/msgpack": "^3.0.0-beta2",
"lru_map": "^0.4.1"
},
"devDependencies": {
"typescript": "^5.5.3",
"wrangler": "^3.78.7"
"wrangler": "3.82.0"
},
"scripts": {
"generate": "wrangler types --experimental-include-runtime",
+104 -79
View File
@@ -53,16 +53,20 @@ export class CacheObject extends DurableObject {
* Get the value of a property.
*/
public async get<Value = unknown>(key: string) {
return timeFn(`get: ${key}`, async () => {
return this.logOperation({ operation: 'get', key }, async (setLog) => {
// Try the memory state first.
const memoryEntry = this.lru.get(key);
if (memoryEntry) {
console.log(`get: (memory, ${!!memoryEntry.match}) ${key}`);
setLog({ memory: true });
setLog({ memoryMatch: !!memoryEntry.match });
if (!memoryEntry.match) {
return;
}
if (memoryEntry.match.expiresAt > Date.now()) {
const isExpired = memoryEntry.match.expiresAt < Date.now();
setLog({ memoryExpired: isExpired });
if (!isExpired) {
return memoryEntry.match.value as Value;
}
}
@@ -75,28 +79,31 @@ export class CacheObject extends DurableObject {
* 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;
return this.logOperation({ operation: 'getFromStorage', key }, async (setLog) => {
const entries = await this.ctx.storage.list<Uint8Array>({
prefix: getStoragePropKey(key),
noCache: true,
});
if (entries.size) {
const entry = decodeChunks<CacheObjectProp<Value>>(entries);
setLog({ chunks: entries.size, chunksSize: entry?.size ?? 0 });
if (entry && entry.value.expiresAt > Date.now()) {
// Found
this.lru.set(key, { match: entry.value });
return entry.value.value;
}
}
}
// Not found
this.lru.set(key, { match: undefined });
// 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 () => {
return this.logOperation({ operation: 'set', key }, async (setLog) => {
const prop: CacheObjectProp<Value> = {
value,
expiresAt,
@@ -105,10 +112,12 @@ export class CacheObject extends DurableObject {
this.lru.set(key, { match: prop });
await this.ctx.storage.transaction(async (tx) => {
const entries = encodeChunks(key, prop);
const chunks = Object.keys(entries).length;
setLog({ chunks });
const clockValue: CacheObjectExp = {
k: key,
c: Object.keys(entries).length,
c: chunks,
};
await tx.put(getGCClockKey(key, expiresAt), clockValue);
@@ -127,76 +136,101 @@ export class CacheObject extends DurableObject {
* Purge all keys in the cache object.
*/
public async purge() {
let result = new Set<string>();
return this.logOperation({ operation: 'purge' }, async (setLog) => {
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);
}
try {
// List all the keys in the cache object.
const entries = await this.ctx.storage.list<CacheObjectExp>({
prefix: 'exp.',
noCache: true,
});
setLog({ entries: entries.size });
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);
await this.reset();
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>();
return this.logOperation({ operation: 'alarm' }, async (setLog) => {
try {
const entries = await this.ctx.storage.list<CacheObjectExp>({
prefix: 'exp.',
noCache: true,
});
setLog({ entries: entries.size });
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));
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));
}
// Delete the keys by batch of 128.
const toDelete = Array.from(toDeleteSet);
setLog({ toDelete: toDelete.length });
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);
// 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();
}
} 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();
return this.logOperation({ operation: 'reset' }, async () => {
this.lru.clear();
await this.ctx.storage.deleteAll();
});
}
/**
* Time and log an operation.
*/
async logOperation<T>(
log: Record<string, unknown>,
fn: (update: (log: Record<string, unknown>) => void) => Promise<T>,
): Promise<T> {
const objectId = this.ctx.id.name ?? this.ctx.id.toString();
let update: Record<string, unknown> = {};
const start = performance.now();
try {
return await fn((arg) => {
Object.assign(update, arg);
});
} finally {
const duration = performance.now() - start;
console.log({ ...log, ...update, objectId, duration });
}
}
}
@@ -228,7 +262,7 @@ function encodeChunks<T>(key: string, value: T): Record<string, Uint8Array> {
return entries;
}
function decodeChunks<T>(entries: Map<string, Uint8Array>): T | undefined {
function decodeChunks<T>(entries: Map<string, Uint8Array>): { value: T; size: number } | undefined {
const chunks = Array.from(entries.entries())
.map(([key, value]) => {
const index = parseInt(key.split('.').pop()!);
@@ -242,7 +276,7 @@ function decodeChunks<T>(entries: Map<string, Uint8Array>): T | undefined {
}
const buf = mergeUint8Array(chunks);
return decode(buf) as T;
return { value: decode(buf) as T, size: buf.length };
}
function chunkUint8Array(input: Uint8Array, chunkSize: number): Uint8Array[] {
@@ -263,12 +297,3 @@ function mergeUint8Array(chunks: Uint8Array[]): Uint8Array {
}
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)`);
}
}
+27 -21
View File
@@ -22,7 +22,7 @@ const doLocationHints: {
* Client to access a cache tag.
*/
export class CacheObjectStub {
private opened: CacheObjectDescriptor | null = null;
private stub: DurableObjectStub<CacheObject>;
constructor(
/** Binding to the CacheObject durable object */
@@ -31,41 +31,42 @@ export class CacheObjectStub {
private locationId: CacheLocationId,
/** Name of the tag */
private tag: string,
) {}
) {
const groupId = getCacheObjectIdName(this.locationId, this.tag);
this.stub = 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],
});
}
/**
* Open the cache object.
* Open a descriptor to the cache object.
* It can be used to perform multiple operations in a single RPC session.
* Ex:
* ```ts
* using desc = cache.open();
* await desc.set('key', 'value', Date.now() + 1000);
* await desc.get('key');
* ```
*/
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;
return await this.stub.open();
}
/**
* Get a value from the cache.
*/
async get<Value = unknown>(key: string) {
const desc = await this.open();
return await desc.get<Value>(key);
return (await this.stub.get(key)) as Value | undefined;
}
/**
* 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);
return await this.stub.set(key, value, expiresAt);
}
/**
@@ -76,7 +77,12 @@ export class CacheObjectStub {
await Promise.all(
allLocations.map(async (locationId) => {
const groupId = getCacheObjectIdName(locationId, this.tag);
const cacheGroup = this.doNamespace.get(this.doNamespace.idFromName(groupId));
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],
});
const locationkeys = await cacheGroup.purge();
locationkeys.forEach((key) => keys.add(key));
}),
+10
View File
@@ -10,8 +10,18 @@ migrations = [
{tag = "v1", new_classes = ["CacheObject"]}
]
[observability]
enabled = false
[env.preview]
name = "gitbook-open-cache-preview"
durable_objects.bindings = [
{name = "CACHE", class_name = "CacheObject"}
]
migrations = [
{tag = "v1", new_classes = ["CacheObject"]}
]
[env.preview.observability]
enabled = true
head_sampling_rate = 0.1
File diff suppressed because it is too large Load Diff
+3
View File
@@ -25,3 +25,6 @@
### Sentry ###
# SENTRY_DSN=xxx
### Silent logs
# SILENT=true
+2 -1
View File
@@ -32,4 +32,5 @@ screenshots/
/playwright/.cache/
# Generated public files
/public/~gitbook/static/
/public/~gitbook/static/*
!/public/~gitbook/static/images
+135
View File
@@ -1,5 +1,140 @@
# gitbook
## 0.5.0
### Minor Changes
- 57cdd25: GitBook Open now supports Ask AI in sites. When asking a question to Ask AI, GitBook will use context from across your site sections and variants to provide the best answer.
- ca134c8: Fix an issue where the active site section indicator appeared above any dropdowns.
- d48926e: Fix an issue where the space dropdown was shown under the site sections in Safari.
- 9fe8142: Fix an issue where Ask AI was erroring due to an object being passed as a param.
- d843e5e: Fix an issue where the space dropdown could appear behind the header.
- a2e5647: Fix the styling of site section tabs on smaller screens.
### Patch Changes
- 076dc48: Fix expandable block anchore resolution
- d9bb9f9: Fix an issue with the cookie banner buttons being non responsive
- 23584c9: Update the site header with new styling, a new search button, and refactored layout
- 664debc: Add support for tint color
- 4d56f11: Update styling of search+ask modal
- 061c0c1: Fix a regression in variant drop-down caused by missing z-index.
- 2f76712: Add breadcrumbs above page title
- 07cf835: Add scroll margin to the top when there are sections
- 5d72b35: Smoother tab transition for sections
- 7c71363: Don't adjust fallback font for mono font.
- 7675c2c: Optimize performances by using new API endpoint for fetching site data.
- 87eea73: Fix margin and image resolution of header logo
- aa2ed0f: Restyle hint blocks
- ffd3937: Fix security issue with image resizing that could be used for phishing
- 2ce59d7: Fix - whitespace added to site section tabs with icons.
- c73e07d: Increase token max length to fix code not highlighted
- 3b3d6e2: Add icons to sections
- 1ed18c0: style: adds missing scalar css variables
- Updated dependencies [b7a5106]
- Updated dependencies [4771c78]
- Updated dependencies [ff50ac2]
- Updated dependencies [867481c]
- Updated dependencies [7ba67fd]
- Updated dependencies [a78c1ec]
- @gitbook/cache-do@0.1.1
- @gitbook/react-openapi@0.7.1
## 0.4.0
### Minor Changes
- e09f747: Revalidate change request cached content when pressing refresh button
- 2fa0851: Add navigation tabs for sections
- a4b63b8: Support resolution of new site URLs with sections
- 5c35f36: Replace all icons, previously imported from Geist, by new package `@gitbook/icons`
- e9b31a5: Unify section tab styles with page item styles
- f12a215: Add support for Norwegian language
- f4c9536: Optimize layout shift while transitioning between pages with full width blocks (ex: OpenAPI blocks)
- 1f24fe4: Add support for page icons
- cda08a9: Add support for searching results in a sections site
- b32e40c: Persist state of tabs and dynamically sync them based on title
- 15d2ee3: Show the caption for file blocks
- f885e88: Improve the toolbar for change-requests and revisions to show more actions
- 07ea45b: Remove deprecated synced block from GitBook Open
- c3675fd: Added support for new Reusable Content block.
- 1f24fe4: Add support for icons style customization for sites
- 4c19014: Prevent search indexation for pages where it's configured as disabled
- 3422ad4: Update rendering of community ads to match new API response, and make it possible to preview ads.
- 1152445: Changed the alternative URL resolution criteria in order to support site URLs without /v/ prefix
- 2c437f7: Fix linking to a tab itself
### Patch Changes
- aa32198: Avoid multiple <h1> in the page by using a <div> for the title in the header
- 51fa3ab: Adds content-visibility css property to OpenAPI Operation for better render performance
- a7066cc: Fix scroll position when navigating pages on mobile
- c754fc9: Add automatic color contrast in site header, restyle search button
- 5fe7adb: RND-3532: drop down menu for hidden links at small screen size
- 6295881: Change dark mode shadow for multi-space search toolbar
- f89b31c: Upgrade the scalar api client package
- 13c7534: Use ellipsis and fix icon color for more links in the header on small screen
- f885e88: Improve consistency of change request preview by removing cache-control on response
- 16e6171: Improve performances of loading pages with embeds by caching them
- 34d36c6: Fix GitBook specific static assets not being served correctly when deployed on Cloudflare
- af9e66e: Only display spaces dropdown in compact header when site is multi-variants
- e3a3d6a: Improve perception of fast loading by not rendering skeletons for individual blocks in the top part of the viewport
- 042b850: Automatically scroll to active item in TOC
- d43202f: Optimize bundle size of the server output by reducing bundle size of shiki (skipping themes)
- bfbed1a: Ensure "Sponsored via GitBook" can be translated in all languages
- fe9e6c1: Update ogimage with new design
- 17f71ba: Use url hash to open Expandable and scroll to anchor
- 3c07e65: Fix margin for paragraphs in quote blocks
- 636b868: Use new cache backend, powered by Durable Objects, alongside the existing ones (KV, etc).
- f16560c: Include offset in calculations of whether scrollable element is in view
- 689f553: Fix inconsistent click area in table because of scroll indicator
- 6ce3cea: Stop using KV cache backend for now, but also improves it for higher performances
- e914903: Synchronize response and response example tabs
- 0f990c7: Show definition title when visible in cards
- e3a3d6a: Fix flickering when displaying an "Ask" answer with code blocks
- 4cbcc5b: Rollback of scalar modal while fixing perf issue
- 3996110: Optimize images rendered in community ads
- 133c3e7: Update design of Checkbox to be more consistent and readable
- 5096f7f: Disable KV cache for docs.gitbook.com as a test, also disable it for change-request to improve consistency
- 0f1565c: Add optional env `GITBOOK_INTEGRATIONS_HOST` to configure the host serving the integrations
- 2ff7ed1: Fix table of contents being visible on mobile when disabled at the page level
- b075f0f: Fix accessibility of the table of contents by using `aria-current` instead of `aria-selected`
- cb782a7: Fix "ip" being passed to BSA for community ads
- a7af3ca: Improving the look and feel of new section tabs
- 0bf985a: Don't show hidden pages in the empty state of a page
- d6c28a0: Update header styling of sections, variant selector, and button links
- Change position of variant selector depending on context (next to logo or in table of contents)
- Update section tab styling and animation
- Make header buttons smaller with a new `medium` button size
- Updated dependencies [51fa3ab]
- Updated dependencies [9b8d519]
- Updated dependencies [cf3045a]
- Updated dependencies [f89b31c]
- Updated dependencies [d0f4860]
- Updated dependencies [ef9d012]
- Updated dependencies [094e9cd]
- Updated dependencies [636b868]
- Updated dependencies [56f5fa1]
- Updated dependencies [5c35f36]
- Updated dependencies [4247361]
- Updated dependencies [aa8c49e]
- Updated dependencies [e914903]
- Updated dependencies [4cbcc5b]
- Updated dependencies [0f1565c]
- Updated dependencies [237b703]
- Updated dependencies [51955da]
- Updated dependencies [a679e72]
- Updated dependencies [c079c3c]
- Updated dependencies [5c35f36]
- Updated dependencies [776bc31]
- @gitbook/react-openapi@0.7.0
- @gitbook/cache-do@0.1.0
- @gitbook/icons@0.1.0
- @gitbook/react-contentkit@0.5.1
- @gitbook/react-math@0.6.0
## 0.3.0
### Minor Changes
+4
View File
@@ -0,0 +1,4 @@
{
"version": 1,
"exclude": ["/~gitbook/static/*"]
}
+675 -28
View File
@@ -1,23 +1,38 @@
import { DeepPartial } from 'ts-essentials';
import { argosScreenshot } from '@argos-ci/playwright';
import {
CustomizationBackground,
CustomizationCorners,
CustomizationFont,
CustomizationHeaderPreset,
CustomizationIconsStyle,
CustomizationLocale,
CustomizationSidebarBackgroundStyle,
CustomizationSidebarListStyle,
CustomizationThemeMode,
SiteCustomizationSettings,
} from '@gitbook/api';
import { test, expect, Page } from '@playwright/test';
import { test, expect, Page, BrowserContext } from '@playwright/test';
import deepMerge from 'deepmerge';
import jwt from 'jsonwebtoken';
import rison from 'rison';
import { DeepPartial } from 'ts-essentials';
import {
getVisitorAuthCookieName,
getVisitorAuthCookieValue,
VISITOR_TOKEN_COOKIE,
} from '@/lib/visitor-token';
import { getContentTestURL } from '../tests/utils';
interface Test {
name: string;
url: string; // URL to visit for testing
cookies?: Parameters<BrowserContext['addCookies']>[0];
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
only?: boolean; // Only run this test
}
interface TestsCase {
@@ -33,6 +48,18 @@ const allLocales: CustomizationLocale[] = [
CustomizationLocale.Zh,
];
const allThemeModes: CustomizationThemeMode[] = [
CustomizationThemeMode.Light,
CustomizationThemeMode.Dark,
];
const allThemePresets: CustomizationHeaderPreset[] = [
CustomizationHeaderPreset.Default,
CustomizationHeaderPreset.Bold,
CustomizationHeaderPreset.Contrast,
CustomizationHeaderPreset.Custom,
];
async function waitForCookiesDialog(page: Page) {
const dialog = page.getByRole('dialog', { name: 'Cookies' });
const accept = dialog.getByRole('button', { name: 'Accept' });
@@ -94,7 +121,9 @@ const testCases: TestsCase[] = [
name: 'Variants dropdown',
url: '',
run: async (page) => {
const spaceDrowpdown = page.locator('[data-testid="space-dropdown-button"]');
const spaceDrowpdown = page
.locator('[data-testid="space-dropdown-button"]')
.locator('visible=true');
await spaceDrowpdown.waitFor();
},
},
@@ -104,13 +133,15 @@ const testCases: TestsCase[] = [
},
{
name: 'RFC variant',
url: 'v/rfcs',
url: 'rfcs',
},
{
name: 'Customized variant titles are displayed',
url: '',
run: async (page) => {
const spaceDrowpdown = page.locator('[data-testid="space-dropdown-button"]');
const spaceDrowpdown = page
.locator('[data-testid="space-dropdown-button"]')
.locator('visible=true');
await spaceDrowpdown.click();
const variantSelectionDropdown = page.locator(
@@ -142,9 +173,9 @@ const testCases: TestsCase[] = [
url: 'api-multi-versions/reference/api-reference/pets',
screenshot: false,
run: async (page) => {
const spaceDrowpdown = await page.waitForSelector(
'[data-testid="space-dropdown-button"]',
);
const spaceDrowpdown = await page
.locator('[data-testid="space-dropdown-button"]')
.locator('visible=true');
await spaceDrowpdown.click();
// Click the second variant in the dropdown
@@ -156,18 +187,18 @@ const testCases: TestsCase[] = [
// 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',
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions/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',
url: 'api-multi-versions-share-links/8tNo6MeXg7CkFMzSSz81/reference/api-reference/pets',
screenshot: false,
run: async (page) => {
const spaceDrowpdown = await page.waitForSelector(
'[data-testid="space-dropdown-button"]',
);
const spaceDrowpdown = await page
.locator('[data-testid="space-dropdown-button"]')
.locator('visible=true');
await spaceDrowpdown.click();
// Click the second variant in the dropdown
@@ -179,7 +210,7 @@ const testCases: TestsCase[] = [
// 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',
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-share-links/8tNo6MeXg7CkFMzSSz81/2.0/reference/api-reference/pets?fallback=true',
);
},
},
@@ -200,9 +231,9 @@ const testCases: TestsCase[] = [
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"]',
);
const spaceDrowpdown = await page
.locator('[data-testid="space-dropdown-button"]')
.locator('visible=true');
await spaceDrowpdown.click();
// Click the second variant in the dropdown
@@ -214,7 +245,7 @@ const testCases: TestsCase[] = [
// 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',
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-va/2.0/reference/api-reference/pets?fallback=true',
);
},
},
@@ -371,6 +402,11 @@ const testCases: TestsCase[] = [
name: 'Math',
url: 'blocks/math',
},
{
name: 'Files',
url: 'blocks/files',
fullPage: true,
},
{
name: 'Embeds',
url: 'blocks/embeds',
@@ -389,6 +425,10 @@ const testCases: TestsCase[] = [
await page.click('[data-testid="annotation-button"]');
},
},
{
name: 'Stepper',
url: 'blocks/stepper',
},
],
},
{
@@ -425,29 +465,149 @@ const testCases: TestsCase[] = [
{
name: 'Customization',
baseUrl: 'https://gitbook.gitbook.io/test-gitbook-open/',
tests: [
tests: allThemeModes.flatMap((theme) => [
{
name: 'Without header',
name: `Without header - Theme ${theme}`,
url: getCustomizationURL({
header: {
preset: CustomizationHeaderPreset.None,
links: [],
},
themes: {
default: theme,
toggeable: false,
},
}),
run: waitForCookiesDialog,
},
{
name: 'With duotone icons',
name: `With duotone icons - Theme ${theme}`,
url:
'page-options/page-with-icon' +
getCustomizationURL({
styling: {
icons: CustomizationIconsStyle.Duotone,
},
themes: {
default: theme,
toggeable: false,
},
}),
run: waitForCookiesDialog,
},
],
{
name: `With header buttons - Theme ${theme}`,
url: getCustomizationURL({
header: {
preset: CustomizationHeaderPreset.Default,
links: [
{
title: 'Secondary button',
to: { kind: 'url', url: 'https://www.gitbook.com' },
style: 'button-secondary',
},
{
title: 'Primary button',
to: { kind: 'url', url: 'https://www.gitbook.com' },
style: 'button-primary',
},
],
},
themes: {
default: theme,
toggeable: false,
},
}),
run: waitForCookiesDialog,
},
{
name: `Without tint - Default preset - Theme ${theme}`,
url: getCustomizationURL({
header: {
preset: CustomizationHeaderPreset.Default,
links: [
{
title: 'Secondary button',
to: { kind: 'url', url: 'https://www.gitbook.com' },
style: 'button-secondary',
},
{
title: 'Primary button',
to: { kind: 'url', url: 'https://www.gitbook.com' },
style: 'button-primary',
},
],
},
themes: {
default: theme,
toggeable: false,
},
}),
run: waitForCookiesDialog,
},
...allThemePresets.flatMap((preset) => ({
name: `With tint - Preset ${preset} - Theme ${theme}`,
url: getCustomizationURL({
styling: {
tint: { color: { light: '#346DDB', dark: '#346DDB' } },
},
header: {
preset,
...(preset === CustomizationHeaderPreset.Custom
? {
backgroundColor: { light: '#C62C68', dark: '#EF96B8' },
linkColor: { light: '#4DDE98', dark: '#0C693D' },
}
: {}),
links: [
{
title: 'Secondary button',
to: { kind: 'url', url: 'https://www.gitbook.com' },
style: 'button-secondary',
},
{
title: 'Primary button',
to: { kind: 'url', url: 'https://www.gitbook.com' },
style: 'button-primary',
},
],
},
themes: {
default: theme,
toggeable: false,
},
}),
run: waitForCookiesDialog,
})),
{
name: `With tint - Legacy background match - Theme ${theme}`,
url: getCustomizationURL({
styling: {
background: CustomizationBackground.Match,
},
header: {
preset: CustomizationHeaderPreset.Default,
links: [
{
title: 'Secondary button',
to: { kind: 'url', url: 'https://www.gitbook.com' },
style: 'button-secondary',
},
{
title: 'Primary button',
to: { kind: 'url', url: 'https://www.gitbook.com' },
style: 'button-primary',
},
],
},
themes: {
default: theme,
toggeable: false,
},
}),
run: waitForCookiesDialog,
},
]),
},
{
name: 'Ads',
@@ -460,13 +620,62 @@ const testCases: TestsCase[] = [
},
],
},
{
name: 'Shared space navigation (first site)',
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/shared-space-uno/',
tests: [
{
name: 'Navigation to shared space',
url: '',
run: async (page) => {
const sharedSpaceLink = page.locator('a.underline');
await sharedSpaceLink.click();
expect(page.locator('h1')).toHaveText('shared');
const url = page.url();
expect(url.includes('shared-space-uno')).toBeTruthy(); // same uno site
expect(url.endsWith('/shared')).toBeTruthy(); // correct page
},
},
],
},
{
name: 'Shared space navigation (second site)',
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/shared-space-dos/',
tests: [
{
name: 'Navigation to shared space',
url: '',
run: async (page) => {
const sharedSpaceLink = page.locator('a.underline');
await sharedSpaceLink.click();
expect(page.locator('h1')).toHaveText('shared');
const url = page.url();
expect(url.includes('shared-space-dos')).toBeTruthy(); // same dos site
expect(url.endsWith('/shared')).toBeTruthy(); // correct page
},
},
],
},
{
name: 'Site Redirects',
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/gitbook-doc/',
tests: [
{
name: 'Redirect to SSO page',
url: 'a/redirect/to/sso',
run: async (page) => {
await expect(page.locator('h1')).toHaveText('SSO');
},
},
],
},
{
name: 'Share links',
baseUrl: 'https://gitbook.gitbook.io/gbo-tests-share-links/',
tests: [
{
name: 'Valid link',
url: 'TGs8PkF4GWVtbmPnWhYL/',
url: 'thDznyWXCeEoT55WB7HC/',
},
{
name: 'Invalid link',
@@ -553,7 +762,7 @@ const testCases: TestsCase[] = [
expiresIn: '24h',
},
);
return `v/spacea?jwt_token=${token}`;
return `spacea?jwt_token=${token}`;
})(),
run: waitForCookiesDialog,
},
@@ -570,7 +779,7 @@ const testCases: TestsCase[] = [
expiresIn: '24h',
},
);
return `v/spaceb?jwt_token=${token}`;
return `spaceb?jwt_token=${token}`;
})(),
run: waitForCookiesDialog,
},
@@ -587,7 +796,7 @@ const testCases: TestsCase[] = [
expiresIn: '24h',
},
);
return `v/spacec?jwt_token=${token}`;
return `spacec?jwt_token=${token}`;
})(),
run: waitForCookiesDialog,
},
@@ -667,6 +876,60 @@ const testCases: TestsCase[] = [
},
],
},
{
name: 'Visitor Auth - Site (redirects to fallback/auth URL)',
baseUrl: `https://gitbook-open-e2e-sites.gitbook.io/va-site-redirects-fallback/`,
tests: [
{
name: 'Redirect to fallback on invalid token pulled from cookie',
url: '',
screenshot: false,
cookies: (() => {
const basePath = '/va-site-redirects-fallback/';
const invalidToken = jwt.sign(
{
name: 'gitbook-open-tests',
},
'invalidKey',
{
expiresIn: '24h',
},
);
return [
{
name: getVisitorAuthCookieName(basePath),
value: getVisitorAuthCookieValue(basePath, invalidToken),
httpOnly: true,
},
];
})(),
run: async (page) => {
await expect(page).toHaveURL(/https:\/\/www.google.com/);
},
},
{
name: 'Show error message when invalid token is passed to url',
screenshot: false,
url: (() => {
const token = jwt.sign(
{
name: 'gitbook-open-tests',
},
'invalidKey',
{
expiresIn: '24h',
},
);
return `?jwt_token=${token}`;
})(),
run: async (page) => {
await expect(page.locator('pre')).toContainText(
'Error while validating the JWT token. Reason: The token signature is invalid.',
);
},
},
],
},
{
name: 'Languages',
baseUrl: 'https://gitbook.gitbook.io/test-gitbook-open/',
@@ -734,14 +997,339 @@ const testCases: TestsCase[] = [
},
],
},
{
name: 'Adaptive Content - VA',
baseUrl: `https://gitbook-open-e2e-sites.gitbook.io/adaptive-content-va/`,
tests: [
{
name: 'isAlphaUser',
url: (() => {
const privateKey = 'afe09cdf-0f43-480a-b54c-8b1f62f174f9';
const token = jwt.sign(
{
name: 'gitbook-open-tests',
isAlphaUser: true,
},
privateKey,
{
expiresIn: '24h',
},
);
return `?jwt_token=${token}`;
})(),
run: async (page) => {
const alphaUserPage = page
.locator('a[class*="group\\/toclink"]')
.filter({ hasText: 'Alpha users' });
const betaUserPage = page
.locator('a[class*="group\\/toclink"]')
.filter({ hasText: 'Beta users' });
await expect(alphaUserPage).toBeVisible();
await expect(betaUserPage).toHaveCount(0);
},
},
{
name: 'isBetaUser',
url: (() => {
const privateKey = 'afe09cdf-0f43-480a-b54c-8b1f62f174f9';
const token = jwt.sign(
{
name: 'gitbook-open-tests',
isBetaUser: true,
},
privateKey,
{
expiresIn: '24h',
},
);
return `?jwt_token=${token}`;
})(),
run: async (page) => {
const alphaUserPage = page
.locator('a[class*="group\\/toclink"]')
.filter({ hasText: 'Alpha users' });
const betaUserPage = page
.locator('a[class*="group\\/toclink"]')
.filter({ hasText: 'Beta users' });
await expect(betaUserPage).toBeVisible();
await expect(alphaUserPage).toHaveCount(0);
},
},
{
name: 'isAlphaUser & isBetaUser',
url: (() => {
const privateKey = 'afe09cdf-0f43-480a-b54c-8b1f62f174f9';
const token = jwt.sign(
{
name: 'gitbook-open-tests',
isAlphaUser: true,
isBetaUser: true,
},
privateKey,
{
expiresIn: '24h',
},
);
return `?jwt_token=${token}`;
})(),
run: async (page) => {
const alphaUserPage = page
.locator('a[class*="group\\/toclink"]')
.filter({ hasText: 'Alpha users' });
const betaUserPage = page
.locator('a[class*="group\\/toclink"]')
.filter({ hasText: 'Beta users' });
await expect(alphaUserPage).toBeVisible();
await expect(betaUserPage).toBeVisible();
},
},
],
},
{
name: 'Adaptive Content - Public',
baseUrl: `https://gitbook-open-e2e-sites.gitbook.io/adaptive-content-public/`,
tests: [
{
name: 'No custom cookie',
url: '',
run: async (page) => {
const welcomePage = page
.locator('a[class*="group\\/toclink"]')
.filter({ hasText: 'Welcome Page' });
const alphaUserPage = page
.locator('a[class*="group\\/toclink"]')
.filter({ hasText: 'Alpha User' });
const betaUserPage = page
.locator('a[class*="group\\/toclink"]')
.filter({ hasText: 'Beta User' });
await expect(welcomePage).toBeVisible();
await expect(alphaUserPage).toHaveCount(0);
await expect(betaUserPage).toHaveCount(0);
},
},
{
name: 'Custom cookie with isAlphaUser claim',
cookies: (() => {
const privateKey = '4ddd3c2f-e4b7-4e73-840b-526c3be19746';
const token = jwt.sign(
{
name: 'gitbook-open-tests',
isAlphaUser: true,
},
privateKey,
{
expiresIn: '24h',
},
);
return [
{
name: VISITOR_TOKEN_COOKIE,
value: token,
httpOnly: true,
},
];
})(),
url: '',
run: async (page) => {
const welcomePage = page
.locator('a[class*="group\\/toclink"]')
.filter({ hasText: 'Welcome Page' });
const alphaUserPage = page
.locator('a[class*="group\\/toclink"]')
.filter({ hasText: 'Alpha User' });
const betaUserPage = page
.locator('a[class*="group\\/toclink"]')
.filter({ hasText: 'Beta User' });
await expect(welcomePage).toBeVisible();
await expect(alphaUserPage).toBeVisible();
await expect(betaUserPage).toHaveCount(0);
},
},
{
name: 'Custom cookie with isBetaUser claim',
cookies: (() => {
const privateKey = '4ddd3c2f-e4b7-4e73-840b-526c3be19746';
const token = jwt.sign(
{
name: 'gitbook-open-tests',
isBetaUser: true,
},
privateKey,
{
expiresIn: '24h',
},
);
return [
{
name: VISITOR_TOKEN_COOKIE,
value: token,
httpOnly: true,
},
];
})(),
url: '',
run: async (page) => {
const welcomePage = page
.locator('a[class*="group\\/toclink"]')
.filter({ hasText: 'Welcome Page' });
const alphaUserPage = page
.locator('a[class*="group\\/toclink"]')
.filter({ hasText: 'Alpha User' });
const betaUserPage = page
.locator('a[class*="group\\/toclink"]')
.filter({ hasText: 'Beta User' });
await expect(welcomePage).toBeVisible();
await expect(betaUserPage).toBeVisible();
await expect(alphaUserPage).toHaveCount(0);
},
},
{
name: 'Custom cookie with isAlphaUser & isBetaUser claims',
cookies: (() => {
const privateKey = '4ddd3c2f-e4b7-4e73-840b-526c3be19746';
const token = jwt.sign(
{
name: 'gitbook-open-tests',
isAlphaUser: true,
isBetaUser: true,
},
privateKey,
{
expiresIn: '24h',
},
);
return [
{
name: VISITOR_TOKEN_COOKIE,
value: token,
httpOnly: true,
},
];
})(),
url: '',
run: async (page) => {
const welcomePage = page
.locator('a[class*="group\\/toclink"]')
.filter({ hasText: 'Welcome Page' });
const alphaUserPage = page
.locator('a[class*="group\\/toclink"]')
.filter({ hasText: 'Alpha User' });
const betaUserPage = page
.locator('a[class*="group\\/toclink"]')
.filter({ hasText: 'Beta User' });
await expect(welcomePage).toBeVisible();
await expect(betaUserPage).toBeVisible();
await expect(alphaUserPage).toBeVisible();
},
},
],
},
{
name: 'Tables',
baseUrl: 'https://gitbook.gitbook.io/test-gitbook-open/',
tests: [
{
name: 'Default table',
url: 'blocks/tables',
run: waitForCookiesDialog,
fullPage: true,
},
{
name: 'Table with straight corners',
url:
'blocks/tables' +
getCustomizationURL({
styling: {
corners: CustomizationCorners.Straight,
},
}),
run: waitForCookiesDialog,
fullPage: true,
},
{
name: 'Table with primary color',
url:
'blocks/tables' +
getCustomizationURL({
styling: {
tint: { color: { light: '#346DDB', dark: '#346DDB' } },
},
}),
run: waitForCookiesDialog,
fullPage: true,
},
// Test dark mode for each variant
...allThemeModes.flatMap((theme) => [
{
name: `Table in ${theme} mode`,
url:
'blocks/tables' +
getCustomizationURL({
themes: {
default: theme,
toggeable: false,
},
}),
run: waitForCookiesDialog,
fullPage: true,
},
{
name: `Table with straight corners in ${theme} mode`,
url:
'blocks/tables' +
getCustomizationURL({
styling: {
corners: CustomizationCorners.Straight,
},
themes: {
default: theme,
toggeable: false,
},
}),
run: waitForCookiesDialog,
fullPage: true,
},
{
name: `Table with primary color in ${theme} mode`,
url:
'blocks/tables' +
getCustomizationURL({
styling: {
tint: { color: { light: '#346DDB', dark: '#346DDB' } },
},
themes: {
default: theme,
toggeable: false,
},
}),
run: waitForCookiesDialog,
fullPage: true,
},
]),
],
},
];
for (const testCase of testCases) {
test.describe(testCase.name, () => {
for (const testEntry of testCase.tests) {
test(testEntry.name, async ({ page, baseURL }) => {
const testFn = testEntry.only ? test.only : test;
testFn(testEntry.name, async ({ page, baseURL, context }) => {
const contentUrl = new URL(testEntry.url, testCase.baseUrl);
const url = getContentTestURL(contentUrl.toString(), baseURL);
if (testEntry.cookies) {
await context.addCookies(
testEntry.cookies.map((cookie) => ({
...cookie,
domain: new URL(url).host,
path: '/',
})),
);
}
await page.goto(url);
if (testEntry.run) {
await testEntry.run(page);
@@ -767,7 +1355,66 @@ for (const testCase of testCases) {
* Create a URL with customization settings.
*/
function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettings>): string {
const encoded = rison.encode_object(partial);
/**
* Default customization settings.
*
* The customization object passed to the URL should be a valid API settings object. Hence we extend the test with necessary defaults.
*/
const DEFAULT_CUSTOMIZATION: SiteCustomizationSettings = {
styling: {
primaryColor: { light: '#346DDB', dark: '#346DDB' },
corners: CustomizationCorners.Rounded,
font: CustomizationFont.Inter,
background: CustomizationBackground.Plain,
icons: CustomizationIconsStyle.Regular,
sidebar: {
background: CustomizationSidebarBackgroundStyle.Default,
list: CustomizationSidebarListStyle.Default,
},
},
internationalization: {
locale: CustomizationLocale.En,
},
favicon: {},
header: {
preset: CustomizationHeaderPreset.Default,
links: [],
},
footer: {
groups: [],
},
themes: {
default: CustomizationThemeMode.Light,
toggeable: true,
},
pdf: {
enabled: true,
},
feedback: {
enabled: false,
},
aiSearch: {
enabled: true,
},
advancedCustomization: {
enabled: true,
},
git: {
showEditLink: false,
},
pagination: {
enabled: true,
},
trademark: {
enabled: true,
},
privacyPolicy: {
url: 'https://www.gitbook.com/privacy',
},
socialPreview: {},
};
const encoded = rison.encode_object(deepMerge(DEFAULT_CUSTOMIZATION, partial));
const searchParams = new URLSearchParams();
searchParams.set('customization', encoded);
+22 -19
View File
@@ -1,11 +1,11 @@
{
"name": "gitbook",
"version": "0.3.0",
"version": "0.5.0",
"private": true,
"scripts": {
"dev": "env-cmd --silent -f ../../.env.local next dev",
"build": "next build",
"build:cloudflare": "next-on-pages",
"build:cloudflare": "next-on-pages --custom-entrypoint=./src/cloudflare-entrypoint.ts",
"start": "next start",
"lint": "next lint",
"typecheck": "tsc --noEmit",
@@ -16,13 +16,13 @@
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static"
},
"dependencies": {
"@gitbook/api": "0.60.0",
"@gitbook/api": "^0.85.0",
"@gitbook/cache-do": "workspace:*",
"@gitbook/emoji-codepoints": "workspace:*",
"@gitbook/icons": "workspace:*",
"@gitbook/react-contentkit": "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",
@@ -41,15 +41,16 @@
"katex": "^0.16.9",
"mathjax": "^3.2.2",
"memoizee": "^0.4.15",
"next": "^14.2.5",
"next": "14.2.15",
"next-themes": "^0.2.1",
"nuqs": "^1.17.4",
"nuqs": "^2.2.3",
"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",
"postcss-color-contrast": "^1.1.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-hotkeys-hook": "^4.4.1",
"recoil": "^0.7.7",
"rehype-sanitize": "^6.0.0",
@@ -59,17 +60,18 @@
"remark-rehype": "^11.1.0",
"rison": "^0.1.1",
"server-only": "^0.0.1",
"shiki": "^1.11.1",
"shiki": "^1.23.1",
"tailwind-merge": "^2.2.0",
"tailwind-shades": "^1.1.2",
"unified": "^11.0.4",
"url-join": "^5.0.0"
"url-join": "^5.0.0",
"usehooks-ts": "^3.1.0"
},
"devDependencies": {
"@argos-ci/playwright": "^2.0.0",
"@playwright/test": "^1.42.1",
"@cloudflare/next-on-pages": "^1.12.1",
"@cloudflare/next-on-pages": "^1.13.5",
"@cloudflare/workers-types": "^4.20240725.0",
"@playwright/test": "^1.42.1",
"@types/js-cookie": "^3.0.6",
"@types/jsontoxml": "^1.0.5",
"@types/jsonwebtoken": "^9.0.6",
@@ -77,10 +79,12 @@
"@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/react": "18.3.13",
"@types/react-dom": "18.3.1",
"@types/rison": "^0.0.9",
"autoprefixer": "^10",
"deepmerge": "^4.3.1",
"env-cmd": "^10.1.0",
"eslint": "^8",
"eslint-config-next": "^14.2.5",
"eslint-plugin-import": "^2.29.1",
@@ -88,8 +92,7 @@
"postcss": "^8",
"psi": "^4.1.0",
"tailwindcss": "^3.4.0",
"typescript": "^5.5.3",
"env-cmd": "^10.1.0",
"ts-essentials": "^10.0.1"
"ts-essentials": "^10.0.1",
"typescript": "^5.5.3"
}
}
+3
View File
@@ -0,0 +1,3 @@
# GitBook immutable static assets
/~gitbook/static/*
cache-control: public,max-age=31536000,immutable
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

@@ -4,6 +4,7 @@ import {
verifyImageSignature,
resizeImage,
CloudflareImageOptions,
imagesResizingSignVersion,
checkIsSizableImageURL,
} from '@/lib/images';
import { parseImageAPIURL } from '@/lib/urls';
@@ -18,28 +19,29 @@ export const runtime = 'edge';
export async function GET(request: NextRequest) {
let urlParam = request.nextUrl.searchParams.get('url');
const signature = request.nextUrl.searchParams.get('sign');
// The current signature algorithm sets version as 1, but we need to support the older version as well
// for previously generated content. In this case, we default to version 0.
const signatureVersion = (request.nextUrl.searchParams.get('sv') as '1') || '0';
// The current signature algorithm sets version as 2, but we need to support the older version as well
const signatureVersion = request.nextUrl.searchParams.get('sv') as string | undefined;
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
// Also prevent infinite redirects.
if (!checkIsSizableImageURL(url)) {
return new Response('Invalid url parameter', { status: 400 });
}
// For older signatures, we redirect to the url.
if (signatureVersion !== imagesResizingSignVersion) {
return Response.redirect(url, 302);
}
// Verify the signature
const verified = await verifyImageSignature(url, { signature, version: signatureVersion });
const verified = await verifyImageSignature(url, { signature });
if (!verified) {
return new Response(`Invalid signature "${signature ?? ''}" for "${url}"`, { status: 400 });
}
@@ -3,15 +3,15 @@
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import React from 'react';
import { useScrollToHash } from '@/components/hooks';
import { useScrollPage } from '@/components/hooks';
/**
* Client component to initialize interactivity for a page.
*/
export function PageClientLayout(props: {}) {
export function PageClientLayout(props: { withSections?: boolean }) {
// We use this hook in the page layout to ensure the elements for the blocks
// are rendered before we scroll to the hash.
useScrollToHash();
// are rendered before we scroll to a hash or to the top of the page
useScrollPage({ scrollMarginTop: props.withSections ? 50 : undefined });
useStripFallbackQueryParam();
return null;
@@ -1,14 +1,15 @@
import { TrackPageViewEvent } from '@/components/Insights';
import { getSpaceLanguage, t } from '@/intl/server';
import { getCurrentSiteLayoutData, getSpaceLayoutData } from '@/lib/api';
import { getSiteData, getSpaceContentData } from '@/lib/api';
import { getSiteContentPointer } from '@/lib/pointer';
import { tcls } from '@/lib/tailwind';
import { getContentPointer } from '../../fetch';
export default async function NotFound() {
const pointer = getContentPointer();
const { customization } = await ('siteId' in pointer
? getCurrentSiteLayoutData(pointer)
: getSpaceLayoutData(pointer.spaceId));
const pointer = getSiteContentPointer();
const [{ space }, { customization }] = await Promise.all([
getSpaceContentData(pointer, pointer.siteShareKey),
getSiteData(pointer),
]);
const language = getSpaceLanguage(customization);
@@ -22,6 +23,9 @@ export default async function NotFound() {
</h2>
<p className={tcls('text-base', 'mb-4')}>{t(language, 'notfound')}</p>
</div>
{/* Track the page not found as a page view */}
<TrackPageViewEvent pageId={null} revisionId={space.revision} />
</div>
);
}
@@ -9,7 +9,6 @@ import { PageHrefContext, absoluteHref, pageHref } from '@/lib/links';
import { getPagePath, resolveFirstDocument } from '@/lib/pages';
import { ContentRefContext } from '@/lib/references';
import { isSpaceIndexable, isPageIndexable } from '@/lib/seo';
import { tcls } from '@/lib/tailwind';
import { getContentTitle } from '@/lib/utils';
import { PageClientLayout } from './PageClientLayout';
@@ -29,11 +28,13 @@ export default async function Page(props: {
const {
content: contentPointer,
contentTarget,
sections,
space,
parent,
site,
customization,
pages,
page,
ancestors,
document,
} = await getPageDataWithFallback({
pagePathParams: params,
@@ -65,48 +66,53 @@ export default async function Page(props: {
const withPageFeedback = customization.feedback.enabled;
const contentRefContext: ContentRefContext = {
siteContext: 'siteId' in contentPointer ? contentPointer : null,
siteContext: contentPointer,
space,
revisionId: contentTarget.revisionId,
pages,
page,
};
const withSections = Boolean(sections && sections.list.length > 0);
const headerOffset = { sectionsHeader: withSections, topHeader: withTopHeader };
return (
<>
{withFullPageCover && page.cover ? (
<PageCover as="full" page={page} cover={page.cover} context={contentRefContext} />
) : null}
<div className={tcls('flex', 'flex-row')}>
{/* We use a flex row reverse to render the aside first because the page is streamed. */}
<div className="flex flex-row-reverse justify-end">
{page.layout.outline ? (
<PageAside
space={space}
site={site}
customization={customization}
page={page}
document={document}
withHeaderOffset={headerOffset}
withFullPageCover={withFullPageCover}
withPageFeedback={withPageFeedback}
context={contentRefContext}
/>
) : null}
<PageBody
space={space}
contentPointer={contentPointer}
pointer={contentPointer}
contentTarget={contentTarget}
customization={customization}
context={contentRefContext}
page={page}
ancestors={ancestors}
document={document}
withPageFeedback={
// Display the page feedback in the page footer if the aside is not visible
withPageFeedback && !page.layout.outline
}
/>
{page.layout.outline ? (
<PageAside
space={space}
site={parent?.object === 'site' ? parent : undefined}
customization={customization}
page={page}
document={document}
withHeaderOffset={withTopHeader}
withFullPageCover={withFullPageCover}
withPageFeedback={withPageFeedback}
context={contentRefContext}
/>
) : null}
</div>
<React.Suspense fallback={null}>
<PageClientLayout />
<PageClientLayout withSections={withSections} />
</React.Suspense>
</>
);
@@ -130,7 +136,7 @@ export async function generateMetadata({
params: PagePathParams;
searchParams: { fallback?: string };
}): Promise<Metadata> {
const { space, pages, page, customization, parent, ancestors } = await getPageDataWithFallback({
const { space, pages, page, customization, site, ancestors } = await getPageDataWithFallback({
pagePathParams: params,
searchParams,
});
@@ -140,12 +146,13 @@ export async function generateMetadata({
}
return {
title: [page.title, getContentTitle(space, customization, parent)]
title: [page.title, getContentTitle(space, customization, site ?? null)]
.filter(Boolean)
.join(' | '),
description: page.description ?? '',
alternates: {
canonical: absoluteHref(getPagePath(pages, page), true),
// Trim trailing slashes in canonical URL to match the redirect behavior
canonical: absoluteHref(getPagePath(pages, page), true).replace(/\/+$/, ''),
},
openGraph: {
images: [
@@ -154,7 +161,7 @@ export async function generateMetadata({
],
},
robots:
isSpaceIndexable({ space, parent }) && isPageIndexable(ancestors, page)
isSpaceIndexable({ space, site: site ?? null }) && isPageIndexable(ancestors, page)
? 'index, follow'
: 'noindex, nofollow',
};
@@ -1,6 +1,7 @@
import { CustomizationThemeMode } from '@gitbook/api';
import { Metadata, Viewport } from 'next';
import { headers } from 'next/headers';
import { NuqsAdapter } from 'nuqs/adapters/next/app';
import React from 'react';
import * as ReactDOM from 'react-dom';
@@ -18,7 +19,7 @@ import { getContentTitle } from '@/lib/utils';
import { ClientContexts } from './ClientContexts';
import { RocketLoaderDetector } from './RocketLoaderDetector';
import { fetchSpaceData } from '../fetch';
import { fetchContentData } from '../fetch';
export const runtime = 'edge';
@@ -35,13 +36,14 @@ export default async function ContentLayout(props: { children: React.ReactNode }
contentTarget,
customization,
pages,
parent,
site,
spaces,
ancestors,
scripts,
} = await fetchSpaceData();
sections,
} = await fetchContentData();
ReactDOM.preconnect(api().endpoint);
ReactDOM.preconnect(api().client.endpoint);
if (assetsDomain) {
ReactDOM.preconnect(assetsDomain);
}
@@ -54,50 +56,53 @@ export default async function ContentLayout(props: { children: React.ReactNode }
});
return (
<ClientContexts
nonce={nonce}
forcedTheme={
getQueryStringTheme() ??
(customization.themes.toggeable ? undefined : customization.themes.default)
}
>
<SpaceLayout
space={space}
contentTarget={contentTarget}
parent={parent}
spaces={spaces}
customization={customization}
pages={pages}
ancestors={ancestors}
content={content}
<NuqsAdapter>
<ClientContexts
nonce={nonce}
forcedTheme={
getQueryStringTheme() ??
(customization.themes.toggeable ? undefined : customization.themes.default)
}
>
{children}
</SpaceLayout>
<SpaceLayout
space={space}
contentTarget={contentTarget}
site={site}
spaces={spaces}
sections={sections}
customization={customization}
pages={pages}
ancestors={ancestors}
content={content}
>
{children}
</SpaceLayout>
{scripts.length > 0 ? (
<>
<LoadIntegrations />
{scripts.map(({ script }) => (
<script key={script} async src={script} nonce={nonce} />
))}
</>
) : null}
{scripts.length > 0 ? (
<>
<LoadIntegrations />
{scripts.map(({ script }) => (
<script key={script} async src={script} nonce={nonce} />
))}
</>
) : null}
{scripts.some((script) => script.cookies) || customization.privacyPolicy.url ? (
<React.Suspense fallback={null}>
<CookiesToast privacyPolicy={customization.privacyPolicy.url} />
</React.Suspense>
) : null}
{scripts.some((script) => script.cookies) || customization.privacyPolicy.url ? (
<React.Suspense fallback={null}>
<CookiesToast privacyPolicy={customization.privacyPolicy.url} />
</React.Suspense>
) : null}
<RocketLoaderDetector nonce={nonce} />
<RocketLoaderDetector nonce={nonce} />
<AdminToolbar space={space} content={content} />
</ClientContexts>
<AdminToolbar space={space} content={content} />
</ClientContexts>
</NuqsAdapter>
);
}
export async function generateViewport(): Promise<Viewport> {
const { customization } = await fetchSpaceData();
const { customization } = await fetchContentData();
return {
colorScheme: customization.themes.toggeable
? customization.themes.default === CustomizationThemeMode.Dark
@@ -108,11 +113,11 @@ export async function generateViewport(): Promise<Viewport> {
}
export async function generateMetadata(): Promise<Metadata> {
const { space, parent, customization } = await fetchSpaceData();
const { space, site, customization } = await fetchContentData();
const customIcon = 'icon' in customization.favicon ? customization.favicon.icon : null;
return {
title: getContentTitle(space, customization, parent),
title: getContentTitle(space, customization, site),
generator: `GitBook (${buildVersion()})`,
metadataBase: new URL(baseUrl()),
icons: {
@@ -133,7 +138,7 @@ export async function generateMetadata(): Promise<Metadata> {
},
],
},
robots: isSpaceIndexable({ space, parent }) ? 'index, follow' : 'noindex, nofollow',
robots: isSpaceIndexable({ space, site }) ? 'index, follow' : 'noindex, nofollow',
};
}
@@ -0,0 +1,34 @@
import { NextRequest } from 'next/server';
import { getSpace, getSite } from '@/lib/api';
import { absoluteHref } from '@/lib/links';
import { getSiteContentPointer } from '@/lib/pointer';
import { isSpaceIndexable } from '@/lib/seo';
export const runtime = 'edge';
/**
* Generate a robots.txt for the current space.
*/
export async function GET(req: NextRequest) {
const pointer = getSiteContentPointer();
const [site, space] = await Promise.all([
getSite(pointer.organizationId, pointer.siteId),
getSpace(pointer.spaceId, pointer.siteShareKey),
]);
const lines = [
`User-agent: *`,
'Disallow: /~gitbook/',
...(isSpaceIndexable({ space, site })
? [`Allow: /`, `Sitemap: ${absoluteHref(`/sitemap.xml`, true)}`]
: [`Disallow: /`]),
];
const content = lines.join('\n');
return new Response(content, {
headers: {
'Content-Type': 'text/plain',
},
});
}
@@ -5,21 +5,17 @@ import { NextRequest } from 'next/server';
import { getSpaceContentData } from '@/lib/api';
import { absoluteHref } from '@/lib/links';
import { getPagePath } from '@/lib/pages';
import { getSiteContentPointer } from '@/lib/pointer';
import { isPageIndexable } from '@/lib/seo';
import { getContentPointer } from '../../fetch';
export const runtime = 'edge';
/**
* Generate a sitemap.xml for the current space.
*/
export async function GET(req: NextRequest) {
const pointer = getContentPointer();
const { pages: rootPages } = await getSpaceContentData(
pointer,
'siteId' in pointer ? pointer.siteShareKey : undefined,
);
const pointer = getSiteContentPointer();
const { pages: rootPages } = await getSpaceContentData(pointer, pointer.siteShareKey);
const pages = flattenPages(rootPages, (page) => !page.hidden && isPageIndexable([], page));
const urls = pages.map(({ page, depth }) => {
@@ -88,10 +84,12 @@ function flattenPages(
return [
...(page.type === 'document' ? [{ page, depth }] : []),
...page.pages.flatMap((child) =>
child.type === 'link' ? [] : flattenPage(child, depth + 1),
child.type === 'document' ? flattenPage(child, depth + 1) : [],
),
];
};
return rootPags.flatMap((page) => (page.type === 'link' ? [] : flattenPage(page, 0)));
return rootPags.flatMap((page) =>
page.type === 'group' || page.type === 'document' ? flattenPage(page, 0) : [],
);
}
@@ -1,18 +1,11 @@
import { ContentVisibility } from '@gitbook/api';
import { notFound } from 'next/navigation';
import { ImageResponse } from 'next/og';
import { NextRequest } from 'next/server';
import React from 'react';
import { getContentPointer } from '@/app/(space)/fetch';
import {
getCollection,
getCurrentSiteCustomization,
getSite,
getSpace,
getSpaceCustomization,
} from '@/lib/api';
import { getSite, getSiteData, getSpace } from '@/lib/api';
import { getEmojiForCode } from '@/lib/emojis';
import { getSiteContentPointer } from '@/lib/pointer';
import { tcls } from '@/lib/tailwind';
import { getContentTitle } from '@/lib/utils';
@@ -42,20 +35,15 @@ export async function GET(req: NextRequest) {
const options = getOptions(req.url);
const size = SIZES[options.size];
const pointer = getContentPointer();
const pointer = getSiteContentPointer();
const spaceId = pointer.spaceId;
const [space, customization] = await Promise.all([
getSpace(spaceId, 'siteId' in pointer ? pointer.siteShareKey : undefined),
'siteId' in pointer ? getCurrentSiteCustomization(pointer) : getSpaceCustomization(spaceId),
const [space, { customization }] = await Promise.all([
getSpace(spaceId, pointer.siteShareKey),
getSiteData(pointer),
]);
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);
const site = await getSite(pointer.organizationId, pointer.siteId);
const contentTitle = getContentTitle(space, customization, site);
return new ImageResponse(
(
@@ -0,0 +1,223 @@
import { CustomizationHeaderPreset } from '@gitbook/api';
import { redirect } from 'next/navigation';
import { ImageResponse } from 'next/og';
import { NextRequest } from 'next/server';
import colorContrast from 'postcss-color-contrast/js';
import React from 'react';
import { absoluteHref } from '@/lib/links';
import { tcls } from '@/lib/tailwind';
import { getContentTitle } from '@/lib/utils';
import { PageIdParams, fetchPageData } from '../../../../fetch';
export const runtime = 'edge';
/**
* Render the OpenGraph image for a space.
*/
export async function GET(req: NextRequest, { params }: { params: PageIdParams }) {
const { space, page, customization, site } = await fetchPageData(params);
if (customization.socialPreview.url) {
// If user configured a custom social preview, we redirect to it.
redirect(customization.socialPreview.url);
}
// TODO: Support all fonts available in GitBook
// Right now this is impossible since next/font/google does not expose the cached font file
// Another option would be to use the Satori prop `loadAdditionalAsset` [example](https://github.com/vercel/satori/blob/main/playground/pages/index.tsx),
// but this prop isn't (yet) exposed through `ImageResponse`.
const interRegular = await fetch(
new URL('../../../../../../fonts/Inter/Inter-Regular.ttf', import.meta.url),
).then((res) => res.arrayBuffer());
const interBold = await fetch(
new URL('../../../../../../fonts/Inter/Inter-Bold.ttf', import.meta.url),
).then((res) => res.arrayBuffer());
const theme = customization.themes.default;
const useLightTheme = theme === 'light';
// We have no access to CSS variables, so we'll have to hardcode some values
const baseColors = {
light: '#ffffff',
dark: '#111827',
};
let colors = {
background: baseColors[theme],
gradient: customization.styling.primaryColor[theme],
title: customization.styling.primaryColor[theme],
body: baseColors[useLightTheme ? 'dark' : 'light'], // Invert text on background
};
const gridWhite = absoluteHref('~gitbook/static/images/ogimage-grid-white.png', true);
const gridBlack = absoluteHref('~gitbook/static/images/ogimage-grid-black.png', true);
let gridAsset = useLightTheme ? gridBlack : gridWhite;
switch (customization.header.preset) {
case CustomizationHeaderPreset.Custom:
colors = {
background: customization.header.backgroundColor?.[theme] || colors.background,
gradient: customization.header.linkColor?.[theme] || colors.gradient,
title: customization.header.linkColor?.[theme] || colors.title,
body: colorContrast(
customization.header.backgroundColor?.[theme] || colors.background,
[baseColors.light, baseColors.dark],
),
};
gridAsset = colors.body == baseColors.light ? gridWhite : gridBlack;
break;
case CustomizationHeaderPreset.Bold:
colors = {
background: customization.styling.primaryColor[theme],
gradient: colorContrast(customization.styling.primaryColor[theme], [
baseColors.light,
baseColors.dark,
]),
title: colorContrast(customization.styling.primaryColor[theme], [
baseColors.light,
baseColors.dark,
]),
body: colorContrast(customization.styling.primaryColor[theme], [
baseColors.light,
baseColors.dark,
]),
};
gridAsset = colors.body == baseColors.light ? gridWhite : gridBlack;
break;
}
const favicon = function () {
if ('icon' in customization.favicon)
return (
<img
src={customization.favicon.icon[theme]}
width={40}
height={40}
tw={tcls('mr-4')}
alt="Icon"
/>
);
if ('emoji' in customization.favicon)
return (
<span tw={tcls('text-4xl', 'mr-4')}>
{String.fromCodePoint(parseInt('0x' + customization.favicon.emoji))}
</span>
);
return (
<img
src={absoluteHref(
`~gitbook/icon?size=medium&theme=${customization.themes.default}`,
true,
)}
alt="Icon"
width={40}
height={40}
tw={tcls('mr-4')}
/>
);
};
return new ImageResponse(
(
<div
tw={tcls(
'justify-between',
'p-20',
'relative',
'w-full',
'h-full',
'flex',
'flex-col',
`bg-[${colors.background}]`,
`text-[${colors.body}]`,
)}
>
{/* Gradient */}
<div
tw={tcls('absolute', 'inset-0')}
style={{
backgroundImage: `radial-gradient(ellipse 100% 100% at top right , ${colors.gradient}, ${colors.gradient}00)`,
opacity: 0.5,
}}
></div>
{/* Grid */}
<img
tw={tcls('absolute', 'inset-0', 'w-[100vw]', 'h-[100vh]')}
src={gridAsset}
alt="Grid"
/>
{/* Logo */}
{customization.header.logo ? (
<img
alt="Logo"
height={60}
src={
useLightTheme
? customization.header.logo.light
: customization.header.logo.dark
}
/>
) : (
<div tw={tcls('flex')}>
{favicon()}
<h3 tw={tcls('text-4xl', 'my-0')}>
{getContentTitle(space, customization, site ?? null)}
</h3>
</div>
)}
{/* Title and description */}
<div tw={tcls('flex', 'flex-col')}>
<h1
tw={tcls(
'text-8xl',
'my-0',
'tracking-tight',
'leading-none',
'text-left',
`text-[${colors.title}]`,
'font-bold',
)}
>
{page
? page.title.length > 64
? page.title.slice(0, 64) + '...'
: page.title
: 'Not found'}
</h1>
{page?.description && page?.title.length <= 64 ? (
<h2 tw={tcls('text-4xl', 'mb-0', 'mt-8', 'w-[75%]', 'font-normal')}>
{page.description.length > 164
? page.description.slice(0, 164) + '...'
: page.description}
</h2>
) : null}
</div>
</div>
),
{
width: 1200,
height: 630,
fonts: [
{
name: 'Inter',
data: interRegular,
weight: 400,
style: 'normal',
},
{
name: 'Inter',
data: interBold,
weight: 700,
style: 'normal',
},
],
},
);
}
+145
View File
@@ -0,0 +1,145 @@
import { RevisionPage } from '@gitbook/api';
import { redirect } from 'next/navigation';
import {
getRevisionPageByPath,
getDocument,
getSpaceContentData,
getSiteData,
getSiteRedirectBySource,
} from '@/lib/api';
import { resolvePagePath, resolvePageId } from '@/lib/pages';
import { getSiteContentPointer } from '@/lib/pointer';
export interface PagePathParams {
pathname?: string[];
}
export interface PageIdParams {
pageId: string;
}
/**
* Fetch all the data needed to render the content layout.
*/
export async function fetchContentData() {
const content = getSiteContentPointer();
const [{ space, contentTarget, pages }, { customization, site, sections, spaces, scripts }] =
await Promise.all([
getSpaceContentData(content, content.siteShareKey),
getSiteData(content),
]);
// we grab the space attached to the parent as it contains overriden customizations
const spaceRelativeToParent = spaces?.find((space) => space.id === content.spaceId);
return {
content,
contentTarget,
space: spaceRelativeToParent ?? space,
pages,
site,
sections,
spaces,
shareKey: content.siteShareKey,
customization,
scripts,
ancestors: [],
};
}
/**
* Fetch all the data needed to render the content.
* Optimized to fetch in parallel as much as possible.
*/
export async function fetchPageData(params: PagePathParams | PageIdParams) {
const contentData = await fetchContentData();
const page = await resolvePage({
organizationId: contentData.space.organization,
siteId: contentData.site.id,
spaceId: contentData.contentTarget.spaceId,
revisionId: contentData.contentTarget.revisionId,
pages: contentData.pages,
shareKey: contentData.shareKey,
params,
});
const document = page?.page.documentId
? await getDocument(contentData.space.id, page.page.documentId)
: null;
return {
...contentData,
...page,
document,
};
}
/**
* Resolve a page from the params.
* If the path can't be found, we try to resolve it from the API to handle redirects.
*/
async function resolvePage(input: {
organizationId: string;
siteId: string;
spaceId: string;
revisionId: string;
shareKey: string | undefined;
pages: RevisionPage[];
params: PagePathParams | PageIdParams;
}) {
const { organizationId, siteId, spaceId, revisionId, pages, shareKey, params } = input;
if ('pageId' in params) {
return resolvePageId(pages, params.pageId);
}
const rawPathname = getPathnameParam(params);
const pathname = normalizePathname(rawPathname);
// When resolving a page, we use the lowercased pathname
const page = resolvePagePath(pages, pathname);
if (page) {
return page;
}
// We don't test path that are too long as GitBook doesn't support them and will return a 404 anyway.
if (rawPathname.length <= 512) {
// If page can't be found, we try with the API, in case we have a redirect at space level.
// We use the raw pathname to handle special/malformed redirects setup by users in the GitSync.
// The page rendering will take care of redirecting to a normalized pathname.
const resolved = await getRevisionPageByPath(spaceId, revisionId, rawPathname);
if (resolved) {
return resolvePageId(pages, resolved.id);
}
// If a page still can't be found, we try with the API, in case we have a redirect at site level.
const resolvedSiteRedirect = await getSiteRedirectBySource({
organizationId,
siteId,
source: rawPathname.startsWith('/') ? rawPathname : `/${rawPathname}`,
siteShareKey: input.shareKey,
});
if (resolvedSiteRedirect) {
return redirect(resolvedSiteRedirect.target);
}
}
return undefined;
}
/**
* Get the page path from the params.
*/
export function getPathnameParam(params: PagePathParams): string {
const { pathname } = params;
return pathname ? pathname.map((part) => decodeURIComponent(part)).join('/') : '';
}
/**
* Normalize the URL pathname into the format used in the revision page path.
*/
export function normalizePathname(pathname: string) {
return pathname.toLowerCase();
}
@@ -0,0 +1,18 @@
import { CustomizationRootLayout } from '@/components/RootLayout';
import { getSiteData } from '@/lib/api';
import { getSiteContentPointer } from '@/lib/pointer';
/**
* Layout to be used for the site root. It fetches the customization data for the site
* and initializes the CustomizationRootLayout with it.
*/
export default async function SiteRootLayout(props: { children: React.ReactNode }) {
const { children } = props;
const pointer = getSiteContentPointer();
const { customization } = await getSiteData(pointer);
return (
<CustomizationRootLayout customization={customization}>{children}</CustomizationRootLayout>
);
}
@@ -1,42 +0,0 @@
import { ContentVisibility } from '@gitbook/api';
import { NextRequest } from 'next/server';
import { getCollection, getSite, getSpace } from '@/lib/api';
import { absoluteHref } from '@/lib/links';
import { isSpaceIndexable } from '@/lib/seo';
import { getContentPointer } from '../../fetch';
export const runtime = 'edge';
/**
* Generate a robots.txt for the current space.
*/
export async function GET(req: NextRequest) {
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/',
...(isSpaceIndexable({ space, parent })
? [`Allow: /`, `Sitemap: ${absoluteHref(`/sitemap.xml`, true)}`]
: [`Disallow: /`]),
];
const content = lines.join('\n');
return new Response(content, {
headers: {
'Content-Type': 'text/plain',
},
});
}
@@ -1,53 +0,0 @@
import { redirect } from 'next/navigation';
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';
/**
* Render the OpenGraph image for a space.
*/
export async function GET(req: NextRequest, { params }: { params: PageIdParams }) {
const { space, page, customization, parent } = await fetchPageData(params);
const url = new URL(space.urls.published ?? space.urls.app);
if (customization.socialPreview.url) {
// If user configured a custom social preview, we redirect to it.
redirect(customization.socialPreview.url);
}
return new ImageResponse(
(
<div
tw="bg-gray-50 py-16 px-14"
style={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
}}
>
<h2 tw="text-7xl font-bold tracking-tight text-left">
{getContentTitle(space, customization, parent)}
</h2>
<div tw="flex flex-1">
<p tw="text-4xl">{page ? page.title : 'Not found'}</p>
</div>
<div tw="flex">
<p tw="text-4xl">
{url.hostname + (url.pathname.length > 1 ? url.pathname : '')}
</p>
</div>
</div>
),
{
width: 1200,
height: 630,
},
);
}
-244
View File
@@ -1,244 +0,0 @@
import { ContentVisibility, RevisionPage, Space } from '@gitbook/api';
import { headers } from 'next/headers';
import {
getCollectionSpaces,
getCollection,
ContentPointer,
getRevisionPageByPath,
getDocument,
getSpaceData,
ContentTarget,
SiteContentPointer,
getCurrentSiteData,
getSite,
getSiteSpaces,
getCurrentSiteCustomization,
} from '@/lib/api';
import { resolvePagePath, resolvePageId } from '@/lib/pages';
export interface PagePathParams {
pathname?: string[];
}
export interface PageIdParams {
pageId: string;
}
/**
* Get the current content pointer from the params.
*/
export function getContentPointer(): ContentPointer | SiteContentPointer {
const headerSet = headers();
const spaceId = headerSet.get('x-gitbook-content-space');
if (!spaceId) {
throw new Error(
'getContentPointer is called outside the scope of a request processed by the middleware',
);
}
const siteId = headerSet.get('x-gitbook-content-site');
if (siteId) {
const organizationId = headerSet.get('x-gitbook-content-organization');
const siteSpaceId = headerSet.get('x-gitbook-content-site-space');
const siteShareKey = headerSet.get('x-gitbook-content-site-share-key');
if (!organizationId) {
throw new Error('Missing site content headers');
}
const siteContent: SiteContentPointer = {
siteId,
spaceId,
siteSpaceId: siteSpaceId ?? undefined,
siteShareKey: siteShareKey ?? undefined,
organizationId,
revisionId: headerSet.get('x-gitbook-content-revision') ?? undefined,
changeRequestId: headerSet.get('x-gitbook-content-changerequest') ?? undefined,
};
return siteContent;
} else {
const content: ContentPointer = {
spaceId,
revisionId: headerSet.get('x-gitbook-content-revision') ?? undefined,
changeRequestId: headerSet.get('x-gitbook-content-changerequest') ?? undefined,
};
return content;
}
}
/**
* Fetch all the data needed to render the space layout.
*/
export async function fetchSpaceData() {
const content = getContentPointer();
const siteShareKey = 'siteId' in content ? content.siteShareKey : undefined;
const [{ space, contentTarget, pages, customization, scripts }, parentSite] = await Promise.all(
'siteId' in content
? [
getCurrentSiteData(content),
fetchParentSite({
organizationId: content.organizationId,
siteId: content.siteId,
siteShareKey,
}),
]
: [getSpaceData(content, siteShareKey)],
);
const parent = await (parentSite ?? fetchParentCollection(space));
// we grab the space attached to the parent as it contains overriden customizations
const spaceRelativeToParent = parent?.spaces.find((space) => space.id === content.spaceId);
return {
content,
contentTarget,
space: spaceRelativeToParent ?? space,
pages,
customization,
scripts,
ancestors: [],
...parent,
};
}
/**
* Fetch all the data needed to render the content.
* Optimized to fetch in parallel as much as possible.
*/
export async function fetchPageData(params: PagePathParams | PageIdParams) {
const content = getContentPointer();
const siteShareKey = 'siteId' in content ? content.siteShareKey : undefined;
const { space, contentTarget, pages, customization, scripts } = await ('siteId' in content
? getCurrentSiteData(content)
: getSpaceData(content, siteShareKey));
const page = await resolvePage(contentTarget, pages, params);
const [parent, document] = await Promise.all([
'siteId' in content
? fetchParentSite({
organizationId: content.organizationId,
siteId: content.siteId,
siteShareKey: content.siteShareKey,
})
: fetchParentCollection(space),
page?.page.documentId ? getDocument(space.id, page.page.documentId) : null,
]);
return {
content,
contentTarget,
space,
pages,
customization,
scripts,
ancestors: [],
...page,
...parent,
document,
};
}
/**
* Resolve a page from the params.
* If the path can't be found, we try to resolve it from the API to handle redirects.
*/
async function resolvePage(
contentTarget: ContentTarget,
pages: RevisionPage[],
params: PagePathParams | PageIdParams,
) {
if ('pageId' in params) {
return resolvePageId(pages, params.pageId);
}
const rawPathname = getPathnameParam(params);
const pathname = normalizePathname(rawPathname);
// When resolving a page, we use the lowercased pathname
const page = resolvePagePath(pages, pathname);
if (page) {
return page;
}
// If page can't be found, we try with the API, in case we have a redirect
// We use the raw pathname to handle special/malformed redirects setup by users in the GitSync.
// The page rendering will take care of redirecting to a normalized pathname.
//
// We don't test path that are too long as GitBook doesn't support them and will return a 404 anyway.
if (rawPathname.length <= 512) {
const resolved = await getRevisionPageByPath(
contentTarget.spaceId,
contentTarget.revisionId,
rawPathname,
);
if (resolved) {
return resolvePageId(pages, resolved.id);
}
}
return undefined;
}
async function fetchParentCollection(space: Space) {
const parentCollectionId =
space.visibility === ContentVisibility.InCollection ? space.parent : undefined;
const [collection, spaces] = await Promise.all([
parentCollectionId ? getCollection(parentCollectionId) : null,
parentCollectionId ? getCollectionSpaces(parentCollectionId) : ([] as Space[]),
]);
return { parent: collection, spaces };
}
async function fetchParentSite(args: {
organizationId: string;
siteId: string;
siteShareKey: string | undefined;
}) {
const { organizationId, siteId, siteShareKey } = args;
const [site, siteSpaces, siteParentCustomizations] = await Promise.all([
getSite(organizationId, siteId),
getSiteSpaces({ organizationId, siteId, siteShareKey }),
getCurrentSiteCustomization({ organizationId, siteId, siteSpaceId: undefined }),
]);
const spaces: Record<string, Space> = {};
siteSpaces.forEach((siteSpace) => {
spaces[siteSpace.space.id] = {
...siteSpace.space,
title: siteSpace.title ?? siteSpace.space.title,
urls: {
...siteSpace.space.urls,
published: siteSpace.urls.published,
},
};
});
// override the title with the customization title
const parent = {
...site,
...(siteParentCustomizations?.title ? { title: siteParentCustomizations.title } : {}),
};
return {
parent,
spaces: Object.values(spaces),
};
}
/**
* Get the page path from the params.
*/
export function getPathnameParam(params: PagePathParams): string {
const { pathname } = params;
return pathname ? pathname.map((part) => decodeURIComponent(part)).join('/') : '';
}
/**
* Normalize the URL pathname into the format used in the revision page path.
*/
export function normalizePathname(pathname: string) {
return pathname.toLowerCase();
}

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