mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-23 19:06:31 +00:00
V2: search and ask AI (#2889)
This commit is contained in:
@@ -57,6 +57,7 @@
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@upstash/redis": "^1.27.1",
|
||||
"ai": "^4.1.46",
|
||||
"ajv": "^8.12.0",
|
||||
"assert-never": "^1.2.1",
|
||||
"bun-types": "^1.1.20",
|
||||
@@ -122,7 +123,7 @@
|
||||
"dependencies": {
|
||||
"@gitbook/api": "0.96.1",
|
||||
"@sindresorhus/fnv1a": "^3.1.0",
|
||||
"next": "canary",
|
||||
"next": "^15.2.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"server-only": "^0.0.1",
|
||||
@@ -138,7 +139,7 @@
|
||||
"name": "@gitbook/icons",
|
||||
"version": "0.1.0",
|
||||
"bin": {
|
||||
"gitbook-icons": "./bin/gitbook-icons.js"
|
||||
"gitbook-icons": "./bin/gitbook-icons.js",
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||
@@ -196,7 +197,7 @@
|
||||
"name": "@gitbook/react-math",
|
||||
"version": "0.6.0",
|
||||
"bin": {
|
||||
"gitbook-math": "./bin/gitbook-math.js"
|
||||
"gitbook-math": "./bin/gitbook-math.js",
|
||||
},
|
||||
"dependencies": {
|
||||
"object-hash": "^3.0.0",
|
||||
@@ -243,6 +244,14 @@
|
||||
"react-dom": "18.3.1",
|
||||
},
|
||||
"packages": {
|
||||
"@ai-sdk/provider": ["@ai-sdk/provider@1.0.9", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-jie6ZJT2ZR0uVOVCDc9R2xCX5I/Dum/wEK28lx21PJx6ZnFAN9EzD2WsPhcDWfCgGx3OAZZ0GyM3CEobXpa9LA=="],
|
||||
|
||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.1.10", "", { "dependencies": { "@ai-sdk/provider": "1.0.9", "eventsource-parser": "^3.0.0", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.0.0" }, "optionalPeers": ["zod"] }, "sha512-4GZ8GHjOFxePFzkl3q42AU0DQOtTQ5w09vmaWUf/pKFXJPizlnzKSUkF0f+VkapIUfDugyMqPMT1ge8XQzVI7Q=="],
|
||||
|
||||
"@ai-sdk/react": ["@ai-sdk/react@1.1.19", "", { "dependencies": { "@ai-sdk/provider-utils": "2.1.10", "@ai-sdk/ui-utils": "1.1.16", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.0.0" }, "optionalPeers": ["react", "zod"] }, "sha512-zqSOWmJxpB45ZrwZ04+Q7Uo3xeGM0tva2eUYz2T4gv9Yk6MQAfOBA6+scsKg8CyIuUy4M4/C4pCY3eWQf7sfQg=="],
|
||||
|
||||
"@ai-sdk/ui-utils": ["@ai-sdk/ui-utils@1.1.16", "", { "dependencies": { "@ai-sdk/provider": "1.0.9", "@ai-sdk/provider-utils": "2.1.10", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.0.0" }, "optionalPeers": ["zod"] }, "sha512-jfblR2yZVISmNK2zyNzJZFtkgX57WDAUQXcmn3XUBJyo8LFsADu+/vYMn5AOyBi9qJT0RBk11PEtIxIqvByw3Q=="],
|
||||
|
||||
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||
|
||||
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
||||
@@ -1351,6 +1360,8 @@
|
||||
|
||||
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
||||
|
||||
"@types/diff-match-patch": ["@types/diff-match-patch@1.0.36", "", {}, "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg=="],
|
||||
|
||||
"@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="],
|
||||
|
||||
"@types/eslint-scope": ["@types/eslint-scope@3.7.7", "", { "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg=="],
|
||||
@@ -1531,6 +1542,8 @@
|
||||
|
||||
"agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="],
|
||||
|
||||
"ai": ["ai@4.1.47", "", { "dependencies": { "@ai-sdk/provider": "1.0.9", "@ai-sdk/provider-utils": "2.1.10", "@ai-sdk/react": "1.1.19", "@ai-sdk/ui-utils": "1.1.16", "@opentelemetry/api": "1.9.0", "jsondiffpatch": "0.6.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.0.0" }, "optionalPeers": ["react", "zod"] }, "sha512-9UZ8Mkv1HlprCJfQ0Kq+rgKbfkrkDtJjslr1WOHBRzvC70jDJJYp8r0Qq4vlF7zs9VBkGyy8Rhm5zQJJIUjvgw=="],
|
||||
|
||||
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||
|
||||
"ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="],
|
||||
@@ -1773,6 +1786,8 @@
|
||||
|
||||
"diff": ["diff@4.0.2", "", {}, "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="],
|
||||
|
||||
"diff-match-patch": ["diff-match-patch@1.0.5", "", {}, "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="],
|
||||
|
||||
"dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="],
|
||||
|
||||
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
|
||||
@@ -2195,6 +2210,8 @@
|
||||
|
||||
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
|
||||
|
||||
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
|
||||
|
||||
"json-schema-to-ts": ["json-schema-to-ts@1.6.4", "", { "dependencies": { "@types/json-schema": "^7.0.6", "ts-toolbelt": "^6.15.5" } }, "sha512-pR4yQ9DHz6itqswtHCm26mw45FSNfQ9rEQjosaZErhn5J3J2sIViQiz8rDaezjKAhFGpmsoczYVBgGHzFw/stA=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
@@ -2205,6 +2222,8 @@
|
||||
|
||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||
|
||||
"jsondiffpatch": ["jsondiffpatch@0.6.0", "", { "dependencies": { "@types/diff-match-patch": "^1.0.36", "chalk": "^5.3.0", "diff-match-patch": "^1.0.5" }, "bin": { "jsondiffpatch": "bin/jsondiffpatch.js" } }, "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ=="],
|
||||
|
||||
"jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
|
||||
|
||||
"jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="],
|
||||
@@ -2747,6 +2766,8 @@
|
||||
|
||||
"schema-utils": ["schema-utils@3.3.0", "", { "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" } }, "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg=="],
|
||||
|
||||
"secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="],
|
||||
|
||||
"semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
|
||||
|
||||
"semver-diff": ["semver-diff@3.1.1", "", { "dependencies": { "semver": "^6.3.0" } }, "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg=="],
|
||||
@@ -2857,6 +2878,8 @@
|
||||
|
||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||
|
||||
"swr": ["swr@2.3.2", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-RosxFpiabojs75IwQ316DGoDRmOqtiAj0tg8wCcbEu4CiLZBs/a9QNtHV7TUfDXmmlgqij/NqzKq/eLelyv9xA=="],
|
||||
|
||||
"tabbable": ["tabbable@6.2.0", "", {}, "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="],
|
||||
|
||||
"tailwind-merge": ["tailwind-merge@2.5.5", "", {}, "sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA=="],
|
||||
@@ -2881,6 +2904,8 @@
|
||||
|
||||
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
|
||||
|
||||
"throttleit": ["throttleit@2.1.0", "", {}, "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw=="],
|
||||
|
||||
"time-span": ["time-span@4.0.0", "", { "dependencies": { "convert-hrtime": "^3.0.0" } }, "sha512-MyqZCTGLDZ77u4k+jqg4UlrzPTPZ49NDlaekU6uuFaJLzPIN1woaRXCbGeqOfxwc3Y37ZROGAJ614Rdv7Olt+g=="],
|
||||
|
||||
"timers-ext": ["timers-ext@0.1.8", "", { "dependencies": { "es5-ext": "^0.10.64", "next-tick": "^1.1.0" } }, "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww=="],
|
||||
@@ -3099,10 +3124,14 @@
|
||||
|
||||
"zod": ["zod@3.23.8", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="],
|
||||
|
||||
"zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="],
|
||||
|
||||
"zustand": ["zustand@5.0.3", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg=="],
|
||||
|
||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||
|
||||
"@ai-sdk/provider-utils/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
|
||||
|
||||
"@ampproject/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||
|
||||
"@argos-ci/core/fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||
@@ -4239,7 +4268,7 @@
|
||||
|
||||
"gaxios/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||
|
||||
"gitbook-v2/next": ["next@15.2.1-canary.2", "", { "dependencies": { "@next/env": "15.2.1-canary.2", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.2.1-canary.2", "@next/swc-darwin-x64": "15.2.1-canary.2", "@next/swc-linux-arm64-gnu": "15.2.1-canary.2", "@next/swc-linux-arm64-musl": "15.2.1-canary.2", "@next/swc-linux-x64-gnu": "15.2.1-canary.2", "@next/swc-linux-x64-musl": "15.2.1-canary.2", "@next/swc-win32-arm64-msvc": "15.2.1-canary.2", "@next/swc-win32-x64-msvc": "15.2.1-canary.2", "sharp": "^0.33.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-Vrl8pSN3I7fuUWYWSqbzRsItKYm0X0OXPwm6/DuX7Pgi6uyYT6uwpvEGiPHSGV5Up1MOOtlsCjIbEQlNJ3t1pg=="],
|
||||
"gitbook-v2/next": ["next@15.2.0", "", { "dependencies": { "@next/env": "15.2.0", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.2.0", "@next/swc-darwin-x64": "15.2.0", "@next/swc-linux-arm64-gnu": "15.2.0", "@next/swc-linux-arm64-musl": "15.2.0", "@next/swc-linux-x64-gnu": "15.2.0", "@next/swc-linux-x64-musl": "15.2.0", "@next/swc-win32-arm64-msvc": "15.2.0", "@next/swc-win32-x64-msvc": "15.2.0", "sharp": "^0.33.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-VaiM7sZYX8KIAHBrRGSFytKknkrexNfGb8GlG6e93JqueCspuGte8i4ybn8z4ww1x3f2uzY4YpTaBEW4/hvsoQ=="],
|
||||
|
||||
"google-auth-library/jws": ["jws@4.0.0", "", { "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } }, "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg=="],
|
||||
|
||||
@@ -5139,23 +5168,23 @@
|
||||
|
||||
"gaxios/https-proxy-agent/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
|
||||
|
||||
"gitbook-v2/next/@next/env": ["@next/env@15.2.1-canary.2", "", {}, "sha512-8bs9W8rN9ln19PA3IdglEkuRxNI+VAjY/Gxkkp24qpSO19uKHbORFz3I5DGyRzHzTRDpCRGHQDKNbcy2b0buOQ=="],
|
||||
"gitbook-v2/next/@next/env": ["@next/env@15.2.0", "", {}, "sha512-eMgJu1RBXxxqqnuRJQh5RozhskoNUDHBFybvi+Z+yK9qzKeG7dadhv/Vp1YooSZmCnegf7JxWuapV77necLZNA=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.2.1-canary.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PvoxT5tucaeREa7uhTxEaSW9sL4Sr8WfluVfSS+bj2axZpQ567gB/I+Eias67FPRW80cZaPMGsorNjyaq+cJuA=="],
|
||||
"gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rlp22GZwNJjFCyL7h5wz9vtpBVuCt3ZYjFWpEPBGzG712/uL1bbSkS675rVAUCRZ4hjoTJ26Q7IKhr5DfJrHDA=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.2.1-canary.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-tPg9F3bT5MMalZpLfd+paiWWaWLvUaWAFZfIGEBgozYgIBd66pROkb+dXsJVd73IlYoBDh3mYcK7me2uYImBOA=="],
|
||||
"gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.2.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-DiU85EqSHogCz80+sgsx90/ecygfCSGl5P3b4XDRVZpgujBm5lp4ts7YaHru7eVTyZMjHInzKr+w0/7+qDrvMA=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.2.1-canary.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-LmnZN/psunvUXofINdGENFNDv7yRIET2szi2FREMr1epKL3NsAQD0k4hv3pwRLwkVb8z6Rj+TXzX0pFChHOlhg=="],
|
||||
"gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-VnpoMaGukiNWVxeqKHwi8MN47yKGyki5q+7ql/7p/3ifuU2341i/gDwGK1rivk0pVYbdv5D8z63uu9yMw0QhpQ=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.2.1-canary.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Ma1ATOCdgei1LRAbziBYciunyuG0uUASoiz7DDw40Ox5YuDD4Tznbg3re5ltWRwMAl9NNQa8ns3u+Q3Zs1NsQ=="],
|
||||
"gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ka97/ssYE5nPH4Qs+8bd8RlYeNeUVBhcnsNUmFM6VWEob4jfN9FTr0NBhXVi1XEJpj3cMfgSRW+LdE3SUZbPrw=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.2.1-canary.2", "", { "os": "linux", "cpu": "x64" }, "sha512-R8bbqvKkVtzjN/KWogQQvWXgBTyWXcOEVgiPMXKgx7/zfybWeL+WjSXeepw4xYyDlQ1vC//HwEqLfFX41Jht4Q=="],
|
||||
"gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-zY1JduE4B3q0k2ZCE+DAF/1efjTXUsKP+VXRtrt/rJCTgDlUyyryx7aOgYXNc1d8gobys/Lof9P9ze8IyRDn7Q=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.2.1-canary.2", "", { "os": "linux", "cpu": "x64" }, "sha512-wWr41f7g3H5FcLguoAS7bvqSqaVkhK97/fk7ANG1l5yW13F9qVPboVZvgVFrRsPFZgqANxs4AmhKAjjjT+mL1g=="],
|
||||
"gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-QqvLZpurBD46RhaVaVBepkVQzh8xtlUN00RlG4Iq1sBheNugamUNPuZEH1r9X1YGQo1KqAe1iiShF0acva3jHQ=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.2.1-canary.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-ogzvioGSaJ9F2VWZXiLQR4K4G6fbBUdlZfKowOJyFKfBhPK+WuwJlEnzWfPAe5YjtiQU2H+eMdZdC25DXDXo6g=="],
|
||||
"gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.2.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-ODZ0r9WMyylTHAN6pLtvUtQlGXBL9voljv6ujSlcsjOxhtXPI1Ag6AhZK0SE8hEpR1374WZZ5w33ChpJd5fsjw=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.2.1-canary.2", "", { "os": "win32", "cpu": "x64" }, "sha512-6sss7xGWck4DrJC7h/Gc2LFN3xh2dxnXxYmkxOD9gOq/yFdX7NSrNdCfNvXg5+6EFcTPURRg9gS9VgkzOCxp8Q=="],
|
||||
"gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.2.0", "", { "os": "win32", "cpu": "x64" }, "sha512-8+4Z3Z7xa13NdUuUAcpVNA6o76lNPniBd9Xbo02bwXQXnZgFvEopwY2at5+z7yHl47X9qbZpvwatZ2BRo3EdZw=="],
|
||||
|
||||
"gitbook-v2/next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.1.1",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"next": "canary",
|
||||
"next": "^15.2.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"@gitbook/api": "0.96.1",
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from '@/components/SiteLayout';
|
||||
import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils';
|
||||
import { GITBOOK_DISABLE_TRACKING } from '@v2/lib/env';
|
||||
import { getThemeFromMiddleware } from '@v2/lib/middleware';
|
||||
import { getThemeFromMiddleware, getVisitorAuthTokenFromMiddleware } from '@v2/lib/middleware';
|
||||
|
||||
interface SiteDynamicLayoutProps {
|
||||
params: Promise<RouteLayoutParams>;
|
||||
@@ -18,6 +18,7 @@ export default async function SiteDynamicLayout({
|
||||
}: React.PropsWithChildren<SiteDynamicLayoutProps>) {
|
||||
const context = await getDynamicSiteContext(await params);
|
||||
const forcedTheme = await getThemeFromMiddleware();
|
||||
const visitorAuthToken = await getVisitorAuthTokenFromMiddleware();
|
||||
|
||||
return (
|
||||
<CustomizationRootLayout customization={context.customization}>
|
||||
@@ -25,6 +26,7 @@ export default async function SiteDynamicLayout({
|
||||
context={context}
|
||||
forcedTheme={forcedTheme}
|
||||
withTracking={!GITBOOK_DISABLE_TRACKING}
|
||||
visitorAuthToken={visitorAuthToken}
|
||||
>
|
||||
{children}
|
||||
</SiteLayout>
|
||||
|
||||
@@ -25,7 +25,11 @@ export default async function SiteStaticLayout({
|
||||
|
||||
return (
|
||||
<CustomizationRootLayout customization={context.customization}>
|
||||
<SiteLayout context={context} withTracking={!GITBOOK_DISABLE_TRACKING}>
|
||||
<SiteLayout
|
||||
context={context}
|
||||
withTracking={!GITBOOK_DISABLE_TRACKING}
|
||||
visitorAuthToken={null}
|
||||
>
|
||||
{children}
|
||||
</SiteLayout>
|
||||
</CustomizationRootLayout>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { fetchSiteContextByURL } from '@v2/lib/context';
|
||||
import { createDataFetcher } from '@v2/lib/data';
|
||||
import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_URL } from '@v2/lib/env';
|
||||
import { createImageResizer } from '@v2/lib/images';
|
||||
import { createLinker } from '@v2/lib/links';
|
||||
import { headers } from 'next/headers';
|
||||
import {
|
||||
fetchSiteContextByURL,
|
||||
fetchSiteContextByURLLookup,
|
||||
getBaseContext,
|
||||
} from '@v2/lib/context';
|
||||
import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';
|
||||
|
||||
export type RouteParamMode = 'url-host' | 'url';
|
||||
|
||||
@@ -21,29 +21,19 @@ export type RouteParams = RouteLayoutParams & {
|
||||
/**
|
||||
* Get the static context when rendering statically a site.
|
||||
*/
|
||||
export async function getStaticSiteContext(params: RouteLayoutParams) {
|
||||
const url = getSiteURLFromParams(params);
|
||||
|
||||
const dataFetcher = createDataFetcher();
|
||||
const { linker, host } = createLinkerFromParams(params);
|
||||
const context = await fetchSiteContextByURL(
|
||||
export function getStaticSiteContext(params: RouteLayoutParams) {
|
||||
const siteURL = getSiteURLFromParams(params);
|
||||
return fetchSiteContextByURL(
|
||||
getBaseContext({
|
||||
siteURL,
|
||||
urlMode: getModeFromParams(params.mode),
|
||||
}),
|
||||
{
|
||||
dataFetcher,
|
||||
linker,
|
||||
},
|
||||
{
|
||||
url: url.toString(),
|
||||
url: siteURL.toString(),
|
||||
visitorAuthToken: null,
|
||||
redirectOnError: false,
|
||||
}
|
||||
);
|
||||
|
||||
context.imageResizer = createImageResizer({
|
||||
host,
|
||||
linker: context.linker,
|
||||
});
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,36 +41,16 @@ export async function getStaticSiteContext(params: RouteLayoutParams) {
|
||||
* The context will depend on the request.
|
||||
*/
|
||||
export async function getDynamicSiteContext(params: RouteLayoutParams) {
|
||||
const url = getSiteURLFromParams(params);
|
||||
const headersSet = await headers();
|
||||
const siteURL = getSiteURLFromParams(params);
|
||||
const siteURLData = await getSiteURLDataFromMiddleware();
|
||||
|
||||
const dataFetcher = createDataFetcher({
|
||||
apiToken: headersSet.get('x-gitbook-token') ?? GITBOOK_API_TOKEN,
|
||||
apiEndpoint: headersSet.get('x-gitbook-api') ?? GITBOOK_API_URL,
|
||||
});
|
||||
|
||||
const { linker, host } = createLinkerFromParams(params);
|
||||
|
||||
const context = await fetchSiteContextByURL(
|
||||
{
|
||||
dataFetcher,
|
||||
linker,
|
||||
},
|
||||
{
|
||||
url: url.toString(),
|
||||
visitorAuthToken: headersSet.get('x-gitbook-visitor-token'),
|
||||
|
||||
// TODO: set it only when the token comes from the cookies.
|
||||
redirectOnError: true,
|
||||
}
|
||||
return fetchSiteContextByURLLookup(
|
||||
getBaseContext({
|
||||
siteURL,
|
||||
urlMode: getModeFromParams(params.mode),
|
||||
}),
|
||||
siteURLData
|
||||
);
|
||||
|
||||
context.imageResizer = createImageResizer({
|
||||
host,
|
||||
linker: context.linker,
|
||||
});
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,39 +61,6 @@ export function getPagePathFromParams(params: RouteParams) {
|
||||
return decoded;
|
||||
}
|
||||
|
||||
function createLinkerFromParams(params: RouteLayoutParams) {
|
||||
const url = getSiteURLFromParams(params);
|
||||
const mode = getModeFromParams(params.mode);
|
||||
|
||||
if (mode === 'url-host') {
|
||||
return {
|
||||
linker: createLinker({
|
||||
host: url.host,
|
||||
pathname: '/',
|
||||
}),
|
||||
host: url.host,
|
||||
};
|
||||
}
|
||||
|
||||
const gitbookURL = new URL(GITBOOK_URL);
|
||||
const linker = createLinker({
|
||||
protocol: gitbookURL.protocol,
|
||||
host: gitbookURL.host,
|
||||
pathname: `/url/${url.host}`,
|
||||
});
|
||||
|
||||
// Create link in the same format for links to other sites/sections.
|
||||
linker.toLinkForContent = (rawURL: string) => {
|
||||
const urlObject = new URL(rawURL);
|
||||
return `/url/${urlObject.host}${urlObject.pathname}`;
|
||||
};
|
||||
|
||||
return {
|
||||
linker,
|
||||
host: gitbookURL.host,
|
||||
};
|
||||
}
|
||||
|
||||
function getSiteURLFromParams(params: RouteLayoutParams) {
|
||||
const decoded = decodeURIComponent(params.siteURL);
|
||||
const url = new URL(`https://${decoded}`);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getSiteStructureSections } from '@/lib/sites';
|
||||
import type {
|
||||
ChangeRequest,
|
||||
PublishedSiteContentLookup,
|
||||
RevisionPage,
|
||||
RevisionPageDocument,
|
||||
Site,
|
||||
@@ -15,8 +16,9 @@ import type {
|
||||
import { type GitBookDataFetcher, createDataFetcher } from '@v2/lib/data';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { assert } from 'ts-essentials';
|
||||
import type { ImageResizer } from './images';
|
||||
import { type GitBookSpaceLinker, appendBasePathToLinker } from './links';
|
||||
import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_URL } from './env';
|
||||
import { type ImageResizer, createImageResizer } from './images';
|
||||
import { type GitBookSpaceLinker, createLinker } from './links';
|
||||
|
||||
/**
|
||||
* Generic context when rendering content.
|
||||
@@ -90,9 +92,6 @@ export type GitBookSiteContext = GitBookSpaceContext & {
|
||||
|
||||
/** Scripts to load for the site. */
|
||||
scripts: SiteIntegrationScript[];
|
||||
|
||||
/** Visitor token used to fetch the site */
|
||||
visitorAuthToken: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -102,6 +101,58 @@ export type GitBookPageContext = (GitBookSpaceContext | GitBookSiteContext) & {
|
||||
page: RevisionPageDocument;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the base context for a request.
|
||||
*/
|
||||
export function getBaseContext(input: {
|
||||
siteURL: URL | string;
|
||||
urlMode: 'url' | 'url-host';
|
||||
apiToken?: string | null;
|
||||
}) {
|
||||
const url = typeof input.siteURL === 'string' ? new URL(input.siteURL) : input.siteURL;
|
||||
const urlMode = input.urlMode;
|
||||
|
||||
const dataFetcher = createDataFetcher({
|
||||
apiToken: input.apiToken ?? GITBOOK_API_TOKEN,
|
||||
apiEndpoint: GITBOOK_API_URL,
|
||||
});
|
||||
const gitbookURL = new URL(GITBOOK_URL);
|
||||
|
||||
const linker =
|
||||
urlMode === 'url-host'
|
||||
? createLinker({
|
||||
host: url.host,
|
||||
pathname: url.pathname,
|
||||
})
|
||||
: createLinker({
|
||||
protocol: gitbookURL.protocol,
|
||||
host: gitbookURL.host,
|
||||
pathname: `/url/${url.host}${url.pathname}`,
|
||||
});
|
||||
|
||||
if (urlMode === 'url') {
|
||||
// Create link in the same format for links to other sites/sections.
|
||||
linker.toLinkForContent = (rawURL: string) => {
|
||||
const urlObject = new URL(rawURL);
|
||||
return `/url/${urlObject.host}${urlObject.pathname}`;
|
||||
};
|
||||
}
|
||||
|
||||
const imageResizer = createImageResizer({
|
||||
host: urlMode === 'url-host' ? url.host : gitbookURL.host,
|
||||
|
||||
// To ensure image resizing work for proxied sites,
|
||||
// we serve images from the root of the site.
|
||||
linker: linker,
|
||||
});
|
||||
|
||||
return {
|
||||
dataFetcher,
|
||||
linker,
|
||||
imageResizer,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the context of a site for a given URL and a base context.
|
||||
*/
|
||||
@@ -120,15 +171,25 @@ export async function fetchSiteContextByURL(
|
||||
redirectOnError: input.redirectOnError,
|
||||
});
|
||||
|
||||
return fetchSiteContextByURLLookup(baseContext, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the context of a site using the resolution of a URL
|
||||
*/
|
||||
export async function fetchSiteContextByURLLookup(
|
||||
baseContext: GitBookBaseContext,
|
||||
data: PublishedSiteContentLookup
|
||||
): Promise<GitBookSiteContext> {
|
||||
const { dataFetcher } = baseContext;
|
||||
if ('redirect' in data) {
|
||||
redirect(data.redirect);
|
||||
}
|
||||
|
||||
const context = await fetchSiteContextByIds(
|
||||
return await fetchSiteContextByIds(
|
||||
{
|
||||
...baseContext,
|
||||
dataFetcher: createDataFetcher({
|
||||
apiEndpoint: dataFetcher.apiEndpoint,
|
||||
dataFetcher: dataFetcher.withToken({
|
||||
apiToken: data.apiToken,
|
||||
}),
|
||||
},
|
||||
@@ -141,16 +202,8 @@ export async function fetchSiteContextByURL(
|
||||
shareKey: data.shareKey,
|
||||
changeRequest: data.changeRequest,
|
||||
revision: data.revision,
|
||||
visitorAuthToken: input.visitorAuthToken,
|
||||
}
|
||||
);
|
||||
|
||||
const siteContext = {
|
||||
...context,
|
||||
linker: appendBasePathToLinker(context.linker, data.basePath),
|
||||
};
|
||||
|
||||
return siteContext;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,7 +220,6 @@ export async function fetchSiteContextByIds(
|
||||
shareKey: string | undefined;
|
||||
changeRequest: string | undefined;
|
||||
revision: string | undefined;
|
||||
visitorAuthToken: string | null;
|
||||
}
|
||||
): Promise<GitBookSiteContext> {
|
||||
const { dataFetcher } = baseContext;
|
||||
@@ -233,7 +285,6 @@ export async function fetchSiteContextByIds(
|
||||
structure: siteStructure,
|
||||
sections,
|
||||
scripts,
|
||||
visitorAuthToken: ids.visitorAuthToken,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,17 @@ export function createDataFetcher(input: DataFetcherInput = commonInput): GitBoo
|
||||
return {
|
||||
apiEndpoint: input.apiEndpoint,
|
||||
|
||||
async api() {
|
||||
return getAPI(input);
|
||||
},
|
||||
|
||||
withToken({ apiToken }) {
|
||||
return createDataFetcher({
|
||||
...input,
|
||||
apiToken,
|
||||
});
|
||||
},
|
||||
|
||||
//
|
||||
// API that are tied to the token
|
||||
//
|
||||
@@ -124,6 +135,9 @@ export function createDataFetcher(input: DataFetcherInput = commonInput): GitBoo
|
||||
spaceId: params.spaceId,
|
||||
});
|
||||
},
|
||||
searchSiteContent(params) {
|
||||
return searchSiteContent(input, params);
|
||||
},
|
||||
|
||||
//
|
||||
// API that are not tied to the token
|
||||
@@ -498,6 +512,23 @@ async function getEmbedByUrl(
|
||||
return res.data;
|
||||
}
|
||||
|
||||
async function searchSiteContent(
|
||||
input: DataFetcherInput,
|
||||
params: Parameters<GitBookDataFetcher['searchSiteContent']>[0]
|
||||
) {
|
||||
'use cache';
|
||||
|
||||
const { organizationId, siteId, query, scope } = params;
|
||||
|
||||
cacheLife('days');
|
||||
|
||||
const res = await getAPI(input).orgs.searchSiteContent(organizationId, siteId, {
|
||||
query,
|
||||
...scope,
|
||||
});
|
||||
return res.data.items;
|
||||
}
|
||||
|
||||
function getAPI(input: DataFetcherInput) {
|
||||
const { apiEndpoint, apiToken } = input;
|
||||
const api = new GitBookAPI({
|
||||
|
||||
@@ -10,6 +10,18 @@ export interface GitBookDataFetcher {
|
||||
*/
|
||||
apiEndpoint: string;
|
||||
|
||||
/**
|
||||
* Get an API client for the current context.
|
||||
*/
|
||||
api(): Promise<api.GitBookAPI>;
|
||||
|
||||
/**
|
||||
* Create a data fetcher authenticated with a specific token.
|
||||
*/
|
||||
withToken(input: {
|
||||
apiToken: string;
|
||||
}): GitBookDataFetcher;
|
||||
|
||||
/**
|
||||
* Get a user by its ID.
|
||||
*/
|
||||
@@ -126,4 +138,19 @@ export interface GitBookDataFetcher {
|
||||
* Get an embed by its URL.
|
||||
*/
|
||||
getEmbedByUrl(params: { url: string; spaceId: string }): Promise<api.Embed>;
|
||||
|
||||
/**
|
||||
* Search content in a site.
|
||||
*/
|
||||
searchSiteContent(params: {
|
||||
organizationId: string;
|
||||
siteId: string;
|
||||
query: string;
|
||||
scope:
|
||||
| { mode: 'all' }
|
||||
| { mode: 'current'; siteSpaceId: string }
|
||||
| { mode: 'specific'; siteSpaceIds: string[] };
|
||||
/** Cache bust to ensure the search results are fresh when the space is updated. */
|
||||
cacheBust?: string;
|
||||
}): Promise<api.SearchSpaceResult[]>;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,51 @@
|
||||
import { CustomizationThemeMode } from '@gitbook/api';
|
||||
import { CustomizationThemeMode, type PublishedSiteContent } from '@gitbook/api';
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
export enum MiddlewareHeaders {
|
||||
/**
|
||||
* Type of the route (static or dynamic)
|
||||
*/
|
||||
RouteType = 'x-gitbook-route-type',
|
||||
|
||||
/**
|
||||
* The URL of the site (without the pathname)
|
||||
*/
|
||||
SiteURL = 'x-gitbook-site-url',
|
||||
|
||||
/**
|
||||
* The data associated with the URL.
|
||||
*/
|
||||
SiteURLData = 'x-gitbook-site-url-data',
|
||||
|
||||
/**
|
||||
* The mode of the URL (url or url-host)
|
||||
*/
|
||||
URLMode = 'x-gitbook-url-mode',
|
||||
|
||||
/**
|
||||
* The theme of the page (light or dark)
|
||||
*/
|
||||
Theme = 'x-gitbook-theme',
|
||||
|
||||
/**
|
||||
* The customization override to apply.
|
||||
*/
|
||||
Customization = 'x-gitbook-customization',
|
||||
|
||||
/**
|
||||
* Token to use for the API.
|
||||
*/
|
||||
APIToken = 'x-gitbook-token',
|
||||
|
||||
/**
|
||||
* The visitor token used to access this content
|
||||
*/
|
||||
VisitorAuthToken = 'x-gitbook-visitor-token',
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL mode from the middleware headers.
|
||||
* This function should only be called in a server action or a dynamic route.
|
||||
*/
|
||||
export async function getURLModeFromMiddleware(): Promise<'url' | 'url-host'> {
|
||||
const headersList = await headers();
|
||||
@@ -20,8 +57,40 @@ export async function getURLModeFromMiddleware(): Promise<'url' | 'url-host'> {
|
||||
return mode as 'url' | 'url-host';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the site URL data from the middleware headers.
|
||||
* This function should only be called in a server action or a dynamic route.
|
||||
*/
|
||||
export async function getSiteURLDataFromMiddleware(): Promise<PublishedSiteContent> {
|
||||
const headersList = await headers();
|
||||
const siteURLData = headersList.get(MiddlewareHeaders.SiteURLData);
|
||||
|
||||
if (!siteURLData) {
|
||||
throw new Error(
|
||||
'Site URL data is not set by the middleware. This should only be called in a server action or a dynamic route.'
|
||||
);
|
||||
}
|
||||
|
||||
return JSON.parse(siteURLData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL from the middleware headers.
|
||||
* This function should only be called in a server action or a dynamic route.
|
||||
*/
|
||||
export async function getSiteURLFromMiddleware(): Promise<string> {
|
||||
const headersList = await headers();
|
||||
const siteURL = headersList.get(MiddlewareHeaders.SiteURL);
|
||||
if (!siteURL) {
|
||||
throw new Error('URL mode is not set by the middleware');
|
||||
}
|
||||
|
||||
return siteURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* For preview, the theme can be set via query string (?theme=light).
|
||||
* This function should only be called in a dynamic route.
|
||||
*/
|
||||
export async function getThemeFromMiddleware() {
|
||||
const headersList = await headers();
|
||||
@@ -34,3 +103,17 @@ export async function getThemeFromMiddleware() {
|
||||
? CustomizationThemeMode.Light
|
||||
: CustomizationThemeMode.Dark;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the visitor auth token from the middleware headers.
|
||||
* This function should only be called in a dynamic route.
|
||||
*/
|
||||
export async function getVisitorAuthTokenFromMiddleware(): Promise<string | null> {
|
||||
const headersList = await headers();
|
||||
const visitorAuthToken = headersList.get(MiddlewareHeaders.VisitorAuthToken);
|
||||
if (!visitorAuthToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return visitorAuthToken;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { type GitBookBaseContext, fetchSiteContextByURLLookup, getBaseContext } from './context';
|
||||
import {
|
||||
getSiteURLDataFromMiddleware,
|
||||
getSiteURLFromMiddleware,
|
||||
getURLModeFromMiddleware,
|
||||
} from './middleware';
|
||||
|
||||
/**
|
||||
* Get the base context for a server action.
|
||||
* This function should only be called in a server action.
|
||||
*/
|
||||
export async function getServerActionBaseContext() {
|
||||
const siteURL = await getSiteURLFromMiddleware();
|
||||
const siteURLData = await getSiteURLDataFromMiddleware();
|
||||
const urlMode = await getURLModeFromMiddleware();
|
||||
|
||||
return getBaseContext({
|
||||
siteURL,
|
||||
urlMode,
|
||||
apiToken: siteURLData.apiToken,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the context for a site in a server action.
|
||||
* The server action is always dynamic and the request is passed through the middleware.
|
||||
*/
|
||||
export async function fetchServerActionSiteContext(baseContext: GitBookBaseContext) {
|
||||
const siteURLData = await getSiteURLDataFromMiddleware();
|
||||
return fetchSiteContextByURLLookup(baseContext, siteURLData);
|
||||
}
|
||||
@@ -64,8 +64,13 @@ async function serveSiteByURL(request: NextRequest, urlWithMode: URLWithMode) {
|
||||
return NextResponse.redirect(data.redirect);
|
||||
}
|
||||
|
||||
const routeType = dynamicHeaders ? 'dynamic' : 'static';
|
||||
|
||||
const requestHeaders = new Headers(request.headers);
|
||||
requestHeaders.set(MiddlewareHeaders.RouteType, routeType);
|
||||
requestHeaders.set(MiddlewareHeaders.URLMode, mode);
|
||||
requestHeaders.set(MiddlewareHeaders.SiteURL, `${url.origin}${data.basePath}`);
|
||||
requestHeaders.set(MiddlewareHeaders.SiteURLData, JSON.stringify(data));
|
||||
if (dynamicHeaders) {
|
||||
for (const [key, value] of Object.entries(dynamicHeaders)) {
|
||||
requestHeaders.set(key, value);
|
||||
@@ -74,16 +79,18 @@ async function serveSiteByURL(request: NextRequest, urlWithMode: URLWithMode) {
|
||||
|
||||
const route = [
|
||||
'sites',
|
||||
dynamicHeaders ? 'dynamic' : 'static',
|
||||
routeType,
|
||||
mode,
|
||||
encodeURIComponent(url.host + data.basePath),
|
||||
encodePathInSiteContent(data.pathname),
|
||||
].join('/');
|
||||
|
||||
console.log('route', route);
|
||||
console.log(`rewriting to ${route}`);
|
||||
|
||||
const response = NextResponse.rewrite(new URL(`/${route}`, request.url), {
|
||||
headers: requestHeaders,
|
||||
request: {
|
||||
headers: requestHeaders,
|
||||
},
|
||||
});
|
||||
|
||||
// Add Content Security Policy header
|
||||
|
||||
@@ -155,6 +155,21 @@ export function runTestCases(testCases: TestsCase[]) {
|
||||
);
|
||||
}
|
||||
|
||||
// Set the header to disable the Vercel toolbar
|
||||
// But only on the main document as it'd cause CORS issues on other resources
|
||||
await page.route('**/*', async (route, request) => {
|
||||
if (request.resourceType() === 'document') {
|
||||
await route.continue({
|
||||
headers: {
|
||||
...request.headers(),
|
||||
'x-vercel-skip-toolbar': '1',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto(url);
|
||||
if (testEntry.run) {
|
||||
await testEntry.run(page);
|
||||
|
||||
@@ -62,7 +62,8 @@
|
||||
"tailwind-merge": "^2.2.0",
|
||||
"tailwind-shades": "^1.1.2",
|
||||
"url-join": "^5.0.0",
|
||||
"usehooks-ts": "^3.1.0"
|
||||
"usehooks-ts": "^3.1.0",
|
||||
"ai": "^4.1.46"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@argos-ci/playwright": "^3.10.0",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getThemeFromMiddleware } from '@v2/lib/middleware';
|
||||
import { getThemeFromMiddleware, getVisitorAuthTokenFromMiddleware } from '@v2/lib/middleware';
|
||||
import type { Metadata, Viewport } from 'next';
|
||||
import type React from 'react';
|
||||
|
||||
@@ -22,12 +22,14 @@ export default async function ContentLayout(props: { children: React.ReactNode }
|
||||
|
||||
const context = await fetchLayoutData();
|
||||
const queryStringTheme = await getThemeFromMiddleware();
|
||||
const visitorAuthToken = await getVisitorAuthTokenFromMiddleware();
|
||||
|
||||
return (
|
||||
<SiteLayout
|
||||
context={context}
|
||||
forcedTheme={queryStringTheme}
|
||||
withTracking={await shouldTrackEvents()}
|
||||
visitorAuthToken={visitorAuthToken}
|
||||
>
|
||||
{children}
|
||||
</SiteLayout>
|
||||
|
||||
@@ -5,6 +5,8 @@ import { headers } from 'next/headers';
|
||||
|
||||
import { getV1BaseContext } from '@/lib/v1';
|
||||
|
||||
import { isV2 } from '@/lib/v2';
|
||||
import { getServerActionBaseContext } from '@v2/lib/server-actions';
|
||||
import { AdClassicRendering } from './AdClassicRendering';
|
||||
import { AdCoverRendering } from './AdCoverRendering';
|
||||
import { AdPixels } from './AdPixels';
|
||||
@@ -41,7 +43,7 @@ interface FetchPlaceholderAdOptions {
|
||||
* and properly access user-agent and IP.
|
||||
*/
|
||||
export async function renderAd(options: FetchAdOptions) {
|
||||
const baseContext = await getV1BaseContext();
|
||||
const context = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
|
||||
|
||||
const mode = options.source === 'live' ? options.mode : 'classic';
|
||||
const result = options.source === 'live' ? await fetchAd(options) : await getPlaceholderAd();
|
||||
@@ -64,9 +66,9 @@ export async function renderAd(options: FetchAdOptions) {
|
||||
children: (
|
||||
<>
|
||||
{mode === 'classic' || !('callToAction' in ad) ? (
|
||||
<AdClassicRendering ad={ad} insightsAd={insightsAd} context={baseContext} />
|
||||
<AdClassicRendering ad={ad} insightsAd={insightsAd} context={context} />
|
||||
) : (
|
||||
<AdCoverRendering ad={ad} insightsAd={insightsAd} context={baseContext} />
|
||||
<AdCoverRendering ad={ad} insightsAd={insightsAd} context={context} />
|
||||
)}
|
||||
{ad.pixel ? <AdPixels rawPixel={ad.pixel} /> : null}
|
||||
</>
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import { readStreamableValue } from 'ai/rsc';
|
||||
import React from 'react';
|
||||
|
||||
import { Loading } from '@/components/primitives';
|
||||
import { useLanguage } from '@/intl/client';
|
||||
import { t } from '@/intl/translate';
|
||||
import type { TranslationLanguage } from '@/intl/translations';
|
||||
import { iterateStreamResponse } from '@/lib/actions';
|
||||
import type { SiteContentPointer } from '@/lib/api';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { useTrackEvent } from '../Insights';
|
||||
@@ -32,14 +31,13 @@ export type SearchAskState =
|
||||
/**
|
||||
* Fetch and render the answers to a question.
|
||||
*/
|
||||
export function SearchAskAnswer(props: { pointer: SiteContentPointer; query: string }) {
|
||||
const { pointer, query } = props;
|
||||
export function SearchAskAnswer(props: { query: string }) {
|
||||
const { query } = props;
|
||||
|
||||
const language = useLanguage();
|
||||
const trackEvent = useTrackEvent();
|
||||
const [, setSearchState] = useSearch();
|
||||
const [askState, setAskState] = useSearchAskContext();
|
||||
const { organizationId, siteId, siteSpaceId } = pointer;
|
||||
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -52,19 +50,19 @@ export function SearchAskAnswer(props: { pointer: SiteContentPointer; query: str
|
||||
query,
|
||||
});
|
||||
|
||||
const response = streamAskQuestion({ pointer, question: query });
|
||||
const stream = iterateStreamResponse(response);
|
||||
|
||||
// When we pass in "ask" mode, the query could still be updated by the client
|
||||
// we ensure that the query is up-to-date before starting the stream.
|
||||
setSearchState((prev) => (prev ? { ...prev, query, ask: true } : null));
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const { stream } = await streamAskQuestion({ question: query });
|
||||
for await (const chunk of readStreamableValue(stream)) {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
setAskState({ type: 'answer', answer: chunk });
|
||||
if (chunk) {
|
||||
setAskState({ type: 'answer', answer: chunk });
|
||||
}
|
||||
}
|
||||
})().catch(() => {
|
||||
if (cancelled) {
|
||||
@@ -81,7 +79,7 @@ export function SearchAskAnswer(props: { pointer: SiteContentPointer; query: str
|
||||
cancelled = true;
|
||||
}
|
||||
};
|
||||
}, [organizationId, siteId, siteSpaceId, query, setAskState, setSearchState, trackEvent]);
|
||||
}, [query, setAskState, setSearchState, trackEvent]);
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
|
||||
@@ -7,7 +7,6 @@ import React from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
import { tString, useLanguage } from '@/intl/client';
|
||||
import type { SiteContentPointer } from '@/lib/api';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { LoadingPane } from '../primitives/LoadingPane';
|
||||
@@ -18,11 +17,9 @@ import { SearchScopeToggle } from './SearchScopeToggle';
|
||||
import { type SearchState, type UpdateSearchState, useSearch } from './useSearch';
|
||||
|
||||
interface SearchModalProps {
|
||||
revisionId: string;
|
||||
spaceTitle: string;
|
||||
isMultiVariants: boolean;
|
||||
withAsk: boolean;
|
||||
pointer: SiteContentPointer;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,16 +141,7 @@ function SearchModalBody(
|
||||
onClose: (to?: string) => void;
|
||||
}
|
||||
) {
|
||||
const {
|
||||
pointer,
|
||||
revisionId,
|
||||
spaceTitle,
|
||||
withAsk,
|
||||
isMultiVariants,
|
||||
state,
|
||||
setSearchState,
|
||||
onClose,
|
||||
} = props;
|
||||
const { spaceTitle, withAsk, isMultiVariants, state, setSearchState, onClose } = props;
|
||||
|
||||
const language = useLanguage();
|
||||
const resultsRef = React.useRef<SearchResultsRef>(null);
|
||||
@@ -299,17 +287,13 @@ function SearchModalBody(
|
||||
{!state.ask || !withAsk ? (
|
||||
<SearchResults
|
||||
ref={resultsRef}
|
||||
pointer={pointer}
|
||||
revisionId={revisionId}
|
||||
global={isMultiVariants && state.global}
|
||||
query={state.query}
|
||||
withAsk={withAsk}
|
||||
onSwitchToAsk={onSwitchToAsk}
|
||||
/>
|
||||
) : null}
|
||||
{state.query && state.ask && withAsk ? (
|
||||
<SearchAskAnswer pointer={pointer} query={state.query} />
|
||||
) : null}
|
||||
{state.query && state.ask && withAsk ? <SearchAskAnswer query={state.query} /> : null}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { captureException } from '@sentry/nextjs';
|
||||
import { readStreamableValue } from 'ai/rsc';
|
||||
import assertNever from 'assert-never';
|
||||
import React from 'react';
|
||||
|
||||
import { t, useLanguage } from '@/intl/client';
|
||||
import { iterateStreamResponse } from '@/lib/actions';
|
||||
import type { SiteContentPointer } from '@/lib/api';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { useTrackEvent } from '../Insights';
|
||||
@@ -48,15 +47,13 @@ export const SearchResults = React.forwardRef(function SearchResults(
|
||||
props: {
|
||||
children?: React.ReactNode;
|
||||
query: string;
|
||||
revisionId: string;
|
||||
global: boolean;
|
||||
withAsk: boolean;
|
||||
pointer: SiteContentPointer;
|
||||
onSwitchToAsk: () => void;
|
||||
},
|
||||
ref: React.Ref<SearchResultsRef>
|
||||
) {
|
||||
const { children, query, pointer, revisionId, withAsk, global, onSwitchToAsk } = props;
|
||||
const { children, query, withAsk, global, onSwitchToAsk } = props;
|
||||
|
||||
const language = useLanguage();
|
||||
const trackEvent = useTrackEvent();
|
||||
@@ -93,10 +90,13 @@ export const SearchResults = React.forwardRef(function SearchResults(
|
||||
return;
|
||||
}
|
||||
|
||||
const response = streamRecommendedQuestions(pointer.organizationId, pointer.siteId);
|
||||
const stream = iterateStreamResponse(response);
|
||||
const response = await streamRecommendedQuestions();
|
||||
for await (const entry of readStreamableValue(response.stream)) {
|
||||
if (!entry) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for await (const { question } of stream) {
|
||||
const { question } = entry;
|
||||
if (questions.has(question)) {
|
||||
continue;
|
||||
}
|
||||
@@ -124,8 +124,8 @@ export const SearchResults = React.forwardRef(function SearchResults(
|
||||
let cancelled = false;
|
||||
const timeout = setTimeout(async () => {
|
||||
const results = await (global
|
||||
? searchAllSiteContent(query, pointer)
|
||||
: searchSiteSpaceContent(query, pointer, revisionId));
|
||||
? searchAllSiteContent(query)
|
||||
: searchSiteSpaceContent(query));
|
||||
|
||||
if (cancelled) {
|
||||
return;
|
||||
@@ -154,7 +154,7 @@ export const SearchResults = React.forwardRef(function SearchResults(
|
||||
cancelled = true;
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [query, global, pointer, revisionId, withAsk, trackEvent]);
|
||||
}, [query, global, withAsk, trackEvent]);
|
||||
|
||||
const results: ResultType[] = React.useMemo(() => {
|
||||
if (!withAsk) {
|
||||
|
||||
@@ -3,22 +3,24 @@
|
||||
import type {
|
||||
RevisionPage,
|
||||
SearchAIAnswer,
|
||||
SearchAIRecommendedQuestionStream,
|
||||
SearchPageResult,
|
||||
SiteSpace,
|
||||
Space,
|
||||
} from '@gitbook/api';
|
||||
import type { GitBookSiteContext } from '@v2/lib/context';
|
||||
import type { GitBookBaseContext, GitBookSiteContext } from '@v2/lib/context';
|
||||
import { fetchServerActionSiteContext, getServerActionBaseContext } from '@v2/lib/server-actions';
|
||||
import { createStreamableValue } from 'ai/rsc';
|
||||
import type * as React from 'react';
|
||||
import { assert } from 'ts-essentials';
|
||||
|
||||
import { streamResponse } from '@/lib/actions';
|
||||
import * as api from '@/lib/api';
|
||||
import { getAbsoluteHref } from '@/lib/links';
|
||||
import { resolvePageId } from '@/lib/pages';
|
||||
import { findSiteSpaceById } from '@/lib/sites';
|
||||
import { filterOutNullable } from '@/lib/typescript';
|
||||
import { fetchV1ContextForSitePointer } from '@/lib/v1';
|
||||
import { getV1BaseContext } from '@/lib/v1';
|
||||
|
||||
import { isV2 } from '@/lib/v2';
|
||||
import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';
|
||||
import { DocumentView } from '../DocumentView';
|
||||
|
||||
export type OrderedComputedResult = ComputedPageResult | ComputedSectionResult;
|
||||
@@ -54,70 +56,13 @@ export interface AskAnswerResult {
|
||||
sources: AskAnswerSource[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for content in a site by scoping the search to all content, a specific spaces or current space.
|
||||
*/
|
||||
async function searchSiteContent(args: {
|
||||
pointer: api.SiteContentPointer;
|
||||
query: string;
|
||||
scope:
|
||||
| { mode: 'all' }
|
||||
| { mode: 'current'; siteSpaceId: string }
|
||||
| { mode: 'specific'; siteSpaceIds: string[] };
|
||||
cacheBust?: string;
|
||||
}): Promise<OrderedComputedResult[]> {
|
||||
const { pointer, scope, query, cacheBust } = args;
|
||||
|
||||
if (query.length <= 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const needsStructure =
|
||||
scope.mode === 'all' ||
|
||||
scope.mode === 'current' ||
|
||||
(scope.mode === 'specific' && scope.siteSpaceIds.length > 1);
|
||||
|
||||
const [searchResults, siteData] = await Promise.all([
|
||||
api.searchSiteContent(pointer.organizationId, pointer.siteId, query, scope, cacheBust),
|
||||
needsStructure ? fetchV1ContextForSitePointer(pointer) : null,
|
||||
]);
|
||||
const siteStructure = siteData?.structure;
|
||||
|
||||
if (siteStructure) {
|
||||
// We are searching all of this Site's content
|
||||
return (
|
||||
await Promise.all(
|
||||
searchResults.items.map(async (spaceItem) => {
|
||||
const siteSpace = findSiteSpaceById(siteStructure, spaceItem.id);
|
||||
|
||||
return Promise.all(
|
||||
spaceItem.pages.map((item) =>
|
||||
transformSitePageResult(item, siteSpace ?? undefined)
|
||||
)
|
||||
);
|
||||
})
|
||||
)
|
||||
).flat(2);
|
||||
}
|
||||
|
||||
return (
|
||||
await Promise.all(
|
||||
searchResults.items.map((spaceItem) => {
|
||||
return Promise.all(spaceItem.pages.map((item) => transformPageResult(item)));
|
||||
})
|
||||
)
|
||||
).flat(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Server action to search content in the entire site.
|
||||
*/
|
||||
export async function searchAllSiteContent(
|
||||
query: string,
|
||||
pointer: api.SiteContentPointer
|
||||
): Promise<OrderedComputedResult[]> {
|
||||
return await searchSiteContent({
|
||||
pointer,
|
||||
export async function searchAllSiteContent(query: string): Promise<OrderedComputedResult[]> {
|
||||
const context = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
|
||||
|
||||
return await searchSiteContent(context, {
|
||||
query,
|
||||
scope: { mode: 'all' },
|
||||
});
|
||||
@@ -126,118 +71,191 @@ export async function searchAllSiteContent(
|
||||
/**
|
||||
* Server action to search content in a space.
|
||||
*/
|
||||
export async function searchSiteSpaceContent(
|
||||
query: string,
|
||||
pointer: api.SiteContentPointer,
|
||||
revisionId: string
|
||||
): Promise<OrderedComputedResult[]> {
|
||||
const siteSpaceId = pointer.siteSpaceId;
|
||||
assert(siteSpaceId, 'Expected siteSpaceId for searchSiteSpaceContent');
|
||||
export async function searchSiteSpaceContent(query: string): Promise<OrderedComputedResult[]> {
|
||||
const context = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
|
||||
const siteURLData = await getSiteURLDataFromMiddleware();
|
||||
|
||||
return await searchSiteContent({
|
||||
pointer,
|
||||
return await searchSiteContent(context, {
|
||||
query,
|
||||
// If we have a siteSectionId that means its a sections site use `current` mode
|
||||
// which searches in the current space + all default spaces of sections
|
||||
scope: pointer.siteSectionId
|
||||
? { mode: 'current', siteSpaceId }
|
||||
: { mode: 'specific', siteSpaceIds: [siteSpaceId] },
|
||||
// We want to break cache for this specific space if the revisionId is different so use it as a cache busting key
|
||||
cacheBust: revisionId,
|
||||
scope: siteURLData.siteSection
|
||||
? { mode: 'current', siteSpaceId: siteURLData.siteSpace }
|
||||
: { mode: 'specific', siteSpaceIds: [siteURLData.siteSpace] },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Server action to ask a question in a space.
|
||||
*/
|
||||
export const streamAskQuestion = streamResponse(async function* ({
|
||||
pointer,
|
||||
export async function streamAskQuestion({
|
||||
question,
|
||||
}: {
|
||||
pointer: api.SiteContentPointer;
|
||||
question: string;
|
||||
}) {
|
||||
const { organizationId, siteId, siteSpaceId } = pointer;
|
||||
const [apiCtx, context] = await Promise.all([api.api(), fetchV1ContextForSitePointer(pointer)]);
|
||||
const responseStream = createStreamableValue<AskAnswerResult | undefined>();
|
||||
|
||||
const stream = apiCtx.client.orgs.streamAskInSite(
|
||||
organizationId,
|
||||
siteId,
|
||||
{
|
||||
question,
|
||||
context: siteSpaceId
|
||||
? {
|
||||
siteSpaceId,
|
||||
}
|
||||
: undefined,
|
||||
scope: {
|
||||
mode: 'default',
|
||||
(async () => {
|
||||
const context = await fetchServerActionSiteContext(
|
||||
isV2() ? await getServerActionBaseContext() : await getV1BaseContext()
|
||||
);
|
||||
|
||||
// Include the current site space regardless.
|
||||
includedSiteSpaces: siteSpaceId ? [siteSpaceId] : undefined,
|
||||
const apiClient = await context.dataFetcher.api();
|
||||
|
||||
const stream = apiClient.orgs.streamAskInSite(
|
||||
context.organizationId,
|
||||
context.site.id,
|
||||
{
|
||||
question,
|
||||
context: {
|
||||
siteSpaceId: context.siteSpace.id,
|
||||
},
|
||||
scope: {
|
||||
mode: 'default',
|
||||
// Include the current site space regardless.
|
||||
includedSiteSpaces: [context.siteSpace.id],
|
||||
},
|
||||
},
|
||||
},
|
||||
{ format: 'document' }
|
||||
);
|
||||
{ format: 'document' }
|
||||
);
|
||||
|
||||
const spacePromises = new Map<string, Promise<RevisionPage[]>>();
|
||||
for await (const chunk of stream) {
|
||||
const answer = chunk.answer;
|
||||
const spacePromises = new Map<string, Promise<RevisionPage[]>>();
|
||||
for await (const chunk of stream) {
|
||||
const answer = chunk.answer;
|
||||
|
||||
// Register the space of each page source into the promise queue.
|
||||
const spaces = answer.sources
|
||||
.map((source) => {
|
||||
if (source.type !== 'page') {
|
||||
return null;
|
||||
}
|
||||
// Register the space of each page source into the promise queue.
|
||||
const spaces = answer.sources
|
||||
.map((source) => {
|
||||
if (source.type !== 'page') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!spacePromises.has(source.space)) {
|
||||
spacePromises.set(
|
||||
source.space,
|
||||
context.dataFetcher.getRevisionPages({
|
||||
spaceId: source.space,
|
||||
revisionId: source.revision,
|
||||
metadata: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
if (!spacePromises.has(source.space)) {
|
||||
spacePromises.set(
|
||||
source.space,
|
||||
context.dataFetcher.getRevisionPages({
|
||||
spaceId: source.space,
|
||||
revisionId: source.revision,
|
||||
metadata: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return source.space;
|
||||
})
|
||||
.filter(filterOutNullable);
|
||||
return source.space;
|
||||
})
|
||||
.filter(filterOutNullable);
|
||||
|
||||
// Get the pages for all spaces referenced by this answer.
|
||||
const pages = await Promise.all(
|
||||
spaces.map(async (space) => {
|
||||
const pages = await spacePromises.get(space);
|
||||
return { space, pages };
|
||||
})
|
||||
).then((results) => {
|
||||
return results.reduce((map, result) => {
|
||||
if (result.pages) {
|
||||
map.set(result.space, result.pages);
|
||||
}
|
||||
return map;
|
||||
}, new Map<string, RevisionPage[]>());
|
||||
// Get the pages for all spaces referenced by this answer.
|
||||
const pages = await Promise.all(
|
||||
spaces.map(async (space) => {
|
||||
const pages = await spacePromises.get(space);
|
||||
return { space, pages };
|
||||
})
|
||||
).then((results) => {
|
||||
return results.reduce((map, result) => {
|
||||
if (result.pages) {
|
||||
map.set(result.space, result.pages);
|
||||
}
|
||||
return map;
|
||||
}, new Map<string, RevisionPage[]>());
|
||||
});
|
||||
responseStream.update(
|
||||
await transformAnswer(context, { answer: chunk.answer, spacePages: pages })
|
||||
);
|
||||
}
|
||||
})()
|
||||
.then(() => {
|
||||
responseStream.done();
|
||||
})
|
||||
.catch((error) => {
|
||||
responseStream.error(error);
|
||||
});
|
||||
yield await transformAnswer(context, { answer: chunk.answer, spacePages: pages });
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
stream: responseStream.value,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream a list of suggested questions for the site.
|
||||
*/
|
||||
export const streamRecommendedQuestions = streamResponse(async function* (
|
||||
organizationId: string,
|
||||
siteId: string
|
||||
) {
|
||||
const apiCtx = await api.api();
|
||||
const stream = apiCtx.client.orgs.streamRecommendedQuestionsInSite(organizationId, siteId);
|
||||
export async function streamRecommendedQuestions() {
|
||||
const siteURLData = await getSiteURLDataFromMiddleware();
|
||||
const context = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
|
||||
|
||||
for await (const chunk of stream) {
|
||||
yield chunk;
|
||||
const responseStream = createStreamableValue<SearchAIRecommendedQuestionStream | undefined>();
|
||||
|
||||
(async () => {
|
||||
const apiClient = await context.dataFetcher.api();
|
||||
const apiStream = apiClient.orgs.streamRecommendedQuestionsInSite(
|
||||
siteURLData.organization,
|
||||
siteURLData.site
|
||||
);
|
||||
|
||||
for await (const chunk of apiStream) {
|
||||
responseStream.update(chunk);
|
||||
}
|
||||
})()
|
||||
.then(() => {
|
||||
responseStream.done();
|
||||
})
|
||||
.catch((error) => {
|
||||
responseStream.error(error);
|
||||
});
|
||||
|
||||
return { stream: responseStream.value };
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for content in a site by scoping the search to all content, a specific spaces or current space.
|
||||
*/
|
||||
async function searchSiteContent(
|
||||
context: GitBookBaseContext,
|
||||
args: {
|
||||
query: string;
|
||||
scope:
|
||||
| { mode: 'all' }
|
||||
| { mode: 'current'; siteSpaceId: string }
|
||||
| { mode: 'specific'; siteSpaceIds: string[] };
|
||||
}
|
||||
});
|
||||
): Promise<OrderedComputedResult[]> {
|
||||
const { dataFetcher } = context;
|
||||
const siteURLData = await getSiteURLDataFromMiddleware();
|
||||
|
||||
const { scope, query } = args;
|
||||
|
||||
if (query.length <= 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const [searchResults, { structure }] = await Promise.all([
|
||||
dataFetcher.searchSiteContent({
|
||||
organizationId: siteURLData.organization,
|
||||
siteId: siteURLData.site,
|
||||
query,
|
||||
scope,
|
||||
}),
|
||||
dataFetcher.getPublishedContentSite({
|
||||
organizationId: siteURLData.organization,
|
||||
siteId: siteURLData.site,
|
||||
siteShareKey: siteURLData.shareKey,
|
||||
}),
|
||||
]);
|
||||
|
||||
return (
|
||||
await Promise.all(
|
||||
searchResults.map(async (spaceItem) => {
|
||||
const siteSpace = findSiteSpaceById(structure, spaceItem.id);
|
||||
|
||||
return Promise.all(
|
||||
spaceItem.pages.map((item) =>
|
||||
transformSitePageResult(item, siteSpace ?? undefined)
|
||||
)
|
||||
);
|
||||
})
|
||||
)
|
||||
).flat(2);
|
||||
}
|
||||
|
||||
async function transformAnswer(
|
||||
context: GitBookSiteContext,
|
||||
@@ -343,16 +361,6 @@ async function transformSitePageResult(item: SearchPageResult, siteSpace?: SiteS
|
||||
return [page, ...sections];
|
||||
}
|
||||
|
||||
async function transformPageResult(item: SearchPageResult, space?: Space) {
|
||||
const [page, sections] = await transformSectionsAndPage({
|
||||
item,
|
||||
space,
|
||||
spaceURL: space?.urls.published ?? space?.urls.app,
|
||||
});
|
||||
|
||||
return [page, ...sections];
|
||||
}
|
||||
|
||||
// Resolve a relative path to an absolute URL
|
||||
// if the search result is relative to another space, we use the space URL
|
||||
async function getURLWithSections(path: string, spaceURL?: string) {
|
||||
|
||||
@@ -24,9 +24,10 @@ export async function SiteLayout(props: {
|
||||
context: GitBookSiteContext;
|
||||
forcedTheme?: CustomizationThemeMode | null;
|
||||
withTracking: boolean;
|
||||
visitorAuthToken: string | null;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { context, nonce, forcedTheme, withTracking, children } = props;
|
||||
const { context, nonce, forcedTheme, withTracking, visitorAuthToken, children } = props;
|
||||
|
||||
const { scripts, customization } = context;
|
||||
|
||||
@@ -51,7 +52,11 @@ export async function SiteLayout(props: {
|
||||
(customization.themes.toggeable ? undefined : customization.themes.default)
|
||||
}
|
||||
>
|
||||
<SpaceLayout context={context} withTracking={withTracking}>
|
||||
<SpaceLayout
|
||||
context={context}
|
||||
withTracking={withTracking}
|
||||
visitorAuthToken={visitorAuthToken}
|
||||
>
|
||||
{children}
|
||||
</SpaceLayout>
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import { CONTAINER_STYLE } from '@/components/layout';
|
||||
import { getSpaceLanguage } from '@/intl/server';
|
||||
import { t } from '@/intl/translate';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { getSitePointerFromContext } from '@/lib/v1';
|
||||
|
||||
import { SpacesDropdown } from '../Header/SpacesDropdown';
|
||||
import { InsightsProvider } from '../Insights';
|
||||
@@ -25,10 +24,13 @@ export function SpaceLayout(props: {
|
||||
/** Whether to enable tracking of events into site insights. */
|
||||
withTracking: boolean;
|
||||
|
||||
/** The visitor token used to access this content */
|
||||
visitorAuthToken: string | null;
|
||||
|
||||
/** The children of the layout. */
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { context, withTracking, children } = props;
|
||||
const { context, withTracking, visitorAuthToken, children } = props;
|
||||
const { siteSpace, customization, sections, siteSpaces } = context;
|
||||
|
||||
const withTopHeader = customization.header.preset !== CustomizationHeaderPreset.None;
|
||||
@@ -46,7 +48,7 @@ export function SpaceLayout(props: {
|
||||
<InsightsProvider
|
||||
enabled={withTracking}
|
||||
apiHost={context.dataFetcher.apiEndpoint}
|
||||
visitorAuthToken={context.visitorAuthToken}
|
||||
visitorAuthToken={visitorAuthToken}
|
||||
organizationId={context.organizationId}
|
||||
siteId={context.site.id}
|
||||
siteSectionId={context.sections?.current?.id ?? null}
|
||||
@@ -129,11 +131,9 @@ export function SpaceLayout(props: {
|
||||
|
||||
<React.Suspense fallback={null}>
|
||||
<SearchModal
|
||||
revisionId={context.revisionId}
|
||||
spaceTitle={siteSpace.title}
|
||||
withAsk={customization.aiSearch.enabled}
|
||||
isMultiVariants={isMultiVariants}
|
||||
pointer={getSitePointerFromContext(context)}
|
||||
/>
|
||||
</React.Suspense>
|
||||
</InsightsProvider>
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
export type StreamResponseChunk<T> = {
|
||||
iteratorResult: IteratorResult<T>;
|
||||
next?: Promise<StreamResponseChunk<T>>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Stream the response of an async generator function.
|
||||
* This function should be used to wrap an async generator server action.
|
||||
*
|
||||
* See https://github.com/vercel/next.js/discussions/51282
|
||||
*/
|
||||
export function streamResponse<T, P extends any[]>(
|
||||
createGenerator: (...args: P) => AsyncGenerator<T>
|
||||
) {
|
||||
return async (...args: Parameters<typeof createGenerator>) => {
|
||||
const generator = createGenerator(...args);
|
||||
return streamChunk<T>(generator);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over the response of an async generator function.
|
||||
*/
|
||||
export function iterateStreamResponse<T>(streamResponse: Promise<StreamResponseChunk<T>>) {
|
||||
return {
|
||||
[Symbol.asyncIterator]: () => ({
|
||||
current: streamResponse,
|
||||
async next() {
|
||||
const { iteratorResult, next } = await this.current;
|
||||
|
||||
if (next) this.current = next;
|
||||
else iteratorResult.done = true;
|
||||
|
||||
return iteratorResult;
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
async function streamChunk<T>(generator: AsyncGenerator<T>) {
|
||||
const next = generator.next();
|
||||
return new Promise<StreamResponseChunk<T>>((resolve, reject) => {
|
||||
next.then((res) => {
|
||||
if (res.done) resolve({ iteratorResult: res });
|
||||
else resolve({ iteratorResult: res, next: streamChunk(generator) });
|
||||
});
|
||||
next.catch((error) => reject(error));
|
||||
});
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
import type { GitBookDataFetcher } from '@v2/lib/data/types';
|
||||
import { createImageResizer } from '@v2/lib/images';
|
||||
import { createLinker } from '@v2/lib/links';
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
import {
|
||||
type SiteContentPointer,
|
||||
@@ -28,6 +27,7 @@ import {
|
||||
getSiteRedirectBySource,
|
||||
getSpace,
|
||||
getUserById,
|
||||
searchSiteContent,
|
||||
} from './api';
|
||||
import { getDynamicCustomizationSettings } from './customization';
|
||||
import { getBasePath, getHost } from './links';
|
||||
@@ -74,9 +74,20 @@ export async function getV1BaseContext(): Promise<GitBookBaseContext> {
|
||||
async function getDataFetcherV1(): Promise<GitBookDataFetcher> {
|
||||
const apiClient = await api();
|
||||
|
||||
return {
|
||||
const dataFetcher: GitBookDataFetcher = {
|
||||
apiEndpoint: apiClient.client.endpoint,
|
||||
|
||||
async api() {
|
||||
const result = await api();
|
||||
return result.client;
|
||||
},
|
||||
|
||||
withToken() {
|
||||
// In v1, the token is global and controlled by the middleware.
|
||||
// We don't need to do anything special here.
|
||||
return dataFetcher;
|
||||
},
|
||||
|
||||
getUserById(userId) {
|
||||
return getUserById(userId);
|
||||
},
|
||||
@@ -145,7 +156,15 @@ async function getDataFetcherV1(): Promise<GitBookDataFetcher> {
|
||||
getEmbedByUrl(params) {
|
||||
return getEmbedByUrlInSpace(params.spaceId, params.url);
|
||||
},
|
||||
|
||||
async searchSiteContent(params) {
|
||||
const { organizationId, siteId, query, cacheBust, scope } = params;
|
||||
const result = await searchSiteContent(organizationId, siteId, query, scope, cacheBust);
|
||||
return result.items;
|
||||
},
|
||||
};
|
||||
|
||||
return dataFetcher;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,7 +172,6 @@ async function getDataFetcherV1(): Promise<GitBookDataFetcher> {
|
||||
*/
|
||||
export async function fetchV1ContextForSitePointer(pointer: SiteContentPointer) {
|
||||
const baseContext = await getV1BaseContext();
|
||||
const headersList = await headers();
|
||||
|
||||
const context = await fetchSiteContextByIds(baseContext, {
|
||||
organization: pointer.organizationId,
|
||||
@@ -164,7 +182,6 @@ export async function fetchV1ContextForSitePointer(pointer: SiteContentPointer)
|
||||
shareKey: pointer.siteShareKey,
|
||||
changeRequest: pointer.changeRequestId,
|
||||
revision: pointer.revisionId,
|
||||
visitorAuthToken: headersList.get('x-gitbook-visitor-token'),
|
||||
});
|
||||
|
||||
context.customization = await getDynamicCustomizationSettings(context.customization);
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
|
||||
import { joinPath, normalizePathname } from '@/lib/paths';
|
||||
import { getProxyModeBasePath } from '@/lib/proxy';
|
||||
import { MiddlewareHeaders } from '@v2/lib/middleware';
|
||||
|
||||
export const config = {
|
||||
matcher:
|
||||
@@ -83,6 +84,8 @@ export type LookupResult = PublishedContentWithCache & {
|
||||
cookies?: LookupCookies;
|
||||
/** Visitor authentication token */
|
||||
visitorToken?: string;
|
||||
/** URL of the site */
|
||||
siteURL?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -199,6 +202,13 @@ export async function middleware(request: NextRequest) {
|
||||
if (resolved.shareKey) {
|
||||
headers.set('x-gitbook-content-site-share-key', resolved.shareKey);
|
||||
}
|
||||
|
||||
// For server actions that use v2 code
|
||||
headers.set(MiddlewareHeaders.SiteURLData, JSON.stringify(resolved));
|
||||
}
|
||||
|
||||
if (resolved.visitorToken) {
|
||||
headers.set(MiddlewareHeaders.VisitorAuthToken, resolved.visitorToken);
|
||||
}
|
||||
|
||||
// For tests, we make it possible to enable search indexation
|
||||
@@ -231,10 +241,6 @@ export async function middleware(request: NextRequest) {
|
||||
headers.set('x-gitbook-api', apiEndpoint);
|
||||
}
|
||||
|
||||
if (resolved.visitorToken) {
|
||||
headers.set('x-gitbook-visitor-token', resolved.visitorToken);
|
||||
}
|
||||
|
||||
const target = new URL(joinPath('/middleware', rewritePathname), request.nextUrl.toString());
|
||||
target.search = url.search;
|
||||
|
||||
@@ -761,14 +767,23 @@ async function lookupSiteByAPI(
|
||||
return null;
|
||||
});
|
||||
|
||||
return (
|
||||
result ?? {
|
||||
error: {
|
||||
code: 404,
|
||||
message: 'No content found',
|
||||
},
|
||||
if (result) {
|
||||
if ('site' in result) {
|
||||
return {
|
||||
...result,
|
||||
siteURL: `${lookupURL.origin}${result.basePath}`,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return {
|
||||
error: {
|
||||
code: 404,
|
||||
message: 'No content found',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user