Compare commits

...

191 Commits

Author SHA1 Message Date
Samy Pessé 0df3849ea2 Version Packages (#3157)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-18 13:40:34 +02:00
Greg Bergé 8ed1bda2f2 Translate OpenAPI blocks (#3166) 2025-04-18 13:29:55 +02:00
Nolann B. d67699a1f1 Add OpenAPI Webhook block (#3160) 2025-04-18 08:35:44 +02:00
Greg Bergé 3363a18856 Merge simple alternatives (#3165) 2025-04-16 09:14:12 +00:00
Amrit Kahlon ad1dc0b914 Bump scalar version (#3161)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2025-04-15 18:00:07 -07:00
Samy Pessé e4c7307c99 Fix CORS error when loading fonts in 2c (#3163) 2025-04-14 17:05:31 -07:00
Samy Pessé f9c942c8ff Fix CORS error when loading icons in 2c (#3162) 2025-04-14 16:52:39 -07:00
Greg Bergé eeb977fc03 Fix Python JSON code example (#3159) 2025-04-14 19:50:40 +00:00
Greg Bergé 4b8a62122a Show sections tabs only if there is at least two sections (#3158) 2025-04-14 14:09:34 +02:00
Addison e44e947fc1 Update cookie banner to use American English (#3154) 2025-04-14 18:43:01 +09:00
Nolann B. 7588cfe1c5 Improve OpenAPIResponses examples and schemas (#3123) 2025-04-14 11:16:35 +02:00
Greg Bergé a9a643f222 Improve icon stabilization (#3147) 2025-04-13 05:41:47 +00:00
Samy Pessé 77d6cb75be Improve error message when image resizing failed (#3148) 2025-04-12 17:44:23 -07:00
Samy Pessé 1942d221df Bump the cache of getPublishedContentByUrl in v1 (#3146) 2025-04-12 07:18:26 -07:00
Greg Bergé 041601ed65 Upgrade Argos (#3145) 2025-04-12 15:48:51 +02:00
Steven H 5567281a85 Bump getPublishedContentByUrl cache. (#3144) 2025-04-11 17:13:01 +01:00
Samy Pessé 4c3753046b Cloudflare: fix image resizing (#3143) 2025-04-11 16:48:43 +02:00
Samy Pessé 89f8e16184 Cloudflare: resize images in v2 using direct fetch (#3141) 2025-04-11 15:18:44 +02:00
Samy Pessé a3e1125ced Hotfix for v1 crashes (#3139) 2025-04-10 22:00:41 +02:00
Samy Pessé 4dae8acfed Cloudflare: fix image resizing (#3138) 2025-04-10 19:30:15 +02:00
Samy Pessé bc081c4344 Fix cache revalidation for computed revisions (#3134) 2025-04-10 16:36:02 +02:00
Samy Pessé e4275b235d Version Packages (#3137)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-10 13:31:27 +02:00
Samy Pessé 77397cac82 Fix version of @gitbook/api referenced in cache-tags (#3136) 2025-04-10 13:30:28 +02:00
Samy Pessé b6a08bc679 Version Packages (#3084)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-10 13:18:22 +02:00
Samy Pessé 116575c811 Changeset for cache-tags (#3135) 2025-04-10 13:16:01 +02:00
Samy Pessé 4ba7bd5ce1 CF Preview URL: Extract worker URL and run visual tests (#3132) 2025-04-09 18:25:37 +02:00
Samy Pessé 223ab5377f Test cloudflare preview URL (#3130) 2025-04-09 17:24:38 +02:00
Samy Pessé ae43ca146c Fix preview URLs on Cloudflare env (#3129) 2025-04-09 16:00:21 +02:00
Taran Vohra 42342893d6 Fix incoming URL for requests that were proxied (#3127) 2025-04-09 19:10:14 +05:30
Nolann B. eedefdd5c9 Handle optional security headers (#3128) 2025-04-09 15:07:33 +02:00
Samy Pessé f5e152dd74 Fix hovering links in docs.gitbook.com (#3125) 2025-04-09 11:43:12 +02:00
Zeno Kapitein 95ea22d9c5 Cache AI Page Link summary (#3106)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2025-04-09 09:59:35 +02:00
Taran Vohra f23023ddfa Use the proxy request identifier for storing the VA cookie in proxy sites (#3091) 2025-04-09 12:49:22 +05:30
Samy Pessé da74077954 Remove proxy package (#3124) 2025-04-08 23:58:19 +02:00
Nolann B. 29aaba5949 Override Scalar's overscroll-behavior (#3120) 2025-04-08 19:16:46 +02:00
Zeno Kapitein daf41fc728 Footer design tweaks (#3109) 2025-04-08 18:44:57 +02:00
Zeno Kapitein b08eb1af59 Fix list of variants overflow (#3114) 2025-04-08 18:44:44 +02:00
Zeno Kapitein 8322cd3e35 Fix sections being displayed when there is only 1 (#3113) 2025-04-08 18:44:08 +02:00
Samy Pessé 444039c142 Fix opening of links when v1 is embedded in an iframe (#3122) 2025-04-08 18:43:06 +02:00
Samy Pessé 3b0eff7f71 Fix getRevisionPageByPath in v2 not encoding the path correctly (#3121) 2025-04-08 16:29:08 +00:00
Samy Pessé c765463e38 Fix ogimage generation crashing when site is using a custom WOFF2 font (#3119) 2025-04-08 16:44:28 +02:00
Samy Pessé e8ca25b67e Fix caching in Cloudflare version using R2 as a backend (#3118) 2025-04-08 13:37:07 +02:00
Brett Jephson 528eee3fc6 RND-548: add subscript and superscript support (#3110) 2025-04-08 11:32:12 +01:00
Steven H 168a4fa998 Add support for buttons to GitBook. (#3117) 2025-04-08 09:18:29 +01:00
Samy Pessé 3ea75adba4 Update turbo to 2.5.0 and fix cache config (#3116) 2025-04-08 09:56:05 +02:00
Nolann B. cbd768a095 Improve OpenAPI codesample (#3090) 2025-04-08 09:35:22 +02:00
Samy Pessé 2d01653a78 Fix ogimage on Cloudflare and resizing of ads image (#3107) 2025-04-08 01:29:34 +02:00
Nolann B. aa3357a57d Fix OpenAPISchemas description padding (#3115) 2025-04-07 21:54:33 +02:00
Samy Pessé de53946896 Sanitize the back URL used when rendering PDF (#3111) 2025-04-07 17:03:09 +02:00
Samy Pessé d51204148e Revert debugging of ogimage errors (#3105) 2025-04-06 13:33:48 +01:00
Samy Pessé 319761a39e Fix revalidation not working because of memoization (#3104) 2025-04-06 01:27:22 +02:00
Samy Pessé 7fbf01a348 Update opennext to 1.0.0-beta.0 (#3102) 2025-04-05 23:51:12 +02:00
Steven Hall c22490bd80 Prefer @vercel/og for icon generation. 2025-04-05 09:22:13 +01:00
Steven Hall 860fd6d02e Use system-ui font in icons. 2025-04-05 09:12:20 +01:00
Steven Hall c43c524804 Fix a typing issue using ArrayBuffer.detached 2025-04-05 09:06:17 +01:00
Steven H 66bb938d85 Use Inter font when generating icons to prevent ArrayBuffer re-use. (#3101) 2025-04-05 09:01:41 +01:00
Steven H 0e6d928496 Use Inter font when generating icons to prevent ArrayBuffer re-use. (#3100) 2025-04-05 08:47:56 +01:00
Steven H f07982d9f8 Investigate issue where icons and ogimage would occasionally fail. (#3099) 2025-04-05 08:15:02 +01:00
Steven H b92ecfad58 Implement retry logic for the DO cache to prevent when revalidating content. (#3098) 2025-04-04 23:44:32 +01:00
Zeno Kapitein 416bde70bc Link tooltips + AI link summaries (#3088)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2025-04-04 20:56:42 +02:00
Steven H 580f7ade15 Better debugging of GitBook v1s revalidate endpoint. (#3097) 2025-04-04 17:36:24 +01:00
Zeno Kapitein cdffd7c47b Desaturate text colours by decreasing chroma for the last steps of the color scale (#3096) 2025-04-04 18:00:01 +02:00
Greg Bergé 5a959cf041 Fix a regression on OpenAPI example bg (#3095) 2025-04-04 15:05:53 +00:00
Zeno Kapitein 31466546a2 Change overlay close icon to close (#3089) 2025-04-04 16:42:47 +02:00
Steven H 90ead98eaa Better error handling in revalidate v1 (#3094) 2025-04-04 14:59:14 +01:00
Samy Pessé 65e9cb9d5c Update next to "canary" for v2 (#3093) 2025-04-04 14:39:43 +02:00
Greg Bergé e59076a074 Improve OpenAPI schemas block ungrouped style (#3092) 2025-04-04 14:28:44 +02:00
Nolann B. 653920dec9 Add tests for generateSchemaExample (#3086) 2025-04-03 19:42:40 +02:00
Greg Bergé 23cedd2d8b Hide deprecated properties in examples (#3085) 2025-04-03 19:40:52 +02:00
Nolann B. 70c4182fd8 Improve OpenAPI schema style (#3079) 2025-04-03 16:10:20 +00:00
Samy Pessé b62b101065 Do not use insights cookie if disabled (#3087) 2025-04-03 17:30:43 +02:00
Zeno Kapitein b9c929beeb Update page breadcrumb color (for links-accent crumbs) (#3082) 2025-04-03 16:34:14 +02:00
Nolann B. 2b6c593289 Remove stable from x-stability (#3083) 2025-04-03 11:33:39 +02:00
Samy Pessé c0b339e406 Version Packages (#3075)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-02 13:36:04 +02:00
Nolann B. da485f5144 Fix read-only in generateSchemaExample (#3069) 2025-04-02 13:03:51 +02:00
Nolann B. 139a8050a5 Fix OpenAPI enum display (#3077) 2025-04-02 10:15:19 +02:00
Nolann B. da7b369ffd Fix missing headers in OpenAPIResponses (#3074) 2025-04-01 21:57:36 +02:00
Samy Pessé 432da64919 Version Packages (#3073)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-01 18:13:52 +02:00
Nolann B. 7d0b422200 Handle grouped OpenAPISchemas (#3071) 2025-04-01 17:24:43 +02:00
Zeno Kapitein fb90eb0acc Limit tinted background on bold theme to sites with filled sidebar (#3072) 2025-04-01 16:43:08 +02:00
Samy Pessé b647538e41 Version Packages (#3033)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-01 16:35:09 +02:00
Samy Pessé e50797a5c1 Adapt new computed content and add tag for translation (#3070) 2025-04-01 15:10:55 +02:00
Nolann B. 5b1e01c786 Support for x-stability property (#3064) 2025-03-31 13:49:37 +02:00
Nolann B. 813b2af06b Support for x-enumDescriptions and x-gitbook-enum (#3066) 2025-03-31 12:18:57 +02:00
Nolann B. 6f71da88d8 Fix padding in OpenAPI schemas (#3067) 2025-03-31 12:03:53 +02:00
Samy Pessé fa91eb7eaf Fix dark mode PDF (#3065) 2025-03-28 18:00:33 +01:00
Zeno Kapitein c756761591 Add breadcrumbs to search results (#2772) 2025-03-28 13:29:05 +01:00
Samy Pessé 434af90366 Fix image resizing when using proxy by passing signing identifier in site URL data (#3063)
Co-authored-by: Taran Vohra <taranvohra@outlook.com>
2025-03-28 12:28:12 +01:00
Valentino Hudhra 0f41e194cb Fix dismissed annoucement banner ignored when switching sites (#3057) 2025-03-28 10:56:18 +01:00
Nolann B. bd353489e9 Fix missing alternative schemas (#3058) 2025-03-28 06:53:28 +01:00
Samy Pessé 77fd393f15 Fix click on search items when embedded in an iframe and track announcement links (#3061) 2025-03-27 20:13:14 +01:00
Taran Vohra 8701b5e078 Use the site canonical URL when rewriting in the middleware (#3060) 2025-03-27 19:39:39 +01:00
Nolann B. 61db166f92 Add OpenAPI write-only indicator (#3059) 2025-03-27 18:21:38 +01:00
Zeno Kapitein 0d9b9cea0f Fix announcement banner margin (#3056) 2025-03-27 15:59:47 +00:00
Nolann B. 7bb37c7f0a Move filterSelectedOpenAPISchemas to @gitbook/openapi-parser (#3054) 2025-03-27 15:53:41 +01:00
Taran Vohra 5b2bf828e3 Use stable site URL data for route rewrite in the middleware (#3053) 2025-03-27 18:26:06 +05:30
Samy Pessé c85d3f9752 Serve a markdown version of the page when adding a .md extension (#3055)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-03-27 12:35:18 +00:00
Zeno Kapitein 4d7088bbf0 Stop variant selector from growing too large (#3043) 2025-03-27 10:42:18 +01:00
Samy Pessé dd6ab6044c Fix crash when using user mention or accessing invalid change requests (#3051) 2025-03-27 01:04:00 +01:00
Samy Pessé d236bf029c Fix flash for site with dark default mode (#3050) 2025-03-26 18:12:45 +01:00
Zeno Kapitein d70d566abe Support site announcement banner (#3008)
Co-authored-by: Valentino Hudhra <v.hudhra@gmail.com>
2025-03-26 16:37:05 +01:00
Nolann B. a25fdedea9 Replace $ref with $reference in json-decycle (#3049) 2025-03-26 14:16:03 +00:00
Nolann B. 373183a2cd Safe parse OpenAPI JSON schema (#3046) 2025-03-26 13:55:22 +01:00
Samy Pessé e9fa50dfb5 Trim search query before doing search (#3042) 2025-03-26 10:18:18 +00:00
Samy Pessé 57ca4e0e88 Fix crash when integration is not installed on the space (#3040) 2025-03-26 10:19:57 +01:00
Samy Pessé bc90adb73a Allow indexation of images to ensure favicons display on Google (#3041) 2025-03-26 10:17:57 +01:00
Nolann B. 1505ddbd5a Fix multiple request examples selector not showing (#3039) 2025-03-25 20:48:57 +01:00
Greg Bergé cd99ed57fb Fix OpenAPI spec rendering (#3037) 2025-03-25 15:13:18 +00:00
Samy Pessé 27de1cd01d Default to the first ID as being active in scroll detection (#3036) 2025-03-25 15:39:45 +01:00
Nolann B. ae78fc5394 Fix XML in code sample (#3030) 2025-03-25 15:11:45 +01:00
Samy Pessé 40e8e69c6d Disallow crawling by web-robots of search/ask URLs (#3035) 2025-03-25 13:51:09 +00:00
Nolann B. e84a46a558 Fix OpenAPI tabs indicator overflow (#3032) 2025-03-25 10:03:58 +01:00
Samy Pessé 18a72d4466 v2: improve caching (#3031) 2025-03-25 09:40:54 +01:00
Zeno Kapitein d7aa5fa003 Remove loading state for recommended questions (#3029) 2025-03-24 19:19:12 +01:00
Samy Pessé 27532a5f41 Version Packages (#2988)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-03-24 17:43:16 +01:00
Nolann B. 48c18c03b9 Add option to pass scalar plugins (#3027) 2025-03-24 14:11:14 +01:00
Samy Pessé bba2e52e24 Fix support for site redirects that include section/variant paths (#3024) 2025-03-24 10:21:26 +01:00
Valentino Hudhra 54ee0149e1 Add support for custom fonts (#2981) 2025-03-24 08:20:58 +01:00
Greg Bergé 8fda9d2fab Update Argos (#3025) 2025-03-22 23:00:25 +01:00
Samy Pessé 77a72d79ce Update next.js to latest v14 and v15 (#3026) 2025-03-22 20:02:15 +01:00
Nolann B. 73e2b472ef Fix write only properties in request example (#3023) 2025-03-21 19:09:25 +01:00
Samy Pessé 2f28cc1323 v2: improve link and navigation between content in a same site (#3020) 2025-03-21 18:18:34 +01:00
Greg Bergé f5516d4e21 Improve OpenAPI TOC display (#3022) 2025-03-21 14:29:12 +00:00
Greg Bergé d2facb2db1 Mark OpenAPI properties as optional if not required (#3021) 2025-03-21 14:28:53 +00:00
Greg Bergé 6eae764b7c Add support for multiple body content types / examples (#2991) 2025-03-21 14:23:42 +01:00
Greg Bergé 970ef8f886 Add missing token (#3019) 2025-03-21 11:04:57 +01:00
Nolann B. ed0720699e Fix OpenAPI path overflow on mobile (#3018) 2025-03-21 11:02:32 +01:00
Greg Bergé ad94d00d5f Improve markdown parsing (#3017) 2025-03-21 10:53:01 +01:00
Greg Bergé c7843f7f52 Remove bun cache (#3016) 2025-03-21 10:42:57 +01:00
Greg Bergé 1fe3286bdb Fix OpenAPI overflow issue (#3015) 2025-03-21 10:42:09 +01:00
Samy Pessé ce030fdf76 v2: Fix redirect to strip jwt_token in url-host mode (#3014) 2025-03-21 00:46:39 +01:00
Samy Pessé 9ba8783952 v2: Fix the redirection when stripping the jwt_token from url (#3013) 2025-03-21 00:25:34 +01:00
Greg Bergé 2f3966cc53 Fix icons flaky (#3012) 2025-03-20 21:08:46 +00:00
Samy Pessé 8f65a22e13 Accept API token in query parameters for normal v2 routes (#3011) 2025-03-20 19:54:55 +01:00
Greg Bergé 2787d884cd Stabilize images & icons (#3009) 2025-03-20 19:40:42 +01:00
Nolann B. fc00b51683 Remove default value in generateSchemaExample (#3007) 2025-03-20 17:24:23 +01:00
Samy Pessé f80c3a7b37 v2: Fix display of integration blocks (#3010) 2025-03-20 15:56:31 +00:00
Samy Pessé 4e5d6c7487 v2: use static route for VA / adaptive content (#2998) 2025-03-20 15:41:17 +01:00
Greg Bergé 96f84b5917 Fix visual tests v2 (#3006) 2025-03-20 11:34:04 +01:00
Greg Bergé 6b8a5fdd1b Trigger tests (#3005) 2025-03-20 10:00:09 +00:00
Greg Bergé 89083ac790 Stabilize visual tests (#3001) 2025-03-20 09:22:09 +01:00
Nolann B. 70be2c6b2b Stringify default value (#3003) 2025-03-19 21:28:40 +01:00
Samy Pessé bcf877841b Update to latest version of the GitBook API (#3002) 2025-03-19 20:11:44 +01:00
Nolann B. a84b06bb19 Update resolveDescription and add minItems/maxItems (#3000) 2025-03-19 17:21:35 +01:00
Zeno Kapitein 24b7808d9a Fix prominent search bar width on md screens (#2996) 2025-03-19 17:14:07 +01:00
Greg Bergé 8364ee988e Fix Argos flakies (#2999) 2025-03-19 16:04:45 +01:00
Samy Pessé 07f6e2d233 Pass page.computedSeed to getComputedDocument (#2995) 2025-03-19 15:02:49 +01:00
Greg Bergé 7212973f34 Update Scalar client (#2993) 2025-03-18 16:02:40 +01:00
Greg Bergé af44e0436b fix typo (#2992) 2025-03-18 14:20:23 +01:00
Greg Bergé 3f9c35d505 Fix emoji display on other platforms than Mac OS (#2990) 2025-03-17 17:49:13 +01:00
Zeno Kapitein 2c13f8b95c Fix horizontal overscroll issue on large screens (#2989) 2025-03-17 15:38:44 +01:00
Zeno Kapitein 4dab1c56e5 Fix alignment of prominent search bar on full-width pages (#2987) 2025-03-17 15:02:23 +01:00
Zeno Kapitein 4723f03ef1 Restyle section group dropdown (#2985) 2025-03-17 13:22:54 +01:00
Samy Pessé 278689e0a0 Version Packages (#2984)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-03-17 11:06:18 +01:00
Nolann B. 886e204e68 Update OpenAPI operation path design (#2972) 2025-03-17 10:18:24 +01:00
Greg Bergé c59947ab39 Remove Sentry (#2986) 2025-03-17 09:00:58 +01:00
Zeno Kapitein 4f0a772a21 Override tint lightness if supplied color is out of bounds (#2983) 2025-03-14 18:24:25 +01:00
Samy Pessé 903f962673 Version Packages (#2964)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-03-14 18:01:38 +01:00
Nolann B. 31d800e48b Changeset for OpenAPI schemas (#2982) 2025-03-14 17:08:21 +01:00
Nolann B. c1c58eb919 Render OpenAPISchemas block (#2959) 2025-03-14 16:24:09 +01:00
Zeno Kapitein eec3eed100 Support prominent search bar style (#2936)
Co-authored-by: Nolann Biron <biron.nolann@gmail.com>
2025-03-14 15:11:21 +01:00
Samy Pessé 99da8df3d1 Resize and optimize ogimage and favicons (#2980) 2025-03-13 18:45:14 +01:00
Steven H c9ea23919e Fix an issue where iframe aspect ratio was not respected. (#2979) 2025-03-13 14:23:50 +00:00
Zeno Kapitein 16292def79 Display sidebar on no-TOC pages (#2942) 2025-03-13 10:28:12 +01:00
Taran Vohra 9108c56ca1 Fix an issue where ContentKit webframes weren't sized properly (#2978) 2025-03-13 13:29:29 +05:30
Samy Pessé b011ea0af8 Simplify deferred codeblock highlighting and fix streaming (#2977) 2025-03-13 00:25:48 +01:00
spastorelli d33171b779 Fix getVisitorAuthCookieMaxAge returning 60s even when token expiry is much later (#2974) 2025-03-12 16:17:16 +01:00
Steven H 6aaeae269e Fix an issue where ContentKit iframes were not resizing. (#2967) 2025-03-12 14:52:27 +00:00
Samy Pessé e5f0cc8663 Respond with all tags of all cache used for page generation (#2973) 2025-03-12 10:30:47 +01:00
Nolann B. 9b5f971aa1 Transparent background for OpenAPI path block (#2963) 2025-03-11 13:32:35 +00:00
Nolann B. ff3b708ad3 Remove read-only properties from codesample (#2965) 2025-03-11 13:32:10 +00:00
Nolann B. e996904013 Update target in shiki javascript engine (#2968) 2025-03-11 14:21:07 +01:00
Samy Pessé f32bf1f15d Use API cache endpoint and service binding in Cloudflare (#2952) 2025-03-11 13:06:06 +01:00
Zeno Kapitein 6cda2dd57b Fix hint hover style (#2966) 2025-03-11 11:00:28 +00:00
Nolann B. c60e9ba27f Handle OpenAPI read-only properties (#2962) 2025-03-11 11:37:11 +01:00
Zeno Kapitein 9bc3d504c8 Info hint background and link color fixes (#2953) 2025-03-11 11:30:28 +01:00
Greg Bergé b5e4fd75e9 Upgrade shiki and bring back support to C/C++ (#2961) 2025-03-11 10:53:51 +01:00
Samy Pessé a92cb49445 Version Packages (#2958)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-03-11 09:38:39 +00:00
Greg Bergé 88f64b0d63 Support Node.js v20 with @gitbook/icons (#2960) 2025-03-11 09:27:26 +00:00
Nolann B. 844059f9a5 Fix spacing in OpenAPISecurities (#2954) 2025-03-11 09:37:34 +01:00
Nolann B. eaf2d68308 OpenAPI operation title fallback in sections (#2955) 2025-03-11 09:37:00 +01:00
Nolann B. 12bdbf988d Fix react-openapi tests (#2956) 2025-03-11 09:20:24 +01:00
Samy Pessé 29bab79005 v2: PDF export for sites and spaces (#2949) 2025-03-10 00:08:33 +01:00
Samy Pessé 8d5de97ddf v2: fix fallback and VA redirects (#2948) 2025-03-08 11:21:49 +01:00
Samy Pessé d9b5a898ab Version Packages (#2947)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-03-08 00:29:16 +01:00
Nolann B. f127d28675 Rename OpenAPIModels to OpenAPISchemas (#2946) 2025-03-07 22:28:37 +01:00
Samy Pessé 65c1bdc455 Version Packages (#2945)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-03-07 19:37:41 +01:00
Samy Pessé 0711a171d4 Fix deployment of 2c to production 2025-03-07 18:26:51 +01:00
Samy Pessé c58c551f9e Simplify envs management for deployments (#2943) 2025-03-07 18:17:44 +01:00
Greg Bergé f574858b9b Fix OpenAPI example (#2944) 2025-03-07 17:54:35 +01:00
Samy Pessé 3e1592e16b Fix configuration for v2 staging (#2940) 2025-03-07 14:46:45 +01:00
375 changed files with 15988 additions and 6660 deletions
+34 -46
View File
@@ -1,6 +1,12 @@
name: 'Deploy cloudflare' name: 'Deploy cloudflare'
description: 'Deploy GitBook to Cloudflare' description: 'Deploy GitBook to Cloudflare'
inputs: inputs:
opItem:
description: '1Password item to load secrets from'
required: true
opServiceAccount:
description: '1Password service account token'
required: true
apiToken: apiToken:
description: 'Cloudflare API token' description: 'Cloudflare API token'
required: true required: true
@@ -8,44 +14,17 @@ inputs:
description: 'Cloudflare account ID' description: 'Cloudflare account ID'
required: true required: true
environment: environment:
description: 'Environment to deploy to' description: 'Cloudflare environment to deploy to (staging, production, preview)'
required: true required: true
deploy: deploy:
description: 'Deploy as main version for all traffic instead of uploading versions' description: 'Deploy as main version for all traffic instead of uploading versions'
required: true required: true
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: commitTag:
description: 'Next server actions encryption key' description: 'Commit branch to associate with the deployment'
required: true required: true
GITBOOK_URL: commitMessage:
description: 'GitBook URL' description: 'Commit message to associate with the deployment'
required: true required: true
GITBOOK_SECRET:
description: 'GitBook secret'
required: true
GITBOOK_APP_URL:
description: 'GitBook app URL'
required: false
GITBOOK_API_URL:
description: 'GitBook API URL'
required: false
GITBOOK_INTEGRATIONS_HOST:
description: 'GitBook integrations host'
required: false
GITBOOK_IMAGE_RESIZE_SIGNING_KEY:
description: 'GitBook image resize signing key'
required: true
GITBOOK_IMAGE_RESIZE_URL:
description: 'GitBook image resize URL'
required: true
GITBOOK_ICONS_URL:
description: 'GitBook icons URL'
required: true
GITBOOK_ICONS_TOKEN:
description: 'GitBook icons token'
required: true
GITBOOK_ASSETS_PREFIX:
description: 'GitBook assets prefix'
required: false
outputs: outputs:
deployment-url: deployment-url:
description: "Deployment URL" description: "Deployment URL"
@@ -60,21 +39,30 @@ runs:
shell: bash shell: bash
env: env:
PUPPETEER_SKIP_DOWNLOAD: 1 PUPPETEER_SKIP_DOWNLOAD: 1
- name: Load secret
uses: 1password/load-secrets-action@v2
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ inputs.opServiceAccount }}
GITBOOK_URL: ${{ inputs.opItem }}/GITBOOK_URL
GITBOOK_ICONS_URL: ${{ inputs.opItem }}/GITBOOK_ICONS_URL
GITBOOK_ICONS_TOKEN: ${{ inputs.opItem }}/GITBOOK_ICONS_TOKEN
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ inputs.opItem }}/NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
GITBOOK_SECRET: ${{ inputs.opItem }}/GITBOOK_SECRET
GITBOOK_APP_URL: ${{ inputs.opItem }}/GITBOOK_APP_URL
GITBOOK_API_URL: ${{ inputs.opItem }}/GITBOOK_API_URL
GITBOOK_API_PUBLIC_URL: ${{ inputs.opItem }}/GITBOOK_API_PUBLIC_URL
GITBOOK_API_TOKEN: ${{ inputs.opItem }}/GITBOOK_API_TOKEN
GITBOOK_INTEGRATIONS_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_HOST
GITBOOK_IMAGE_RESIZE_SIGNING_KEY: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SIGNING_KEY
GITBOOK_IMAGE_RESIZE_URL: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_URL
GITBOOK_IMAGE_RESIZE_MODE: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_MODE
GITBOOK_ASSETS_PREFIX: ${{ inputs.opItem }}/GITBOOK_ASSETS_PREFIX
GITBOOK_FONTS_URL: ${{ inputs.opItem }}/GITBOOK_FONTS_URL
- name: Build worker - name: Build worker
run: bun run turbo build:v2:cloudflare run: bun run turbo build:v2:cloudflare
shell: bash
env: env:
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ inputs.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY }} GITBOOK_RUNTIME: cloudflare
GITBOOK_URL: ${{ inputs.GITBOOK_URL }} shell: bash
GITBOOK_SECRET: ${{ inputs.GITBOOK_SECRET }}
GITBOOK_APP_URL: ${{ inputs.GITBOOK_APP_URL }}
GITBOOK_API_URL: ${{ inputs.GITBOOK_API_URL }}
GITBOOK_INTEGRATIONS_HOST: ${{ inputs.GITBOOK_INTEGRATIONS_HOST }}
GITBOOK_IMAGE_RESIZE_SIGNING_KEY: ${{ inputs.GITBOOK_IMAGE_RESIZE_SIGNING_KEY }}
GITBOOK_IMAGE_RESIZE_URL: ${{ inputs.GITBOOK_IMAGE_RESIZE_URL }}
GITBOOK_ICONS_URL: ${{ inputs.GITBOOK_ICONS_URL }}
GITBOOK_ICONS_TOKEN: ${{ inputs.GITBOOK_ICONS_TOKEN }}
GITBOOK_ASSETS_PREFIX: ${{ inputs.GITBOOK_ASSETS_PREFIX }}
- id: deploy - id: deploy
name: Deploy to Cloudflare name: Deploy to Cloudflare
uses: cloudflare/wrangler-action@v3.14.0 uses: cloudflare/wrangler-action@v3.14.0
@@ -82,9 +70,9 @@ runs:
apiToken: ${{ inputs.apiToken }} apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }} accountId: ${{ inputs.accountId }}
workingDirectory: ./ workingDirectory: ./
wranglerVersion: '3.112.0' wranglerVersion: '4.10.0'
environment: ${{ inputs.environment }} environment: ${{ inputs.environment }}
command: ${{ inputs.deploy && 'deploy' || 'versions upload' }} --config ./packages/gitbook-v2/wrangler.toml command: ${{ inputs.deploy == 'true' && 'deploy' || format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook-v2/wrangler.jsonc
- name: Outputs - name: Outputs
shell: bash shell: bash
env: env:
+38 -16
View File
@@ -1,15 +1,21 @@
name: 'Deploy vercel' name: 'Deploy vercel'
description: 'Deploy GitBook to Vercel' description: 'Deploy GitBook to Vercel'
inputs: inputs:
vercel-org: vercelOrg:
description: 'Vercel organization' description: 'Vercel organization'
required: true required: true
vercel-project: vercelProject:
description: 'Vercel project' description: 'Vercel project'
required: true required: true
vercel-token: vercelToken:
description: 'Vercel token' description: 'Vercel token'
required: true required: true
opItem:
description: '1Password item to load secrets from'
required: true
opServiceAccount:
description: '1Password service account token'
required: true
environment: environment:
description: 'Environment to deploy to' description: 'Environment to deploy to'
required: true required: true
@@ -27,32 +33,48 @@ runs:
shell: bash shell: bash
env: env:
PUPPETEER_SKIP_DOWNLOAD: 1 PUPPETEER_SKIP_DOWNLOAD: 1
- name: Sets env vars for environment
shell: bash
run: |
echo "VERCEL_ENVIRONMENT=${{ inputs.environment }}" >> $GITHUB_ENV
- name: Pull Vercel Environment Information - name: Pull Vercel Environment Information
run: bun run vercel pull --yes --environment=$VERCEL_ENVIRONMENT --token=${{ inputs.vercel-token }} run: bun run vercel pull --yes --environment=${{ inputs.environment }} --token=${{ inputs.vercelToken }}
shell: bash shell: bash
env: env:
VERCEL_ORG_ID: ${{ inputs.vercel-org }} VERCEL_ORG_ID: ${{ inputs.vercelOrg }}
VERCEL_PROJECT_ID: ${{ inputs.vercel-project }} VERCEL_PROJECT_ID: ${{ inputs.vercelProject }}
- name: Load secret
uses: 1password/load-secrets-action@v2
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ inputs.opServiceAccount }}
GITBOOK_URL: ${{ inputs.opItem }}/GITBOOK_URL
GITBOOK_ICONS_URL: ${{ inputs.opItem }}/GITBOOK_ICONS_URL
GITBOOK_ICONS_TOKEN: ${{ inputs.opItem }}/GITBOOK_ICONS_TOKEN
GITBOOK_SECRET: ${{ inputs.opItem }}/GITBOOK_SECRET
GITBOOK_APP_URL: ${{ inputs.opItem }}/GITBOOK_APP_URL
GITBOOK_API_URL: ${{ inputs.opItem }}/GITBOOK_API_URL
GITBOOK_API_PUBLIC_URL: ${{ inputs.opItem }}/GITBOOK_API_PUBLIC_URL
GITBOOK_API_TOKEN: ${{ inputs.opItem }}/GITBOOK_API_TOKEN
GITBOOK_INTEGRATIONS_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_HOST
GITBOOK_IMAGE_RESIZE_SIGNING_KEY: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SIGNING_KEY
GITBOOK_IMAGE_RESIZE_URL: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_URL
GITBOOK_IMAGE_RESIZE_MODE: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_MODE
GITBOOK_ASSETS_PREFIX: ${{ inputs.opItem }}/GITBOOK_ASSETS_PREFIX
GITBOOK_FONTS_URL: ${{ inputs.opItem }}/GITBOOK_FONTS_URL
- name: Build Project Artifacts - name: Build Project Artifacts
run: bun run vercel build --target=$VERCEL_ENVIRONMENT --token=${{ inputs.vercel-token }} run: bun run vercel build --target=${{ inputs.environment }} --token=${{ inputs.vercelToken }}
shell: bash shell: bash
env: env:
VERCEL_ORG_ID: ${{ inputs.vercel-org }} VERCEL_ORG_ID: ${{ inputs.vercelOrg }}
VERCEL_PROJECT_ID: ${{ inputs.vercel-project }} VERCEL_PROJECT_ID: ${{ inputs.vercelProject }}
GITBOOK_RUNTIME: vercel
- name: Deploy Project Artifacts to Vercel - name: Deploy Project Artifacts to Vercel
id: deploy id: deploy
shell: bash shell: bash
run: | run: |
DEPLOYMENT_URL=$(bun run vercel deploy --prebuilt --target=$VERCEL_ENVIRONMENT --token=${{ inputs.vercel-token }}) DEPLOYMENT_URL=$(bun run vercel deploy --prebuilt --target=${{ inputs.environment }} --token=${{ inputs.vercelToken }})
echo "deployment-url=$DEPLOYMENT_URL" >> "$GITHUB_OUTPUT" echo "deployment-url=$DEPLOYMENT_URL" >> "$GITHUB_OUTPUT"
env: env:
VERCEL_ORG_ID: ${{ inputs.vercel-org }} VERCEL_ORG_ID: ${{ inputs.vercelOrg }}
VERCEL_PROJECT_ID: ${{ inputs.vercel-project }} VERCEL_PROJECT_ID: ${{ inputs.vercelProject }}
- name: Outputs - name: Outputs
shell: bash shell: bash
run: | run: |
echo "URL: ${{ steps.deploy.outputs.deployment-url }}" echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
-9
View File
@@ -3,15 +3,6 @@ description: 'Install Bun and cache dependencies'
runs: runs:
using: 'composite' using: 'composite'
steps: 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 - name: Setup bun
uses: oven-sh/setup-bun@v2 uses: oven-sh/setup-bun@v2
with: with:
+91 -52
View File
@@ -10,6 +10,9 @@ jobs:
deploy-v1-cloudflare: deploy-v1-cloudflare:
name: Deploy v1 to Cloudflare Pages name: Deploy v1 to Cloudflare Pages
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment:
name: ${{ github.ref == 'refs/heads/main' && '1c-production' || '1c-preview' }}
url: ${{ steps.deploy.outputs.deployment-url }}
permissions: permissions:
contents: read contents: read
deployments: write deployments: write
@@ -32,21 +35,12 @@ jobs:
- name: Sets env vars for production - name: Sets env vars for production
if: github.ref == 'refs/heads/main' if: github.ref == 'refs/heads/main'
run: | run: |
echo "SENTRY_ENVIRONMENT=production" >> $GITHUB_ENV
echo "GITBOOK_ASSETS_PREFIX=https://static.gitbook.com" >> $GITHUB_ENV echo "GITBOOK_ASSETS_PREFIX=https://static.gitbook.com" >> $GITHUB_ENV
- name: Sets env vars for preview
if: github.ref != 'refs/heads/main'
run: |
echo "SENTRY_ENVIRONMENT=preview" >> $GITHUB_ENV
- name: Build Next.js with next-on-pages - name: Build Next.js with next-on-pages
run: bun run turbo gitbook#build:cloudflare run: bun run turbo gitbook#build:cloudflare
env: env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ secrets.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY }} NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ secrets.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY }}
SENTRY_ORG: ${{ vars.SENTRY_ORG }} GITBOOK_RUNTIME: cloudflare
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}
SENTRY_DSN: ${{ vars.SENTRY_DSN }}
SENTRY_RELEASE: ${{ github.sha }}
- id: deploy - id: deploy
name: Deploy to Cloudflare name: Deploy to Cloudflare
uses: cloudflare/wrangler-action@v3.14.0 uses: cloudflare/wrangler-action@v3.14.0
@@ -63,59 +57,58 @@ jobs:
deploy-v2-vercel: deploy-v2-vercel:
name: Deploy v2 to Vercel (preview) name: Deploy v2 to Vercel (preview)
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: environment:
contents: read name: 2v-preview
deployments: write url: ${{ steps.deploy.outputs.deployment-url }}
issues: write
pull-requests: write
checks: write
statuses: write
outputs: outputs:
deployment-url: ${{ steps.deploy.outputs.deployment-url }} deployment-url: ${{ steps.deploy.outputs.deployment-url }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Deploy ${{ github.ref == 'refs/heads/main' && 'production' || 'preview' }} - name: Deploy to Vercel
id: deploy id: deploy
uses: ./.github/composite/deploy-vercel uses: ./.github/composite/deploy-vercel
with: with:
environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'preview' }} environment: preview
vercel-org: ${{ secrets.VERCEL_ORG_ID }} vercelOrg: ${{ secrets.VERCEL_ORG_ID }}
vercel-project: ${{ secrets.VERCEL_PROJECT_ID }} vercelProject: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-token: ${{ secrets.VERCEL_TOKEN }} vercelToken: ${{ secrets.VERCEL_TOKEN }}
opItem: op://gitbook-open/2v-preview
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
deploy-v2-cloudflare: deploy-v2-cloudflare:
name: Deploy v2 to Cloudflare Worker (preview) name: Deploy v2 to Cloudflare Worker (preview)
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: environment:
contents: read name: 2c-preview
deployments: write url: ${{ steps.deploy.outputs.deployment-url }}
issues: write
pull-requests: write
checks: write
statuses: write
outputs: outputs:
deployment-url: ${{ steps.deploy.outputs.deployment-url }} deployment-url: ${{ steps.deploy.outputs.deployment-url || steps.extract-worker-id.outputs.worker-url }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Deploy ${{ github.ref == 'refs/heads/main' && 'production' || 'preview' }} - name: Deploy to Cloudflare
id: deploy id: deploy
uses: ./.github/composite/deploy-cloudflare uses: ./.github/composite/deploy-cloudflare
with: with:
environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'preview' }} environment: preview
deploy: ${{ github.ref == 'refs/heads/main' }} deploy: ${{ github.ref == 'refs/heads/main' }}
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ secrets.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY }} opItem: op://gitbook-open/2c-preview
GITBOOK_URL: ${{ github.ref == 'refs/heads/main' && vars.PRODUCTION_2C_GITBOOK_URL || vars.PREVIEW_2C_GITBOOK_URL }} opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
GITBOOK_IMAGE_RESIZE_SIGNING_KEY: ${{ secrets.PREVIEW_GITBOOK_IMAGE_RESIZE_SIGNING_KEY }} commitTag: ${{ github.ref == 'refs/heads/main' && 'main' || format('pr{0}', github.event.pull_request.number) }}
GITBOOK_IMAGE_RESIZE_URL: ${{ vars.GITBOOK_IMAGE_RESIZE_URL }} commitMessage: ${{ github.sha }}
GITBOOK_ICONS_URL: ${{ vars.GITBOOK_ICONS_URL }} - name: Extract Worker ID
GITBOOK_ICONS_TOKEN: ${{ vars.GITBOOK_ICONS_TOKEN }} id: extract-worker-id
GITBOOK_SECRET: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_GITBOOK_SECRET|| '' }} if: ${{ !steps.deploy.outputs.deployment-url }}
run: |
if [[ "${{ steps.deploy.outputs.command-output }}" =~ Worker\ Version\ ID:\ ([0-9a-f]{8})-([0-9a-f-]+) ]]; then
WORKER_ID_FIRST_PART="${BASH_REMATCH[1]}"
echo "worker-url=https://${WORKER_ID_FIRST_PART}-gitbook-open-v2-preview.gitbook.workers.dev/" >> $GITHUB_OUTPUT
fi
- name: Outputs - name: Outputs
run: | run: |
echo "URL: ${{ steps.deploy.outputs.deployment-url }}" echo "URL: ${{ steps.deploy.outputs.deployment-url || steps.extract-worker-id.outputs.worker-url }}"
comment-deployments: comment-deployments:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Comment Deployments (preview) name: Comment Deployments (preview)
@@ -141,14 +134,14 @@ jobs:
body: | body: |
Summary of the deployments: Summary of the deployments:
### Version 1 (production) ### Version 1
| Version | URL | Status | | Version | URL | Status |
| --- | --- | --- | | --- | --- | --- |
| Latest commit | [${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}](${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}) | ${{ needs.deploy-v1-cloudflare.result == 'success' && '✅' || '❌' }} | | Latest commit | [${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}](${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}) | ${{ needs.deploy-v1-cloudflare.result == 'success' && '✅' || '❌' }} |
| PR | [${{ needs.deploy-v1-cloudflare.outputs.deployment-alias-url }}](${{ needs.deploy-v1-cloudflare.outputs.deployment-alias-url }}) | ${{ needs.deploy-v1-cloudflare.result == 'success' && '✅' || '❌' }} | | PR | [${{ needs.deploy-v1-cloudflare.outputs.deployment-alias-url }}](${{ needs.deploy-v1-cloudflare.outputs.deployment-alias-url }}) | ${{ needs.deploy-v1-cloudflare.result == 'success' && '✅' || '❌' }} |
### Version 2 (experimental) ### Version 2
| Version | URL | Status | | Version | URL | Status |
| --- | --- | --- | | --- | --- | --- |
@@ -157,10 +150,10 @@ jobs:
### Test content ### Test content
| Site | v1 | v2 | | Site | `v1` | `2v` | `2c` |
| --- | --- | --- | | --- | --- | --- | --- |
| GitBook | [${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/docs.gitbook.com](${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/docs.gitbook.com) | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/docs.gitbook.com](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/docs.gitbook.com) | | GitBook | [${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/docs.gitbook.com](${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/docs.gitbook.com) | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/docs.gitbook.com](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/docs.gitbook.com) | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/docs.gitbook.com](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/docs.gitbook.com) |
| E2E | [${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/gitbook.gitbook.io/test-gitbook-open) | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open) | | E2E | [${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/gitbook.gitbook.io/test-gitbook-open) | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open) | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open) |
edit-mode: replace edit-mode: replace
visual-testing-v1: visual-testing-v1:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -198,14 +191,37 @@ jobs:
- name: Run Playwright tests - name: Run Playwright tests
run: bun e2e run: bun e2e
env: env:
BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/ BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }} ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: 'v2-vercel' ARGOS_BUILD_NAME: 'v2-vercel'
visual-testing-v2-cloudflare:
runs-on: ubuntu-latest
name: Visual Testing v2 (Cloudflare)
needs: deploy-v2-cloudflare
timeout-minutes: 10
if: startsWith(github.head_ref || github.ref_name, 'cloudflare/') || github.ref == 'refs/heads/main'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
- name: Run Playwright tests
run: bun e2e
env:
BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: 'v2-cloudflare'
visual-testing-customers-v1: visual-testing-customers-v1:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Visual Testing Customers v1 name: Visual Testing Customers v1
needs: deploy-v1-cloudflare needs: deploy-v1-cloudflare
timeout-minutes: 6 timeout-minutes: 8
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -225,7 +241,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Visual Testing Customers v2 name: Visual Testing Customers v2
needs: deploy-v2-vercel needs: deploy-v2-vercel
timeout-minutes: 6 timeout-minutes: 8
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -238,7 +254,30 @@ jobs:
- name: Run Playwright tests - name: Run Playwright tests
run: bun e2e-customers run: bun e2e-customers
env: env:
BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/ BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: 'customers-v2'
visual-testing-customers-v2-cloudflare:
runs-on: ubuntu-latest
name: Visual Testing Customers v2 (Cloudflare)
needs: deploy-v2-cloudflare
timeout-minutes: 8
if: startsWith(github.head_ref || github.ref_name, 'cloudflare/') || github.ref == 'refs/heads/main'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
- name: Run Playwright tests
run: bun e2e-customers
env:
BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }} ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: 'customers-v2' ARGOS_BUILD_NAME: 'customers-v2'
pagespeed-testing-v1: pagespeed-testing-v1:
@@ -255,7 +294,7 @@ jobs:
env: env:
PUPPETEER_SKIP_DOWNLOAD: 1 PUPPETEER_SKIP_DOWNLOAD: 1
- name: Run pagespeed tests - name: Run pagespeed tests
run: bun ./packages/gitbook/tests/pagespeed-testing.ts $DEPLOYMENT_URL run: bun ./packages/gitbook/tests/pagespeed-testing.ts
env: env:
DEPLOYMENT_URL: ${{needs.deploy-v1-cloudflare.outputs.deployment-url}} BASE_URL: ${{needs.deploy-v1-cloudflare.outputs.deployment-url}}
PAGESPEED_API_KEY: ${{ secrets.PAGESPEED_API_KEY }} PAGESPEED_API_KEY: ${{ secrets.PAGESPEED_API_KEY }}
+19 -13
View File
@@ -9,22 +9,30 @@ jobs:
deploy-v2-vercel: deploy-v2-vercel:
name: Deploy v2 to Vercel (production) name: Deploy v2 to Vercel (production)
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment:
name: 2v-production
url: ${{ steps.deploy.outputs.deployment-url }}
outputs: outputs:
deployment-url: ${{ steps.deploy.outputs.deployment-url }} deployment-url: ${{ steps.deploy.outputs.deployment-url }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Deploy staging - name: Deploy
id: deploy id: deploy
uses: ./.github/composite/deploy-vercel uses: ./.github/composite/deploy-vercel
with: with:
environment: production environment: production
vercel-org: ${{ secrets.VERCEL_ORG_ID }} vercelOrg: ${{ secrets.VERCEL_ORG_ID }}
vercel-project: ${{ secrets.VERCEL_PROJECT_ID }} vercelProject: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-token: ${{ secrets.VERCEL_TOKEN }} vercelToken: ${{ secrets.VERCEL_TOKEN }}
opItem: op://gitbook-open/2v-production
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
deploy-v2-cloudflare: deploy-v2-cloudflare:
name: Deploy v2 to Cloudflare Worker (production) name: Deploy v2 to Cloudflare Worker (production)
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment:
name: 2c-production
url: ${{ steps.deploy.outputs.deployment-url }}
outputs: outputs:
deployment-url: ${{ steps.deploy.outputs.deployment-url }} deployment-url: ${{ steps.deploy.outputs.deployment-url }}
steps: steps:
@@ -34,16 +42,14 @@ jobs:
id: deploy id: deploy
uses: ./.github/composite/deploy-cloudflare uses: ./.github/composite/deploy-cloudflare
with: with:
environment: staging environment: production
deploy: true
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ secrets.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY }} opItem: op://gitbook-open/2c-production
GITBOOK_URL: ${{ vars.STAGING_2C_GITBOOK_URL }} opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
GITBOOK_IMAGE_RESIZE_SIGNING_KEY: ${{ secrets.STAGING_GITBOOK_IMAGE_RESIZE_SIGNING_KEY }} commitTag: main
GITBOOK_IMAGE_RESIZE_URL: ${{ vars.GITBOOK_IMAGE_RESIZE_URL }} commitMessage: ${{ github.sha }}
GITBOOK_ICONS_URL: ${{ vars.GITBOOK_ICONS_URL }}
GITBOOK_ICONS_TOKEN: ${{ vars.GITBOOK_ICONS_TOKEN }}
GITBOOK_ASSETS_PREFIX: ${{ vars.STAGING_2C_GITBOOK_ASSETS_PREFIX }}
- name: Outputs - name: Outputs
run: | run: |
echo "URL: ${{ steps.deploy.outputs.deployment-url }}" echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
+17 -16
View File
@@ -9,22 +9,30 @@ jobs:
deploy-v2-vercel: deploy-v2-vercel:
name: Deploy v2 to Vercel (staging) name: Deploy v2 to Vercel (staging)
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment:
name: 2v-staging
url: ${{ steps.deploy.outputs.deployment-url }}
outputs: outputs:
deployment-url: ${{ steps.deploy.outputs.deployment-url }} deployment-url: ${{ steps.deploy.outputs.deployment-url }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Deploy staging - name: Deploy
id: deploy id: deploy
uses: ./.github/composite/deploy-vercel uses: ./.github/composite/deploy-vercel
with: with:
environment: staging environment: staging
vercel-org: ${{ secrets.VERCEL_ORG_ID }} vercelOrg: ${{ secrets.VERCEL_ORG_ID }}
vercel-project: ${{ secrets.VERCEL_PROJECT_ID }} vercelProject: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-token: ${{ secrets.VERCEL_TOKEN }} vercelToken: ${{ secrets.VERCEL_TOKEN }}
opItem: op://gitbook-open/2v-staging
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
deploy-v2-cloudflare: deploy-v2-cloudflare:
name: Deploy v2 to Cloudflare Worker (staging) name: Deploy v2 to Cloudflare Worker (staging)
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment:
name: 2c-staging
url: ${{ steps.deploy.outputs.deployment-url }}
outputs: outputs:
deployment-url: ${{ steps.deploy.outputs.deployment-url }} deployment-url: ${{ steps.deploy.outputs.deployment-url }}
steps: steps:
@@ -38,17 +46,10 @@ jobs:
deploy: true deploy: true
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ secrets.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY }} opItem: op://gitbook-open/2c-staging
GITBOOK_URL: ${{ vars.STAGING_2C_GITBOOK_URL }} opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
GITBOOK_SECRET: ${{ secrets.STAGING_GITBOOK_SECRET }} commitTag: main
GITBOOK_APP_URL: https://app.gitbook-staging.com commitMessage: ${{ github.sha }}
GITBOOK_API_URL: https://api.gitbook-staging.com
GITBOOK_INTEGRATIONS_HOST: https://integrations.gitbook-staging.com
GITBOOK_IMAGE_RESIZE_SIGNING_KEY: ${{ secrets.STAGING_GITBOOK_IMAGE_RESIZE_SIGNING_KEY }}
GITBOOK_IMAGE_RESIZE_URL: ${{ vars.GITBOOK_IMAGE_RESIZE_URL }}
GITBOOK_ICONS_URL: ${{ vars.GITBOOK_ICONS_URL }}
GITBOOK_ICONS_TOKEN: ${{ vars.GITBOOK_ICONS_TOKEN }}
GITBOOK_ASSETS_PREFIX: ${{ vars.STAGING_2C_GITBOOK_ASSETS_PREFIX }}
- name: Outputs - name: Outputs
run: | run: |
echo "URL: ${{ steps.deploy.outputs.deployment-url }}" echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
+3
View File
@@ -5,6 +5,9 @@ on:
branches: branches:
- main - main
env:
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
concurrency: ${{ github.workflow }}-${{ github.ref }} concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs: jobs:
-17
View File
@@ -1,17 +0,0 @@
.next
.vercel
.open-next
# Generated
packages/emoji-codepoints/index.ts
packages/gitbook/public/~gitbook/static/
packages/icons/src/data/*.json
# Build files
dist/
README.md
CHANGELOG.md
# v2
packages/gitbook-v2/public/~gitbook/static/
-5
View File
@@ -1,5 +0,0 @@
{
"printWidth": 100,
"singleQuote": true,
"tabWidth": 4
}
+3
View File
@@ -13,5 +13,8 @@
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.organizeImports.biome": "explicit", "source.organizeImports.biome": "explicit",
"source.fixAll.biome": "explicit" "source.fixAll.biome": "explicit"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
} }
} }
+3 -3
View File
@@ -59,15 +59,15 @@ bun install
4. Start your local development server. 4. Start your local development server.
``` ```
bun dev bun dev:v2
``` ```
5. Open a published GitBook space in your web browser, prefixing it with `http://localhost:3000/`. 5. Open a published GitBook space in your web browser, prefixing it with `http://localhost:3000/`.
examples: examples:
- http://localhost:3000/docs.gitbook.com - http://localhost:3000/url/docs.gitbook.com
- http://localhost:3000/open-source.gitbook.io/midjourney - http://localhost:3000/url/open-source.gitbook.io/midjourney
Any published GitBook site can be accessed through your local development instance, and any updates you make to the codebase will be reflected in your browser. Any published GitBook site can be accessed through your local development instance, and any updates you make to the codebase will be reflected in your browser.
+4 -1
View File
@@ -18,7 +18,10 @@
"**/.vercel/**/*", "**/.vercel/**/*",
"**/.cache/**/*", "**/.cache/**/*",
"**/.wrangler/**/*", "**/.wrangler/**/*",
"packages/openapi-parser/src/fixtures/**/*" "packages/openapi-parser/src/fixtures/**/*",
"packages/emoji-codepoints/index.ts",
"packages/icons/src/data/*.json",
"packages/cache-do/worker-configuration.d.ts"
] ]
}, },
"formatter": { "formatter": {
+506 -665
View File
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -4,14 +4,15 @@
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.9.4", "@biomejs/biome": "^1.9.4",
"@changesets/cli": "^2.27.12", "@changesets/cli": "^2.27.12",
"turbo": "^2.4.4", "turbo": "^2.5.0",
"vercel": "^39.3.0" "vercel": "^39.3.0"
}, },
"packageManager": "bun@1.2.4", "packageManager": "bun@1.2.8",
"overrides": { "overrides": {
"@codemirror/state": "6.4.1", "@codemirror/state": "6.4.1",
"react": "18.3.1", "react": "18.3.1",
"react-dom": "18.3.1" "react-dom": "18.3.1",
"@gitbook/api": "0.111.0"
}, },
"private": true, "private": true,
"scripts": { "scripts": {
+2 -2
View File
@@ -21,10 +21,10 @@
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5.5.3", "typescript": "^5.5.3",
"wrangler": "^3.112.0" "wrangler": "^4.10.0"
}, },
"scripts": { "scripts": {
"generate": "wrangler types --experimental-include-runtime", "generate": "wrangler types",
"build": "tsc", "build": "tsc",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"dev": "tsc -w", "dev": "tsc -w",
+1 -1
View File
@@ -14,7 +14,7 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"incremental": true, "incremental": true,
"types": ["./.wrangler/types/runtime.d.ts"] "types": ["./worker-configuration.d.ts"]
}, },
"include": ["src/**/*.ts"], "include": ["src/**/*.ts"],
"exclude": ["node_modules"] "exclude": ["node_modules"]
+8
View File
@@ -0,0 +1,8 @@
{
"extends": ["//"],
"tasks": {
"generate": {
"outputs": ["worker-configuration.d.ts"]
}
}
}
+19 -1
View File
@@ -1,7 +1,25 @@
# @gitbook/cache-tags # @gitbook/cache-tags
## 0.3.1
### Patch Changes
- 77397ca: Fix version of @gitbook/api referenced in package.json
## 0.3.0
### Minor Changes
- 116575c: Improve typing of getComputedContentSourceCacheTags to match latest API specification
## 0.2.0
### Minor Changes
- f32bf1f: Export function `getCacheTagForURL` to easily get the cache tag for a URL.
## 0.1.0 ## 0.1.0
### Minor Changes ### Minor Changes
- 05ffd0e: Initial version of the package - 05ffd0e: Initial version of the package
+2 -2
View File
@@ -8,9 +8,9 @@
"default": "./dist/index.js" "default": "./dist/index.js"
} }
}, },
"version": "0.1.0", "version": "0.3.1",
"dependencies": { "dependencies": {
"@gitbook/api": "0.96.1", "@gitbook/api": "^0.111.0",
"assert-never": "^1.2.1" "assert-never": "^1.2.1"
}, },
"devDependencies": { "devDependencies": {
+43 -9
View File
@@ -60,7 +60,7 @@ export function getCacheTag(
| { | {
tag: 'computed-document'; tag: 'computed-document';
space: string; space: string;
integration: string; sourceType: string;
} }
/** /**
* All data related to the URL of a content * All data related to the URL of a content
@@ -84,6 +84,14 @@ export function getCacheTag(
organization: string; organization: string;
openAPISpec: string; openAPISpec: string;
} }
/**
* All data related to a translation
*/
| {
tag: 'translation';
organization: string;
translationSettings: string;
}
): string { ): string {
switch (spec.tag) { switch (spec.tag) {
case 'user': case 'user':
@@ -99,18 +107,31 @@ export function getCacheTag(
case 'document': case 'document':
return `space:${spec.space}:document:${spec.document}`; return `space:${spec.space}:document:${spec.document}`;
case 'computed-document': case 'computed-document':
return `space:${spec.space}:computed-document:${spec.integration}`; return `space:${spec.space}:computed-document:${spec.sourceType}`;
case 'site': case 'site':
return `site:${spec.site}`; return `site:${spec.site}`;
case 'integration': case 'integration':
return `integration:${spec.integration}`; return `integration:${spec.integration}`;
case 'openapi': case 'openapi':
return `organization:${spec.organization}:openapi:${spec.openAPISpec}`; return `organization:${spec.organization}:openapi:${spec.openAPISpec}`;
case 'translation':
return `organization:${spec.organization}:translation:${spec.translationSettings}`;
default: default:
assertNever(spec); assertNever(spec);
} }
} }
/**
* Get the cache tag for a given URL.
*/
export function getCacheTagForURL(url: string | URL) {
const parsedURL = url instanceof URL ? url : new URL(url);
return getCacheTag({
tag: 'url',
hostname: parsedURL.hostname,
});
}
/** /**
* Get the tags for a computed content source. * Get the tags for a computed content source.
*/ */
@@ -146,6 +167,15 @@ export function getComputedContentSourceCacheTags(
}) })
); );
break; break;
case 'translation-language':
tags.push(
getCacheTag({
tag: 'translation',
organization: inContext.organizationId,
translationSettings: dependency.ref.translationSettings,
})
);
break;
default: default:
// Do not throw for unknown dependency types // Do not throw for unknown dependency types
// as it might mean we are lacking behind the API version // as it might mean we are lacking behind the API version
@@ -158,18 +188,22 @@ export function getComputedContentSourceCacheTags(
getCacheTag({ getCacheTag({
tag: 'computed-document', tag: 'computed-document',
space: inContext.spaceId, space: inContext.spaceId,
integration: source.integration, sourceType: source.type,
}) })
); );
} }
// We invalidate the computed content when a new version of the integration is deployed. // We invalidate the computed content when a new version of the integration is deployed.
tags.push(
getCacheTag({ if (source.type.startsWith('integration:')) {
tag: 'integration', const integration = source.type.split(':')[1];
integration: source.integration, tags.push(
}) getCacheTag({
); tag: 'integration',
integration,
})
);
}
return tags; return tags;
} }
+19 -1
View File
@@ -1,7 +1,25 @@
# @gitbook/colors # @gitbook/colors
## 0.3.2
### Patch Changes
- cdffd7c: Desaturate text colors by decreasing chroma for the last steps of the color scale
## 0.3.1
### Patch Changes
- fb90eb0: Reduce chroma of first color scale step
## 0.3.0
### Minor Changes
- 4f0a772: Override tint lightness if supplied color is out of bounds
## 0.2.0 ## 0.2.0
### Minor Changes ### Minor Changes
- 445baaa: Initial release - 445baaa: Initial release
+1 -1
View File
@@ -8,7 +8,7 @@
"default": "./dist/index.js" "default": "./dist/index.js"
} }
}, },
"version": "0.2.0", "version": "0.3.2",
"devDependencies": { "devDependencies": {
"typescript": "^5.5.3" "typescript": "^5.5.3"
}, },
+16 -2
View File
@@ -184,6 +184,7 @@ export function colorScale(
const mixColor = mix?.color ? rgbToOklch(hexToRgbArray(mix.color)) : null; const mixColor = mix?.color ? rgbToOklch(hexToRgbArray(mix.color)) : null;
const foregroundColor = rgbToOklch(hexToRgbArray(foreground)); const foregroundColor = rgbToOklch(hexToRgbArray(foreground));
const backgroundColor = rgbToOklch(hexToRgbArray(background)); const backgroundColor = rgbToOklch(hexToRgbArray(background));
let mapping = darkMode ? colorMixMapping.dark : colorMixMapping.light;
if (mixColor && mix?.ratio && mix.ratio > 0) { if (mixColor && mix?.ratio && mix.ratio > 0) {
// If defined, we mix in a (tiny) bit of the mix color with the base color. // If defined, we mix in a (tiny) bit of the mix color with the base color.
@@ -192,7 +193,20 @@ export function colorScale(
baseColor.H = mix.color === DEFAULT_TINT_COLOR ? baseColor.H : mixColor.H; baseColor.H = mix.color === DEFAULT_TINT_COLOR ? baseColor.H : mixColor.H;
} }
const mapping = darkMode ? colorMixMapping.dark : colorMixMapping.light; if (
(darkMode && baseColor.L < backgroundColor.L) ||
(!darkMode && baseColor.L > backgroundColor.L)
) {
// If the supplied color is outside of our lightness bounds, use the supplied color's lightness.
// This is mostly used to allow darker-than-dark backgrounds for brands that specifically want that look.
const difference = (backgroundColor.L - baseColor.L) / backgroundColor.L;
backgroundColor.L = baseColor.L;
// At the edges of the scale, the subtle lightness changes stop being perceptible. We need to amp up our mapping to still stand out.
const amplifier = 1;
mapping = mapping.map((step, index) =>
index < 9 ? step + step * amplifier * difference : step
);
}
const result = []; const result = [];
@@ -206,7 +220,7 @@ export function colorScale(
continue; continue;
} }
const chromaRatio = index < 8 ? (index + 1) * 0.05 : 1; const chromaRatio = index === 8 || index === 9 ? 1 : index * 0.05;
const shade = { const shade = {
L: targetL, // Blend lightness L: targetL, // Blend lightness
+41 -6
View File
@@ -1,27 +1,62 @@
# gitbook-v2 # gitbook-v2
## 0.2.5
### Patch Changes
- Updated dependencies [77397ca]
- @gitbook/cache-tags@0.3.1
## 0.2.4
### Patch Changes
- 4234289: Fix incoming URL for requests that were proxied
- Updated dependencies [116575c]
- @gitbook/cache-tags@0.3.0
## 0.2.3
### Patch Changes
- 5b2bf82: Use stable site URL data for route rewrite in the middleware
## 0.2.2
### Patch Changes
- 54ee014: Add initial support for loading custom fonts
- bba2e52: Fix site redirects when it includes a section/variant path
## 0.2.1
### Patch Changes
- Updated dependencies [f32bf1f]
- @gitbook/cache-tags@0.2.0
## 0.2.0 ## 0.2.0
### Minor Changes ### Minor Changes
- 76c7974: Add route to revalidate cached data - 76c7974: Add route to revalidate cached data
## 0.1.2 ## 0.1.2
### Patch Changes ### Patch Changes
- 05ffd0e: Improving data cache management for computed content - 05ffd0e: Improving data cache management for computed content
- Updated dependencies [05ffd0e] - Updated dependencies [05ffd0e]
- @gitbook/cache-tags@0.1.0 - @gitbook/cache-tags@0.1.0
## 0.1.1 ## 0.1.1
### Patch Changes ### Patch Changes
- 3e11678: fix: lost section groups - 3e11678: fix: lost section groups
## 0.1.0 ## 0.1.0
### Minor Changes ### Minor Changes
- cfccc44: Setup structure and deployment for new version - cfccc44: Setup structure and deployment for new version
+38 -4
View File
@@ -5,21 +5,37 @@
*/ */
const nextConfig = { const nextConfig = {
experimental: { experimental: {
// This is needed to throw "forbidden" when the api token expired during revalidation
authInterrupts: true,
useCache: true, useCache: true,
// Content is fully static, we can cache it in the session memory cache for a long time
staleTimes: {
dynamic: 3600, // 1 hour
static: 3600, // 1 hour
},
}, },
env: { env: {
BUILD_VERSION: (process.env.GITHUB_SHA ?? '').slice(0, 7), BUILD_VERSION: (process.env.GITHUB_SHA ?? '').slice(0, 7),
GITBOOK_URL: process.env.GITBOOK_URL,
GITBOOK_SECRET: process.env.GITBOOK_SECRET, // GitBook envs
GITBOOK_API_URL: process.env.GITBOOK_API_URL, GITBOOK_API_URL: process.env.GITBOOK_API_URL,
GITBOOK_APP_URL: process.env.GITBOOK_APP_URL, GITBOOK_APP_URL: process.env.GITBOOK_APP_URL,
GITBOOK_INTEGRATIONS_HOST: process.env.GITBOOK_INTEGRATIONS_HOST, GITBOOK_INTEGRATIONS_HOST: process.env.GITBOOK_INTEGRATIONS_HOST,
GITBOOK_IMAGE_RESIZE_SIGNING_KEY: process.env.GITBOOK_IMAGE_RESIZE_SIGNING_KEY,
GITBOOK_IMAGE_RESIZE_URL: process.env.GITBOOK_IMAGE_RESIZE_URL, GITBOOK_IMAGE_RESIZE_URL: process.env.GITBOOK_IMAGE_RESIZE_URL,
GITBOOK_ASSETS_PREFIX: process.env.GITBOOK_ASSETS_PREFIX,
GITBOOK_ICONS_URL: process.env.GITBOOK_ICONS_URL, GITBOOK_ICONS_URL: process.env.GITBOOK_ICONS_URL,
GITBOOK_ICONS_TOKEN: process.env.GITBOOK_ICONS_TOKEN, GITBOOK_ICONS_TOKEN: process.env.GITBOOK_ICONS_TOKEN,
GITBOOK_URL: process.env.GITBOOK_URL,
GITBOOK_API_TOKEN: process.env.GITBOOK_API_TOKEN,
GITBOOK_ASSETS_PREFIX: process.env.GITBOOK_ASSETS_PREFIX,
GITBOOK_SECRET: process.env.GITBOOK_SECRET,
GITBOOK_IMAGE_RESIZE_SIGNING_KEY: process.env.GITBOOK_IMAGE_RESIZE_SIGNING_KEY,
GITBOOK_IMAGE_RESIZE_MODE: process.env.GITBOOK_IMAGE_RESIZE_MODE,
GITBOOK_FONTS_URL: process.env.GITBOOK_FONTS_URL,
GITBOOK_RUNTIME: process.env.GITBOOK_RUNTIME,
// Next.js envs
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY, NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY,
// Used to detect if the app is running in V2 mode // Used to detect if the app is running in V2 mode
@@ -37,6 +53,24 @@ const nextConfig = {
}, },
], ],
}, },
async headers() {
return [
{
source: '/~gitbook/static/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
{
key: 'Access-Control-Allow-Origin',
value: '*',
},
],
},
];
},
}; };
export default nextConfig; export default nextConfig;
+14 -6
View File
@@ -1,10 +1,18 @@
import { defineCloudflareConfig } from '@opennextjs/cloudflare'; import { defineCloudflareConfig } from '@opennextjs/cloudflare';
import d1TagCache from '@opennextjs/cloudflare/d1-tag-cache'; import r2IncrementalCache from '@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache';
import kvIncrementalCache from '@opennextjs/cloudflare/kv-cache'; import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache';
import memoryQueue from '@opennextjs/cloudflare/memory-queue'; import doQueue from '@opennextjs/cloudflare/overrides/queue/do-queue';
import doShardedTagCache from '@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache';
export default defineCloudflareConfig({ export default defineCloudflareConfig({
incrementalCache: kvIncrementalCache, incrementalCache: withRegionalCache(r2IncrementalCache, { mode: 'long-lived' }),
queue: memoryQueue, tagCache: doShardedTagCache({
tagCache: d1TagCache, baseShardSize: 12,
regionalCache: true,
shardReplication: {
numberOfSoftReplicas: 2,
numberOfHardReplicas: 1,
},
}),
queue: doQueue,
}); });
+12 -7
View File
@@ -1,20 +1,24 @@
{ {
"name": "gitbook-v2", "name": "gitbook-v2",
"version": "0.2.0", "version": "0.2.5",
"private": true, "private": true,
"dependencies": { "dependencies": {
"next": "^15.2.1", "next": "canary",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"@gitbook/api": "0.96.1", "@gitbook/api": "*",
"@gitbook/cache-tags": "workspace:*", "@gitbook/cache-tags": "workspace:*",
"@sindresorhus/fnv1a": "^3.1.0", "@sindresorhus/fnv1a": "^3.1.0",
"server-only": "^0.0.1", "server-only": "^0.0.1",
"warn-once": "^0.1.1" "warn-once": "^0.1.1",
"rison": "^0.1.1",
"jwt-decode": "^4.0.0",
"assert-never": "^1.2.1"
}, },
"devDependencies": { "devDependencies": {
"gitbook": "*", "gitbook": "*",
"@opennextjs/cloudflare": "https://pkg.pr.new/opennextjs/opennextjs-cloudflare/@opennextjs/cloudflare@40fec7d", "@opennextjs/cloudflare": "^1.0.0-beta.3",
"@types/rison": "^0.0.9",
"tailwindcss": "^3.4.0", "tailwindcss": "^3.4.0",
"postcss": "^8" "postcss": "^8"
}, },
@@ -24,8 +28,9 @@
"build": "next build", "build": "next build",
"build:v2": "next build", "build:v2": "next build",
"start": "next start", "start": "next start",
"build:v2:cloudflare": "opennextjs-cloudflare", "build:v2:cloudflare": "opennextjs-cloudflare build",
"dev:v2:cloudflare": "wrangler dev --port 8771", "dev:v2:cloudflare": "wrangler dev --port 8771",
"unit": "bun test" "unit": "bun test",
"typecheck": "tsc --noEmit"
} }
} }
@@ -13,26 +13,24 @@ type PageProps = {
export default async function Page(props: PageProps) { export default async function Page(props: PageProps) {
const params = await props.params; const params = await props.params;
const context = await getDynamicSiteContext(params); const { context } = await getDynamicSiteContext(params);
const pathname = getPagePathFromParams(params); const pathname = getPagePathFromParams(params);
return <SitePage context={context} pageParams={{ pathname }} redirectOnFallback={true} />; return <SitePage context={context} pageParams={{ pathname }} />;
} }
export async function generateViewport(props: PageProps): Promise<Viewport> { export async function generateViewport(props: PageProps): Promise<Viewport> {
const context = await getDynamicSiteContext(await props.params); const { context } = await getDynamicSiteContext(await props.params);
return generateSitePageViewport(context); return generateSitePageViewport(context);
} }
export async function generateMetadata(props: PageProps): Promise<Metadata> { export async function generateMetadata(props: PageProps): Promise<Metadata> {
const [params, searchParams] = await Promise.all([props.params, props.searchParams]); const params = await props.params;
const context = await getDynamicSiteContext(params); const { context } = await getDynamicSiteContext(params);
const pathname = getPagePathFromParams(params); const pathname = getPagePathFromParams(params);
return generateSitePageMetadata({ return generateSitePageMetadata({
context, context,
pageParams: { pathname }, pageParams: { pathname },
redirectOnFallback: true,
fallback: !!searchParams.fallback,
}); });
} }
@@ -6,7 +6,7 @@ import {
} from '@/components/SiteLayout'; } from '@/components/SiteLayout';
import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils'; import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils';
import { GITBOOK_DISABLE_TRACKING } from '@v2/lib/env'; import { GITBOOK_DISABLE_TRACKING } from '@v2/lib/env';
import { getThemeFromMiddleware, getVisitorAuthTokenFromMiddleware } from '@v2/lib/middleware'; import { getThemeFromMiddleware } from '@v2/lib/middleware';
interface SiteDynamicLayoutProps { interface SiteDynamicLayoutProps {
params: Promise<RouteLayoutParams>; params: Promise<RouteLayoutParams>;
@@ -16,17 +16,16 @@ export default async function SiteDynamicLayout({
params, params,
children, children,
}: React.PropsWithChildren<SiteDynamicLayoutProps>) { }: React.PropsWithChildren<SiteDynamicLayoutProps>) {
const context = await getDynamicSiteContext(await params); const { context, visitorAuthClaims } = await getDynamicSiteContext(await params);
const forcedTheme = await getThemeFromMiddleware(); const forcedTheme = await getThemeFromMiddleware();
const visitorAuthToken = await getVisitorAuthTokenFromMiddleware();
return ( return (
<CustomizationRootLayout customization={context.customization}> <CustomizationRootLayout forcedTheme={forcedTheme} customization={context.customization}>
<SiteLayout <SiteLayout
context={context} context={context}
forcedTheme={forcedTheme} forcedTheme={forcedTheme}
withTracking={!GITBOOK_DISABLE_TRACKING} withTracking={!GITBOOK_DISABLE_TRACKING}
visitorAuthToken={visitorAuthToken} visitorAuthClaims={visitorAuthClaims}
> >
{children} {children}
</SiteLayout> </SiteLayout>
@@ -35,11 +34,11 @@ export default async function SiteDynamicLayout({
} }
export async function generateViewport({ params }: SiteDynamicLayoutProps) { export async function generateViewport({ params }: SiteDynamicLayoutProps) {
const context = await getDynamicSiteContext(await params); const { context } = await getDynamicSiteContext(await params);
return generateSiteLayoutViewport(context); return generateSiteLayoutViewport(context);
} }
export async function generateMetadata({ params }: SiteDynamicLayoutProps) { export async function generateMetadata({ params }: SiteDynamicLayoutProps) {
const context = await getDynamicSiteContext(await params); const { context } = await getDynamicSiteContext(await params);
return generateSiteLayoutMetadata(context); return generateSiteLayoutMetadata(context);
} }
@@ -7,6 +7,6 @@ export async function GET(
request: NextRequest, request: NextRequest,
{ params }: { params: Promise<RouteLayoutParams> } { params }: { params: Promise<RouteLayoutParams> }
) { ) {
const context = await getDynamicSiteContext(await params); const { context } = await getDynamicSiteContext(await params);
return serveIcon(context, request); return serveIcon(context, request);
} }
@@ -8,6 +8,6 @@ export async function GET(
_request: NextRequest, _request: NextRequest,
{ params }: { params: Promise<RouteLayoutParams & PageIdParams> } { params }: { params: Promise<RouteLayoutParams & PageIdParams> }
) { ) {
const context = await getDynamicSiteContext(await params); const { context } = await getDynamicSiteContext(await params);
return serveOGImage(context, await params); return serveOGImage(context, await params);
} }
@@ -0,0 +1,12 @@
import { PDFRootLayout } from '@/components/PDF';
import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils';
export default async function RootLayout(props: {
params: Promise<RouteLayoutParams>;
children: React.ReactNode;
}) {
const { params, children } = props;
const { context } = await getDynamicSiteContext(await params);
return <PDFRootLayout context={context}>{children}</PDFRootLayout>;
}
@@ -0,0 +1,20 @@
import { PDFPage, generatePDFMetadata } from '@/components/PDF';
import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils';
export async function generateMetadata({
params,
}: {
params: Promise<RouteLayoutParams>;
}) {
const { context } = await getDynamicSiteContext(await params);
return generatePDFMetadata(context);
}
export default async function Page(props: {
params: Promise<RouteLayoutParams>;
searchParams: Promise<{ [key: string]: string }>;
}) {
const { params, searchParams } = props;
const { context } = await getDynamicSiteContext(await params);
return <PDFPage context={context} searchParams={await searchParams} />;
}
@@ -1,12 +0,0 @@
import type { NextRequest } from 'next/server';
import { serveLLMsTxt } from '@/routes/llms';
import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils';
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<RouteLayoutParams> }
) {
const context = await getDynamicSiteContext(await params);
return serveLLMsTxt(context);
}
@@ -1,12 +0,0 @@
import type { NextRequest } from 'next/server';
import { serveRobotsTxt } from '@/routes/robots';
import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils';
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<RouteLayoutParams> }
) {
const context = await getDynamicSiteContext(await params);
return serveRobotsTxt(context);
}
@@ -1,12 +0,0 @@
import type { NextRequest } from 'next/server';
import { servePagesSitemap } from '@/routes/sitemap';
import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils';
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<RouteLayoutParams> }
) {
const context = await getDynamicSiteContext(await params);
return servePagesSitemap(context);
}
@@ -1,12 +0,0 @@
import type { NextRequest } from 'next/server';
import { serveRootSitemap } from '@/routes/sitemap';
import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils';
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<RouteLayoutParams> }
) {
const context = await getDynamicSiteContext(await params);
return serveRootSitemap(context);
}
@@ -3,10 +3,9 @@ import {
generateSitePageMetadata, generateSitePageMetadata,
generateSitePageViewport, generateSitePageViewport,
} from '@/components/SitePage'; } from '@/components/SitePage';
import { getCacheTag } from '@gitbook/cache-tags';
import { type RouteParams, getPagePathFromParams, getStaticSiteContext } from '@v2/app/utils'; import { type RouteParams, getPagePathFromParams, getStaticSiteContext } from '@v2/app/utils';
import type { Metadata, Viewport } from 'next'; import type { Metadata, Viewport } from 'next';
import { unstable_cacheTag as cacheTag } from 'next/cache';
export const dynamic = 'force-static'; export const dynamic = 'force-static';
@@ -15,35 +14,25 @@ type PageProps = {
}; };
export default async function Page(props: PageProps) { export default async function Page(props: PageProps) {
'use cache';
const params = await props.params; const params = await props.params;
const context = await getStaticSiteContext(params); const { context } = await getStaticSiteContext(params);
const pathname = getPagePathFromParams(params); const pathname = getPagePathFromParams(params);
cacheTag( return <SitePage context={context} pageParams={{ pathname }} />;
getCacheTag({
tag: 'site',
site: context.site.id,
})
);
return <SitePage context={context} pageParams={{ pathname }} redirectOnFallback={true} />;
} }
export async function generateViewport(props: PageProps): Promise<Viewport> { export async function generateViewport(props: PageProps): Promise<Viewport> {
const context = await getStaticSiteContext(await props.params); const { context } = await getStaticSiteContext(await props.params);
return generateSitePageViewport(context); return generateSitePageViewport(context);
} }
export async function generateMetadata(props: PageProps): Promise<Metadata> { export async function generateMetadata(props: PageProps): Promise<Metadata> {
const params = await props.params; const params = await props.params;
const context = await getStaticSiteContext(params); const { context } = await getStaticSiteContext(params);
const pathname = getPagePathFromParams(params); const pathname = getPagePathFromParams(params);
return generateSitePageMetadata({ return generateSitePageMetadata({
context, context,
pageParams: { pathname }, pageParams: { pathname },
redirectOnFallback: true,
}); });
} }
@@ -4,10 +4,8 @@ import {
generateSiteLayoutMetadata, generateSiteLayoutMetadata,
generateSiteLayoutViewport, generateSiteLayoutViewport,
} from '@/components/SiteLayout'; } from '@/components/SiteLayout';
import { getCacheTag } from '@gitbook/cache-tags';
import { type RouteLayoutParams, getStaticSiteContext } from '@v2/app/utils'; import { type RouteLayoutParams, getStaticSiteContext } from '@v2/app/utils';
import { GITBOOK_DISABLE_TRACKING } from '@v2/lib/env'; import { GITBOOK_DISABLE_TRACKING } from '@v2/lib/env';
import { unstable_cacheTag as cacheTag } from 'next/cache';
interface SiteStaticLayoutProps { interface SiteStaticLayoutProps {
params: Promise<RouteLayoutParams>; params: Promise<RouteLayoutParams>;
@@ -17,23 +15,14 @@ export default async function SiteStaticLayout({
params, params,
children, children,
}: React.PropsWithChildren<SiteStaticLayoutProps>) { }: React.PropsWithChildren<SiteStaticLayoutProps>) {
'use cache'; const { context, visitorAuthClaims } = await getStaticSiteContext(await params);
const context = await getStaticSiteContext(await params);
cacheTag(
getCacheTag({
tag: 'site',
site: context.site.id,
})
);
return ( return (
<CustomizationRootLayout customization={context.customization}> <CustomizationRootLayout customization={context.customization}>
<SiteLayout <SiteLayout
context={context} context={context}
withTracking={!GITBOOK_DISABLE_TRACKING} withTracking={!GITBOOK_DISABLE_TRACKING}
visitorAuthToken={null} visitorAuthClaims={visitorAuthClaims}
> >
{children} {children}
</SiteLayout> </SiteLayout>
@@ -42,11 +31,11 @@ export default async function SiteStaticLayout({
} }
export async function generateViewport({ params }: SiteStaticLayoutProps) { export async function generateViewport({ params }: SiteStaticLayoutProps) {
const context = await getStaticSiteContext(await params); const { context } = await getStaticSiteContext(await params);
return generateSiteLayoutViewport(context); return generateSiteLayoutViewport(context);
} }
export async function generateMetadata({ params }: SiteStaticLayoutProps) { export async function generateMetadata({ params }: SiteStaticLayoutProps) {
const context = await getStaticSiteContext(await params); const { context } = await getStaticSiteContext(await params);
return generateSiteLayoutMetadata(context); return generateSiteLayoutMetadata(context);
} }
@@ -9,6 +9,6 @@ export async function GET(
_request: NextRequest, _request: NextRequest,
{ params }: { params: Promise<RouteLayoutParams> } { params }: { params: Promise<RouteLayoutParams> }
) { ) {
const context = await getStaticSiteContext(await params); const { context } = await getStaticSiteContext(await params);
return serveLLMsTxt(context); return serveLLMsTxt(context, { withMarkdownPages: true });
} }
@@ -9,6 +9,6 @@ export async function GET(
_request: NextRequest, _request: NextRequest,
{ params }: { params: Promise<RouteLayoutParams> } { params }: { params: Promise<RouteLayoutParams> }
) { ) {
const context = await getStaticSiteContext(await params); const { context } = await getStaticSiteContext(await params);
return serveRobotsTxt(context); return serveRobotsTxt(context);
} }
@@ -9,6 +9,6 @@ export async function GET(
_request: NextRequest, _request: NextRequest,
{ params }: { params: Promise<RouteLayoutParams> } { params }: { params: Promise<RouteLayoutParams> }
) { ) {
const context = await getStaticSiteContext(await params); const { context } = await getStaticSiteContext(await params);
return servePagesSitemap(context); return servePagesSitemap(context);
} }
@@ -9,6 +9,6 @@ export async function GET(
_request: NextRequest, _request: NextRequest,
{ params }: { params: Promise<RouteLayoutParams> } { params }: { params: Promise<RouteLayoutParams> }
) { ) {
const context = await getStaticSiteContext(await params); const { context } = await getStaticSiteContext(await params);
return serveRootSitemap(context); return serveRootSitemap(context);
} }
@@ -9,6 +9,6 @@ export async function GET(
request: NextRequest, request: NextRequest,
{ params }: { params: Promise<RouteLayoutParams> } { params }: { params: Promise<RouteLayoutParams> }
) { ) {
const context = await getStaticSiteContext(await params); const { context } = await getStaticSiteContext(await params);
return serveIcon(context, request); return serveIcon(context, request);
} }
@@ -0,0 +1,11 @@
import { servePageMarkdown } from '@/routes/markdownPage';
import { type RouteParams, getPagePathFromParams, getStaticSiteContext } from '@v2/app/utils';
import type { NextRequest } from 'next/server';
export const dynamic = 'force-static';
export async function GET(_request: NextRequest, { params }: { params: Promise<RouteParams> }) {
const { context } = await getStaticSiteContext(await params);
const pathname = getPagePathFromParams(await params);
return servePageMarkdown(context, pathname);
}
@@ -10,6 +10,6 @@ export async function GET(
_request: NextRequest, _request: NextRequest,
{ params }: { params: Promise<RouteLayoutParams & PageIdParams> } { params }: { params: Promise<RouteLayoutParams & PageIdParams> }
) { ) {
const context = await getStaticSiteContext(await params); const { context } = await getStaticSiteContext(await params);
return serveOGImage(context, await params); return serveOGImage(context, await params);
} }
+43 -15
View File
@@ -1,9 +1,9 @@
import { import { getVisitorAuthClaims, getVisitorAuthClaimsFromToken } from '@/lib/adaptive';
fetchSiteContextByURL, import type { SiteAPIToken } from '@gitbook/api';
fetchSiteContextByURLLookup, import { type SiteURLData, fetchSiteContextByURLLookup, getBaseContext } from '@v2/lib/context';
getBaseContext, import { jwtDecode } from 'jwt-decode';
} from '@v2/lib/context'; import { forbidden } from 'next/navigation';
import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware'; import rison from 'rison';
export type RouteParamMode = 'url-host' | 'url'; export type RouteParamMode = 'url-host' | 'url';
@@ -12,6 +12,9 @@ export type RouteLayoutParams = {
/** URL encoded site URL */ /** URL encoded site URL */
siteURL: string; siteURL: string;
/** URL and Rison encoded site data from getPublishedContentByUrl */
siteData: string;
}; };
export type RouteParams = RouteLayoutParams & { export type RouteParams = RouteLayoutParams & {
@@ -21,19 +24,30 @@ export type RouteParams = RouteLayoutParams & {
/** /**
* Get the static context when rendering statically a site. * Get the static context when rendering statically a site.
*/ */
export function getStaticSiteContext(params: RouteLayoutParams) { export async function getStaticSiteContext(params: RouteLayoutParams) {
const siteURL = getSiteURLFromParams(params); const siteURL = getSiteURLFromParams(params);
return fetchSiteContextByURL( const siteURLData = getSiteURLDataFromParams(params);
// For static routes, we check the expiration of the JWT token
// as the route might be revalidated after expiration
const decoded = jwtDecode<SiteAPIToken & { exp: number }>(siteURLData.apiToken);
if (decoded.exp && decoded.exp < Date.now() / 1000 + 120) {
forbidden();
}
const context = await fetchSiteContextByURLLookup(
getBaseContext({ getBaseContext({
siteURL, siteURL,
siteURLData,
urlMode: getModeFromParams(params.mode), urlMode: getModeFromParams(params.mode),
}), }),
{ siteURLData
url: siteURL.toString(),
visitorAuthToken: null,
redirectOnError: false,
}
); );
return {
context,
visitorAuthClaims: getVisitorAuthClaimsFromToken(decoded),
};
} }
/** /**
@@ -42,15 +56,21 @@ export function getStaticSiteContext(params: RouteLayoutParams) {
*/ */
export async function getDynamicSiteContext(params: RouteLayoutParams) { export async function getDynamicSiteContext(params: RouteLayoutParams) {
const siteURL = getSiteURLFromParams(params); const siteURL = getSiteURLFromParams(params);
const siteURLData = await getSiteURLDataFromMiddleware(); const siteURLData = getSiteURLDataFromParams(params);
return fetchSiteContextByURLLookup( const context = await fetchSiteContextByURLLookup(
getBaseContext({ getBaseContext({
siteURL, siteURL,
siteURLData,
urlMode: getModeFromParams(params.mode), urlMode: getModeFromParams(params.mode),
}), }),
siteURLData siteURLData
); );
return {
context,
visitorAuthClaims: getVisitorAuthClaims(siteURLData),
};
} }
/** /**
@@ -74,3 +94,11 @@ function getModeFromParams(mode: string): RouteParamMode {
return 'url'; return 'url';
} }
/**
* Get the decoded site data from the params.
*/
function getSiteURLDataFromParams(params: RouteLayoutParams): SiteURLData {
const decoded = decodeURIComponent(params.siteData);
return rison.decode(decoded);
}
+14
View File
@@ -1,11 +1,17 @@
import { type NextRequest, NextResponse } from 'next/server'; import { type NextRequest, NextResponse } from 'next/server';
import { import {
GITBOOK_API_PUBLIC_URL,
GITBOOK_API_TOKEN,
GITBOOK_API_URL, GITBOOK_API_URL,
GITBOOK_APP_URL, GITBOOK_APP_URL,
GITBOOK_ASSETS_URL, GITBOOK_ASSETS_URL,
GITBOOK_DISABLE_TRACKING, GITBOOK_DISABLE_TRACKING,
GITBOOK_FONTS_URL,
GITBOOK_ICONS_URL,
GITBOOK_IMAGE_RESIZE_SIGNING_KEY,
GITBOOK_INTEGRATIONS_HOST, GITBOOK_INTEGRATIONS_HOST,
GITBOOK_SECRET,
GITBOOK_URL, GITBOOK_URL,
GITBOOK_USER_AGENT, GITBOOK_USER_AGENT,
} from '@v2/lib/env'; } from '@v2/lib/env';
@@ -18,9 +24,17 @@ export async function GET(_req: NextRequest) {
GITBOOK_URL, GITBOOK_URL,
GITBOOK_APP_URL, GITBOOK_APP_URL,
GITBOOK_API_URL, GITBOOK_API_URL,
GITBOOK_API_PUBLIC_URL,
GITBOOK_ASSETS_URL, GITBOOK_ASSETS_URL,
GITBOOK_FONTS_URL,
GITBOOK_ICONS_URL,
GITBOOK_USER_AGENT, GITBOOK_USER_AGENT,
GITBOOK_INTEGRATIONS_HOST, GITBOOK_INTEGRATIONS_HOST,
GITBOOK_DISABLE_TRACKING, GITBOOK_DISABLE_TRACKING,
// Secret envs
GITBOOK_SECRET: !!GITBOOK_SECRET,
GITBOOK_API_TOKEN: !!GITBOOK_API_TOKEN,
GITBOOK_IMAGE_RESIZE_SIGNING_KEY: !!GITBOOK_IMAGE_RESIZE_SIGNING_KEY,
}); });
} }
@@ -0,0 +1,59 @@
import {
type GitBookBaseContext,
type GitBookSpaceContext,
fetchSpaceContextByIds,
} from '@v2/lib/context';
import { createDataFetcher } from '@v2/lib/data';
import { createLinker } from '@v2/lib/links';
import { getAPITokenFromMiddleware } from '@v2/lib/middleware';
export type SpacePDFRouteParams = {
spaceId: string;
changeRequestId?: string;
revisionId?: string;
};
export async function getSpacePDFContext(
params: SpacePDFRouteParams
): Promise<GitBookSpaceContext> {
const { spaceId } = params;
const apiToken = await getAPITokenFromMiddleware();
const basePath = getPDFRoutePath(params);
const linker = createLinker({
spaceBasePath: basePath,
siteBasePath: basePath,
});
const dataFetcher = createDataFetcher({
apiToken: apiToken,
});
const baseContext: GitBookBaseContext = {
linker,
dataFetcher,
};
return await fetchSpaceContextByIds(baseContext, {
space: spaceId,
shareKey: undefined,
changeRequest: params.changeRequestId,
revision: params.revisionId,
});
}
function getPDFRoutePath(params: SpacePDFRouteParams) {
let path = `/~space/${params.spaceId}`;
if (params.changeRequestId) {
path += `/~/changes/${params.changeRequestId}`;
}
if (params.revisionId) {
path += `/~/revisions/${params.revisionId}`;
}
path += '~gitbook/pdf';
return path;
}
@@ -0,0 +1,2 @@
import RootLayout from '@v2/app/~space/[spaceId]/~gitbook/pdf/layout';
export default RootLayout;
@@ -0,0 +1,4 @@
import PDFPage, { generateMetadata } from '@v2/app/~space/[spaceId]/~gitbook/pdf/page';
export default PDFPage;
export { generateMetadata };
@@ -0,0 +1,2 @@
import RootLayout from '@v2/app/~space/[spaceId]/~gitbook/pdf/layout';
export default RootLayout;
@@ -0,0 +1,4 @@
import PDFPage, { generateMetadata } from '@v2/app/~space/[spaceId]/~gitbook/pdf/page';
export default PDFPage;
export { generateMetadata };
@@ -0,0 +1,12 @@
import { PDFRootLayout } from '@/components/PDF';
import { type SpacePDFRouteParams, getSpacePDFContext } from '@v2/app/~space/[spaceId]/pdf';
export default async function RootLayout(props: {
params: Promise<SpacePDFRouteParams>;
children: React.ReactNode;
}) {
const { params, children } = props;
const context = await getSpacePDFContext(await params);
return <PDFRootLayout context={context}>{children}</PDFRootLayout>;
}
@@ -0,0 +1,20 @@
import { PDFPage, generatePDFMetadata } from '@/components/PDF';
import { type SpacePDFRouteParams, getSpacePDFContext } from '@v2/app/~space/[spaceId]/pdf';
export async function generateMetadata({
params,
}: {
params: Promise<SpacePDFRouteParams>;
}) {
const context = await getSpacePDFContext(await params);
return generatePDFMetadata(context);
}
export default async function Page(props: {
params: Promise<SpacePDFRouteParams>;
searchParams: Promise<{ [key: string]: string }>;
}) {
const { params, searchParams } = props;
const context = await getSpacePDFContext(await params);
return <PDFPage context={context} searchParams={await searchParams} />;
}
+76 -69
View File
@@ -1,7 +1,7 @@
import { getSiteStructureSections } from '@/lib/sites'; import { getSiteStructureSections } from '@/lib/sites';
import type { import type {
ChangeRequest, ChangeRequest,
PublishedSiteContentLookup, PublishedSiteContent,
RevisionPage, RevisionPage,
RevisionPageDocument, RevisionPageDocument,
Site, Site,
@@ -13,12 +13,41 @@ import type {
SiteStructure, SiteStructure,
Space, Space,
} from '@gitbook/api'; } from '@gitbook/api';
import { type GitBookDataFetcher, createDataFetcher, throwIfDataError } from '@v2/lib/data'; import {
import { redirect } from 'next/navigation'; type GitBookDataFetcher,
createDataFetcher,
getDataOrNull,
throwIfDataError,
} from '@v2/lib/data';
import { notFound } from 'next/navigation';
import { assert } from 'ts-essentials'; import { assert } from 'ts-essentials';
import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_URL } from './env'; import { GITBOOK_URL } from './env';
import { type ImageResizer, createImageResizer } from './images'; import { type ImageResizer, createImageResizer } from './images';
import { type GitBookSpaceLinker, createLinker } from './links'; import { type GitBookLinker, createLinker } from './links';
/**
* Data about the site URL. Provided by the middleware.
* These data are stable between pages in the same site space.
*/
export type SiteURLData = Pick<
PublishedSiteContent,
| 'organization'
| 'apiToken'
| 'site'
| 'siteSpace'
| 'space'
| 'revision'
| 'changeRequest'
| 'shareKey'
| 'siteSection'
| 'siteBasePath'
| 'basePath'
> & {
/**
* Identifier used for image resizing.
*/
imagesContextId: string;
};
/** /**
* Generic context when rendering content. * Generic context when rendering content.
@@ -32,7 +61,7 @@ export type GitBookBaseContext = {
/** /**
* Linker to generate links in the current space. * Linker to generate links in the current space.
*/ */
linker: GitBookSpaceLinker; linker: GitBookLinker;
/** /**
* Image resizer to resize images. * Image resizer to resize images.
@@ -102,44 +131,45 @@ export type GitBookPageContext = (GitBookSpaceContext | GitBookSiteContext) & {
}; };
/** /**
* Get the base context for a request. * Get the base context for a request on a site.
*/ */
export function getBaseContext(input: { export function getBaseContext(input: {
siteURL: URL | string; siteURL: URL | string;
siteURLData: SiteURLData;
urlMode: 'url' | 'url-host'; urlMode: 'url' | 'url-host';
apiToken?: string | null;
}) { }) {
const url = typeof input.siteURL === 'string' ? new URL(input.siteURL) : input.siteURL; const { urlMode, siteURLData } = input;
const urlMode = input.urlMode; const siteURL = typeof input.siteURL === 'string' ? new URL(input.siteURL) : input.siteURL;
const dataFetcher = createDataFetcher({ const dataFetcher = createDataFetcher({
apiToken: input.apiToken ?? GITBOOK_API_TOKEN, apiToken: siteURLData.apiToken ?? null,
apiEndpoint: GITBOOK_API_URL,
}); });
const gitbookURL = GITBOOK_URL ? new URL(GITBOOK_URL) : undefined;
const gitbookURL = GITBOOK_URL ? new URL(GITBOOK_URL) : undefined;
const linker = const linker =
urlMode === 'url-host' urlMode === 'url-host'
? createLinker({ ? createLinker({
host: url.host, host: siteURL.host,
pathname: url.pathname, siteBasePath: siteURLData.siteBasePath,
spaceBasePath: siteURLData.basePath,
}) })
: createLinker({ : createLinker({
protocol: gitbookURL?.protocol, protocol: gitbookURL?.protocol,
host: gitbookURL?.host, host: gitbookURL?.host,
pathname: `/url/${url.host}${url.pathname}`, siteBasePath: `/url/${siteURL.host}${siteURLData.siteBasePath}`,
spaceBasePath: `/url/${siteURL.host}${siteURLData.basePath}`,
}); });
if (urlMode === 'url') { if (urlMode === 'url') {
// Create link in the same format for links to other sites/sections. // Create link in the same format for links to other sites/sections.
linker.toLinkForContent = (rawURL: string) => { linker.toLinkForContent = (rawURL: string) => {
const urlObject = new URL(rawURL); const urlObject = new URL(rawURL);
return `/url/${urlObject.host}${urlObject.pathname}`; return `/url/${urlObject.host}${urlObject.pathname}${urlObject.search}`;
}; };
} }
const imageResizer = createImageResizer({ const imageResizer = createImageResizer({
host: url.host, imagesContextId: siteURLData.imagesContextId,
// To ensure image resizing work for proxied sites, // To ensure image resizing work for proxied sites,
// we serve images from the root of the site. // we serve images from the root of the site.
linker: linker, linker: linker,
@@ -152,59 +182,23 @@ export function getBaseContext(input: {
}; };
} }
/**
* Fetch the context of a site for a given URL and a base context.
*/
export async function fetchSiteContextByURL(
baseContext: GitBookBaseContext,
input: {
url: string;
visitorAuthToken: string | null;
redirectOnError: boolean;
}
): Promise<GitBookSiteContext> {
const { dataFetcher } = baseContext;
const data = await throwIfDataError(
dataFetcher.getPublishedContentByUrl({
url: input.url,
visitorAuthToken: input.visitorAuthToken,
redirectOnError: input.redirectOnError,
})
);
return fetchSiteContextByURLLookup(baseContext, data);
}
/** /**
* Fetch the context of a site using the resolution of a URL * Fetch the context of a site using the resolution of a URL
*/ */
export async function fetchSiteContextByURLLookup( export async function fetchSiteContextByURLLookup(
baseContext: GitBookBaseContext, baseContext: GitBookBaseContext,
data: PublishedSiteContentLookup data: SiteURLData
): Promise<GitBookSiteContext> { ): Promise<GitBookSiteContext> {
const { dataFetcher } = baseContext; return await fetchSiteContextByIds(baseContext, {
if ('redirect' in data) { organization: data.organization,
redirect(data.redirect); site: data.site,
} siteSection: data.siteSection,
siteSpace: data.siteSpace,
return await fetchSiteContextByIds( space: data.space,
{ shareKey: data.shareKey,
...baseContext, changeRequest: data.changeRequest,
dataFetcher: dataFetcher.withToken({ revision: data.revision,
apiToken: data.apiToken, });
}),
},
{
organization: data.organization,
site: data.site,
siteSection: data.siteSection,
siteSpace: data.siteSpace,
space: data.space,
shareKey: data.shareKey,
changeRequest: data.changeRequest,
revision: data.revision,
}
);
} }
/** /**
@@ -313,7 +307,7 @@ export async function fetchSpaceContextByIds(
}) })
), ),
ids.changeRequest ids.changeRequest
? throwIfDataError( ? getDataOrNull(
dataFetcher.getChangeRequest({ dataFetcher.getChangeRequest({
spaceId: ids.space, spaceId: ids.space,
changeRequestId: ids.changeRequest, changeRequestId: ids.changeRequest,
@@ -322,17 +316,30 @@ export async function fetchSpaceContextByIds(
: null, : null,
]); ]);
const revisionId = changeRequest?.revision ?? ids.revision ?? space.revision; if (ids.changeRequest && !changeRequest) {
// When trying to render a change request with an invalid / non-existing ID,
// we should return a 404.
notFound();
}
const pages = await throwIfDataError( const revisionId = ids.revision ?? changeRequest?.revision ?? space.revision;
const pages = await getDataOrNull(
dataFetcher.getRevisionPages({ dataFetcher.getRevisionPages({
spaceId: ids.space, spaceId: ids.space,
revisionId, revisionId,
// We only care about the Git metadata when the Git sync is enabled, // We only care about the Git metadata when the Git sync is enabled,
// otherwise we can optimize performance by not fetching it // otherwise we can optimize performance by not fetching it
metadata: !!space.gitSync, metadata: !!space.gitSync,
}) }),
// When trying to render a revision with an invalid / non-existing ID,
// we should handle gracefully the 404 and throw notFound.
ids.revision ? [404] : undefined
); );
if (!pages) {
notFound();
}
return { return {
...baseContext, ...baseContext,
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,25 @@
import { getCloudflareContext as getCloudflareContextOpenNext } from '@opennextjs/cloudflare';
import { GITBOOK_RUNTIME } from '../env';
/**
* Return the Cloudflare context or null when not running in Cloudflare.
*/
export function getCloudflareContext() {
if (GITBOOK_RUNTIME !== 'cloudflare') {
return null;
}
return getCloudflareContextOpenNext();
}
/**
* Return an object representing the current request.
*/
export function getCloudflareRequestGlobal() {
const context = getCloudflareContext();
if (!context) {
return null;
}
return context.cf;
}
+12 -1
View File
@@ -93,8 +93,15 @@ export async function wrapDataFetcherError<T>(
} }
} }
function getExposableError(error: Error): DataFetcherErrorData { /**
* Get a data fetcher exposable error from a JS error.
*/
export function getExposableError(error: Error): DataFetcherErrorData {
if (error instanceof GitBookAPIError) { if (error instanceof GitBookAPIError) {
if (error.code >= 500) {
throw error;
}
return { return {
code: error.code, code: error.code,
message: error.errorMessage, message: error.errorMessage,
@@ -102,6 +109,10 @@ function getExposableError(error: Error): DataFetcherErrorData {
} }
if (error instanceof DataFetcherError) { if (error instanceof DataFetcherError) {
if (error.code >= 500) {
throw error;
}
return { return {
code: error.code, code: error.code,
message: error.message, message: error.message,
@@ -3,3 +3,6 @@ export * from './types';
export * from './pages'; export * from './pages';
export * from './urls'; export * from './urls';
export * from './errors'; export * from './errors';
export * from './lookup';
export * from './proxy';
export * from './visitor';
+140
View File
@@ -0,0 +1,140 @@
import { race, tryCatch } from '@/lib/async';
import { joinPath, joinPathWithBaseURL } from '@/lib/paths';
import { trace } from '@/lib/tracing';
import type { PublishedSiteContentLookup } from '@gitbook/api';
import { apiClient } from './api';
import { getExposableError } from './errors';
import type { DataFetcherResponse } from './types';
import { getURLLookupAlternatives, stripURLSearch } from './urls';
/**
* Lookup a content by its URL using the GitBook API.
* To optimize caching, we try multiple lookup alternatives and return the first one that matches.
*/
export async function getPublishedContentByURL(input: {
url: string;
visitorAuthToken: string | null;
redirectOnError: boolean;
apiToken: string | null;
}): Promise<DataFetcherResponse<PublishedSiteContentLookup>> {
const lookupURL = new URL(input.url);
const url = stripURLSearch(lookupURL);
const lookup = getURLLookupAlternatives(url);
const result = await race(lookup.urls, async (alternative, { signal }) => {
const api = await apiClient({ apiToken: input.apiToken });
const callResult = await trace(
{
operation: 'getPublishedContentByURL',
name: alternative.url,
},
() =>
tryCatch(
api.urls.getPublishedContentByUrl(
{
url: alternative.url,
visitorAuthToken: input.visitorAuthToken ?? undefined,
redirectOnError: input.redirectOnError,
// As this endpoint is cached by our API, we version the request
// to void getting stale data with missing properties.
// this could be improved by ensuring our API cache layer is versioned
// or invalidated when needed
// @ts-expect-error - cacheVersion is not a real query param
cacheVersion: 'v2',
},
{
signal,
}
)
)
);
if (callResult.error) {
if (alternative.primary) {
// We only return an error for the primary alternative (full URL),
// as other parts could result in errors due to the URL being incomplete (share links, etc).
return { error: callResult.error };
}
return null;
}
const {
data: { data },
} = callResult;
if ('redirect' in data) {
if (alternative.primary) {
// Append the path to the redirect URL
// because we might have matched a shorter path and the redirect is relative to it
if (alternative.extraPath) {
if (data.target === 'content') {
const redirect = new URL(data.redirect);
redirect.pathname = joinPath(redirect.pathname, alternative.extraPath);
data.redirect = redirect.toString();
} else {
const redirect = new URL(data.redirect);
if (redirect.searchParams.has('location')) {
redirect.searchParams.set(
'location',
joinPath(
redirect.searchParams.get('location') ?? '',
alternative.extraPath
)
);
data.redirect = redirect.toString();
}
}
}
return { data };
}
return null;
}
/**
* We use the following criteria to determine if the lookup result is the right one:
* - the primary alternative was resolved (because that's the longest or most inclusive path)
* - the resolution of the site URL is complete (because we want to resolve the deepest path possible)
*
* In both cases, the idea is to use the deepest/longest/most inclusive path to resolve the content.
*/
if (alternative.primary || ('site' in data && data.complete)) {
const changeRequest = data.changeRequest ?? lookup.changeRequest;
const revision = data.revision ?? lookup.revision;
const siteResult: PublishedSiteContentLookup = {
...data,
canonicalUrl: joinPathWithBaseURL(data.canonicalUrl, alternative.extraPath),
basePath: joinPath(data.basePath, lookup.basePath ?? ''),
pathname: joinPath(data.pathname, alternative.extraPath),
...(changeRequest ? { changeRequest } : {}),
...(revision ? { revision } : {}),
};
return { data: siteResult };
}
return null;
});
if (!result) {
return {
error: {
code: 404,
message: 'No content found',
},
};
}
if (result.error) {
return {
error: getExposableError(result.error),
};
}
return {
data: result.data,
};
}
@@ -0,0 +1,52 @@
import { describe, expect, it, mock } from 'bun:test';
import { AsyncLocalStorage } from 'node:async_hooks';
import { withCacheKey, withoutConcurrentExecution } from './memoize';
describe('withoutConcurrentExecution', () => {
it('should memoize the function based on the cache key', async () => {
const fn = mock(async (_cacheKey: string, a: number, b: number) => a + b);
const memoized = withoutConcurrentExecution(() => null, fn);
const p1 = memoized('c1', 1, 2);
const p2 = memoized('c1', 1, 2);
const p3 = memoized('c3', 2, 3);
expect(await p1).toBe(await p2);
expect(await p1).not.toBe(await p3);
expect(fn.mock.calls.length).toBe(2);
});
it('should support caching per request', async () => {
const fn = mock(async () => Math.random());
const request1 = { id: 'request1' };
const request2 = { id: 'request2' };
const requestContext = new AsyncLocalStorage<{ id: string }>();
const memoized = withoutConcurrentExecution(() => requestContext.getStore(), fn);
// Both in the same request
const promise1 = requestContext.run(request1, () => memoized('c1'));
const promise2 = requestContext.run(request1, () => memoized('c1'));
// In a different request
const promise3 = requestContext.run(request2, () => memoized('c1'));
expect(await promise1).toBe(await promise2);
expect(await promise1).not.toBe(await promise3);
expect(fn.mock.calls.length).toBe(2);
});
});
describe('withCacheKey', () => {
it('should wrap the function by passing the cache key', async () => {
const fn = mock(
async (cacheKey: string, arg: { a: number; b: number }, c: number) =>
`${cacheKey}, result=${arg.a + arg.b + c}`
);
const memoized = withCacheKey(fn);
expect(await memoized({ a: 1, b: 2 }, 4)).toBe('[[["a",1],["b",2]],4], result=7');
expect(fn.mock.calls.length).toBe(1);
});
});
@@ -0,0 +1,87 @@
/**
* Wrap a function by preventing concurrent executions of the same function.
* With a logic to work per-request in Cloudflare Workers.
*/
export function withoutConcurrentExecution<ArgsType extends any[], ReturnType>(
getGlobalContext: () => object | null | undefined,
wrapped: (key: string, ...args: ArgsType) => Promise<ReturnType>
): (cacheKey: string, ...args: ArgsType) => Promise<ReturnType> {
const globalPromiseCache = new WeakMap<object, Map<string, Promise<ReturnType>>>();
return (key: string, ...args: ArgsType) => {
const globalContext = getGlobalContext() ?? globalThis;
/**
* Cache storage that is scoped to the current request when executed in Cloudflare Workers,
* to avoid "Cannot perform I/O on behalf of a different request" errors.
*/
const promiseCache =
globalPromiseCache.get(globalContext) ?? new Map<string, Promise<ReturnType>>();
globalPromiseCache.set(globalContext, promiseCache);
const concurrent = promiseCache.get(key);
if (concurrent) {
return concurrent;
}
const promise = (async () => {
try {
const result = await wrapped(key, ...args);
return result;
} finally {
promiseCache.delete(key);
}
})();
promiseCache.set(key, promise);
return promise;
};
}
/**
* Wrap a function by passing it a cache key that is computed from the function arguments.
*/
export function withCacheKey<ArgsType extends any[], ReturnType>(
wrapped: (cacheKey: string, ...args: ArgsType) => Promise<ReturnType>
): (...args: ArgsType) => Promise<ReturnType> {
return (...args: ArgsType) => {
const cacheKey = getCacheKey(args);
return wrapped(cacheKey, ...args);
};
}
/**
* Compute a cache key from the function arguments.
*/
function getCacheKey(args: any[]) {
return JSON.stringify(deepSortValue(args));
}
function deepSortValue(value: unknown): unknown {
if (
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean' ||
value === null ||
value === undefined
) {
return value;
}
if (Array.isArray(value)) {
return value.map(deepSortValue);
}
if (value && typeof value === 'object') {
return Object.entries(value)
.map(([key, subValue]) => {
return [key, deepSortValue(subValue)] as const;
})
.sort((a, b) => {
return a[0].localeCompare(b[0]);
});
}
return value;
}
+2 -1
View File
@@ -15,12 +15,13 @@ export async function getPageDocument(
dataFetcher.getDocument({ spaceId: space.id, documentId: page.documentId }) dataFetcher.getDocument({ spaceId: space.id, documentId: page.documentId })
); );
} }
if (page.computed) { if ('computed' in page && page.computed) {
return getDataOrNull( return getDataOrNull(
dataFetcher.getComputedDocument({ dataFetcher.getComputedDocument({
organizationId: space.organization, organizationId: space.organization,
spaceId: space.id, spaceId: space.id,
source: page.computed, source: page.computed,
seed: page.computedSeed,
}) })
); );
} }
@@ -0,0 +1,21 @@
import { describe, expect, it } from 'bun:test';
import { getProxyRequestIdentifier, isProxyRequest } from './proxy';
describe('isProxyRequest', () => {
it('should return true for proxy requests', () => {
const proxyRequestURL = new URL('https://proxy.gitbook.site/sites/site_foo/hello/world');
expect(isProxyRequest(proxyRequestURL)).toBe(true);
});
it('should return false for non-proxy requests', () => {
const nonProxyRequestURL = new URL('https://example.com/docs/foo/hello/world');
expect(isProxyRequest(nonProxyRequestURL)).toBe(false);
});
});
describe('getProxyRequestIdentifier', () => {
it('should return the correct identifier for proxy requests', () => {
const proxyRequestURL = new URL('https://proxy.gitbook.site/sites/site_foo/hello/world');
expect(getProxyRequestIdentifier(proxyRequestURL)).toBe('sites/site_foo');
});
});
+15
View File
@@ -0,0 +1,15 @@
/**
* Check if the request to the site was through a proxy.
*/
export function isProxyRequest(requestURL: URL): boolean {
return (
requestURL.host === 'proxy.gitbook.site' || requestURL.host === 'proxy.gitbook-staging.site'
);
}
export function getProxyRequestIdentifier(requestURL: URL): string {
// For proxy requests, we extract the site ID from the pathname
// e.g. https://proxy.gitbook.site/site/siteId/...
const pathname = requestURL.pathname.slice(1).split('/');
return pathname.slice(0, 2).join('/');
}
+29 -14
View File
@@ -20,11 +20,6 @@ export type DataFetcherResponse<T> =
* It is used between v1 and v2. * It is used between v1 and v2.
*/ */
export interface GitBookDataFetcher { export interface GitBookDataFetcher {
/**
* Endpoint of the API.
*/
apiEndpoint: string;
/** /**
* Get an API client for the current context. * Get an API client for the current context.
*/ */
@@ -42,15 +37,6 @@ export interface GitBookDataFetcher {
*/ */
getUserById(userId: string): Promise<DataFetcherResponse<api.User>>; getUserById(userId: string): Promise<DataFetcherResponse<api.User>>;
/**
* Get a published content by its URL.
*/
getPublishedContentByUrl(params: {
url: string;
visitorAuthToken: string | null;
redirectOnError: boolean;
}): Promise<DataFetcherResponse<api.PublishedSiteContentLookup>>;
/** /**
* Get a published content site by its organization ID and site ID. * Get a published content site by its organization ID and site ID.
*/ */
@@ -111,6 +97,15 @@ export interface GitBookDataFetcher {
path: string; path: string;
}): Promise<DataFetcherResponse<api.RevisionPageDocument | api.RevisionPageGroup>>; }): Promise<DataFetcherResponse<api.RevisionPageDocument | api.RevisionPageGroup>>;
/**
* Get the markdown content of a page by its path.
*/
getRevisionPageMarkdown(params: {
spaceId: string;
revisionId: string;
pageId: string;
}): Promise<DataFetcherResponse<string>>;
/** /**
* Get a document by its space ID and document ID. * Get a document by its space ID and document ID.
*/ */
@@ -125,6 +120,7 @@ export interface GitBookDataFetcher {
organizationId: string; organizationId: string;
spaceId: string; spaceId: string;
source: api.ComputedContentSource; source: api.ComputedContentSource;
seed: string;
}): Promise<DataFetcherResponse<api.JSONDocument>>; }): Promise<DataFetcherResponse<api.JSONDocument>>;
/** /**
@@ -175,4 +171,23 @@ export interface GitBookDataFetcher {
/** Cache bust to ensure the search results are fresh when the space is updated. */ /** Cache bust to ensure the search results are fresh when the space is updated. */
cacheBust?: string; cacheBust?: string;
}): Promise<DataFetcherResponse<api.SearchSpaceResult[]>>; }): Promise<DataFetcherResponse<api.SearchSpaceResult[]>>;
/**
* Render an integration UI.
*/
renderIntegrationUi(params: {
integrationName: string;
request: api.RenderIntegrationUI;
}): Promise<DataFetcherResponse<api.ContentKitRenderOutput>>;
/**
* Stream an AI response.
*/
streamAIResponse(params: {
organizationId: string;
siteId: string;
input: api.AIMessageInput[];
output: api.AIOutputFormat;
model: api.AIModel;
}): AsyncGenerator<api.AIStreamResponse, void, unknown>;
} }
-118
View File
@@ -1,121 +1,3 @@
import { race, tryCatch } from '@/lib/async';
import { joinPath } from '@/lib/paths';
import { GitBookAPI, type GitBookAPIError, type PublishedSiteContentLookup } from '@gitbook/api';
import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env';
/**
* Lookup a content by its URL using the GitBook API.
* To optimize caching, we try multiple lookup alternatives and return the first one that matches.
*/
export async function getPublishedContentByURL(input: {
url: string;
visitorAuthToken: string | null;
redirectOnError: boolean;
}): Promise<
| { data: PublishedSiteContentLookup; error?: undefined }
| { data?: undefined; error: Error | GitBookAPIError }
> {
const lookupURL = new URL(input.url);
const url = stripURLSearch(lookupURL);
const lookup = getURLLookupAlternatives(url);
const result = await race(lookup.urls, async (alternative, { signal }) => {
const api = new GitBookAPI({
authToken: GITBOOK_API_TOKEN ?? undefined,
endpoint: GITBOOK_API_URL,
userAgent: GITBOOK_USER_AGENT,
});
const callResult = await tryCatch(
api.urls.getPublishedContentByUrl(
{
url: alternative.url,
visitorAuthToken: input.visitorAuthToken ?? undefined,
redirectOnError: input.redirectOnError,
cache: true,
},
{
signal,
headers: {
'x-gitbook-force-cache': 'true',
},
}
)
);
if (callResult.error) {
if (alternative.primary) {
// We only return an error for the primary alternative (full URL),
// as other parts could result in errors due to the URL being incomplete (share links, etc).
return { error: callResult.error };
}
return null;
}
const {
data: { data },
} = callResult;
if ('redirect' in data) {
if (alternative.primary) {
// Append the path to the redirect URL
// because we might have matched a shorter path and the redirect is relative to it
if (alternative.extraPath) {
if (data.target === 'content') {
const redirect = new URL(data.redirect);
redirect.pathname = joinPath(redirect.pathname, alternative.extraPath);
data.redirect = redirect.toString();
} else {
const redirect = new URL(data.redirect);
if (redirect.searchParams.has('location')) {
redirect.searchParams.set(
'location',
joinPath(
redirect.searchParams.get('location') ?? '',
alternative.extraPath
)
);
data.redirect = redirect.toString();
}
}
}
return { data };
}
return null;
}
/**
* We use the following criteria to determine if the lookup result is the right one:
* - the primary alternative was resolved (because that's the longest or most inclusive path)
* - the resolution of the site URL is complete (because we want to resolve the deepest path possible)
*
* In both cases, the idea is to use the deepest/longest/most inclusive path to resolve the content.
*/
if (alternative.primary || ('site' in data && data.complete)) {
const siteResult: PublishedSiteContentLookup = {
...data,
changeRequest: data.changeRequest ?? lookup.changeRequest,
revision: data.revision ?? lookup.revision,
basePath: joinPath(data.basePath, lookup.basePath ?? ''),
pathname: joinPath(data.pathname, alternative.extraPath),
};
return { data: siteResult };
}
return null;
});
if (!result) {
return {
error: new Error('No content found'),
};
}
return result;
}
/** /**
* For a given GitBook URL, return a list of alternative URLs that could be matched against to lookup the content. * For a given GitBook URL, return a list of alternative URLs that could be matched against to lookup the content.
* The approach is optimized to aim at reusing cached lookup results as much as possible. * The approach is optimized to aim at reusing cached lookup results as much as possible.
@@ -0,0 +1,41 @@
import { describe, expect, it } from 'bun:test';
import { getVisitorAuthBasePath } from './visitor';
describe('getVisitorAuthBasePath', () => {
it('should return the correct base path for proxy requests', () => {
expect(
getVisitorAuthBasePath(
new URL('https://proxy.gitbook.site/sites/site_foo/hello/world'),
{
site: 'site_foo',
siteSpace: 'sitesp_foo',
basePath: '/foo',
siteBasePath: '/foo',
organization: 'org_foo',
space: 'space_foo',
pathname: '/hello/world',
complete: false,
apiToken: 'api_token_foo',
canonicalUrl: 'https://example.com/docs/foo/hello/world',
}
)
).toBe('/sites/site_foo/');
});
it('should return the correct base path for non-proxy requests', () => {
expect(
getVisitorAuthBasePath(new URL('https://example.com/docs/foo/hello/world'), {
site: 'site_foo',
siteSpace: 'sitesp_foo',
basePath: '/foo/',
siteBasePath: '/foo/',
organization: 'org_foo',
space: 'space_foo',
pathname: '/hello/world',
complete: false,
apiToken: 'api_token_foo',
canonicalUrl: 'https://example.com/docs/foo/hello/world',
})
).toBe('/foo/');
});
});
@@ -0,0 +1,19 @@
import { withLeadingSlash, withTrailingSlash } from '@/lib/paths';
import type { PublishedSiteContent } from '@gitbook/api';
import { getProxyRequestIdentifier, isProxyRequest } from './proxy';
/**
* Get the appropriate base path for the visitor authentication cookie.
*/
export function getVisitorAuthBasePath(
siteRequestURL: URL,
siteURLData: PublishedSiteContent
): string {
// The siteRequestURL for proxy requests is of the form `https://proxy.gitbook.com/site/siteId/...`
// In such cases, we should not use the resolved siteBasePath for the cookie because for subsequent requests
// we will not have the siteBasePath in the request URL in order to retrieve the cookie. So we use the
// proxy identifier instead.
return isProxyRequest(siteRequestURL)
? withLeadingSlash(withTrailingSlash(getProxyRequestIdentifier(siteRequestURL)))
: siteURLData.siteBasePath;
}
+58 -3
View File
@@ -1,3 +1,19 @@
import 'server-only';
/*
* Support both Cloudflare and Vercel, environment variables can be bundled.
* To avoid leaking them on the client-side, they should be accessed from this file
* and not from the `process.env` object.
*/
/**
* Runtime environment.
*/
export const GITBOOK_RUNTIME = (process.env.GITBOOK_RUNTIME ?? 'unknown') as
| 'vercel'
| 'cloudflare'
| 'unknown';
/** /**
* Main host on which GitBook is running. * Main host on which GitBook is running.
*/ */
@@ -14,23 +30,28 @@ export const GITBOOK_URL =
export const GITBOOK_ASSETS_URL = export const GITBOOK_ASSETS_URL =
process.env.NODE_ENV === 'development' process.env.NODE_ENV === 'development'
? 'http://localhost:3000' ? 'http://localhost:3000'
: process.env.GITBOOK_ASSETS_PREFIX || GITBOOK_URL; : process.env.GITBOOK_ASSETS_PREFIX;
/** /**
* GitBook app URL. * GitBook app URL.
*/ */
export const GITBOOK_APP_URL = process.env.NEXT_PUBLIC_GITBOOK_APP_URL || 'https://app.gitbook.com'; export const GITBOOK_APP_URL = process.env.GITBOOK_APP_URL || 'https://app.gitbook.com';
/** /**
* Default GitBook API URL endpoint. * Default GitBook API URL endpoint.
*/ */
export const GITBOOK_API_URL = process.env.GITBOOK_API_URL || 'https://api.gitbook.com'; export const GITBOOK_API_URL = process.env.GITBOOK_API_URL || 'https://api.gitbook.com';
/**
* Default GitBook API URL endpoint, to be shared with the client.
*/
export const GITBOOK_API_PUBLIC_URL = process.env.GITBOOK_API_PUBLIC_URL || GITBOOK_API_URL;
/** /**
* Default GitBook API token. * Default GitBook API token.
* It can be use to avoid rate-limiting. * It can be use to avoid rate-limiting.
*/ */
export const GITBOOK_API_TOKEN = process.env.GITBOOK_API_TOKEN ?? null; export const GITBOOK_API_TOKEN = process.env.GITBOOK_API_TOKEN || null;
/** /**
* User agent to use for API requests. * User agent to use for API requests.
@@ -51,6 +72,11 @@ export const GITBOOK_DISABLE_TRACKING = Boolean(
export const GITBOOK_INTEGRATIONS_HOST = export const GITBOOK_INTEGRATIONS_HOST =
process.env.GITBOOK_INTEGRATIONS_HOST || 'integrations.gitbook.com'; process.env.GITBOOK_INTEGRATIONS_HOST || 'integrations.gitbook.com';
/**
* Hostname for fonts.
*/
export const GITBOOK_FONTS_URL = process.env.GITBOOK_FONTS_URL || 'https://fonts.gitbook.com';
/** /**
* Endpoint to use for resizing images. * Endpoint to use for resizing images.
* It should be a Cloudflare domain with image resizing enabled. * It should be a Cloudflare domain with image resizing enabled.
@@ -59,7 +85,36 @@ export const GITBOOK_IMAGE_RESIZE_URL = process.env.GITBOOK_IMAGE_RESIZE_URL ??
export const GITBOOK_IMAGE_RESIZE_SIGNING_KEY = export const GITBOOK_IMAGE_RESIZE_SIGNING_KEY =
process.env.GITBOOK_IMAGE_RESIZE_SIGNING_KEY ?? null; process.env.GITBOOK_IMAGE_RESIZE_SIGNING_KEY ?? null;
/**
* Mode used for resizing images.
*/
export const GITBOOK_IMAGE_RESIZE_MODE = enforceEnum(
'GITBOOK_IMAGE_RESIZE_MODE',
process.env.GITBOOK_IMAGE_RESIZE_MODE || 'cdn-cgi',
['cdn-cgi', 'cf-fetch']
);
/**
* Endpoint where icons are served.
*/
export const GITBOOK_ICONS_URL =
process.env.GITBOOK_ICONS_URL || `${GITBOOK_ASSETS_URL || ''}/~gitbook/static/icons`;
/**
* Token passed to the icons endpoint.
*/
export const GITBOOK_ICONS_TOKEN = process.env.GITBOOK_ICONS_TOKEN;
/** /**
* Secret used to validate requests from the GitBook app. * Secret used to validate requests from the GitBook app.
*/ */
export const GITBOOK_SECRET = process.env.GITBOOK_SECRET ?? null; export const GITBOOK_SECRET = process.env.GITBOOK_SECRET ?? null;
function enforceEnum<T extends string>(key: string, value: string, enumValues: T[]): T {
if (!enumValues.includes(value as T)) {
throw new Error(
`Invalid value for ${key}: "${value}", expected one of: ${enumValues.join(', ')}`
);
}
return value as T;
}
@@ -0,0 +1,27 @@
import { checkIsHttpURL } from '@/lib/urls';
/**
* Check if an image URL is resizable.
* Skip it for non-http(s) URLs (data, etc).
* Skip it for SVGs.
* Skip it for GitBook images (to avoid recursion).
*/
export function checkIsSizableImageURL(input: string): boolean {
if (!URL.canParse(input)) {
return false;
}
if (input.includes('/~gitbook/image')) {
return false;
}
const parsed = new URL(input);
if (parsed.pathname.endsWith('.svg') || parsed.pathname.endsWith('.avif')) {
return false;
}
if (!checkIsHttpURL(parsed)) {
return false;
}
return true;
}
@@ -1,45 +1,22 @@
import 'server-only'; import 'server-only';
import { GITBOOK_IMAGE_RESIZE_SIGNING_KEY, GITBOOK_IMAGE_RESIZE_URL } from '../env'; import { GITBOOK_IMAGE_RESIZE_SIGNING_KEY, GITBOOK_IMAGE_RESIZE_URL } from '../env';
import type { GitBookSpaceLinker } from '../links'; import type { GitBookLinker } from '../links';
import { checkIsSizableImageURL } from './checkIsSizableImageURL';
import { getImageSize } from './resizer';
import { type SignatureVersion, generateImageSignature } from './signatures'; import { type SignatureVersion, generateImageSignature } from './signatures';
import type { ImageResizer } from './types'; import type { ImageResizer } from './types';
interface CloudflareImageJsonFormat {
width: number;
height: number;
original: {
file_size: number;
width: number;
height: number;
format: string;
};
}
/**
* https://developers.cloudflare.com/images/image-resizing/resize-with-workers/
*/
export interface CloudflareImageOptions {
format?: 'webp' | 'avif' | 'json' | 'jpeg';
fit?: 'scale-down' | 'contain' | 'cover' | 'crop' | 'pad';
width?: number;
height?: number;
dpr?: number;
anim?: boolean;
quality?: number;
}
/** /**
* Create an image resizer for a rendering context. * Create an image resizer for a rendering context.
*/ */
export function createImageResizer({ export function createImageResizer({
host, imagesContextId,
linker, linker,
}: { }: {
/** The linker to use to create URLs. */ /** The linker to use to create URLs. */
linker: GitBookSpaceLinker; linker: GitBookLinker;
/** The host name of the current site. */ /** The site identifier to use for verifying the image signature. */
host: string; imagesContextId: string;
}): ImageResizer { }): ImageResizer {
if (!GITBOOK_IMAGE_RESIZE_URL || !GITBOOK_IMAGE_RESIZE_SIGNING_KEY) { if (!GITBOOK_IMAGE_RESIZE_URL || !GITBOOK_IMAGE_RESIZE_SIGNING_KEY) {
return createNoopImageResizer(); return createNoopImageResizer();
@@ -58,11 +35,11 @@ export function createImageResizer({
return async (options) => { return async (options) => {
cachedSignature ??= await generateImageSignature({ cachedSignature ??= await generateImageSignature({
host, imagesContextId,
url: urlInput, url: urlInput,
}); });
const url = linker.toAbsoluteURL(linker.toPathInContent('/~gitbook/image')); const url = linker.toAbsoluteURL(linker.toPathInSite('/~gitbook/image'));
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
searchParams.set('url', getImageAPIUrl(urlInput)); searchParams.set('url', getImageAPIUrl(urlInput));
@@ -106,124 +83,6 @@ export function createNoopImageResizer(): ImageResizer {
}; };
} }
/**
* Check if a URL is an HTTP URL.
*/
export function checkIsHttpURL(input: string | URL): boolean {
if (!URL.canParse(input)) {
return false;
}
const parsed = new URL(input);
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
}
/**
* Check if an image URL is resizable.
* Skip it for non-http(s) URLs (data, etc).
* Skip it for SVGs.
* Skip it for GitBook images (to avoid recursion).
*/
export function checkIsSizableImageURL(input: string): boolean {
if (!URL.canParse(input)) {
return false;
}
if (input.includes('/~gitbook/image')) {
return false;
}
const parsed = new URL(input);
if (parsed.pathname.endsWith('.svg') || parsed.pathname.endsWith('.avif')) {
return false;
}
if (!checkIsHttpURL(parsed)) {
return false;
}
return true;
}
/**
* Get the size of an image.
*/
export async function getImageSize(
input: string,
defaultSize: Partial<CloudflareImageOptions> = {}
): Promise<{ width: number; height: number } | null> {
if (!checkIsSizableImageURL(input)) {
return null;
}
try {
const response = await resizeImage(input, {
// Abort the request after 2 seconds to avoid blocking rendering for too long
signal: AbortSignal.timeout(2000),
// Measure size and resize it to the most common size
// to optimize caching
...defaultSize,
format: 'json',
anim: false,
});
const json = (await response.json()) as CloudflareImageJsonFormat;
return {
width: json.original.width,
height: json.original.height,
};
} catch (_error) {
return null;
}
}
/**
* Execute a Cloudflare Image Resize operation on an image.
*/
export async function resizeImage(
input: string,
options: CloudflareImageOptions & {
signal?: AbortSignal;
}
): Promise<Response> {
const { signal, ...resizeOptions } = options;
const parsed = new URL(input);
if (parsed.protocol === 'data:') {
throw new Error('Cannot resize data: URLs');
}
if (parsed.hostname === 'localhost') {
throw new Error('Cannot resize localhost URLs');
}
// Since Cloudflare Images options on fetch are not supported on Cloudflare Pages,
// we need to use the Cloudflare Image Resize API directly.
if (!GITBOOK_IMAGE_RESIZE_URL) {
throw new Error('GITBOOK_IMAGE_RESIZE_URL is not set');
}
return await fetch(
`${GITBOOK_IMAGE_RESIZE_URL}${stringifyOptions(
resizeOptions
)}/${encodeURIComponent(input)}`,
{
headers: {
// Pass the `Accept` header, as Cloudflare uses this to validate the format.
Accept:
resizeOptions.format === 'json'
? 'application/json'
: `image/${resizeOptions.format || 'jpeg'}`,
},
signal,
}
);
}
function stringifyOptions(options: CloudflareImageOptions): string {
return Object.entries({ ...options }).reduce((rest, [key, value]) => {
return `${rest}${rest ? ',' : ''}${key}=${value}`;
}, '');
}
/** /**
* Because of a bug in Cloudflare, 127.0.0.1 is replaced by localhost. * Because of a bug in Cloudflare, 127.0.0.1 is replaced by localhost.
* We protect against it by converting to a special token, and then parsing * We protect against it by converting to a special token, and then parsing
@@ -0,0 +1,13 @@
import { getProxyRequestIdentifier, isProxyRequest } from '../data';
/**
* Get the site identifier to use for image resizing for an incoming request.
* This identifier can be obtained before resolving the request URL.
*/
export function getImageResizingContextId(url: URL): string {
if (isProxyRequest(url)) {
return getProxyRequestIdentifier(url);
}
return url.host;
}
@@ -2,3 +2,6 @@ export * from './types';
export * from './createImageResizer'; export * from './createImageResizer';
export * from './signatures'; export * from './signatures';
export * from './utils'; export * from './utils';
export * from './getImageResizingContextId';
export * from './resizer';
export * from './checkIsSizableImageURL';
@@ -0,0 +1,48 @@
import { GITBOOK_IMAGE_RESIZE_URL } from '@v2/lib/env';
import type { CloudflareImageOptions } from './types';
import { copyImageResponse } from './utils';
/**
* Resize an image by doing a request to a /cdn/cgi/ endpoint.
* https://developers.cloudflare.com/images/transform-images/transform-via-url/
*/
export async function resizeImageWithCDNCgi(
input: string,
options: CloudflareImageOptions & {
signal?: AbortSignal;
}
): Promise<Response> {
const { signal, ...resizeOptions } = options;
// Since Cloudflare Images options on fetch are not supported on Cloudflare Pages,
// we need to use the Cloudflare Image Resize API directly.
if (!GITBOOK_IMAGE_RESIZE_URL) {
throw new Error('GITBOOK_IMAGE_RESIZE_URL is not set for cdn-cgi image resize mode');
}
const resizeURL = `${GITBOOK_IMAGE_RESIZE_URL}${stringifyOptions(
resizeOptions
)}/${encodeURIComponent(input)}`;
// biome-ignore lint/suspicious/noConsole: this log is useful for debugging
console.log(`resize image using cdn-cgi: ${resizeURL}`);
return copyImageResponse(
await fetch(resizeURL, {
headers: {
// Pass the `Accept` header, as Cloudflare uses this to validate the format.
Accept:
resizeOptions.format === 'json'
? 'application/json'
: `image/${resizeOptions.format || 'jpeg'}`,
},
signal,
})
);
}
function stringifyOptions(options: CloudflareImageOptions): string {
return Object.entries({ ...options }).reduce((rest, [key, value]) => {
return `${rest}${rest ? ',' : ''}${key}=${value}`;
}, '');
}
@@ -0,0 +1,34 @@
import type { CloudflareImageOptions } from './types';
import { copyImageResponse } from './utils';
/**
* Resize an image by doing a request to the image itself using the Cloudflare fetch.
* https://developers.cloudflare.com/images/transform-images/transform-via-workers/
*
* This method doesn't work in Cloudflare Pages and is only supported in workers.
*/
export async function resizeImageWithCFFetch(
input: string,
options: CloudflareImageOptions & {
signal?: AbortSignal;
}
): Promise<Response> {
const { signal, ...resizeOptions } = options;
// biome-ignore lint/suspicious/noConsole: this log is useful for debugging
console.log(`resize image using cf-fetch: ${input}`);
return copyImageResponse(
await fetch(input, {
headers: {
// Pass the `Accept` header, as Cloudflare uses this to validate the format.
Accept:
resizeOptions.format === 'json'
? 'application/json'
: `image/${resizeOptions.format || 'jpeg'}`,
},
signal,
cf: { image: resizeOptions },
})
);
}
@@ -0,0 +1,2 @@
export * from './types';
export * from './resizeImage';
@@ -0,0 +1,68 @@
import 'server-only';
import assertNever from 'assert-never';
import { GITBOOK_IMAGE_RESIZE_MODE } from '../../env';
import { checkIsSizableImageURL } from '../checkIsSizableImageURL';
import { resizeImageWithCDNCgi } from './cdn-cgi';
import { resizeImageWithCFFetch } from './cf-fetch';
import type { CloudflareImageJsonFormat, CloudflareImageOptions } from './types';
/**
* Get the size of an image.
*/
export async function getImageSize(
input: string,
defaultSize: Partial<CloudflareImageOptions> = {}
): Promise<{ width: number; height: number } | null> {
if (!checkIsSizableImageURL(input)) {
return null;
}
try {
const response = await resizeImage(input, {
// Abort the request after 2 seconds to avoid blocking rendering for too long
signal: AbortSignal.timeout(2000),
// Measure size and resize it to the most common size
// to optimize caching
...defaultSize,
format: 'json',
anim: false,
});
const json = (await response.json()) as CloudflareImageJsonFormat;
return {
width: json.original.width,
height: json.original.height,
};
} catch (error) {
console.warn(`Error getting image size for ${input}:`, error);
return null;
}
}
/**
* Execute a Cloudflare Image Resize operation on an image.
*/
export async function resizeImage(
input: string,
options: CloudflareImageOptions & {
signal?: AbortSignal;
}
): Promise<Response> {
const parsed = new URL(input);
if (parsed.protocol === 'data:') {
throw new Error('Cannot resize data: URLs');
}
if (parsed.hostname === 'localhost') {
throw new Error('Cannot resize localhost URLs');
}
switch (GITBOOK_IMAGE_RESIZE_MODE) {
case 'cdn-cgi':
return resizeImageWithCDNCgi(input, options);
case 'cf-fetch':
return resizeImageWithCFFetch(input, options);
default:
assertNever(GITBOOK_IMAGE_RESIZE_MODE);
}
}
@@ -0,0 +1,23 @@
export interface CloudflareImageJsonFormat {
width: number;
height: number;
original: {
file_size: number;
width: number;
height: number;
format: string;
};
}
/**
* https://developers.cloudflare.com/images/image-resizing/resize-with-workers/
*/
export interface CloudflareImageOptions {
format?: 'webp' | 'avif' | 'json' | 'jpeg';
fit?: 'scale-down' | 'contain' | 'cover' | 'crop' | 'pad';
width?: number;
height?: number;
dpr?: number;
anim?: boolean;
quality?: number;
}
@@ -0,0 +1,7 @@
/**
* Copy a response to make sure it can be mutated by the rest of the middleware.
* To avoid errors "Can't modify immutable headers".
*/
export function copyImageResponse(response: Response) {
return new Response(response.body, response);
}
@@ -18,7 +18,7 @@ export const CURRENT_SIGNATURE_VERSION: SignatureVersion = '2';
type SignFnInput = { type SignFnInput = {
url: string; url: string;
host: string; imagesContextId: string;
}; };
type SignFn = (input: SignFnInput) => MaybePromise<string>; type SignFn = (input: SignFnInput) => MaybePromise<string>;
@@ -32,6 +32,11 @@ export async function verifyImageSignature(
): Promise<boolean> { ): Promise<boolean> {
const generator = IMAGE_SIGNATURE_FUNCTIONS[version]; const generator = IMAGE_SIGNATURE_FUNCTIONS[version];
const generated = await generator(input); const generated = await generator(input);
// biome-ignore lint/suspicious/noConsole: we want to log the signature comparison
console.log(
`comparing image signature for "${input.url}" on identifier "${input.imagesContextId}": "${generated}" (expected) === "${signature}" (actual)`
);
return generated === signature; return generated === signature;
} }
@@ -60,12 +65,14 @@ const generateSignatureV2: SignFn = async (input) => {
assert(GITBOOK_IMAGE_RESIZE_SIGNING_KEY, 'GITBOOK_IMAGE_RESIZE_SIGNING_KEY is not set'); assert(GITBOOK_IMAGE_RESIZE_SIGNING_KEY, 'GITBOOK_IMAGE_RESIZE_SIGNING_KEY is not set');
const all = [ const all = [
input.url, input.url,
input.host, // The hostname is used to avoid serving images from other sites on the same domain input.imagesContextId, // The hostname is used to avoid serving images from other sites on the same domain
GITBOOK_IMAGE_RESIZE_SIGNING_KEY, GITBOOK_IMAGE_RESIZE_SIGNING_KEY,
] ]
.filter(Boolean) .filter(Boolean)
.join(':'); .join(':');
return fnv1a(all, { utf8Buffer: fnv1aUtf8Buffer }).toString(16);
const signature = fnv1a(all, { utf8Buffer: fnv1aUtf8Buffer }).toString(16);
return signature;
}; };
// Reused buffer for FNV-1a hashing in the v1 algorithm // Reused buffer for FNV-1a hashing in the v1 algorithm
+43 -27
View File
@@ -1,25 +1,51 @@
import { describe, expect, it } from 'bun:test'; import { describe, expect, it } from 'bun:test';
import { appendBasePathToLinker, createLinker } from './links'; import { createLinker } from './links';
const root = createLinker({ const root = createLinker({
host: 'docs.company.com', host: 'docs.company.com',
pathname: '/', spaceBasePath: '/',
siteBasePath: '/',
}); });
const variantInSection = createLinker({ const variantInSection = createLinker({
host: 'docs.company.com', host: 'docs.company.com',
pathname: '/section/variant', spaceBasePath: '/section/variant',
siteBasePath: '/',
});
const siteGitBookIO = createLinker({
host: 'org.gitbook.io',
spaceBasePath: '/sitename/variant/',
siteBasePath: '/sitename/',
}); });
describe('toPathInContent', () => { describe('toPathInContent', () => {
it('should return the correct path', () => { it('should return the correct path', () => {
expect(root.toPathInContent('some/path')).toBe('/some/path'); expect(root.toPathInSpace('some/path')).toBe('/some/path');
expect(variantInSection.toPathInContent('some/path')).toBe('/section/variant/some/path'); expect(variantInSection.toPathInSpace('some/path')).toBe('/section/variant/some/path');
}); });
it('should handle leading slash', () => { it('should handle leading slash', () => {
expect(root.toPathInContent('/some/path')).toBe('/some/path'); expect(root.toPathInSpace('/some/path')).toBe('/some/path');
expect(variantInSection.toPathInContent('/some/path')).toBe('/section/variant/some/path'); expect(variantInSection.toPathInSpace('/some/path')).toBe('/section/variant/some/path');
});
});
describe('toPathInSite', () => {
it('should return the correct path', () => {
expect(root.toPathInSite('some/path')).toBe('/some/path');
expect(siteGitBookIO.toPathInSite('some/path')).toBe('/sitename/some/path');
});
});
describe('toRelativePathInSite', () => {
it('should return the correct path', () => {
expect(root.toRelativePathInSite('/some/path')).toBe('some/path');
expect(siteGitBookIO.toRelativePathInSite('/sitename/some/path')).toBe('some/path');
});
it('should preserve absolute paths outside of the site', () => {
expect(siteGitBookIO.toRelativePathInSite('/outside/some/path')).toBe('/outside/some/path');
}); });
}); });
@@ -32,27 +58,17 @@ describe('toAbsoluteURL', () => {
}); });
}); });
describe('appendBasePathToLinker', () => { describe('toLinkForContent', () => {
const prefixedRoot = appendBasePathToLinker(root, '/section/variant'); it('should return the correct path', () => {
const prefixedVariantInSection = appendBasePathToLinker(variantInSection, '/base'); expect(root.toLinkForContent('https://docs.company.com/some/path')).toBe('/some/path');
expect(siteGitBookIO.toLinkForContent('https://org.gitbook.io/sitename/some/path')).toBe(
describe('toPathInContent', () => { '/sitename/some/path'
it('should return the correct path', () => { );
expect(prefixedRoot.toPathInContent('some/path')).toBe('/section/variant/some/path');
expect(prefixedVariantInSection.toPathInContent('some/path')).toBe(
'/section/variant/base/some/path'
);
});
}); });
describe('toAbsoluteURL', () => { it('should preserve an absolute URL if the site is not the same', () => {
it('should return the correct path', () => { expect(siteGitBookIO.toLinkForContent('https://org.gitbook.io/anothersite/some/path')).toBe(
expect(prefixedRoot.toAbsoluteURL('some/path')).toBe( 'https://org.gitbook.io/anothersite/some/path'
'https://docs.company.com/some/path' );
);
expect(prefixedVariantInSection.toAbsoluteURL('some/path')).toBe(
'https://docs.company.com/some/path'
);
});
}); });
}); });
+55 -44
View File
@@ -1,4 +1,5 @@
import { getPagePath } from '@/lib/pages'; import { getPagePath } from '@/lib/pages';
import { withLeadingSlash, withTrailingSlash } from '@/lib/paths';
import type { RevisionPage, RevisionPageDocument, RevisionPageGroup } from '@gitbook/api'; import type { RevisionPage, RevisionPageDocument, RevisionPageGroup } from '@gitbook/api';
import warnOnce from 'warn-once'; import warnOnce from 'warn-once';
@@ -7,17 +8,28 @@ import warnOnce from 'warn-once';
* *
* URL levels: * URL levels:
* *
* https://docs.company.com/section/variant/page * https://docs.company.com/basename/section/variant/page
* *
* toPathInContent('some/path') => /section/variant/some/path * toPathInSpace('some/path') => /basename/section/variant/some/path
* toPathForPage({ pages, page }) => /section/variant/some/path * toPathInSite('some/path') => /basename/some/path
* toPathForPage({ pages, page }) => /basename/section/variant/some/path
* toAbsoluteURL('some/path') => https://docs.company.com/some/path * toAbsoluteURL('some/path') => https://docs.company.com/some/path
*/ */
export interface GitBookSpaceLinker { export interface GitBookLinker {
/** /**
* Generate an absolute path for a relative path to the current content. * Generate an absolute path for a relative path to the current content.
*/ */
toPathInContent(relativePath: string): string; toPathInSpace(relativePath: string): string;
/**
* Generate an absolute path for a relative path to the current site.
*/
toPathInSite(relativePath: string): string;
/**
* Transform an absolute path in a site, to a relative path from the root of the site.
*/
toRelativePathInSite(absolutePath: string): string;
/** /**
* Generate an absolute path for a page in the current content. * Generate an absolute path for a page in the current content.
@@ -48,14 +60,36 @@ export function createLinker(
servedOn: { servedOn: {
protocol?: string; protocol?: string;
host?: string; host?: string;
pathname: string;
/** The base path of the space */
spaceBasePath: string;
/** The base path of the site */
siteBasePath: string;
} }
): GitBookSpaceLinker { ): GitBookLinker {
warnOnce(!servedOn.host, 'No host provided to createLinker. It can lead to issues with links.'); warnOnce(!servedOn.host, 'No host provided to createLinker. It can lead to issues with links.');
const linker: GitBookSpaceLinker = { const siteBasePath = withTrailingSlash(withLeadingSlash(servedOn.siteBasePath));
toPathInContent(relativePath: string): string { const spaceBasePath = withTrailingSlash(withLeadingSlash(servedOn.spaceBasePath));
return joinPaths(servedOn.pathname, relativePath);
const linker: GitBookLinker = {
toPathInSpace(relativePath: string): string {
return joinPaths(spaceBasePath, relativePath);
},
toPathInSite(relativePath: string): string {
return joinPaths(siteBasePath, relativePath);
},
toRelativePathInSite(absolutePath: string): string {
const normalizedPath = withLeadingSlash(absolutePath);
if (!normalizedPath.startsWith(servedOn.siteBasePath)) {
return normalizedPath;
}
return normalizedPath.slice(servedOn.siteBasePath.length);
}, },
toAbsoluteURL(absolutePath: string): string { toAbsoluteURL(absolutePath: string): string {
@@ -67,48 +101,25 @@ export function createLinker(
}, },
toPathForPage({ pages, page, anchor }) { toPathForPage({ pages, page, anchor }) {
return linker.toPathInContent(getPagePath(pages, page)) + (anchor ? `#${anchor}` : ''); return linker.toPathInSpace(getPagePath(pages, page)) + (anchor ? `#${anchor}` : '');
}, },
toLinkForContent(url: string): string { toLinkForContent(rawURL: string): string {
return url; const url = new URL(rawURL);
// If the link points to a content in the same site, we return an absolute path
// instead of a full URL; it makes it possible to use router navigation
if (url.hostname === servedOn.host && url.pathname.startsWith(servedOn.siteBasePath)) {
return url.pathname;
}
return rawURL;
}, },
}; };
return linker; return linker;
} }
/**
* Append a prefix to a linker.
*/
export function appendBasePathToLinker(
linker: GitBookSpaceLinker,
basePath: string
): GitBookSpaceLinker {
const linkerWithPrefix: GitBookSpaceLinker = {
toPathInContent(relativePath: string): string {
return linker.toPathInContent(joinPaths(basePath, relativePath));
},
toAbsoluteURL(absolutePath: string): string {
return linker.toAbsoluteURL(absolutePath);
},
toPathForPage({ pages, page, anchor }) {
return (
linkerWithPrefix.toPathInContent(getPagePath(pages, page)) +
(anchor ? `#${anchor}` : '')
);
},
toLinkForContent(url: string): string {
return linker.toLinkForContent(url);
},
};
return linkerWithPrefix;
}
function joinPaths(prefix: string, path: string): string { function joinPaths(prefix: string, path: string): string {
const prefixPath = prefix.endsWith('/') ? prefix : `${prefix}/`; const prefixPath = prefix.endsWith('/') ? prefix : `${prefix}/`;
const suffixPath = path.startsWith('/') ? path.slice(1) : path; const suffixPath = path.startsWith('/') ? path.slice(1) : path;
+12 -10
View File
@@ -1,5 +1,6 @@
import { CustomizationThemeMode, type PublishedSiteContent } from '@gitbook/api'; import { CustomizationThemeMode } from '@gitbook/api';
import { headers } from 'next/headers'; import { headers } from 'next/headers';
import type { SiteURLData } from './context';
export enum MiddlewareHeaders { export enum MiddlewareHeaders {
/** /**
@@ -33,9 +34,10 @@ export enum MiddlewareHeaders {
Customization = 'x-gitbook-customization', Customization = 'x-gitbook-customization',
/** /**
* The visitor token used to access this content * The API token used to fetch the content.
* This should only be passed for non-site dynamic routes.
*/ */
VisitorAuthToken = 'x-gitbook-visitor-token', APIToken = 'x-gitbook-api-token',
} }
/** /**
@@ -56,7 +58,7 @@ export async function getURLModeFromMiddleware(): Promise<'url' | 'url-host'> {
* Get the site URL data from the middleware headers. * Get the site URL data from the middleware headers.
* This function should only be called in a server action or a dynamic route. * This function should only be called in a server action or a dynamic route.
*/ */
export async function getSiteURLDataFromMiddleware(): Promise<PublishedSiteContent> { export async function getSiteURLDataFromMiddleware(): Promise<SiteURLData> {
const headersList = await headers(); const headersList = await headers();
const siteURLData = headersList.get(MiddlewareHeaders.SiteURLData); const siteURLData = headersList.get(MiddlewareHeaders.SiteURLData);
@@ -100,15 +102,15 @@ export async function getThemeFromMiddleware() {
} }
/** /**
* Get the visitor auth token from the middleware headers. * Get the API token from the middleware headers.
* This function should only be called in a dynamic route. * This function should only be called in a dynamic route.
*/ */
export async function getVisitorAuthTokenFromMiddleware(): Promise<string | null> { export async function getAPITokenFromMiddleware(): Promise<string> {
const headersList = await headers(); const headersList = await headers();
const visitorAuthToken = headersList.get(MiddlewareHeaders.VisitorAuthToken); const apiToken = headersList.get(MiddlewareHeaders.APIToken);
if (!visitorAuthToken) { if (!apiToken) {
return null; throw new Error('API token is not set by the middleware');
} }
return visitorAuthToken; return apiToken;
} }
@@ -16,8 +16,8 @@ export async function getServerActionBaseContext() {
return getBaseContext({ return getBaseContext({
siteURL, siteURL,
siteURLData,
urlMode, urlMode,
apiToken: siteURLData.apiToken,
}); });
} }
+338 -111
View File
@@ -1,16 +1,30 @@
import { CustomizationThemeMode, GitBookAPIError } from '@gitbook/api'; import { CustomizationThemeMode } from '@gitbook/api';
import type { NextRequest } from 'next/server'; import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import rison from 'rison';
import { getContentSecurityPolicy } from '@/lib/csp'; import { getContentSecurityPolicy } from '@/lib/csp';
import { validateSerializedCustomization } from '@/lib/customization'; import { validateSerializedCustomization } from '@/lib/customization';
import { removeLeadingSlash, removeTrailingSlash } from '@/lib/paths'; import { removeLeadingSlash, removeTrailingSlash } from '@/lib/paths';
import { getResponseCookiesForVisitorAuth, getVisitorToken } from '@/lib/visitor-token'; import {
type ResponseCookies,
getPathScopedCookieName,
getResponseCookiesForVisitorAuth,
getVisitorToken,
normalizeVisitorAuthURL,
} from '@/lib/visitor-token';
import { serveResizedImage } from '@/routes/image'; import { serveResizedImage } from '@/routes/image';
import { getPublishedContentByURL } from '@v2/lib/data'; import {
DataFetcherError,
getPublishedContentByURL,
getVisitorAuthBasePath,
normalizeURL,
throwIfDataError,
} from '@v2/lib/data';
import { isGitBookAssetsHostURL, isGitBookHostURL } from '@v2/lib/env'; import { isGitBookAssetsHostURL, isGitBookHostURL } from '@v2/lib/env';
import { getImageResizingContextId } from '@v2/lib/images';
import { MiddlewareHeaders } from '@v2/lib/middleware'; import { MiddlewareHeaders } from '@v2/lib/middleware';
import type { SiteURLData } from './lib/context';
export const config = { export const config = {
matcher: [ matcher: [
'/((?!_next/static|_next/image|~gitbook/static|~gitbook/revalidate|~gitbook/monitoring|~scalar/proxy).*)', '/((?!_next/static|_next/image|~gitbook/static|~gitbook/revalidate|~gitbook/monitoring|~scalar/proxy).*)',
@@ -21,27 +35,21 @@ type URLWithMode = { url: URL; mode: 'url' | 'url-host' };
export async function middleware(request: NextRequest) { export async function middleware(request: NextRequest) {
try { try {
// Route all requests to a site const requestURL = new URL(request.url);
const extracted = getSiteURLFromRequest(request);
if (extracted) {
/**
* Serve image resizing requests (all requests containing `/~gitbook/image`).
* All URLs containing `/~gitbook/image` are rewritten to `/~gitbook/image`
* and serve from a single route handler.
*
* In GitBook v1: image resizing was done at the root of the hostname (docs.company.com/~gitbook/image)
* In GitBook v2: image resizing is done at the content level (docs.company.com/section/variant/~gitbook/image)
*/
if (extracted.url.pathname.endsWith('/~gitbook/image')) {
return await serveResizedImage(request, {
host: extracted.url.host,
});
}
return await serveSiteByURL(request, extracted); // Redirect to normalize the URL
const normalized = normalizeURL(requestURL);
if (normalized.toString() !== requestURL.toString()) {
return NextResponse.redirect(normalized.toString());
}
for (const handler of [serveSiteRoutes, serveSpacePDFRoutes]) {
const result = await handler(requestURL, request);
if (result) {
return result;
}
} }
// Handle the rest with the router default logic
return NextResponse.next(); return NextResponse.next();
} catch (error) { } catch (error) {
return serveErrorResponse(error as Error); return serveErrorResponse(error as Error);
@@ -49,110 +57,260 @@ export async function middleware(request: NextRequest) {
} }
/** /**
* Serve site by URL. * Handle request that are targetting the site routes group.
*/ */
async function serveSiteByURL(request: NextRequest, urlWithMode: URLWithMode) { async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
const { url, mode } = urlWithMode; const match = getSiteURLFromRequest(request);
if (!match) {
return null;
}
// Visitor authentication const { url: siteRequestURL, mode } = match;
const imagesContextId = getImageResizingContextId(siteRequestURL);
/**
* Serve image resizing requests (all requests containing `/~gitbook/image`).
* All URLs containing `/~gitbook/image` are rewritten to `/~gitbook/image`
* and serve from a single route handler.
*
* In GitBook v1: image resizing was done at the root of the hostname (docs.company.com/~gitbook/image)
* In GitBook v2: image resizing is done at the content level (docs.company.com/section/variant/~gitbook/image)
*/
if (siteRequestURL.pathname.endsWith('/~gitbook/image')) {
return await serveResizedImage(request, {
imagesContextId: imagesContextId,
});
}
//
// Detect and extract the visitor authentication token from the request
//
// @ts-ignore - request typing // @ts-ignore - request typing
const visitorToken = getVisitorToken(request, url); const visitorToken = getVisitorToken({
cookies: request.cookies.getAll(),
const result = await getPublishedContentByURL({ url: siteRequestURL,
url: url.toString(),
visitorAuthToken: visitorToken?.token ?? null,
// When the visitor auth token is pulled from the cookie, set redirectOnError when calling getPublishedContentByUrl to allow
// redirecting when the token is invalid as we could be dealing with stale token stored in the cookie.
// For example when the VA backend signature has changed but the token stored in the cookie is not yet expired.
redirectOnError: visitorToken?.source === 'visitor-auth-cookie',
}); });
if (result.error) { const withAPIToken = async (apiToken: string | null) => {
throw result.error; const siteURLData = await throwIfDataError(
} getPublishedContentByURL({
url: siteRequestURL.toString(),
visitorAuthToken: visitorToken?.token ?? null,
// When the visitor auth token is pulled from the cookie, set redirectOnError when calling getPublishedContentByUrl to allow
// redirecting when the token is invalid as we could be dealing with stale token stored in the cookie.
// For example when the VA backend signature has changed but the token stored in the cookie is not yet expired.
redirectOnError: visitorToken?.source === 'visitor-auth-cookie',
const { data } = result; // Use the API token passed in the request, if any
// as it could be used for .preview hostnames
apiToken,
})
);
const cookies: ResponseCookies = [];
if ('redirect' in data) { //
return NextResponse.redirect(data.redirect); // Handle redirects
} //
if ('redirect' in siteURLData) {
// biome-ignore lint/suspicious/noConsole: we want to log the redirect
console.log('redirect', siteURLData.redirect);
if (siteURLData.target === 'content') {
let contentRedirect = new URL(siteURLData.redirect, request.url);
// When visitor has authentication (adaptive content or VA), we serve dynamic routes. // For content redirects, we redirect using the /url/:url format
let routeType = visitorToken ? 'dynamic' : 'static'; // during development and testing in 'url' mode.
if (mode === 'url') {
const urlObject = new URL(siteURLData.redirect);
contentRedirect = new URL(
`/url/${urlObject.host}${urlObject.pathname}${urlObject.search}`,
request.url
);
}
const requestHeaders = new Headers(request.headers); // Keep the same search params as the original request
requestHeaders.set(MiddlewareHeaders.RouteType, routeType); // as it might contain a VA token
requestHeaders.set(MiddlewareHeaders.URLMode, mode); contentRedirect.search = request.nextUrl.search;
requestHeaders.set(MiddlewareHeaders.SiteURL, `${url.origin}${data.basePath}`);
requestHeaders.set(MiddlewareHeaders.SiteURLData, JSON.stringify(data));
// Preview of customization/theme return NextResponse.redirect(contentRedirect);
const customization = url.searchParams.get('customization'); }
if (customization && validateSerializedCustomization(customization)) {
routeType = 'dynamic';
requestHeaders.set(MiddlewareHeaders.Customization, customization);
}
const theme = url.searchParams.get('theme');
if (theme === CustomizationThemeMode.Dark || theme === CustomizationThemeMode.Light) {
routeType = 'dynamic';
requestHeaders.set(MiddlewareHeaders.Theme, theme);
}
// We support forcing dynamic routes by setting a `gitbook-dynamic-route` cookie return NextResponse.redirect(siteURLData.redirect);
// This is useful for testing dynamic routes.
if (request.cookies.has('gitbook-dynamic-route')) {
routeType = 'dynamic';
}
// Pass a x-forwarded-host and origin that are equal to ensure Next doesn't block server actions when proxied
requestHeaders.set('x-forwarded-host', request.nextUrl.host);
requestHeaders.set('origin', request.nextUrl.origin);
const siteURL = `${url.host}${data.basePath}`;
const route = [
'sites',
routeType,
mode,
encodeURIComponent(siteURL),
encodePathInSiteContent(data.pathname),
].join('/');
console.log(`rewriting ${request.nextUrl.toString()} to ${route}`);
const rewrittenURL = new URL(`/${route}`, request.nextUrl.toString());
const response = NextResponse.rewrite(rewrittenURL, {
request: {
headers: requestHeaders,
},
});
// Add Content Security Policy header
response.headers.set('content-security-policy', getContentSecurityPolicy());
// Basic security headers
response.headers.set('strict-transport-security', 'max-age=31536000');
response.headers.set('referrer-policy', 'no-referrer-when-downgrade');
response.headers.set('x-content-type-options', 'nosniff');
// Debug header
response.headers.set('x-gitbook-route-type', routeType);
response.headers.set('x-gitbook-site-url', siteURL);
if (visitorToken) {
const cookies = getResponseCookiesForVisitorAuth(data.basePath, visitorToken);
for (const [key, value] of Object.entries(cookies)) {
response.cookies.set(key, value.value, value.options);
} }
cookies.push(
...getResponseCookiesForVisitorAuth(
getVisitorAuthBasePath(siteRequestURL, siteURLData),
visitorToken
)
);
// We use the host/origin from the canonical URL to ensure the links are
// correctly generated when the site is proxied. e.g. https://proxy.gitbook.com/site/siteId/...
const siteCanonicalURL = new URL(siteURLData.canonicalUrl);
let incomingURL = requestURL;
// For cases where the site is proxied, we use the canonical URL
// as the incoming URL along with all the search params from the request.
if (mode !== 'url') {
incomingURL = siteCanonicalURL;
incomingURL.search = requestURL.search;
}
//
// Make sure the URL is clean of any va token after a successful lookup
// The token is stored in a cookie that is set on the redirect response
//
const incomingURLWithoutToken = normalizeVisitorAuthURL(incomingURL);
if (incomingURLWithoutToken.toString() !== incomingURL.toString()) {
return writeResponseCookies(
NextResponse.redirect(incomingURLWithoutToken.toString()),
cookies
);
}
//
// Render and serve the content
//
// The route is static, except when using dynamic parameters from query params
// (customization override, theme, etc)
let routeType: 'dynamic' | 'static' = 'static';
// We pick only stable data from the siteURL data to prevent re-rendering of
// the root layout when changing pages..
const stableSiteURLData: SiteURLData = {
site: siteURLData.site,
siteSection: siteURLData.siteSection,
siteSpace: siteURLData.siteSpace,
siteBasePath: siteURLData.siteBasePath,
basePath: siteURLData.basePath,
space: siteURLData.space,
organization: siteURLData.organization,
changeRequest: siteURLData.changeRequest,
revision: siteURLData.revision,
shareKey: siteURLData.shareKey,
apiToken: siteURLData.apiToken,
imagesContextId: imagesContextId,
};
const requestHeaders = new Headers(request.headers);
requestHeaders.set(MiddlewareHeaders.RouteType, routeType);
requestHeaders.set(MiddlewareHeaders.URLMode, mode);
requestHeaders.set(
MiddlewareHeaders.SiteURL,
`${siteCanonicalURL.origin}${siteURLData.basePath}`
);
requestHeaders.set(MiddlewareHeaders.SiteURLData, JSON.stringify(stableSiteURLData));
// Preview of customization/theme
const customization = siteRequestURL.searchParams.get('customization');
if (customization && validateSerializedCustomization(customization)) {
routeType = 'dynamic';
requestHeaders.set(MiddlewareHeaders.Customization, customization);
}
const theme = siteRequestURL.searchParams.get('theme');
if (theme === CustomizationThemeMode.Dark || theme === CustomizationThemeMode.Light) {
routeType = 'dynamic';
requestHeaders.set(MiddlewareHeaders.Theme, theme);
}
// We support forcing dynamic routes by setting a `gitbook-dynamic-route` cookie
// This is useful for testing dynamic routes.
if (request.cookies.has('gitbook-dynamic-route')) {
routeType = 'dynamic';
}
// Pass a x-forwarded-host and origin that are equal to ensure Next doesn't block server actions when proxied
requestHeaders.set('x-forwarded-host', request.nextUrl.host);
requestHeaders.set('origin', request.nextUrl.origin);
const siteURLWithoutProtocol = `${siteCanonicalURL.host}${siteURLData.basePath}`;
const { pathname, routeType: routeTypeFromPathname } = encodePathInSiteContent(
siteURLData.pathname
);
routeType = routeTypeFromPathname ?? routeType;
const route = [
'sites',
routeType,
mode,
encodeURIComponent(siteURLWithoutProtocol),
encodeURIComponent(
rison.encode(
// rison can't encode undefined values
Object.fromEntries(
Object.entries(stableSiteURLData).filter(
([_, v]) => typeof v !== 'undefined'
)
)
)
),
pathname,
].join('/');
console.log(`rewriting ${request.nextUrl.toString()} to ${route}`);
const rewrittenURL = new URL(`/${route}`, request.nextUrl.toString());
const response = NextResponse.rewrite(rewrittenURL, {
request: {
headers: requestHeaders,
},
});
// Add Content Security Policy header
response.headers.set('content-security-policy', getContentSecurityPolicy());
// Basic security headers
response.headers.set('strict-transport-security', 'max-age=31536000');
response.headers.set('referrer-policy', 'no-referrer-when-downgrade');
response.headers.set('x-content-type-options', 'nosniff');
// Debug header
response.headers.set('x-gitbook-route-type', routeType);
response.headers.set('x-gitbook-route-site', siteURLWithoutProtocol);
return writeResponseCookies(response, cookies);
};
// For https://preview/<siteURL> requests,
if (siteRequestURL.hostname === 'preview') {
return serveWithQueryAPIToken(
// We scope the API token to the site ID.
`${siteRequestURL.hostname}/${requestURL.pathname.slice(1).split('/')[0]}`,
request,
withAPIToken
);
} }
return response; return withAPIToken(null);
}
/**
* Serve routes for PDF export for a space: /~space/:spaceId/~gitbook/pdf
*/
async function serveSpacePDFRoutes(requestURL: URL, request: NextRequest) {
const pathnameParts = requestURL.pathname.slice(1).split('/');
if (pathnameParts[0] !== '~space' && pathnameParts[0] !== '~site') {
return null;
}
return serveWithQueryAPIToken(
pathnameParts.slice(0, 2).join('/'),
request,
async (apiToken) => {
// Handle the rest with the router default logic
return NextResponse.next({
headers: {
[MiddlewareHeaders.APIToken]: apiToken,
},
});
}
);
} }
/** /**
* Serve an error response. * Serve an error response.
*/ */
function serveErrorResponse(error: Error) { function serveErrorResponse(error: Error) {
if (error instanceof GitBookAPIError) { if (error instanceof DataFetcherError) {
return new Response(error.errorMessage, { return new Response(error.message, {
status: error.code, status: error.code,
headers: { 'content-type': 'text/plain' }, headers: { 'content-type': 'text/plain' },
}); });
@@ -161,6 +319,45 @@ function serveErrorResponse(error: Error) {
throw error; throw error;
} }
/**
* Server a response with an API token obtained from the query params.
*/
async function serveWithQueryAPIToken(
scopePath: string,
request: NextRequest,
serve: (apiToken: string) => Promise<NextResponse>
) {
// We store the API token in a cookie that is scoped to the specific route
// to avoid errors when multiple previews are opened in different tabs.
const cookieName = getPathScopedCookieName('gitbook-api-token', scopePath);
// Extract a potential GitBook API token passed in the request
// If found, we redirect to the same URL but with the token in the cookie
const queryAPIToken = request.nextUrl.searchParams.get('token');
if (queryAPIToken) {
request.nextUrl.searchParams.delete('token');
return writeResponseCookies(NextResponse.redirect(request.nextUrl.toString()), [
{
name: cookieName,
value: queryAPIToken,
options: {
httpOnly: true,
sameSite: process.env.NODE_ENV === 'production' ? 'none' : undefined,
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 60, // 1 hour
},
},
]);
}
const apiToken = request.cookies.get(cookieName)?.value;
if (!apiToken) {
throw new DataFetcherError('Missing API token', 400);
}
return serve(apiToken);
}
/** /**
* The URL of the GitBook content can be passed in 3 different ways (in order of priority): * The URL of the GitBook content can be passed in 3 different ways (in order of priority):
* - The request has a `X-GitBook-URL` header: * - The request has a `X-GitBook-URL` header:
@@ -219,22 +416,41 @@ function getSiteURLFromRequest(request: NextRequest): URLWithMode | null {
* Encode path in a site content. * Encode path in a site content.
* Special paths are not encoded and passed to be handled by the route handlers. * Special paths are not encoded and passed to be handled by the route handlers.
*/ */
function encodePathInSiteContent(rawPathname: string) { function encodePathInSiteContent(rawPathname: string): {
pathname: string;
routeType?: 'static' | 'dynamic';
} {
const pathname = removeLeadingSlash(removeTrailingSlash(rawPathname)); const pathname = removeLeadingSlash(removeTrailingSlash(rawPathname));
if (pathname.match(/^~gitbook\/ogimage\/\S+$/)) { if (pathname.match(/^~gitbook\/ogimage\/\S+$/)) {
return pathname; return { pathname };
}
// If the pathname is a markdown file, we rewrite it to ~gitbook/markdown/:pathname
if (pathname.match(/\.md$/)) {
const pagePathWithoutMD = pathname.slice(0, -3);
return {
pathname: `~gitbook/markdown/${encodeURIComponent(pagePathWithoutMD)}`,
// The markdown content is always static and doesn't depend on the dynamic parameter (customization, theme, etc)
routeType: 'static',
};
} }
switch (pathname) { switch (pathname) {
case '~gitbook/icon': case '~gitbook/icon':
case '~gitbook/image': return { pathname };
case 'llms.txt': case 'llms.txt':
case 'sitemap.xml': case 'sitemap.xml':
case 'sitemap-pages.xml':
case 'robots.txt': case 'robots.txt':
return pathname; // LLMs.txt, sitemap, sitemap-pages and robots.txt are always static
// as they only depend on the site structure / pages.
return { pathname, routeType: 'static' };
case '~gitbook/pdf':
// PDF routes are always dynamic as they depend on the search params.
return { pathname, routeType: 'dynamic' };
default: default:
return encodeURIComponent(pathname || '/'); return { pathname: encodeURIComponent(pathname || '/') };
} }
} }
@@ -248,3 +464,14 @@ function appendQueryParams(url: URL, from: URLSearchParams) {
return url; return url;
} }
/**
* Write the cookies to a response.
*/
function writeResponseCookies<R extends NextResponse>(response: R, cookies: ResponseCookies): R {
cookies.forEach((cookie) => {
response.cookies.set(cookie.name, cookie.value, cookie.options);
});
return response;
}
+137
View File
@@ -0,0 +1,137 @@
{
"main": ".open-next/worker.js",
"name": "gitbook-open-v2",
"compatibility_date": "2025-04-14",
"compatibility_flags": ["nodejs_compat", "allow_importable_env"],
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS"
},
"observability": {
"enabled": true
},
"env": {
"preview": {
"r2_buckets": [
{
"binding": "NEXT_INC_CACHE_R2_BUCKET",
"bucket_name": "gitbook-open-v2-cache-preview"
}
],
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "gitbook-open-v2-preview"
},
{
"binding": "GITBOOK_API",
"service": "gitbook-x-prod-api-cache"
}
]
// No durable objects on preview, as they block the generation of preview URLs
// and we don't need tags invalidation on preview
},
"staging": {
"routes": [
{
"pattern": "open-2c.gitbook-staging.com/*",
"zone_name": "gitbook-staging.com"
},
{
"pattern": "static-2c.gitbook-staging.com/*",
"zone_name": "gitbook-staging.com"
}
],
"r2_buckets": [
{
"binding": "NEXT_INC_CACHE_R2_BUCKET",
"bucket_name": "gitbook-open-v2-cache-staging"
}
],
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "gitbook-open-v2-staging"
},
{
"binding": "GITBOOK_API",
"service": "gitbook-x-staging-api-cache"
}
],
"tail_consumers": [
{
"service": "gitbook-x-staging-tail"
}
],
"durable_objects": {
"bindings": [
{
"name": "NEXT_CACHE_DO_QUEUE",
"class_name": "DOQueueHandler"
},
{
"name": "NEXT_TAG_CACHE_DO_SHARDED",
"class_name": "DOShardedTagCache"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache"]
}
]
},
"production": {
"routes": [
{
"pattern": "open-2c.gitbook.com/*",
"zone_name": "gitbook.com"
},
{
"pattern": "static-2c.gitbook.com/*",
"zone_name": "gitbook.com"
}
],
"r2_buckets": [
{
"binding": "NEXT_INC_CACHE_R2_BUCKET",
"bucket_name": "gitbook-open-v2-cache-production"
}
],
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "gitbook-open-v2-production"
},
{
"binding": "GITBOOK_API",
"service": "gitbook-x-prod-api-cache"
}
],
"tail_consumers": [
{
"service": "gitbook-x-prod-tail"
}
],
"durable_objects": {
"bindings": [
{
"name": "NEXT_CACHE_DO_QUEUE",
"class_name": "DOQueueHandler"
},
{
"name": "NEXT_TAG_CACHE_DO_SHARDED",
"class_name": "DOShardedTagCache"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache"]
}
]
}
}
}
-38
View File
@@ -1,38 +0,0 @@
main = ".open-next/worker.js"
name = "gitbook-open-v2"
compatibility_date = "2024-09-23"
compatibility_flags = ["nodejs_compat"]
assets = { directory = ".open-next/assets", binding = "ASSETS" }
observability = { enabled = true }
[env.preview]
kv_namespaces = [
{ binding = "NEXT_CACHE_WORKERS_KV", id = "b7dd9cf58bf2458f84812a2d83b3760c" } # gitbook-open-v2-cache-preview
]
d1_databases = [
{ binding = "NEXT_CACHE_D1", database_id = "f59ddb40-ad72-4312-9395-0ac6a129af8e", database_name = "gitbook-open-v2-tags-preview" }
]
[env.staging]
routes = [
{ pattern = "open-2c.gitbook-staging.com/*", zone_name = "gitbook-staging.com" },
{ pattern = "static-2c.gitbook-staging.com/*", zone_name = "gitbook-staging.com" }
]
kv_namespaces = [
{ binding = "NEXT_CACHE_WORKERS_KV", id = "a446e25f12b741afb185f1e5b4474f0a" } # gitbook-open-v2-cache-staging
]
d1_databases = [
{ binding = "NEXT_CACHE_D1", database_id = "9df62e39-1f35-4066-83aa-e9b8ed3ac8d5", database_name = "gitbook-open-v2-tags-staging" }
]
[env.production]
routes = [
{ pattern = "open-2c.gitbook.com/*", zone_name = "gitbook.com" },
{ pattern = "static-2c.gitbook.com/*", zone_name = "gitbook.com" }
]
kv_namespaces = [
{ binding = "NEXT_CACHE_WORKERS_KV", id = "72379746280d4e79acf24440eea950dc" } # gitbook-open-v2-cache-production
]
d1_databases = [
{ binding = "NEXT_CACHE_D1", database_id = "a6f16fce-5f45-43a9-89a4-7b83ddf25b77", database_name = "gitbook-open-v2-tags-production" }
]
-3
View File
@@ -23,8 +23,5 @@
## Caching ## Caching
# GITBOOK_OUTPUT_CACHE=true # GITBOOK_OUTPUT_CACHE=true
### Sentry ###
# SENTRY_DSN=xxx
### Silent logs ### Silent logs
# SILENT=true # SILENT=true
-3
View File
@@ -23,9 +23,6 @@ next-env.d.ts
# visual tests # visual tests
screenshots/ screenshots/
# Sentry Config File
.sentryclirc
/test-results/ /test-results/
/playwright-report/ /playwright-report/
/blob-report/ /blob-report/
+553 -345
View File
@@ -1,435 +1,643 @@
# gitbook # gitbook
## 0.11.0
### Minor Changes
- d67699a: Add OpenAPI Webhook block
### Patch Changes
- 4b8a621: Show sections tabs only if there is at least two sections
- 8ed1bda: Translate OpenAPI blocks
- 7588cfe: Improve OpenAPIResponses examples and schemas
- Updated dependencies [eeb977f]
- Updated dependencies [3363a18]
- Updated dependencies [d67699a]
- Updated dependencies [8ed1bda]
- Updated dependencies [7588cfe]
- Updated dependencies [ad1dc0b]
- @gitbook/react-openapi@1.2.0
## 0.10.1
### Patch Changes
- Updated dependencies [77397ca]
- @gitbook/cache-tags@0.3.1
## 0.10.0
### Minor Changes
- b62b101: Do not set cookie to identify visitor for insights when disabled.
### Patch Changes
- 95ea22d: Cache AI Page Link summary
- daf41fc: Tweak footer design (and refactor)
- de53946: Fix security issue with injection of "javacript:` url in the back button of PDFs
- b92ecfa: Implement retry logic for the DO cache to prevent when revalidating content.
- 528eee3: Add superscript and subscript text rendering
- aa3357a: Fix OpenAPISchemas description padding
- 168a4fa: Add support for buttons to GitBook.
- 70c4182: Improve OpenAPI schema style
- 2b6c593: Remove stable from x-stability
- 580f7ad: Improve the error message returned by the revalidate endpoint.
- cbd768a: Improve OpenAPI codesample (add OpenAPISelect component)
- c765463: Fix ogimage generation crashing when site is using a custom WOFF2 font
- e59076a: Improve OpenAPI schemas block ungrouped style. Classnames have changed, please refer to this PR to update GBX.
- 29aaba5: Override Scalar's overscroll-behavior
- 90ead98: Better error handling in cache revalidation.
- Updated dependencies [116575c]
- Updated dependencies [cdffd7c]
- Updated dependencies [70c4182]
- Updated dependencies [2b6c593]
- Updated dependencies [cbd768a]
- Updated dependencies [e59076a]
- Updated dependencies [eedefdd]
- Updated dependencies [23cedd2]
- @gitbook/cache-tags@0.3.0
- @gitbook/colors@0.3.2
- @gitbook/react-openapi@1.1.10
- @gitbook/openapi-parser@2.1.3
## 0.9.2
### Patch Changes
- da7b369: Fix missing headers in OpenAPIResponses
- 139a805: Fix OpenAPI enum display
- Updated dependencies [da7b369]
- Updated dependencies [da485f5]
- Updated dependencies [139a805]
- @gitbook/react-openapi@1.1.9
## 0.9.1
### Patch Changes
- fb90eb0: Limit tinted background on bold theme to sites with filled sidebar
- 7d0b422: Handle grouped OpenAPISchemas
- Updated dependencies [7d0b422]
- Updated dependencies [fb90eb0]
- @gitbook/react-openapi@1.1.8
- @gitbook/colors@0.3.1
## 0.9.0
### Minor Changes
- 77fd393: Track event when visitor is opening a search result.
- d70d566: Support site announcement banner
- 77fd393: Track event when clicking announcement banner link.
### Patch Changes
- e84a46a: Fix OpenAPI tabs indicator overflow
- bc90adb: Fix favicon not being displayed in Google because `robots.txt` was preventing the indexation of the image route
- 434af90: Fix image resizing when using the proxy feature in a site.
- c756761: Add breadcrumbs to search results
- 40e8e69: Disallow crawling by web-robots of search/ask URLs
- 77fd393: Fix clicking search results when the site is embedded in an iframe.
- 1505ddb: Fix multiple request examples selector not showing
- 61db166: Add OpenAPI write-only indicator
- 6f71da8: Fix padding in schemas
- fa91eb7: Fix PDF generation when user has dark mode configured.
- 5b1e01c: Support for x-stability property
- 57ca4e0: Fix a crash when a page contains a block of an integration that is no longer installed
- d236bf0: Fix flash when loading sites with dark mode as default theme
- cd99ed5: Fix spec properties rendering and missing keys
- 813b2af: Support for x-enumDescriptions and x-gitbook-enum
- e9fa50d: Trim the search query to avoid showing a loading state when typing
- Updated dependencies [bd35348]
- Updated dependencies [ae78fc5]
- Updated dependencies [7bb37c7]
- Updated dependencies [373183a]
- Updated dependencies [1505ddb]
- Updated dependencies [61db166]
- Updated dependencies [5b1e01c]
- Updated dependencies [cd99ed5]
- Updated dependencies [813b2af]
- Updated dependencies [a25fded]
- @gitbook/react-openapi@1.1.7
- @gitbook/openapi-parser@2.1.2
## 0.8.2
### Patch Changes
- ed07206: Fix OpenAPI path overflow on mobile
- 4dab1c5: Fix alignment of `prominent` search bar on full-width pages
- 6eae764: Support body examples
- 54ee014: Add initial support for loading custom fonts
- d2facb2: Mark properties as optional if not required
- bba2e52: Fix site redirects when it includes a section/variant path
- 4723f03: Restyle section group dropdown
- 24b7808: Fix `prominent` search bar width on `md` screens
- 1fe3286: Fix OpenAPI block overflow issue
- Updated dependencies [48c18c0]
- Updated dependencies [6eae764]
- Updated dependencies [7212973]
- Updated dependencies [d2facb2]
- Updated dependencies [73e2b47]
- Updated dependencies [70be2c6]
- Updated dependencies [fc00b51]
- Updated dependencies [a84b06b]
- @gitbook/openapi-parser@2.1.1
- @gitbook/react-openapi@1.1.6
## 0.8.1
### Patch Changes
- 886e204: Update OpenAPI operation path design
- Updated dependencies [886e204]
- Updated dependencies [4f0a772]
- @gitbook/react-openapi@1.1.5
- @gitbook/colors@0.3.0
## 0.8.0
### Minor Changes
- eec3eed: Add styling for prominent search bar option
### Patch Changes
- 16292de: Display sidebar on no-TOC pages
- 9b5f971: Transparent background for OpenAPI path block
- 99da8df: Optimize favicons and og:image using the image resizer
- b011ea0: Fix rendering of code blocks in Ask AI when being streamed
- 9bc3d50: Info hint background and link color fixes
- 31d800e: Render OpenAPISchemas block
- Updated dependencies [6aaeae2]
- Updated dependencies [c60e9ba]
- Updated dependencies [9108c56]
- Updated dependencies [31d800e]
- Updated dependencies [ff3b708]
- Updated dependencies [f32bf1f]
- Updated dependencies [c9ea239]
- @gitbook/react-contentkit@0.7.0
- @gitbook/react-openapi@1.1.4
- @gitbook/cache-tags@0.2.0
## 0.7.3
### Patch Changes
- eaf2d68: OpenAPI operation title fallback in sections
- Updated dependencies [844059f]
- Updated dependencies [88f64b0]
- @gitbook/react-openapi@1.1.3
- @gitbook/icons@0.2.0
- @gitbook/react-contentkit@0.6.2
## 0.7.2
### Patch Changes
- f127d28: Rename OpenAPIModels to OpenAPISchemas
- Updated dependencies [f127d28]
- @gitbook/react-openapi@1.1.2
## 0.7.1
### Patch Changes
- Updated dependencies [f574858]
- @gitbook/react-openapi@1.1.1
## 0.7.0 ## 0.7.0
### Minor Changes ### Minor Changes
- bb3ca9c: Implement OpenAPI models blocks - bb3ca9c: Implement OpenAPI models blocks
### Patch Changes ### Patch Changes
- 5907bd9: Adjust hint block spacing - 5907bd9: Adjust hint block spacing
- 9ffc3b6: Fix content overflowing out of its container in tabs - 9ffc3b6: Fix content overflowing out of its container in tabs
- Updated dependencies [0278a14] - Updated dependencies [0278a14]
- Updated dependencies [bb3ca9c] - Updated dependencies [bb3ca9c]
- Updated dependencies [3173d8e] - Updated dependencies [3173d8e]
- Updated dependencies [052e07a] - Updated dependencies [052e07a]
- @gitbook/openapi-parser@2.1.0 - @gitbook/openapi-parser@2.1.0
- @gitbook/react-openapi@1.1.0 - @gitbook/react-openapi@1.1.0
## 0.6.5 ## 0.6.5
### Patch Changes ### Patch Changes
- 05ffd0e: Improving data cache management for computed content - 05ffd0e: Improving data cache management for computed content
- 8beb5d6: Add input elements to ContentKit - 8beb5d6: Add input elements to ContentKit
- Updated dependencies [53f5dbe] - Updated dependencies [53f5dbe]
- Updated dependencies [05ffd0e] - Updated dependencies [05ffd0e]
- Updated dependencies [8beb5d6] - Updated dependencies [8beb5d6]
- @gitbook/openapi-parser@2.0.2 - @gitbook/openapi-parser@2.0.2
- @gitbook/cache-tags@0.1.0 - @gitbook/cache-tags@0.1.0
- @gitbook/react-contentkit@0.6.1 - @gitbook/react-contentkit@0.6.1
- @gitbook/react-openapi@1.0.5 - @gitbook/react-openapi@1.0.5
## 0.6.4 ## 0.6.4
### Patch Changes ### Patch Changes
- 9b914d1: Fix getProxyModeBasePath that was computing incorrect base path in some scenarios - 9b914d1: Fix getProxyModeBasePath that was computing incorrect base path in some scenarios
- 2ae76f9: Change how a site in proxy mode is resolved - 2ae76f9: Change how a site in proxy mode is resolved
- 027a859: Add support for links style customization option - 027a859: Add support for links style customization option
- 3e11678: fix: lost section groups - 3e11678: fix: lost section groups
- 3319375: Support OpenAPI operation block - 3319375: Support OpenAPI operation block
- Updated dependencies [722f02e] - Updated dependencies [722f02e]
- Updated dependencies [0924259] - Updated dependencies [0924259]
- @gitbook/react-openapi@1.0.4 - @gitbook/react-openapi@1.0.4
- @gitbook/openapi-parser@2.0.1 - @gitbook/openapi-parser@2.0.1
## 0.6.3 ## 0.6.3
### Patch Changes ### Patch Changes
- a820739: Remove unused search api method from gitbook/api/lib - a820739: Remove unused search api method from gitbook/api/lib
- a054554: Implement a trusted mode to speed up OpenAPI spec validation - a054554: Implement a trusted mode to speed up OpenAPI spec validation
- 66d0fc0: Update design for hint block: use semantic colors (info, warning, danger, success) and add alternative styling for hints with headings - 66d0fc0: Update design for hint block: use semantic colors (info, warning, danger, success) and add alternative styling for hints with headings
- 9f0de74: Add support for new OpenAPI ref - 9f0de74: Add support for new OpenAPI ref
- da55fac: Render GitBook blocks in OpenAPI operation description - da55fac: Render GitBook blocks in OpenAPI operation description
- Updated dependencies [c808bb1] - Updated dependencies [c808bb1]
- Updated dependencies [dc2dbc5] - Updated dependencies [dc2dbc5]
- Updated dependencies [f1d1d2f] - Updated dependencies [f1d1d2f]
- Updated dependencies [e24206e] - Updated dependencies [e24206e]
- Updated dependencies [a054554] - Updated dependencies [a054554]
- Updated dependencies [05e1d8c] - Updated dependencies [05e1d8c]
- Updated dependencies [b4a12d6] - Updated dependencies [b4a12d6]
- Updated dependencies [9f0de74] - Updated dependencies [9f0de74]
- Updated dependencies [da55fac] - Updated dependencies [da55fac]
- @gitbook/openapi-parser@2.0.0 - @gitbook/openapi-parser@2.0.0
- @gitbook/react-openapi@1.0.3 - @gitbook/react-openapi@1.0.3
## 0.6.2 ## 0.6.2
### Patch Changes ### Patch Changes
- 359bb97: Fix opening external links when the GitBook page is embedded in an iframe - 359bb97: Fix opening external links when the GitBook page is embedded in an iframe
- 6157583: Improve Markdown parsing - 6157583: Improve Markdown parsing
- 82cd9f2: Add support for anchor links in OpenAPI blocks - 82cd9f2: Add support for anchor links in OpenAPI blocks
- Updated dependencies [445baaa] - Updated dependencies [445baaa]
- Updated dependencies [bb5c6a4] - Updated dependencies [bb5c6a4]
- Updated dependencies [a3f1fea] - Updated dependencies [a3f1fea]
- Updated dependencies [6157583] - Updated dependencies [6157583]
- Updated dependencies [7419ee7] - Updated dependencies [7419ee7]
- Updated dependencies [82cd9f2] - Updated dependencies [82cd9f2]
- @gitbook/colors@0.2.0 - @gitbook/colors@0.2.0
- @gitbook/react-openapi@1.0.2 - @gitbook/react-openapi@1.0.2
- @gitbook/openapi-parser@1.0.1 - @gitbook/openapi-parser@1.0.1
## 0.6.1 ## 0.6.1
### Patch Changes ### Patch Changes
- dddb4ec: Fix long tab group description - dddb4ec: Fix long tab group description
- Updated dependencies [f8d4c76] - Updated dependencies [f8d4c76]
- Updated dependencies [dddb4ec] - Updated dependencies [dddb4ec]
- Updated dependencies [f8d4c76] - Updated dependencies [f8d4c76]
- @gitbook/react-openapi@1.0.1 - @gitbook/react-openapi@1.0.1
## 0.6.0 ## 0.6.0
### Minor Changes ### Minor Changes
- 98245e5: Adapt code to pull token from customer backend generated custom cookies - 98245e5: Adapt code to pull token from customer backend generated custom cookies
- af3c6a9: Reintroduce a safety check around search whilst we continue investigating caching. - af3c6a9: Reintroduce a safety check around search whilst we continue investigating caching.
- 95f2aa4: Track new events for site insights when ads are being clicked - 95f2aa4: Track new events for site insights when ads are being clicked
- 08acea6: Investigate an issue causing caches to return empty objects instead of null/undefined. - 08acea6: Investigate an issue causing caches to return empty objects instead of null/undefined.
- 1138d59: Add support for sidebar background styles - 1138d59: Add support for sidebar background styles
- 9e18ae6: Overhaul colour scale & Tailwind colour classes - 9e18ae6: Overhaul colour scale & Tailwind colour classes
- e86e51f: Fix an issue where the redirects of potentially malicious images were not going through. - e86e51f: Fix an issue where the redirects of potentially malicious images were not going through.
- 7059c2b: Add support for computed content by fetching computed documents for pages. - 7059c2b: Add support for computed content by fetching computed documents for pages.
- c71d159: Track events for site insights using the new dedicated API. - c71d159: Track events for site insights using the new dedicated API.
- eb7c22f: Revert scalar to 1.0.87 to mitigate an issue with ApiClientModalProvider - eb7c22f: Revert scalar to 1.0.87 to mitigate an issue with ApiClientModalProvider
- ea1468c: Send redirectOnError param to getPublishedContent when token is pulled from cookie - ea1468c: Send redirectOnError param to getPublishedContent when token is pulled from cookie
- 7ee9158: Restyle PageAside to use sidebar list styles - 7ee9158: Restyle PageAside to use sidebar list styles
- dbba50c: Fix an issue where search and Ask AI triggers unnecessary renders when in a Visitor Authenticated site. - dbba50c: Fix an issue where search and Ask AI triggers unnecessary renders when in a Visitor Authenticated site.
- 1417279: Track clicks on links (header, footer, content) for site insights. - 1417279: Track clicks on links (header, footer, content) for site insights.
- 9eca010: Improve the display of recommended questions by streaming them. - 9eca010: Improve the display of recommended questions by streaming them.
- 160fca1: new OpenAPI blocks design - 160fca1: new OpenAPI blocks design
- 71688a8: Introduction of new themes: Clean, Muted, Bold, and Gradient - 71688a8: Introduction of new themes: Clean, Muted, Bold, and Gradient
- 1823101: Fix internal properties appearing in OpenAPI docs. - 1823101: Fix internal properties appearing in OpenAPI docs.
- 6a073e1: Add antialiasing for text rendering - 6a073e1: Add antialiasing for text rendering
- 8126a83: Improve readability of tables with hover style and vertical dividers - 8126a83: Improve readability of tables with hover style and vertical dividers
- db74ea3: Image optimization endpoint redirects to underlying image URL if the signature is not the latest. - db74ea3: Image optimization endpoint redirects to underlying image URL if the signature is not the latest.
- 99579ac: Fix a vulnerability issue for images using an older version of the image signing parameter. - 99579ac: Fix a vulnerability issue for images using an older version of the image signing parameter.
- e4e2f52: Track an event into site insights when visitor is opening the Scalar API client. - e4e2f52: Track an event into site insights when visitor is opening the Scalar API client.
### Patch Changes ### Patch Changes
- d876399: Fix UI search without ask AI enabled and fix error with questions not returned from API - d876399: Fix UI search without ask AI enabled and fix error with questions not returned from API
- c30bc24: Fix empty sitemap - c30bc24: Fix empty sitemap
- e90c96f: page outline on the right remains visible when scrolling, move mode toggler to PageAside - e90c96f: page outline on the right remains visible when scrolling, move mode toggler to PageAside
- 5b4e710: Support llms.txt - 5b4e710: Support llms.txt
- b6c3870: Add support for keyboard marks - b6c3870: Add support for keyboard marks
- 6059efe: Fix search no results error showing while there are results - 6059efe: Fix search no results error showing while there are results
- c77142a: Log component stack in Sentry - c77142a: Log component stack in Sentry
- 1de9d1a: Apply antialiasing on any text that are not code inline/blocks to avoid contrast issues - 1de9d1a: Apply antialiasing on any text that are not code inline/blocks to avoid contrast issues
- 32aa1f9: Handle security issue with cookies on Safari - 32aa1f9: Handle security issue with cookies on Safari
- d935fb1: Don't add extra page scroll when footer is not present - d935fb1: Don't add extra page scroll when footer is not present
- 53de5b1: Fix site section URL resolution in Ask AI sources - 53de5b1: Fix site section URL resolution in Ask AI sources
- 24f5249: Fix vertical section overflow color - 24f5249: Fix vertical section overflow color
- 1762f85: Reduce gap between subsequent header buttons - 1762f85: Reduce gap between subsequent header buttons
- c1e27cc: Fix pass Sentry release properly - c1e27cc: Fix pass Sentry release properly
- 5ae1b88: Fix shrinking page icons - 5ae1b88: Fix shrinking page icons
- 8f046a9: Start using tint in more places, TOC and PageAside - 8f046a9: Start using tint in more places, TOC and PageAside
- 665b6be: Ignore invalid API calls to `getSiteRedirectFromSource` API - 665b6be: Ignore invalid API calls to `getSiteRedirectFromSource` API
To reduce the load on the API and also avoid errors. To reduce the load on the API and also avoid errors.
- 26e6401: Remove KV cache backend and only rely on DO as an external cache backend - 26e6401: Remove KV cache backend and only rely on DO as an external cache backend
- 8cfa67c: Fix default outline list styling - 8cfa67c: Fix default outline list styling
- d66c184: Ignore errors from event flushing - d66c184: Ignore errors from event flushing
- 6088fa5: Simplify search results logic to investigate a bug - 6088fa5: Simplify search results logic to investigate a bug
- 68287d3: Cache API spec for 24 hours, revalidated every 2 hours - 68287d3: Cache API spec for 24 hours, revalidated every 2 hours
- 09c7c30: Try to fix error on og image generation - 09c7c30: Try to fix error on og image generation
- ae99f87: Improve emoji setup, align with GitBook app - ae99f87: Improve emoji setup, align with GitBook app
- 2906e60: Downgrade to Next.js v14 to fix incompatibilities with next-on-pages causing multiple bugs. - 2906e60: Downgrade to Next.js v14 to fix incompatibilities with next-on-pages causing multiple bugs.
- 3a7210d: Fix zoom image view transition on Safari - 3a7210d: Fix zoom image view transition on Safari
- 718a8a5: Position the variant picker in the ToC - 718a8a5: Position the variant picker in the ToC
- e5dc05e: Update footer styling and allow for more than 4 footer groups - e5dc05e: Update footer styling and allow for more than 4 footer groups
- 8276ba0: Make cookies access safer - 8276ba0: Make cookies access safer
- 1b8a456: Fix Image blocks zoomable behaviour - 1b8a456: Fix Image blocks zoomable behaviour
- 56c52e0: Handle Firefox security error on localStorage - 56c52e0: Handle Firefox security error on localStorage
- 0510b6f: Add section description to SectionGroupTile - 0510b6f: Add section description to SectionGroupTile
- 1fcc807: Fix errors from customization not found - 1fcc807: Fix errors from customization not found
- 46edde9: Improve the OpenAPI package API - 46edde9: Improve the OpenAPI package API
- 8af1abc: Improve contrast of search box placeholder - 8af1abc: Improve contrast of search box placeholder
- 92b7668: Improve header offset - 92b7668: Improve header offset
- d9c8d57: Do not dereference before caching OpenAPI spec. - d9c8d57: Do not dereference before caching OpenAPI spec.
- 94876e3: Fix regression issue with page icons for multi-line titles - 94876e3: Fix regression issue with page icons for multi-line titles
- 47971dc: Fix OG image generation for non-latin characters - 47971dc: Fix OG image generation for non-latin characters
- 82dc9c4: Simplify the `useHash` algo used. - 82dc9c4: Simplify the `useHash` algo used.
- 128ad20: Ignore cache invalidation error from local backend - 128ad20: Ignore cache invalidation error from local backend
- ff05e20: Improvements to inline images in headings - ff05e20: Improvements to inline images in headings
- cb100d5: Allow only good values for theme query parameter. Avoid having a 500 error when we pass an invalid value. - cb100d5: Allow only good values for theme query parameter. Avoid having a 500 error when we pass an invalid value.
- d5aaccd: Remove use of deprecatd API createSitesPageFeedback - d5aaccd: Remove use of deprecatd API createSitesPageFeedback
- 48ab59f: Improve colour contrast of list item decoration - 48ab59f: Improve colour contrast of list item decoration
- d2bc567: Set Sentry release - d2bc567: Set Sentry release
- 37d13d8: Avoid error on fetch by passing a string URL - 37d13d8: Avoid error on fetch by passing a string URL
- d3e573c: Generate sitemap for all sections and spaces - d3e573c: Generate sitemap for all sections and spaces
- f7b801b: Add feedback form to page rating control - f7b801b: Add feedback form to page rating control
- d370a3f: Update the routes for changes/revisions in multi-id mode to match the normal mode - d370a3f: Update the routes for changes/revisions in multi-id mode to match the normal mode
- 46f63cb: Fix code format overriding inline link styles - 46f63cb: Fix code format overriding inline link styles
- 5950657: Fix emojis display - 5950657: Fix emojis display
- 528a053: Fix server actions stability leading to no results found sometimes on search - 528a053: Fix server actions stability leading to no results found sometimes on search
- eac1314: Lazy load iframely script to make page more responsive - eac1314: Lazy load iframely script to make page more responsive
- ad19060: Cards stand out slighly more on tinted and dark mode sites, and have better support for headings inside them - ad19060: Cards stand out slighly more on tinted and dark mode sites, and have better support for headings inside them
- 6f54826: New highlight colors - 6f54826: New highlight colors
- 5c87ec7: Implement a safer way to interact with localStorage. - 5c87ec7: Implement a safer way to interact with localStorage.
If it's disabled on the browser it should not throw error. If it's disabled on the browser it should not throw error.
- 02d876e: Fix search UI behaviour - 02d876e: Fix search UI behaviour
- f4a90de: Fix two issues where pages would crash due Recoil not behaving correctly in RSC. - f4a90de: Fix two issues where pages would crash due Recoil not behaving correctly in RSC.
- 5576906: Fix table of content displaying arrow next to page with only hidden pages - 5576906: Fix table of content displaying arrow next to page with only hidden pages
- aaab157: Visual fix for section group in Safari - aaab157: Visual fix for section group in Safari
- cbe6139: Fix dynamic tabs infinite loop - cbe6139: Fix dynamic tabs infinite loop
- 65cc4af: Fix error when accessing a change request not found - 65cc4af: Fix error when accessing a change request not found
- 727bde2: Improve and split OpenAPI parser into its own package - 727bde2: Improve and split OpenAPI parser into its own package
- 0b6ddca: Fix variant selector contrast for non-default themes - 0b6ddca: Fix variant selector contrast for non-default themes
- 87b8ea8: Fix issue leading to increase the storage write and the stability of the platform - 87b8ea8: Fix issue leading to increase the storage write and the stability of the platform
- fde32e2: Force route handler to be dynamic to avoid errors - fde32e2: Force route handler to be dynamic to avoid errors
- a025118: Change card layout depending on cover aspect ratio - a025118: Change card layout depending on cover aspect ratio
- 300f7bf: Fix search loading state - 300f7bf: Fix search loading state
- 29d5979: Disable C/C++ highlight temporarily - 29d5979: Disable C/C++ highlight temporarily
- 18953b2: Subtler tint color when based on the primary color, by mixing in some gray - 18953b2: Subtler tint color when based on the primary color, by mixing in some gray
- 1c97536: Fix Sentry instrumentation - 1c97536: Fix Sentry instrumentation
- b0bd871: Even safer localStorage - b0bd871: Even safer localStorage
- b950a64: Avoid errors on legacy browsers - b950a64: Avoid errors on legacy browsers
- 38061bd: Add section groups to section tabs - 38061bd: Add section groups to section tabs
- 160fca1: Support deprecated and x-deprecated-sunset in OpenAPI spec - 160fca1: Support deprecated and x-deprecated-sunset in OpenAPI spec
- 0e601e2: Improve styling of header buttons with shadows and high-contrast styles - 0e601e2: Improve styling of header buttons with shadows and high-contrast styles
- 6691492: Fix viewing PDF from space - 6691492: Fix viewing PDF from space
- e8e64bf: Fix bullet list display on full size blocks - e8e64bf: Fix bullet list display on full size blocks
- 16194c5: Vertical orientation for sections list on sites without header - 16194c5: Vertical orientation for sections list on sites without header
- b41d425: Improve OpenAPI rendering performances by caching markdown parsing - b41d425: Improve OpenAPI rendering performances by caching markdown parsing
- 1f8e416: Improve performances by highlighting code client-side if the code block is offscreen - 1f8e416: Improve performances by highlighting code client-side if the code block is offscreen
- 1429384: Fix error when accessing some not found pages. - 1429384: Fix error when accessing some not found pages.
- 21cbd9e: Change link color to primary-subtle - 21cbd9e: Change link color to primary-subtle
- 5dab70f: Fix "Parser" language syntax highlighting - 5dab70f: Fix "Parser" language syntax highlighting
- deb8c54: Upgrade Next.js to v15, upgrade Shiki and use JS RegExp engine - deb8c54: Upgrade Next.js to v15, upgrade Shiki and use JS RegExp engine
- 56331d2: Fix breadcrumbs emoji display + add contrast styles - 56331d2: Fix breadcrumbs emoji display + add contrast styles
- a6f6591: Fix server actions cache compromised. Leading to some bugs on frontend. - a6f6591: Fix server actions cache compromised. Leading to some bugs on frontend.
- 44a20fe: Improve smoothness of scroll listener - 44a20fe: Improve smoothness of scroll listener
- 5664e5a: Fix variant dropdown styling in header - 5664e5a: Fix variant dropdown styling in header
- 6b50360: Fix view transition error on Safari - 6b50360: Fix view transition error on Safari
- 741dd49: Bump `heading-3` font size to offset it from paragraphs - 741dd49: Bump `heading-3` font size to offset it from paragraphs
- 5112e3e: Fix Sentry instrumentation server-side - 5112e3e: Fix Sentry instrumentation server-side
- 1de338c: Remove animation on section tabs. Page is reloaded (for technical reasons), so the animation is not accurate here. - 1de338c: Remove animation on section tabs. Page is reloaded (for technical reasons), so the animation is not accurate here.
- Updated dependencies [d9029c7] - Updated dependencies [d9029c7]
- Updated dependencies [6e54a06] - Updated dependencies [6e54a06]
- Updated dependencies [162b4b7] - Updated dependencies [162b4b7]
- Updated dependencies [e4e2f52] - Updated dependencies [e4e2f52]
- Updated dependencies [0c03676] - Updated dependencies [0c03676]
- Updated dependencies [3e5e458] - Updated dependencies [3e5e458]
- Updated dependencies [46edde9] - Updated dependencies [46edde9]
- Updated dependencies [d9c8d57] - Updated dependencies [d9c8d57]
- Updated dependencies [ccf2cff] - Updated dependencies [ccf2cff]
- Updated dependencies [dda0cc6] - Updated dependencies [dda0cc6]
- Updated dependencies [eb7c22f] - Updated dependencies [eb7c22f]
- Updated dependencies [ea1468c] - Updated dependencies [ea1468c]
- Updated dependencies [648f0e9] - Updated dependencies [648f0e9]
- Updated dependencies [160fca1] - Updated dependencies [160fca1]
- Updated dependencies [f92e906] - Updated dependencies [f92e906]
- Updated dependencies [e721f17] - Updated dependencies [e721f17]
- Updated dependencies [727bde2] - Updated dependencies [727bde2]
- Updated dependencies [dff08ae] - Updated dependencies [dff08ae]
- Updated dependencies [fc7b16f] - Updated dependencies [fc7b16f]
- Updated dependencies [fe8acc9] - Updated dependencies [fe8acc9]
- Updated dependencies [1823101] - Updated dependencies [1823101]
- Updated dependencies [a652958] - Updated dependencies [a652958]
- Updated dependencies [2f73db7] - Updated dependencies [2f73db7]
- Updated dependencies [160fca1] - Updated dependencies [160fca1]
- Updated dependencies [12c7862] - Updated dependencies [12c7862]
- Updated dependencies [b41d425] - Updated dependencies [b41d425]
- @gitbook/react-openapi@1.0.0 - @gitbook/react-openapi@1.0.0
- @gitbook/openapi-parser@1.0.0 - @gitbook/openapi-parser@1.0.0
- @gitbook/react-contentkit@0.6.0 - @gitbook/react-contentkit@0.6.0
## 0.5.0 ## 0.5.0
### Minor Changes ### 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. - 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. - 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. - 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. - 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. - d843e5e: Fix an issue where the space dropdown could appear behind the header.
- a2e5647: Fix the styling of site section tabs on smaller screens. - a2e5647: Fix the styling of site section tabs on smaller screens.
### Patch Changes ### Patch Changes
- 076dc48: Fix expandable block anchore resolution - 076dc48: Fix expandable block anchore resolution
- d9bb9f9: Fix an issue with the cookie banner buttons being non responsive - 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 - 23584c9: Update the site header with new styling, a new search button, and refactored layout
- 664debc: Add support for tint color - 664debc: Add support for tint color
- 4d56f11: Update styling of search+ask modal - 4d56f11: Update styling of search+ask modal
- 061c0c1: Fix a regression in variant drop-down caused by missing z-index. - 061c0c1: Fix a regression in variant drop-down caused by missing z-index.
- 2f76712: Add breadcrumbs above page title - 2f76712: Add breadcrumbs above page title
- 07cf835: Add scroll margin to the top when there are sections - 07cf835: Add scroll margin to the top when there are sections
- 5d72b35: Smoother tab transition for sections - 5d72b35: Smoother tab transition for sections
- 7c71363: Don't adjust fallback font for mono font. - 7c71363: Don't adjust fallback font for mono font.
- 7675c2c: Optimize performances by using new API endpoint for fetching site data. - 7675c2c: Optimize performances by using new API endpoint for fetching site data.
- 87eea73: Fix margin and image resolution of header logo - 87eea73: Fix margin and image resolution of header logo
- aa2ed0f: Restyle hint blocks - aa2ed0f: Restyle hint blocks
- ffd3937: Fix security issue with image resizing that could be used for phishing - ffd3937: Fix security issue with image resizing that could be used for phishing
- 2ce59d7: Fix - whitespace added to site section tabs with icons. - 2ce59d7: Fix - whitespace added to site section tabs with icons.
- c73e07d: Increase token max length to fix code not highlighted - c73e07d: Increase token max length to fix code not highlighted
- 3b3d6e2: Add icons to sections - 3b3d6e2: Add icons to sections
- 1ed18c0: style: adds missing scalar css variables - 1ed18c0: style: adds missing scalar css variables
- Updated dependencies [b7a5106] - Updated dependencies [b7a5106]
- Updated dependencies [4771c78] - Updated dependencies [4771c78]
- Updated dependencies [ff50ac2] - Updated dependencies [ff50ac2]
- Updated dependencies [867481c] - Updated dependencies [867481c]
- Updated dependencies [7ba67fd] - Updated dependencies [7ba67fd]
- Updated dependencies [a78c1ec] - Updated dependencies [a78c1ec]
- @gitbook/cache-do@0.1.1 - @gitbook/cache-do@0.1.1
- @gitbook/react-openapi@0.7.1 - @gitbook/react-openapi@0.7.1
## 0.4.0 ## 0.4.0
### Minor Changes ### Minor Changes
- e09f747: Revalidate change request cached content when pressing refresh button - e09f747: Revalidate change request cached content when pressing refresh button
- 2fa0851: Add navigation tabs for sections - 2fa0851: Add navigation tabs for sections
- a4b63b8: Support resolution of new site URLs with sections - a4b63b8: Support resolution of new site URLs with sections
- 5c35f36: Replace all icons, previously imported from Geist, by new package `@gitbook/icons` - 5c35f36: Replace all icons, previously imported from Geist, by new package `@gitbook/icons`
- e9b31a5: Unify section tab styles with page item styles - e9b31a5: Unify section tab styles with page item styles
- f12a215: Add support for Norwegian language - f12a215: Add support for Norwegian language
- f4c9536: Optimize layout shift while transitioning between pages with full width blocks (ex: OpenAPI blocks) - f4c9536: Optimize layout shift while transitioning between pages with full width blocks (ex: OpenAPI blocks)
- 1f24fe4: Add support for page icons - 1f24fe4: Add support for page icons
- cda08a9: Add support for searching results in a sections site - cda08a9: Add support for searching results in a sections site
- b32e40c: Persist state of tabs and dynamically sync them based on title - b32e40c: Persist state of tabs and dynamically sync them based on title
- 15d2ee3: Show the caption for file blocks - 15d2ee3: Show the caption for file blocks
- f885e88: Improve the toolbar for change-requests and revisions to show more actions - f885e88: Improve the toolbar for change-requests and revisions to show more actions
- 07ea45b: Remove deprecated synced block from GitBook Open - 07ea45b: Remove deprecated synced block from GitBook Open
- c3675fd: Added support for new Reusable Content block. - c3675fd: Added support for new Reusable Content block.
- 1f24fe4: Add support for icons style customization for sites - 1f24fe4: Add support for icons style customization for sites
- 4c19014: Prevent search indexation for pages where it's configured as disabled - 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. - 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 - 1152445: Changed the alternative URL resolution criteria in order to support site URLs without /v/ prefix
- 2c437f7: Fix linking to a tab itself - 2c437f7: Fix linking to a tab itself
### Patch Changes ### Patch Changes
- aa32198: Avoid multiple <h1> in the page by using a <div> for the title in the header - 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 - 51fa3ab: Adds content-visibility css property to OpenAPI Operation for better render performance
- a7066cc: Fix scroll position when navigating pages on mobile - a7066cc: Fix scroll position when navigating pages on mobile
- c754fc9: Add automatic color contrast in site header, restyle search button - c754fc9: Add automatic color contrast in site header, restyle search button
- 5fe7adb: RND-3532: drop down menu for hidden links at small screen size - 5fe7adb: RND-3532: drop down menu for hidden links at small screen size
- 6295881: Change dark mode shadow for multi-space search toolbar - 6295881: Change dark mode shadow for multi-space search toolbar
- f89b31c: Upgrade the scalar api client package - f89b31c: Upgrade the scalar api client package
- 13c7534: Use ellipsis and fix icon color for more links in the header on small screen - 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 - f885e88: Improve consistency of change request preview by removing cache-control on response
- 16e6171: Improve performances of loading pages with embeds by caching them - 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 - 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 - 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 - 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 - 042b850: Automatically scroll to active item in TOC
- d43202f: Optimize bundle size of the server output by reducing bundle size of shiki (skipping themes) - 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 - bfbed1a: Ensure "Sponsored via GitBook" can be translated in all languages
- fe9e6c1: Update ogimage with new design - fe9e6c1: Update ogimage with new design
- 17f71ba: Use url hash to open Expandable and scroll to anchor - 17f71ba: Use url hash to open Expandable and scroll to anchor
- 3c07e65: Fix margin for paragraphs in quote blocks - 3c07e65: Fix margin for paragraphs in quote blocks
- 636b868: Use new cache backend, powered by Durable Objects, alongside the existing ones (KV, etc). - 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 - f16560c: Include offset in calculations of whether scrollable element is in view
- 689f553: Fix inconsistent click area in table because of scroll indicator - 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 - 6ce3cea: Stop using KV cache backend for now, but also improves it for higher performances
- e914903: Synchronize response and response example tabs - e914903: Synchronize response and response example tabs
- 0f990c7: Show definition title when visible in cards - 0f990c7: Show definition title when visible in cards
- e3a3d6a: Fix flickering when displaying an "Ask" answer with code blocks - e3a3d6a: Fix flickering when displaying an "Ask" answer with code blocks
- 4cbcc5b: Rollback of scalar modal while fixing perf issue - 4cbcc5b: Rollback of scalar modal while fixing perf issue
- 3996110: Optimize images rendered in community ads - 3996110: Optimize images rendered in community ads
- 133c3e7: Update design of Checkbox to be more consistent and readable - 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 - 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 - 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 - 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` - 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 - cb782a7: Fix "ip" being passed to BSA for community ads
- a7af3ca: Improving the look and feel of new section tabs - a7af3ca: Improving the look and feel of new section tabs
- 0bf985a: Don't show hidden pages in the empty state of a page - 0bf985a: Don't show hidden pages in the empty state of a page
- d6c28a0: Update header styling of sections, variant selector, and button links - 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) - Change position of variant selector depending on context (next to logo or in table of contents)
- Update section tab styling and animation - Update section tab styling and animation
- Make header buttons smaller with a new `medium` button size - Make header buttons smaller with a new `medium` button size
- Updated dependencies [51fa3ab] - Updated dependencies [51fa3ab]
- Updated dependencies [9b8d519] - Updated dependencies [9b8d519]
- Updated dependencies [cf3045a] - Updated dependencies [cf3045a]
- Updated dependencies [f89b31c] - Updated dependencies [f89b31c]
- Updated dependencies [d0f4860] - Updated dependencies [d0f4860]
- Updated dependencies [ef9d012] - Updated dependencies [ef9d012]
- Updated dependencies [094e9cd] - Updated dependencies [094e9cd]
- Updated dependencies [636b868] - Updated dependencies [636b868]
- Updated dependencies [56f5fa1] - Updated dependencies [56f5fa1]
- Updated dependencies [5c35f36] - Updated dependencies [5c35f36]
- Updated dependencies [4247361] - Updated dependencies [4247361]
- Updated dependencies [aa8c49e] - Updated dependencies [aa8c49e]
- Updated dependencies [e914903] - Updated dependencies [e914903]
- Updated dependencies [4cbcc5b] - Updated dependencies [4cbcc5b]
- Updated dependencies [0f1565c] - Updated dependencies [0f1565c]
- Updated dependencies [237b703] - Updated dependencies [237b703]
- Updated dependencies [51955da] - Updated dependencies [51955da]
- Updated dependencies [a679e72] - Updated dependencies [a679e72]
- Updated dependencies [c079c3c] - Updated dependencies [c079c3c]
- Updated dependencies [5c35f36] - Updated dependencies [5c35f36]
- Updated dependencies [776bc31] - Updated dependencies [776bc31]
- @gitbook/react-openapi@0.7.0 - @gitbook/react-openapi@0.7.0
- @gitbook/cache-do@0.1.0 - @gitbook/cache-do@0.1.0
- @gitbook/icons@0.1.0 - @gitbook/icons@0.1.0
- @gitbook/react-contentkit@0.5.1 - @gitbook/react-contentkit@0.5.1
- @gitbook/react-math@0.6.0 - @gitbook/react-math@0.6.0
## 0.3.0 ## 0.3.0
### Minor Changes ### Minor Changes
- 24b785c: Update shiki for code block syntax highlighting, with support for more languages and fixes for diffs. It also patches the deployment on Cloudflare to support edge functions larger than 4MB. - 24b785c: Update shiki for code block syntax highlighting, with support for more languages and fixes for diffs. It also patches the deployment on Cloudflare to support edge functions larger than 4MB.
### Patch Changes ### Patch Changes
- acc3f2f: Fix error with the "Try it" of OpenAPI block because of the Scalar proxy failing on Cloudflare with the `cache` option - acc3f2f: Fix error with the "Try it" of OpenAPI block because of the Scalar proxy failing on Cloudflare with the `cache` option
- Updated dependencies [709f1a1] - Updated dependencies [709f1a1]
- Updated dependencies [ede2335] - Updated dependencies [ede2335]
- Updated dependencies [0426312] - Updated dependencies [0426312]
- @gitbook/react-openapi@0.6.0 - @gitbook/react-openapi@0.6.0
## 0.2.2 ## 0.2.2
### Patch Changes ### Patch Changes
- Updated dependencies [3445db4] - Updated dependencies [3445db4]
- @gitbook/react-contentkit@0.5.0 - @gitbook/react-contentkit@0.5.0
- @gitbook/react-openapi@0.5.0 - @gitbook/react-openapi@0.5.0
- @gitbook/react-math@0.5.0 - @gitbook/react-math@0.5.0
## 0.2.1 ## 0.2.1
### Patch Changes ### Patch Changes
- Updated dependencies [24cd72e] - Updated dependencies [24cd72e]
- @gitbook/react-contentkit@0.4.0 - @gitbook/react-contentkit@0.4.0
- @gitbook/react-math@0.4.0 - @gitbook/react-math@0.4.0
- @gitbook/react-openapi@0.4.0 - @gitbook/react-openapi@0.4.0
## 0.2.0 ## 0.2.0
### Minor Changes ### Minor Changes
- de747b7: Refactor the repository to be a proper monorepo and publish JS files on NPM instead of TypeScript files. - de747b7: Refactor the repository to be a proper monorepo and publish JS files on NPM instead of TypeScript files.
### Patch Changes ### Patch Changes
- Updated dependencies [de747b7] - Updated dependencies [de747b7]
- Updated dependencies [de747b7] - Updated dependencies [de747b7]
- @gitbook/react-contentkit@0.3.0 - @gitbook/react-contentkit@0.3.0
- @gitbook/react-openapi@0.3.0 - @gitbook/react-openapi@0.3.0
- @gitbook/react-math@0.3.0 - @gitbook/react-math@0.3.0
+93 -75
View File
@@ -1,234 +1,252 @@
import { type TestsCase, runTestCases } from './util'; import { type TestsCase, runTestCases, waitForCookiesDialog } from './util';
/** A list of test cases to run on the customers' docs sites. */ /** A list of test cases to run on the customers' docs sites. */
const testCases: TestsCase[] = [ const testCases: TestsCase[] = [
{ {
name: 'Snyk', name: 'Snyk',
baseUrl: 'https://docs.snyk.io', contentBaseURL: 'https://docs.snyk.io',
tests: [ tests: [
{ name: 'Home', url: '/' }, { name: 'Home', url: '/', run: waitForCookiesDialog },
{ name: 'OpenAPI', url: '/snyk-api/reference/apps' }, { name: 'OpenAPI', url: '/snyk-api/reference/apps', run: waitForCookiesDialog },
], ],
}, },
{ // {
name: 'Nexthink', // name: 'Nexthink',
baseUrl: 'https://docs.nexthink.com', // contentBaseURL: 'https://docs.nexthink.com',
tests: [{ name: 'Home', url: '/', screenshot: { waitForTOCScrolling: false } }], // tests: [
}, // {
// name: 'Home',
// url: '/',
// screenshot: { waitForTOCScrolling: false },
// run: waitForCookiesDialog,
// },
// ],
// },
{ {
name: 'asiksupport-stg.dto.kemkes.go.id', name: 'asiksupport-stg.dto.kemkes.go.id',
baseUrl: 'https://asiksupport-stg.dto.kemkes.go.id', contentBaseURL: 'https://asiksupport-stg.dto.kemkes.go.id',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/' }],
}, },
{ {
name: 'jasons-tutorials.gitbook.io', name: 'jasons-tutorials.gitbook.io',
baseUrl: 'https://jasons-tutorials.gitbook.io', contentBaseURL: 'https://jasons-tutorials.gitbook.io',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/' }],
}, },
{ {
name: 'faq.deltaemulator.com', name: 'faq.deltaemulator.com',
baseUrl: 'https://faq.deltaemulator.com', contentBaseURL: 'https://faq.deltaemulator.com',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/' }],
}, },
{ {
name: 'docs.dify.ai', name: 'docs.dify.ai',
baseUrl: 'https://docs.dify.ai', contentBaseURL: 'https://docs.dify.ai',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
}, },
{ {
name: 'seeddao.gitbook.io', name: 'seeddao.gitbook.io',
baseUrl: 'https://seeddao.gitbook.io', contentBaseURL: 'https://seeddao.gitbook.io',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/' }],
}, },
{ {
name: 'faq.altstore.io', name: 'faq.altstore.io',
baseUrl: 'https://faq.altstore.io', contentBaseURL: 'https://faq.altstore.io',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/' }],
}, },
{ {
name: 'support.audacityteam.org', name: 'support.audacityteam.org',
baseUrl: 'https://support.audacityteam.org', contentBaseURL: 'https://support.audacityteam.org',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/' }],
}, },
{ {
name: 'docs.gmgn.ai', name: 'docs.gmgn.ai',
baseUrl: 'https://docs.gmgn.ai', contentBaseURL: 'https://docs.gmgn.ai',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/' }],
}, },
{ {
name: 'docs.spicychat.ai', name: 'docs.spicychat.ai',
baseUrl: 'https://docs.spicychat.ai', contentBaseURL: 'https://docs.spicychat.ai',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/' }],
}, },
{ {
name: 'docs.portainer.io', name: 'docs.portainer.io',
baseUrl: 'https://docs.portainer.io', contentBaseURL: 'https://docs.portainer.io',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
}, },
{ {
name: 'docs.chirptoken.io', name: 'docs.chirptoken.io',
baseUrl: 'https://docs.chirptoken.io', contentBaseURL: 'https://docs.chirptoken.io',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
}, },
{ {
name: 'docs.dexscreener.com', name: 'docs.dexscreener.com',
baseUrl: 'https://docs.dexscreener.com', contentBaseURL: 'https://docs.dexscreener.com',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
}, },
{ {
name: 'docs.pancakeswap.finance', name: 'docs.pancakeswap.finance',
baseUrl: 'https://docs.pancakeswap.finance', contentBaseURL: 'https://docs.pancakeswap.finance',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
}, },
{ {
name: 'book.character.ai', name: 'book.character.ai',
baseUrl: 'https://book.character.ai', contentBaseURL: 'https://book.character.ai',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
},
{
name: 'docs.tradeonnova.io',
baseUrl: 'https://docs.tradeonnova.io',
tests: [{ name: 'Home', url: '/' }],
}, },
{ {
name: 'azcoiner.gitbook.io', name: 'azcoiner.gitbook.io',
baseUrl: 'https://azcoiner.gitbook.io', contentBaseURL: 'https://azcoiner.gitbook.io',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/' }],
}, },
{ {
name: 'docs.midas.app', name: 'docs.midas.app',
baseUrl: 'https://docs.midas.app', contentBaseURL: 'https://docs.midas.app',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/' }],
}, },
{ {
name: 'docs.keeper.io', name: 'docs.keeper.io',
baseUrl: 'https://docs.keeper.io', contentBaseURL: 'https://docs.keeper.io',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
}, },
{ {
name: 'adiblar.gitbook.io', name: 'adiblar.gitbook.io',
baseUrl: 'https://adiblar.gitbook.io', contentBaseURL: 'https://adiblar.gitbook.io',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/' }],
}, },
{ {
name: 'docs.gradient.network', name: 'docs.gradient.network',
baseUrl: 'https://docs.gradient.network', contentBaseURL: 'https://docs.gradient.network',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/' }],
}, },
{ {
name: 'mygate-network.gitbook.io', name: 'mygate-network.gitbook.io',
baseUrl: 'https://mygate-network.gitbook.io', contentBaseURL: 'https://mygate-network.gitbook.io',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/' }],
}, },
{ {
name: 'treasurenft.gitbook.io', name: 'treasurenft.gitbook.io',
baseUrl: 'https://treasurenft.gitbook.io', contentBaseURL: 'https://treasurenft.gitbook.io',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/' }],
}, },
{ {
name: 'browndust2.gitbook.io', name: 'browndust2.gitbook.io',
baseUrl: 'https://browndust2.gitbook.io', contentBaseURL: 'https://browndust2.gitbook.io',
tests: [{ name: 'Home', url: '/', screenshot: { waitForTOCScrolling: false } }], tests: [{ name: 'Home', url: '/', screenshot: { waitForTOCScrolling: false } }],
}, },
{ {
name: 'junookyo.gitbook.io', name: 'junookyo.gitbook.io',
baseUrl: 'https://junookyo.gitbook.io', contentBaseURL: 'https://junookyo.gitbook.io',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/' }],
}, },
{ {
name: 'meshnet.nordvpn.com', name: 'meshnet.nordvpn.com',
baseUrl: 'https://meshnet.nordvpn.com', contentBaseURL: 'https://meshnet.nordvpn.com',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
}, },
{ {
name: 'manual.bubble.io', name: 'manual.bubble.io',
baseUrl: 'https://manual.bubble.io', contentBaseURL: 'https://manual.bubble.io',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
}, },
{ {
name: 'docs.tickettool.xyz', name: 'docs.tickettool.xyz',
baseUrl: 'https://docs.tickettool.xyz', contentBaseURL: 'https://docs.tickettool.xyz',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/' }],
}, },
{ {
name: 'wiki.redmodding.org', name: 'wiki.redmodding.org',
baseUrl: 'https://wiki.redmodding.org', contentBaseURL: 'https://wiki.redmodding.org',
tests: [{ name: 'Home', url: '/' }],
},
{
name: 'docs.cherry-ai.com',
baseUrl: 'https://docs.cherry-ai.com',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/' }],
}, },
// {
// name: 'docs.cherry-ai.com',
// contentBaseURL: 'https://docs.cherry-ai.com',
// tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
// },
{ {
name: 'docs.snyk.io', name: 'docs.snyk.io',
baseUrl: 'https://docs.snyk.io', contentBaseURL: 'https://docs.snyk.io',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
}, },
{ {
name: 'docs.realapp.link', name: 'docs.realapp.link',
baseUrl: 'https://docs.realapp.link', contentBaseURL: 'https://docs.realapp.link',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/' }],
}, },
{ {
name: 'docs.plaza.finance', name: 'docs.plaza.finance',
baseUrl: 'https://docs.plaza.finance', contentBaseURL: 'https://docs.plaza.finance',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/' }],
}, },
{ {
name: 'docs.publicai.io', name: 'docs.publicai.io',
baseUrl: 'https://docs.publicai.io', contentBaseURL: 'https://docs.publicai.io',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/' }],
}, },
{ {
name: 'hyperliquid.gitbook.io', name: 'hyperliquid.gitbook.io',
baseUrl: 'https://hyperliquid.gitbook.io', contentBaseURL: 'https://hyperliquid.gitbook.io',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/' }],
}, },
{ {
name: 'docs.umbraco.com', name: 'docs.umbraco.com',
baseUrl: 'https://docs.umbraco.com', contentBaseURL: 'https://docs.umbraco.com',
tests: [{ name: 'Home', url: '/welcome', screenshot: { waitForTOCScrolling: false } }], tests: [
{
name: 'Home',
url: '/welcome',
run: waitForCookiesDialog,
screenshot: { waitForTOCScrolling: false },
},
],
}, },
{ {
name: 'sosovalue-white-paper.gitbook.io', name: 'sosovalue-white-paper.gitbook.io',
baseUrl: 'https://sosovalue-white-paper.gitbook.io', contentBaseURL: 'https://sosovalue-white-paper.gitbook.io',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/' }],
}, },
{ {
name: 'docs.revrobotics.com', name: 'docs.revrobotics.com',
baseUrl: 'https://docs.revrobotics.com', contentBaseURL: 'https://docs.revrobotics.com',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
}, },
{ {
name: 'chartschool.stockcharts.com', name: 'chartschool.stockcharts.com',
baseUrl: 'https://chartschool.stockcharts.com', contentBaseURL: 'https://chartschool.stockcharts.com',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
}, },
{ {
name: 'docs.soniclabs.com', name: 'docs.soniclabs.com',
baseUrl: 'https://docs.soniclabs.com', contentBaseURL: 'https://docs.soniclabs.com',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/' }],
}, },
{ {
name: 'docs.meshchain.ai', name: 'docs.meshchain.ai',
baseUrl: 'https://docs.meshchain.ai', contentBaseURL: 'https://docs.meshchain.ai',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/' }],
}, },
{ {
name: 'docs.thousandeyes.com', name: 'docs.thousandeyes.com',
baseUrl: 'https://docs.thousandeyes.com', contentBaseURL: 'https://docs.thousandeyes.com',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
}, },
{ {
name: 'docs.raydium.io', name: 'docs.raydium.io',
baseUrl: 'https://docs.raydium.io', contentBaseURL: 'https://docs.raydium.io',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/' }],
}, },
{ {
name: 'docs.fluentbit.io', name: 'docs.fluentbit.io',
baseUrl: 'https://docs.fluentbit.io', contentBaseURL: 'https://docs.fluentbit.io',
tests: [{ name: 'Home', url: '/' }], tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
},
{
name: 'run-ai-docs.nvidia.com',
contentBaseURL: 'https://run-ai-docs.nvidia.com',
skip: process.env.ARGOS_BUILD_NAME !== 'customers-v2',
tests: [
{ name: 'Home', url: '/' },
{ name: 'OG Image', url: '/~gitbook/ogimage/h17zQIFwy3MaafVNmItO', mode: 'image' },
],
}, },
]; ];
+256 -66
View File
@@ -14,10 +14,12 @@ import {
getVisitorAuthCookieValue, getVisitorAuthCookieValue,
} from '@/lib/visitor-token'; } from '@/lib/visitor-token';
import { getSiteAPIToken } from '../tests/utils';
import { import {
type TestsCase, type TestsCase,
allDeprecatedThemePresets, allDeprecatedThemePresets,
allLocales, allLocales,
allSearchStyles,
allSidebarBackgroundStyles, allSidebarBackgroundStyles,
allThemeModes, allThemeModes,
allThemes, allThemes,
@@ -26,12 +28,13 @@ import {
headerLinks, headerLinks,
runTestCases, runTestCases,
waitForCookiesDialog, waitForCookiesDialog,
waitForNotFound,
} from './util'; } from './util';
const testCases: TestsCase[] = [ const testCases: TestsCase[] = [
{ {
name: 'GitBook Site (Single Variant)', name: 'GitBook Site (Single Variant)',
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/gitbook-doc/', contentBaseURL: 'https://gitbook-open-e2e-sites.gitbook.io/gitbook-doc/',
tests: [ tests: [
{ {
name: 'Home', name: 'Home',
@@ -84,7 +87,7 @@ const testCases: TestsCase[] = [
}, },
{ {
name: 'GitBook Site (Multi Variants)', name: 'GitBook Site (Multi Variants)',
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/multi-variants/', contentBaseURL: 'https://gitbook-open-e2e-sites.gitbook.io/multi-variants/',
tests: [ tests: [
{ {
name: 'Variants dropdown', name: 'Variants dropdown',
@@ -135,7 +138,7 @@ const testCases: TestsCase[] = [
}, },
{ {
name: 'GitBook Site (Navigation when switching variant)', name: 'GitBook Site (Navigation when switching variant)',
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/', contentBaseURL: 'https://gitbook-open-e2e-sites.gitbook.io/',
tests: [ tests: [
{ {
name: 'Keep navigation path/route when switching variant (Public)', name: 'Keep navigation path/route when switching variant (Public)',
@@ -155,8 +158,8 @@ const testCases: TestsCase[] = [
.click(); .click();
// It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant // It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant
await page.waitForURL( await page.waitForURL((url) =>
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions/2.0/reference/api-reference/pets?fallback=true' url.pathname.includes('api-multi-versions/2.0/reference/api-reference/pets')
); );
}, },
}, },
@@ -178,15 +181,17 @@ const testCases: TestsCase[] = [
.click(); .click();
// It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant // It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant
await page.waitForURL( await page.waitForURL((url) =>
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-share-links/8tNo6MeXg7CkFMzSSz81/2.0/reference/api-reference/pets?fallback=true' url.pathname.includes(
'api-multi-versions-share-links/8tNo6MeXg7CkFMzSSz81/2.0/reference/api-reference/pets'
)
); );
}, },
}, },
{ {
name: 'Keep navigation path/route when switching variant (VA)', name: 'Keep navigation path/route when switching variant (VA)',
screenshot: false, screenshot: false,
url: (() => { url: () => {
const privateKey = 'c26190fc-74b2-4b54-9fc7-df9941104953'; const privateKey = 'c26190fc-74b2-4b54-9fc7-df9941104953';
const token = jwt.sign( const token = jwt.sign(
{ {
@@ -198,7 +203,7 @@ const testCases: TestsCase[] = [
} }
); );
return `api-multi-versions-va/reference/api-reference/pets?jwt_token=${token}`; return `api-multi-versions-va/reference/api-reference/pets?jwt_token=${token}`;
})(), },
run: async (page) => { run: async (page) => {
const spaceDrowpdown = await page const spaceDrowpdown = await page
.locator('[data-testid="space-dropdown-button"]') .locator('[data-testid="space-dropdown-button"]')
@@ -213,8 +218,10 @@ const testCases: TestsCase[] = [
.click(); .click();
// It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant // It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant
await page.waitForURL( await page.waitForURL((url) =>
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-va/2.0/reference/api-reference/pets?fallback=true' url.pathname.includes(
'api-multi-versions-va/2.0/reference/api-reference/pets'
)
); );
}, },
}, },
@@ -222,7 +229,7 @@ const testCases: TestsCase[] = [
}, },
{ {
name: 'GitBook Site (Sections and Section Groups)', name: 'GitBook Site (Sections and Section Groups)',
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/sections/', contentBaseURL: 'https://gitbook-open-e2e-sites.gitbook.io/sections/',
tests: [ tests: [
{ {
name: 'Site with sections and section groups', name: 'Site with sections and section groups',
@@ -244,16 +251,14 @@ const testCases: TestsCase[] = [
const sectionGroupDropdown = await page.getByText('Test Section Group 1'); const sectionGroupDropdown = await page.getByText('Test Section Group 1');
await sectionGroupDropdown.hover(); await sectionGroupDropdown.hover();
await page.getByText('Section B').click(); await page.getByText('Section B').click();
await page.waitForURL( await page.waitForURL((url) => url.pathname.includes('/sections/sections-4'));
'https://gitbook-open-e2e-sites.gitbook.io/sections/sections-4'
);
}, },
}, },
], ],
}, },
{ {
name: 'GitBook', name: 'GitBook',
baseUrl: 'https://docs.gitbook.com', contentBaseURL: 'https://docs.gitbook.com',
tests: [ tests: [
{ {
name: 'Home', name: 'Home',
@@ -297,18 +302,30 @@ const testCases: TestsCase[] = [
}, },
{ {
name: 'Versioning', name: 'Versioning',
baseUrl: 'https://gitbook.gitbook.io/test-gitbook-open/', contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/',
tests: [ tests: [
{ {
name: 'Revision', name: 'Revision',
url: '~/revisions/S55pwsEr5UVoroaOiWnP/blocks/headings', url: '~/revisions/S55pwsEr5UVoroaOiWnP/blocks/headings',
run: waitForCookiesDialog, run: waitForCookiesDialog,
}, },
{
name: 'Invalid revision',
url: '~/revisions/idnotfound/blocks/headings',
run: waitForNotFound,
screenshot: false,
},
{
name: 'Invalid change request',
url: '~/changes/idnotfound/blocks/headings',
run: waitForNotFound,
screenshot: false,
},
], ],
}, },
{ {
name: 'PDF', name: 'PDF',
baseUrl: 'https://gitbook.gitbook.io/test-gitbook-open/', contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/',
tests: [ tests: [
{ {
name: 'PDF', name: 'PDF',
@@ -316,12 +333,132 @@ const testCases: TestsCase[] = [
screenshot: { screenshot: {
waitForTOCScrolling: false, waitForTOCScrolling: false,
}, },
run: async (page) => {
await expect(page.locator('[data-testid="print-button"]')).toBeVisible();
},
},
],
},
{
name: 'Space PDF',
tests: [
{
name: 'Main content',
url: async () => {
const data = await getSiteAPIToken(
'https://gitbook.gitbook.io/test-gitbook-open/'
);
const searchParams = new URLSearchParams();
searchParams.set('limit', '10');
searchParams.set('token', data.apiToken);
return `~space/${data.space}/~gitbook/pdf?${searchParams.toString()}`;
},
screenshot: false,
run: async (page) => {
await expect(page.locator('[data-testid="print-button"]')).toBeVisible();
},
},
{
name: 'Change request',
url: async () => {
const data = await getSiteAPIToken(
'https://gitbook.gitbook.io/test-gitbook-open/'
);
const searchParams = new URLSearchParams();
searchParams.set('limit', '10');
searchParams.set('token', data.apiToken);
return `~space/${data.space}/~/changes/HrtgUd5MlFusCMv1elA7/~gitbook/pdf?${searchParams.toString()}`;
},
screenshot: false,
run: async (page) => {
await expect(page.locator('[data-testid="print-button"]')).toBeVisible();
},
},
],
},
{
name: 'Site Preview',
skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel',
tests: [
{
name: 'Main content',
url: async () => {
const data = await getSiteAPIToken(
'https://gitbook.gitbook.io/test-gitbook-open/'
);
const searchParams = new URLSearchParams();
searchParams.set('token', data.apiToken);
return `url/preview/${data.site}/?${searchParams.toString()}`;
},
screenshot: false,
run: async (page) => {
await expect(page.locator('[data-testid="table-of-contents"]')).toBeVisible();
},
},
],
},
{
name: 'Markdown page',
skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel',
contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/',
tests: [
{
name: 'Text page',
url: 'text-page.md',
screenshot: false,
run: async (_page, response) => {
expect(response?.status()).toBe(200);
expect(response?.headers()['content-type']).toContain('text/markdown');
},
},
],
},
{
name: 'Site subdirectory (proxy)',
skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel',
contentBaseURL: 'https://nextjs-gbo-proxy.vercel.app/documentation/',
tests: [
{
name: 'Main',
url: '',
fullPage: true,
},
],
},
{
name: 'Site subdirectory (proxy) with VA',
skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel',
contentBaseURL: 'https://nextjs-gbo-proxy-va.vercel.app/va/docs/',
tests: [
{
name: 'Main',
url: () => {
const privateKey =
'rqSfA6x7eAKx1qDRCDq9aCXwivpUvQ8YkXeDdFvCCUa9QchIcM7pF1iJ4o7AGOU49spmOWjKoIPtX0pVUVQ81w==';
const token = jwt.sign(
{
name: 'gitbook-open-tests',
},
privateKey,
{
expiresIn: '24h',
}
);
return `?jwt_token=${token}`;
},
fullPage: true,
}, },
], ],
}, },
{ {
name: 'Content tests', name: 'Content tests',
baseUrl: 'https://gitbook.gitbook.io/test-gitbook-open/', contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/',
tests: [ tests: [
{ {
name: 'Text', name: 'Text',
@@ -338,7 +475,7 @@ const testCases: TestsCase[] = [
url: 'blocks/block-images', url: 'blocks/block-images',
run: waitForCookiesDialog, run: waitForCookiesDialog,
fullPage: true, fullPage: true,
screenshot: { threshold: 0.8 }, screenshot: { threshold: 0.9 },
}, },
{ {
name: 'Images (with zoom)', name: 'Images (with zoom)',
@@ -349,7 +486,6 @@ const testCases: TestsCase[] = [
await zoomImage.first().click(); await zoomImage.first().click();
await expect(page.getByTestId('zoom-image-modal')).toBeVisible(); await expect(page.getByTestId('zoom-image-modal')).toBeVisible();
}, },
fullPage: true,
screenshot: { threshold: 0.8 }, screenshot: { threshold: 0.8 },
}, },
{ {
@@ -384,7 +520,12 @@ const testCases: TestsCase[] = [
{ {
name: 'Integration Blocks', name: 'Integration Blocks',
url: 'blocks/integrations', url: 'blocks/integrations',
run: waitForCookiesDialog, run: async (page) => {
await waitForCookiesDialog(page);
const mermaidIframe = page.locator('iframe[title*="mermaid"]').contentFrame();
await expect(mermaidIframe.getByText('Mermaid', { exact: true })).toBeVisible();
await expect(mermaidIframe.getByText('Diagram', { exact: true })).toBeVisible();
},
}, },
{ {
name: 'Tables', name: 'Tables',
@@ -484,7 +625,7 @@ const testCases: TestsCase[] = [
}, },
{ {
name: 'Page options', name: 'Page options',
baseUrl: 'https://gitbook.gitbook.io/test-gitbook-open/', contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/',
tests: [ tests: [
{ {
name: 'Hidden', name: 'Hidden',
@@ -518,7 +659,7 @@ const testCases: TestsCase[] = [
}, },
{ {
name: 'Customization', name: 'Customization',
baseUrl: 'https://gitbook.gitbook.io/test-gitbook-open/', contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/',
tests: allThemeModes.flatMap((themeMode) => [ tests: allThemeModes.flatMap((themeMode) => [
{ {
name: `Without header - Theme mode ${themeMode}`, name: `Without header - Theme mode ${themeMode}`,
@@ -600,6 +741,23 @@ const testCases: TestsCase[] = [
run: waitForCookiesDialog, run: waitForCookiesDialog,
})), })),
]), ]),
...allSearchStyles.flatMap((searchStyle) => ({
name: `Theme ${theme} Search ${searchStyle} Mode ${themeMode}`,
url: getCustomizationURL({
styling: {
theme,
search: searchStyle,
},
header: {
links: headerLinks,
},
themes: {
default: themeMode,
toggeable: false,
},
}),
run: waitForCookiesDialog,
})),
]), ]),
// Deprecated header themes // Deprecated header themes
...allDeprecatedThemePresets.flatMap((preset) => [ ...allDeprecatedThemePresets.flatMap((preset) => [
@@ -652,7 +810,7 @@ const testCases: TestsCase[] = [
}, },
{ {
name: 'Ads', name: 'Ads',
baseUrl: 'https://gitbook.gitbook.io/test-gitbook-open/', contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/',
tests: [ tests: [
{ {
name: 'Without previewed ads', name: 'Without previewed ads',
@@ -663,7 +821,7 @@ const testCases: TestsCase[] = [
}, },
{ {
name: 'Shared space navigation (first site)', name: 'Shared space navigation (first site)',
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/shared-space-uno/', contentBaseURL: 'https://gitbook-open-e2e-sites.gitbook.io/shared-space-uno/',
tests: [ tests: [
{ {
name: 'Navigation to shared space', name: 'Navigation to shared space',
@@ -684,7 +842,7 @@ const testCases: TestsCase[] = [
}, },
{ {
name: 'Shared space navigation (second site)', name: 'Shared space navigation (second site)',
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/shared-space-dos/', contentBaseURL: 'https://gitbook-open-e2e-sites.gitbook.io/shared-space-dos/',
tests: [ tests: [
{ {
name: 'Navigation to shared space', name: 'Navigation to shared space',
@@ -704,7 +862,7 @@ const testCases: TestsCase[] = [
}, },
{ {
name: 'Site Redirects', name: 'Site Redirects',
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/gitbook-doc/', contentBaseURL: 'https://gitbook-open-e2e-sites.gitbook.io/gitbook-doc/',
tests: [ tests: [
{ {
name: 'Redirect to SSO page', name: 'Redirect to SSO page',
@@ -716,9 +874,38 @@ const testCases: TestsCase[] = [
}, },
], ],
}, },
{
name: 'Content Redirects',
contentBaseURL: 'https://gitbook-open-e2e-sites.gitbook.io/gitbook-doc/',
tests: [
{
name: 'Redirect to new location',
url: '/content-editor/editing-content/inline/redirect-test',
run: async (page) => {
await expect(page.locator('h1')).toHaveText('Redirect test');
},
screenshot: false,
},
],
},
{
name: 'Site Redirects with sections',
contentBaseURL: 'https://gitbook-open-e2e-sites.gitbook.io/sections/',
tests: [
{
// This test that a redirect that incudes a section path works
name: 'Redirect to Quickstart page',
url: 'sections-2/redirect-test',
run: async (page) => {
await expect(page.locator('h1')).toHaveText('Quickstart');
},
screenshot: false,
},
],
},
{ {
name: 'Share links', name: 'Share links',
baseUrl: 'https://gitbook.gitbook.io/gbo-tests-share-links/', contentBaseURL: 'https://gitbook.gitbook.io/gbo-tests-share-links/',
tests: [ tests: [
{ {
name: 'Valid link', name: 'Valid link',
@@ -738,11 +925,11 @@ const testCases: TestsCase[] = [
}, },
{ {
name: 'Visitor Auth - Space', name: 'Visitor Auth - Space',
baseUrl: 'https://gitbook.gitbook.io/gbo-va-space/', contentBaseURL: 'https://gitbook.gitbook.io/gbo-va-space/',
tests: [ tests: [
{ {
name: 'First', name: 'First',
url: (() => { url: () => {
const privateKey = '70b844d0-c519-4532-8586-5970ce48c537'; const privateKey = '70b844d0-c519-4532-8586-5970ce48c537';
const token = jwt.sign( const token = jwt.sign(
{ {
@@ -754,7 +941,7 @@ const testCases: TestsCase[] = [
} }
); );
return `first?jwt_token=${token}`; return `first?jwt_token=${token}`;
})(), },
run: async (page) => { run: async (page) => {
await expect( await expect(
page.getByRole('heading', { level: 1, name: 'first' }) page.getByRole('heading', { level: 1, name: 'first' })
@@ -764,7 +951,7 @@ const testCases: TestsCase[] = [
}, },
{ {
name: 'Second', name: 'Second',
url: (() => { url: () => {
const privateKey = '70b844d0-c519-4532-8586-5970ce48c537'; const privateKey = '70b844d0-c519-4532-8586-5970ce48c537';
const token = jwt.sign( const token = jwt.sign(
{ {
@@ -776,7 +963,7 @@ const testCases: TestsCase[] = [
} }
); );
return `second?jwt_token=${token}`; return `second?jwt_token=${token}`;
})(), },
run: async (page) => { run: async (page) => {
await expect( await expect(
page.getByRole('heading', { level: 1, name: 'second' }) page.getByRole('heading', { level: 1, name: 'second' })
@@ -788,11 +975,11 @@ const testCases: TestsCase[] = [
}, },
{ {
name: 'Visitor Auth - Collection', name: 'Visitor Auth - Collection',
baseUrl: 'https://gitbook.gitbook.io/gbo-va-collection/', contentBaseURL: 'https://gitbook.gitbook.io/gbo-va-collection/',
tests: [ tests: [
{ {
name: 'Root', name: 'Root',
url: (() => { url: () => {
const privateKey = 'af5688dc-f0b6-4146-9b1d-6d834c62c980'; const privateKey = 'af5688dc-f0b6-4146-9b1d-6d834c62c980';
const token = jwt.sign( const token = jwt.sign(
{ {
@@ -804,12 +991,12 @@ const testCases: TestsCase[] = [
} }
); );
return `?jwt_token=${token}`; return `?jwt_token=${token}`;
})(), },
run: waitForCookiesDialog, run: waitForCookiesDialog,
}, },
{ {
name: 'Primary (Space A)', name: 'Primary (Space A)',
url: (() => { url: () => {
const privateKey = 'af5688dc-f0b6-4146-9b1d-6d834c62c980'; const privateKey = 'af5688dc-f0b6-4146-9b1d-6d834c62c980';
const token = jwt.sign( const token = jwt.sign(
{ {
@@ -820,13 +1007,16 @@ const testCases: TestsCase[] = [
expiresIn: '24h', expiresIn: '24h',
} }
); );
// Test that when accessing the non-canonical URL, we are redirected to the canonical URL
// with the jwt token in the query string
return `spacea?jwt_token=${token}`; return `spacea?jwt_token=${token}`;
})(), },
run: waitForCookiesDialog, run: waitForCookiesDialog,
}, },
{ {
name: 'Space B', name: 'Space B',
url: (() => { url: () => {
const privateKey = 'af5688dc-f0b6-4146-9b1d-6d834c62c980'; const privateKey = 'af5688dc-f0b6-4146-9b1d-6d834c62c980';
const token = jwt.sign( const token = jwt.sign(
{ {
@@ -838,12 +1028,12 @@ const testCases: TestsCase[] = [
} }
); );
return `spaceb?jwt_token=${token}`; return `spaceb?jwt_token=${token}`;
})(), },
run: waitForCookiesDialog, run: waitForCookiesDialog,
}, },
{ {
name: 'Space C', name: 'Space C',
url: (() => { url: () => {
const privateKey = 'af5688dc-f0b6-4146-9b1d-6d834c62c980'; const privateKey = 'af5688dc-f0b6-4146-9b1d-6d834c62c980';
const token = jwt.sign( const token = jwt.sign(
{ {
@@ -855,18 +1045,18 @@ const testCases: TestsCase[] = [
} }
); );
return `spacec?jwt_token=${token}`; return `spacec?jwt_token=${token}`;
})(), },
run: waitForCookiesDialog, run: waitForCookiesDialog,
}, },
], ],
}, },
{ {
name: 'Visitor Auth - Space (custom domain)', name: 'Visitor Auth - Space (custom domain)',
baseUrl: 'https://test.gitbook.community/', contentBaseURL: 'https://test.gitbook.community/',
tests: [ tests: [
{ {
name: 'Root', name: 'Root',
url: (() => { url: () => {
const privateKey = '19c8166f-c436-4ed1-a24e-60954b804021'; const privateKey = '19c8166f-c436-4ed1-a24e-60954b804021';
const token = jwt.sign( const token = jwt.sign(
{ {
@@ -878,12 +1068,12 @@ const testCases: TestsCase[] = [
} }
); );
return `?jwt_token=${token}`; return `?jwt_token=${token}`;
})(), },
run: waitForCookiesDialog, run: waitForCookiesDialog,
}, },
{ {
name: 'First', name: 'First',
url: (() => { url: () => {
const privateKey = '19c8166f-c436-4ed1-a24e-60954b804021'; const privateKey = '19c8166f-c436-4ed1-a24e-60954b804021';
const token = jwt.sign( const token = jwt.sign(
{ {
@@ -895,7 +1085,7 @@ const testCases: TestsCase[] = [
} }
); );
return `first?jwt_token=${token}`; return `first?jwt_token=${token}`;
})(), },
run: async (page) => { run: async (page) => {
await expect( await expect(
page.getByRole('heading', { level: 1, name: 'first' }) page.getByRole('heading', { level: 1, name: 'first' })
@@ -905,7 +1095,7 @@ const testCases: TestsCase[] = [
}, },
{ {
name: 'Custom page', name: 'Custom page',
url: (() => { url: () => {
const privateKey = '19c8166f-c436-4ed1-a24e-60954b804021'; const privateKey = '19c8166f-c436-4ed1-a24e-60954b804021';
const token = jwt.sign( const token = jwt.sign(
{ {
@@ -917,12 +1107,12 @@ const testCases: TestsCase[] = [
} }
); );
return `custom-page?jwt_token=${token}`; return `custom-page?jwt_token=${token}`;
})(), },
run: waitForCookiesDialog, run: waitForCookiesDialog,
}, },
{ {
name: 'Inner page', name: 'Inner page',
url: (() => { url: () => {
const privateKey = '19c8166f-c436-4ed1-a24e-60954b804021'; const privateKey = '19c8166f-c436-4ed1-a24e-60954b804021';
const token = jwt.sign( const token = jwt.sign(
{ {
@@ -934,14 +1124,14 @@ const testCases: TestsCase[] = [
} }
); );
return `custom-page/inner-page?jwt_token=${token}`; return `custom-page/inner-page?jwt_token=${token}`;
})(), },
run: waitForCookiesDialog, run: waitForCookiesDialog,
}, },
], ],
}, },
{ {
name: 'Visitor Auth - Site (redirects to fallback/auth URL)', name: 'Visitor Auth - Site (redirects to fallback/auth URL)',
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/va-site-redirects-fallback/', contentBaseURL: 'https://gitbook-open-e2e-sites.gitbook.io/va-site-redirects-fallback/',
tests: [ tests: [
{ {
name: 'Redirect to fallback on invalid token pulled from cookie', name: 'Redirect to fallback on invalid token pulled from cookie',
@@ -973,7 +1163,7 @@ const testCases: TestsCase[] = [
{ {
name: 'Show error message when invalid token is passed to url', name: 'Show error message when invalid token is passed to url',
screenshot: false, screenshot: false,
url: (() => { url: () => {
const token = jwt.sign( const token = jwt.sign(
{ {
name: 'gitbook-open-tests', name: 'gitbook-open-tests',
@@ -984,7 +1174,7 @@ const testCases: TestsCase[] = [
} }
); );
return `?jwt_token=${token}`; return `?jwt_token=${token}`;
})(), },
run: async (page) => { run: async (page) => {
await expect(page.locator('pre')).toContainText( await expect(page.locator('pre')).toContainText(
'Error while validating the JWT token. Reason: The token signature is invalid.' 'Error while validating the JWT token. Reason: The token signature is invalid.'
@@ -995,7 +1185,7 @@ const testCases: TestsCase[] = [
}, },
{ {
name: 'Languages', name: 'Languages',
baseUrl: 'https://gitbook.gitbook.io/test-gitbook-open/', contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/',
tests: allLocales.map((locale) => ({ tests: allLocales.map((locale) => ({
name: locale, name: locale,
url: getCustomizationURL({ url: getCustomizationURL({
@@ -1011,7 +1201,7 @@ const testCases: TestsCase[] = [
}, },
{ {
name: 'SEO', name: 'SEO',
baseUrl: 'https://gitbook.gitbook.io/test-gitbook-open/', contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/',
tests: [ tests: [
{ {
name: 'Index by default', name: 'Index by default',
@@ -1062,11 +1252,11 @@ const testCases: TestsCase[] = [
}, },
{ {
name: 'Adaptive Content - VA', name: 'Adaptive Content - VA',
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/adaptive-content-va/', contentBaseURL: 'https://gitbook-open-e2e-sites.gitbook.io/adaptive-content-va/',
tests: [ tests: [
{ {
name: 'isAlphaUser', name: 'isAlphaUser',
url: (() => { url: () => {
const privateKey = 'afe09cdf-0f43-480a-b54c-8b1f62f174f9'; const privateKey = 'afe09cdf-0f43-480a-b54c-8b1f62f174f9';
const token = jwt.sign( const token = jwt.sign(
{ {
@@ -1079,7 +1269,7 @@ const testCases: TestsCase[] = [
} }
); );
return `?jwt_token=${token}`; return `?jwt_token=${token}`;
})(), },
run: async (page) => { run: async (page) => {
const alphaUserPage = page const alphaUserPage = page
.locator('a[class*="group\\/toclink"]') .locator('a[class*="group\\/toclink"]')
@@ -1093,7 +1283,7 @@ const testCases: TestsCase[] = [
}, },
{ {
name: 'isBetaUser', name: 'isBetaUser',
url: (() => { url: () => {
const privateKey = 'afe09cdf-0f43-480a-b54c-8b1f62f174f9'; const privateKey = 'afe09cdf-0f43-480a-b54c-8b1f62f174f9';
const token = jwt.sign( const token = jwt.sign(
{ {
@@ -1106,7 +1296,7 @@ const testCases: TestsCase[] = [
} }
); );
return `?jwt_token=${token}`; return `?jwt_token=${token}`;
})(), },
run: async (page) => { run: async (page) => {
const alphaUserPage = page const alphaUserPage = page
.locator('a[class*="group\\/toclink"]') .locator('a[class*="group\\/toclink"]')
@@ -1120,7 +1310,7 @@ const testCases: TestsCase[] = [
}, },
{ {
name: 'isAlphaUser & isBetaUser', name: 'isAlphaUser & isBetaUser',
url: (() => { url: () => {
const privateKey = 'afe09cdf-0f43-480a-b54c-8b1f62f174f9'; const privateKey = 'afe09cdf-0f43-480a-b54c-8b1f62f174f9';
const token = jwt.sign( const token = jwt.sign(
{ {
@@ -1134,7 +1324,7 @@ const testCases: TestsCase[] = [
} }
); );
return `?jwt_token=${token}`; return `?jwt_token=${token}`;
})(), },
run: async (page) => { run: async (page) => {
const alphaUserPage = page const alphaUserPage = page
.locator('a[class*="group\\/toclink"]') .locator('a[class*="group\\/toclink"]')
@@ -1150,7 +1340,7 @@ const testCases: TestsCase[] = [
}, },
{ {
name: 'Adaptive Content - Public', name: 'Adaptive Content - Public',
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/adaptive-content-public/', contentBaseURL: 'https://gitbook-open-e2e-sites.gitbook.io/adaptive-content-public/',
tests: [ tests: [
{ {
name: 'No custom cookie', name: 'No custom cookie',
@@ -1293,7 +1483,7 @@ const testCases: TestsCase[] = [
}, },
{ {
name: 'Tables', name: 'Tables',
baseUrl: 'https://gitbook.gitbook.io/test-gitbook-open/', contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/',
tests: [ tests: [
{ {
name: 'Default table', name: 'Default table',
+141 -54
View File
@@ -2,12 +2,13 @@ import { argosScreenshot } from '@argos-ci/playwright';
import { import {
CustomizationBackground, CustomizationBackground,
CustomizationCorners, CustomizationCorners,
CustomizationFont, CustomizationDefaultFont,
type CustomizationHeaderItem, type CustomizationHeaderItem,
CustomizationHeaderPreset, CustomizationHeaderPreset,
CustomizationIconsStyle, CustomizationIconsStyle,
CustomizationLinksStyle, CustomizationLinksStyle,
CustomizationLocale, CustomizationLocale,
CustomizationSearchStyle,
CustomizationSidebarBackgroundStyle, CustomizationSidebarBackgroundStyle,
CustomizationSidebarListStyle, CustomizationSidebarListStyle,
CustomizationTheme, CustomizationTheme,
@@ -15,24 +16,28 @@ import {
type CustomizationThemedColor, type CustomizationThemedColor,
type SiteCustomizationSettings, type SiteCustomizationSettings,
} from '@gitbook/api'; } from '@gitbook/api';
import { type BrowserContext, type Page, expect, test } from '@playwright/test'; import { type BrowserContext, type Page, type Response, expect, test } from '@playwright/test';
import deepMerge from 'deepmerge'; import deepMerge from 'deepmerge';
import rison from 'rison'; import rison from 'rison';
import type { DeepPartial } from 'ts-essentials'; import type { DeepPartial } from 'ts-essentials';
import { getContentTestURL } from '../tests/utils'; import { getContentTestURL, getTestURL } from '../tests/utils';
export interface Test { export interface Test {
name: string; name: string;
/** /**
* URL to visit for testing. * URL to visit for testing.
*/ */
url: string; url: string | (() => string | Promise<string>);
cookies?: Parameters<BrowserContext['addCookies']>[0]; cookies?: Parameters<BrowserContext['addCookies']>[0];
/** /**
* Test to run * Test to run
*/ */
run?: (page: Page) => Promise<unknown>; run?: (page: Page, response: Response | null) => Promise<unknown>;
/**
* Mode for the test.
*/
mode?: 'page' | 'image';
/** /**
* Whether the test should be fullscreened during testing. * Whether the test should be fullscreened during testing.
*/ */
@@ -60,11 +65,12 @@ export interface Test {
only?: boolean; only?: boolean;
} }
export interface TestsCase { export type TestsCase = {
name: string; name: string;
baseUrl: string; skip?: boolean;
tests: Array<Test>; tests: Array<Test>;
} contentBaseURL?: string;
};
export const allLocales: CustomizationLocale[] = [ export const allLocales: CustomizationLocale[] = [
CustomizationLocale.Fr, CustomizationLocale.Fr,
@@ -109,6 +115,11 @@ export const allSidebarBackgroundStyles: CustomizationSidebarBackgroundStyle[] =
CustomizationSidebarBackgroundStyle.Filled, CustomizationSidebarBackgroundStyle.Filled,
]; ];
export const allSearchStyles: CustomizationSearchStyle[] = [
CustomizationSearchStyle.Prominent,
CustomizationSearchStyle.Subtle,
];
// Common customization settings // Common customization settings
export const headerLinks: CustomizationHeaderItem[] = [ export const headerLinks: CustomizationHeaderItem[] = [
@@ -127,11 +138,13 @@ export const headerLinks: CustomizationHeaderItem[] = [
]; ];
export async function waitForCookiesDialog(page: Page) { export async function waitForCookiesDialog(page: Page) {
const dialog = page.getByRole('dialog', { name: 'Cookies' }); const dialog = page.getByTestId('cookies-dialog');
const accept = dialog.getByRole('button', { name: 'Accept' }); await expect(dialog).toBeVisible();
const reject = dialog.getByRole('button', { name: 'Reject' }); }
await expect(accept).toBeVisible();
await expect(reject).toBeVisible(); export async function waitForNotFound(_page: Page, response: Response | null) {
expect(response).not.toBeNull();
expect(response?.status()).toBe(404);
} }
/** /**
@@ -139,12 +152,22 @@ export async function waitForCookiesDialog(page: Page) {
*/ */
export function runTestCases(testCases: TestsCase[]) { export function runTestCases(testCases: TestsCase[]) {
for (const testCase of testCases) { for (const testCase of testCases) {
if (testCase.skip) {
continue;
}
test.describe(testCase.name, () => { test.describe(testCase.name, () => {
for (const testEntry of testCase.tests) { for (const testEntry of testCase.tests) {
const { mode = 'page' } = testEntry;
const testFn = testEntry.only ? test.only : test; const testFn = testEntry.only ? test.only : test;
testFn(testEntry.name, async ({ page, baseURL, context }) => { testFn(testEntry.name, async ({ page, context }) => {
const contentUrl = new URL(testEntry.url, testCase.baseUrl); const testEntryPathname =
const url = getContentTestURL(contentUrl.toString(), baseURL); typeof testEntry.url === 'function' ? await testEntry.url() : testEntry.url;
const url = testCase.contentBaseURL
? getContentTestURL(
new URL(testEntryPathname, testCase.contentBaseURL).toString()
)
: getTestURL(testEntryPathname);
if (testEntry.cookies) { if (testEntry.cookies) {
await context.addCookies( await context.addCookies(
testEntry.cookies.map((cookie) => ({ testEntry.cookies.map((cookie) => ({
@@ -170,29 +193,39 @@ export function runTestCases(testCases: TestsCase[]) {
} }
}); });
await page.goto(url); const response = await page.goto(url);
if (testEntry.run) { if (testEntry.run) {
await testEntry.run(page); await testEntry.run(page, response);
} }
const screenshotOptions = testEntry.screenshot; const screenshotOptions = testEntry.screenshot;
if (screenshotOptions !== false) { if (screenshotOptions !== false) {
await argosScreenshot(page, `${testCase.name} - ${testEntry.name}`, { const screenshotName = `${testCase.name} - ${testEntry.name}`;
viewports: ['macbook-16', 'macbook-13', 'ipad-2', 'iphone-x'], if (mode === 'image') {
argosCSS: ` await argosScreenshot(page, screenshotName, {
/* Hide Intercom */ viewports: ['macbook-13'],
.intercom-lightweight-app { threshold: screenshotOptions?.threshold ?? undefined,
display: none !important; fullPage: true,
});
} else {
await argosScreenshot(page, screenshotName, {
viewports: ['macbook-16', 'macbook-13', 'ipad-2', 'iphone-x'],
argosCSS: `
/* Hide Intercom */
.intercom-lightweight-app {
display: none !important;
} }
`, `,
threshold: screenshotOptions?.threshold ?? undefined, threshold: screenshotOptions?.threshold ?? undefined,
fullPage: testEntry.fullPage ?? false, fullPage: testEntry.fullPage ?? false,
beforeScreenshot: async () => { beforeScreenshot: async ({ runStabilization }) => {
await waitForIcons(page); await runStabilization();
if (screenshotOptions?.waitForTOCScrolling !== false) { if (screenshotOptions?.waitForTOCScrolling !== false) {
await waitForTOCScrolling(page); await waitForTOCScrolling(page);
} }
}, await waitForIcons(page);
}); },
});
}
} }
}); });
} }
@@ -244,7 +277,7 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
dangerColor: { light: '#FB2C36', dark: '#FB2C36' }, dangerColor: { light: '#FB2C36', dark: '#FB2C36' },
successColor: { light: '#00C950', dark: '#00C950' }, successColor: { light: '#00C950', dark: '#00C950' },
corners: CustomizationCorners.Rounded, corners: CustomizationCorners.Rounded,
font: CustomizationFont.Inter, font: CustomizationDefaultFont.Inter,
background: CustomizationBackground.Plain, background: CustomizationBackground.Plain,
icons: CustomizationIconsStyle.Regular, icons: CustomizationIconsStyle.Regular,
links: CustomizationLinksStyle.Default, links: CustomizationLinksStyle.Default,
@@ -252,10 +285,14 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
background: CustomizationSidebarBackgroundStyle.Default, background: CustomizationSidebarBackgroundStyle.Default,
list: CustomizationSidebarListStyle.Default, list: CustomizationSidebarListStyle.Default,
}, },
search: CustomizationSearchStyle.Subtle,
}, },
internationalization: { internationalization: {
locale: CustomizationLocale.En, locale: CustomizationLocale.En,
}, },
insights: {
trackingCookie: true,
},
favicon: {}, favicon: {},
header: { header: {
preset: CustomizationHeaderPreset.Default, preset: CustomizationHeaderPreset.Default,
@@ -307,29 +344,79 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
* Wait for all icons present on the page to be loaded. * Wait for all icons present on the page to be loaded.
*/ */
async function waitForIcons(page: Page) { async function waitForIcons(page: Page) {
await page.waitForFunction(async () => { await page.waitForFunction(() => {
function loadImage(src: string) { const urlStates: Record<string, 'pending' | 'loaded'> =
return new Promise((resolve, reject) => { (window as any).__ICONS_STATES__ || {};
const img = new Image(); (window as any).__ICONS_STATES__ = urlStates;
img.onload = () => resolve(img);
img.onerror = (_error) => reject(new Error(`Failed to load image: ${src}`)); const loadUrl = (url: string) => {
img.src = src; // Mark the URL as pending.
}); urlStates[url] = 'pending';
}
const img = new Image();
img.onload = () => {
urlStates[url] = 'loaded';
};
img.src = url;
};
const icons = Array.from(document.querySelectorAll('svg.gb-icon')); const icons = Array.from(document.querySelectorAll('svg.gb-icon'));
await Promise.all( const results = icons.map((icon) => {
icons.map(async (icon) => { if (!(icon instanceof SVGElement)) {
// url("https://ka-p.fontawesome.com/releases/v6.6.0/svgs/light/moon.svg?v=2&token=a463935e93") throw new Error('Icon is not an SVGElement');
const maskImage = window.getComputedStyle(icon).getPropertyValue('mask-image'); }
const urlMatch = maskImage.match(/url\("([^"]+)"\)/);
const url = urlMatch ? urlMatch[1] : null; // Ignore icons that are not visible.
if (!url) { if (!icon.checkVisibility()) {
throw new Error('No mask-image'); return true;
}
const state = icon.getAttribute('data-argos-state');
if (state === 'pending') {
return false;
}
if (state === 'loaded') {
return true;
}
// url("https://ka-p.fontawesome.com/releases/v6.6.0/svgs/light/moon.svg?v=2&token=a463935e93")
const maskImage = window.getComputedStyle(icon).getPropertyValue('mask-image');
const urlMatch = maskImage.match(/url\("([^"]+)"\)/);
const url = urlMatch?.[1];
// If URL is invalid we throw an error.
if (!url) {
throw new Error('No mask-image');
}
// If the URL is already queued for loading, we return the state.
if (urlStates[url]) {
if (urlStates[url] === 'loaded') {
icon.setAttribute('data-argos-state', 'pending');
const bckMaskImage = icon.style.maskImage;
const bckDisplay = icon.style.display;
icon.style.maskImage = '';
icon.style.display = 'none';
requestAnimationFrame(() => {
icon.style.maskImage = bckMaskImage;
icon.style.display = bckDisplay;
requestAnimationFrame(() => {
icon.setAttribute('data-argos-state', 'loaded');
});
});
return false;
} }
await loadImage(url);
}) return false;
); }
loadUrl(url);
return false;
});
return results.every((x) => x);
}); });
} }
+48 -81
View File
@@ -1,83 +1,50 @@
const { withSentryConfig } = require('@sentry/nextjs'); module.exports = {
env: {
module.exports = withSentryConfig( BUILD_VERSION: (process.env.GITHUB_SHA ?? '').slice(0, 7),
{ GITBOOK_ASSETS_PREFIX: process.env.GITBOOK_ASSETS_PREFIX,
env: { GITBOOK_ICONS_URL: process.env.GITBOOK_ICONS_URL,
BUILD_VERSION: (process.env.GITHUB_SHA ?? '').slice(0, 7), GITBOOK_ICONS_TOKEN: process.env.GITBOOK_ICONS_TOKEN,
SENTRY_RELEASE: process.env.SENTRY_RELEASE ?? '', NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY,
SENTRY_DSN: process.env.SENTRY_DSN ?? '', GITBOOK_RUNTIME: process.env.GITBOOK_RUNTIME,
SENTRY_ENVIRONMENT: process.env.SENTRY_ENVIRONMENT ?? 'development',
GITBOOK_ASSETS_PREFIX: process.env.GITBOOK_ASSETS_PREFIX,
GITBOOK_ICONS_URL: process.env.GITBOOK_ICONS_URL,
GITBOOK_ICONS_TOKEN: process.env.GITBOOK_ICONS_TOKEN,
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY,
},
webpack(config, { dev, webpack }) {
config.resolve.fallback = {
...config.resolve.fallback,
// Required for `swagger2openapi` to work:
fs: false,
path: false,
http: false,
};
// Tree shake debug code for Sentry
// https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/tree-shaking/#tree-shaking-with-nextjs
if (!dev) {
config.plugins.push(
new webpack.DefinePlugin({
__SENTRY_DEBUG__: false,
// We always init Sentry with enableTracing: false for now, so this is useless
__SENTRY_TRACING__: false,
__RRWEB_EXCLUDE_IFRAME__: true,
__RRWEB_EXCLUDE_SHADOW_DOM__: true,
__SENTRY_EXCLUDE_REPLAY_WORKER__: true,
})
);
}
return config;
},
async headers() {
return [
// Cache all static assets for 1 year
{
source: '/~gitbook/static/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
];
},
assetPrefix: process.env.GITBOOK_ASSETS_PREFIX,
poweredByHeader: false,
images: {
remotePatterns: [
{
protocol: 'https',
hostname: '*.gitbook.io',
},
],
},
}, },
{
release: process.env.SENTRY_RELEASE,
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
// Upload a larger set of source maps for prettier stack traces (increases build time) webpack(config) {
widenClientFileUpload: true, config.resolve.fallback = {
// Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load) ...config.resolve.fallback,
tunnelRoute: '/~gitbook/monitoring',
disableLogger: true, // Required for `swagger2openapi` to work:
} fs: false,
); path: false,
http: false,
};
return config;
},
async headers() {
return [
// Cache all static assets for 1 year
{
source: '/~gitbook/static/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
];
},
assetPrefix: process.env.GITBOOK_ASSETS_PREFIX,
poweredByHeader: false,
images: {
remotePatterns: [
{
protocol: 'https',
hostname: '*.gitbook.io',
},
],
},
};
+24 -16
View File
@@ -1,6 +1,6 @@
{ {
"name": "gitbook", "name": "gitbook",
"version": "0.7.0", "version": "0.11.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "env-cmd --silent -f ../../.env.local next dev", "dev": "env-cmd --silent -f ../../.env.local next dev",
@@ -16,7 +16,7 @@
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math" "clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math"
}, },
"dependencies": { "dependencies": {
"@gitbook/api": "0.96.1", "@gitbook/api": "*",
"@gitbook/cache-do": "workspace:*", "@gitbook/cache-do": "workspace:*",
"@gitbook/cache-tags": "workspace:*", "@gitbook/cache-tags": "workspace:*",
"@gitbook/colors": "workspace:*", "@gitbook/colors": "workspace:*",
@@ -29,25 +29,23 @@
"@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-navigation-menu": "^1.2.3", "@radix-ui/react-navigation-menu": "^1.2.3",
"@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-popover": "^1.0.7",
"@sentry/nextjs": "8.35.0", "@radix-ui/react-tooltip": "^1.1.8",
"@sindresorhus/fnv1a": "^3.1.0", "@sindresorhus/fnv1a": "^3.1.0",
"@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
"@upstash/redis": "^1.27.1", "ai": "^4.2.2",
"ajv": "^8.12.0",
"assert-never": "^1.2.1", "assert-never": "^1.2.1",
"bun-types": "^1.1.20", "bun-types": "^1.1.20",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"framer-motion": "^10.16.14", "framer-motion": "^10.16.14",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"jsontoxml": "^1.0.1", "jsontoxml": "^1.0.1",
"jwt-decode": "^4.0.0",
"katex": "^0.16.9", "katex": "^0.16.9",
"mathjax": "^3.2.2", "mathjax": "^3.2.2",
"mdast-util-to-markdown": "^2.1.2", "mdast-util-to-markdown": "^2.1.2",
"memoizee": "^0.4.15", "memoizee": "^0.4.17",
"micromark": "^4.0.1", "next": "14.2.26",
"micromark-extension-gfm": "^3.0.0",
"next": "14.2.23",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"nuqs": "^2.2.3", "nuqs": "^2.2.3",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
@@ -57,22 +55,30 @@
"react": "18.3.1", "react": "18.3.1",
"react-dom": "18.3.1", "react-dom": "18.3.1",
"react-hotkeys-hook": "^4.4.1", "react-hotkeys-hook": "^4.4.1",
"rehype-sanitize": "^6.0.0",
"rehype-stringify": "^10.0.1",
"remark-gfm": "^4.0.1",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.1",
"rison": "^0.1.1", "rison": "^0.1.1",
"server-only": "^0.0.1", "server-only": "^0.0.1",
"shiki": "^1.27.2", "shiki": "^3.2.0",
"tailwind-merge": "^2.2.0", "tailwind-merge": "^2.2.0",
"tailwind-shades": "^1.1.2", "tailwind-shades": "^1.1.2",
"unified": "^11.0.5",
"url-join": "^5.0.0", "url-join": "^5.0.0",
"usehooks-ts": "^3.1.0", "usehooks-ts": "^3.1.0",
"ai": "^4.1.46", "zod": "^3.24.2",
"jwt-decode": "^4.0.0" "zod-to-json-schema": "^3.24.5",
"event-iterator": "^2.0.0",
"partial-json": "^0.1.7",
"zustand": "^5.0.3"
}, },
"devDependencies": { "devDependencies": {
"@argos-ci/playwright": "^3.10.0", "@argos-ci/playwright": "^5.0.3",
"@cloudflare/next-on-pages": "1.13.7", "@cloudflare/next-on-pages": "1.13.7",
"vercel": "^39.3.0",
"@cloudflare/workers-types": "^4.20241230.0", "@cloudflare/workers-types": "^4.20241230.0",
"@playwright/test": "^1.49.1", "@playwright/test": "^1.51.1",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"@types/jsontoxml": "^1.0.5", "@types/jsontoxml": "^1.0.5",
"@types/jsonwebtoken": "^9.0.6", "@types/jsonwebtoken": "^9.0.6",
@@ -90,8 +96,10 @@
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"postcss": "^8", "postcss": "^8",
"psi": "^4.1.0", "psi": "^4.1.0",
"stylelint": "^16.16.0",
"tailwindcss": "^3.4.0", "tailwindcss": "^3.4.0",
"ts-essentials": "^10.0.1", "ts-essentials": "^10.0.1",
"typescript": "^5.5.3" "typescript": "^5.5.3",
"vercel": "^39.3.0"
} }
} }

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