Compare commits

...

12 Commits

Author SHA1 Message Date
Brett Jephson 140fc02e5c Merge branch 'main' into brett/rnd-13702 2026-11-09 11:42:41 +00:00
Peter White 6c128729b2 Expand 3-digit hex colours correctly (#4748)
Co-authored-by: monem <119044801+pucedoteth@users.noreply.github.com>
2026-11-09 11:29:12 +00:00
Brett Jephson fa8f0bccf0 Remove italic from annotation dfn 2026-11-09 11:16:34 +00:00
Nolann B. 6344646974 Fix bash highlighting of the last letter before > (RND-13792) (#4747) 2026-11-09 10:47:47 +01:00
Nolann B. 192863526f Remove docs.cartographer3d.com from customer visual tests (#4749) 2026-11-09 10:28:06 +01:00
Greg Bergé 09a8310c9d Serve robots.txt without sign-in on authenticated access sites (RND-13789) (#4746)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 13:52:58 +01:00
Peter White a023a7131b Warn visitors not to share sensitive personal info in the Assistant (#4745) 2026-11-05 17:48:53 +00:00
conico974 198cafe978 Bump Next.js to 16.4.5 and OpenNext to 1.20.16 (#4743)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 11:22:58 +01:00
conico974 268fa83683 Fix formatting in README.md (#4741) 2026-11-03 19:29:19 +01:00
conico974 123454d2ac Dummy PR (#4739)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 12:58:05 +01:00
Nolann B. e6b8502036 Resolve .md page URLs in the MCP getPage tool (RND-13634) (#4735) 2026-11-02 12:59:34 +01:00
Peter White 3837c484a6 Redirect non-normalized proxied URLs to their public URL (RND-13627) (#4734) 2026-11-02 10:18:10 +01:00
68 changed files with 321 additions and 56 deletions
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Warn visitors not to share sensitive personal info in the Assistant context popover.
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix bash code blocks colouring the last letter of a word before `>` differently, as in `<name>`.
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Bump Next.js to 16.4.5 and OpenNext (Cloudflare 1.20.16, AWS 4.1.15), keeping the dev-mode OOM patch.
+5
View File
@@ -0,0 +1,5 @@
---
"@gitbook/colors": patch
---
Expand 3-digit hex colors correctly in `hexToRgbArray`.
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Remove italic from dfn styling
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix the MCP `getPage` tool returning "Page not found" for the Markdown URL of a page (`<page>.md`).
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix encoded or non-normalized links on proxied sites redirecting to the internal proxy path instead of the public URL.
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Serve a disallow-all robots.txt instead of redirecting to sign-in on authenticated access sites.
+1 -1
View File
@@ -161,4 +161,4 @@ GitBook wouldn't be possible without these projects:
## Legacy GitBook (Deprecated)
Our previous version of GitBook and it's CLI tool are now deprecated. You can still view the old repository and it's commits on this [branch](https://github.com/GitbookIO/gitbook/tree/legacy).
Our previous version of GitBook and it's CLI tool are now deprecated. You can still view the old repository and it's commits on this [branch](https://github.com/GitbookIO/gitbook/tree/legacy).
+17 -16
View File
@@ -129,8 +129,8 @@
"@gitbook/react-openapi": "workspace:*",
"@mermaid-js/mermaid-zenuml": "^0.2.2",
"@modelcontextprotocol/sdk": "1.17.5",
"@opennextjs/aws": "4.1.11",
"@opennextjs/cloudflare": "1.20.12",
"@opennextjs/aws": "4.1.15",
"@opennextjs/cloudflare": "1.20.16",
"@panzoom/panzoom": "^4.6.1",
"@sindresorhus/fnv1a": "^3.1.0",
"@tailwindcss/container-queries": "^0.1.1",
@@ -160,7 +160,7 @@
"micromark-extension-gfm": "^3.0.0",
"motion": "^12.23.24",
"negotiator": "^1.0.0",
"next": "^16.4.3",
"next": "^16.4.5",
"next-themes": "^0.4.6",
"nuqs": "^2.2.3",
"object-hash": "^3.0.0",
@@ -342,8 +342,9 @@
},
"patchedDependencies": {
"decode-named-character-reference@1.0.2": "patches/decode-named-character-reference@1.0.2.patch",
"next@16.4.5": "patches/next@16.4.5.patch",
"@shikijs/langs@3.21.0": "patches/@shikijs%2Flangs@3.21.0.patch",
"mermaid@11.16.1": "patches/mermaid@11.16.1.patch",
"next@16.4.3": "patches/next@16.4.3.patch",
},
"overrides": {
"@codemirror/state": "6.4.1",
@@ -888,23 +889,23 @@
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.6", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" } }, "sha512-DXj75ewm11LIWUk198QSKUTxjyRjsBwk09MuMk5DGK+GDUtyPhhEHOGP/Xwwj3DjQXXkivoBirmOnKrLfc0+9g=="],
"@next/env": ["@next/env@16.4.3", "", {}, "sha512-a7tz1QCcWWkwC/rPJcPMJk/UNRu9CFhDd3OHYE5hddbJrs8BqRTalxI6GlKCWEGzRLHN+S/B7a4PAHCtm25aBQ=="],
"@next/env": ["@next/env@16.4.5", "", {}, "sha512-JwSPwpdkZL5VMl2jA/q+JI8MZdweAL3xmw5/tb9y/TLtmS0YXZnmYDzSihwk8xTj1OL+WM53gTqaj96yuLp6Xw=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.4.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4yq5EXBt+VDtAZwTyfijaFp5D2C5oz3Ikw5a2kiX2K9nw72vrZX2q5UuAlkPtUMO1tQz6JTKpaKWjS7OOefTDQ=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.4.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W5TniBzWKASbNNwHZ/S/f1MxQUXU/LdjklVMWA0Ujbfl7SZnUzFls45hO0i/sF7Bx1jJBWp7WWmoQajXtX/O8A=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.4.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-1Rvkn/LUhnEF8WxnRkOrqGhbwXui1bkWFxONKIbN+r7C+9KHDSZ+iUoj4CzymVMWUyZIyx9szcb9+kbKhD500A=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.4.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-uWq4qkHapBDS1oFltWS4YxTb9pLHAtLNcpbuK6SmRsebLYZqpBML/j/C0op+50JYGTpZgVL/DWNt/6oWlamrrw=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.4.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-dCKpXErGg3abUM6t5JTJV/ottVBTStJB5lRJTbgdUdQaQ+jNHiK56Hsc0YAEg/rinSh42hYG3HWiYmEMdilnxg=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-FdRcyOx5u5P51mlAqLB9EXdd8jZO5fsdHtk4BAQSkqOywCsbTNN/rmw+jO7pnmjNdAtu2AG46NzbCL2FPDrrgw=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.4.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-WwYbhWkhYBvBtib1K4W0kL8MW2snBD2A5b3SN4vPpqalZbZs5+bQ1yzAdepVoOqrFfEW3dmREy4Zs1MrWcrFbw=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-cyvmmgtRXR8aGVT86wi9Py53IlfyCx7P2fZxuAXAoTummDnhKtUyjrMeXfapqp4qgD+gT5RqyGD/VfA16jEK7Q=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.4.3", "", { "os": "linux", "cpu": "x64" }, "sha512-QymWvfUwDEC5/lpJOsxX5VAbyoR2D13+2Jsjz4be7gT4LPqEgdpkeLIR/HFfCLgKzoMhpJbp6I/JnogyNZHcaA=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-hPc+Fl+N22DAvqZHq559NmamXboOX5Lu8I5Wl0DezfrhTk1Zz21NQeYoZDmNI/NjTcrdsVxM72Fzn2lI+Q+vAw=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.4.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Vmjd9D7DpA4Vv3cUlr82QWgJD/BQZmns801vtC5VWHg6jCIA4a5vMKpQnfrM2pyCrp2eV0ENdUg4PlN3bAc2Yw=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-gbACYE8IP/jlprNTpiWXHb8N9CofOzpnEwH2SrIz1dUbiJf1FbDssDupCAPeP1Taxe8mgG20sGZamJI7KFOdZw=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.4.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-lG1pPdPLk7gTyGb00n5JIMLdBJgjb+UEhSdZvFbhUMg0DyC6z4B7RnpSKF9VCd4imkLObVbXOFA0kFCpw7yFow=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.4.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-+8gj++ZMo8H20Xh2ijjNqyFk5LPLw0AC4Fs4Tq/G94rjjQXhOjN4jgSWsWKPFsyY26hfIeemJIwKtMxOsp5GBA=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.4.3", "", { "os": "win32", "cpu": "x64" }, "sha512-NmQC9RkJdnPXRaMxtkCzobB3teVjqF33oSOnwDfi+XYGWWYpt+xlGK3mtktSKLbdjwaWbJu2OkkMiRFGUUncKg=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.4.5", "", { "os": "win32", "cpu": "x64" }, "sha512-qNZj66pYXnaWLiF9R9GUbfBdrCv7kC6TaZfIwB2KH5WPc/8pEM3nVOj8hpQhWJU8S94rlH5MPDcX15+RrgUhiw=="],
"@node-minify/core": ["@node-minify/core@9.0.2", "", { "dependencies": { "@node-minify/utils": "9.0.1", "glob": "10.3.3", "mkdirp": "3.0.1" } }, "sha512-FNhv29Wom6wKrrFKaeAfmZqz7TX5A1E6P+bpd0VIc+DYWMLUIhAViS8riaZg3A1oD0s06s+5BG2Fg7RqMKiKHw=="],
@@ -934,9 +935,9 @@
"@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
"@opennextjs/aws": ["@opennextjs/aws@4.1.11", "", { "dependencies": { "@ast-grep/napi": "^0.45.3", "@aws-sdk/client-cloudfront": "3.984.0", "@aws-sdk/client-dynamodb": "3.984.0", "@aws-sdk/client-lambda": "3.984.0", "@aws-sdk/client-s3": "3.984.0", "@aws-sdk/client-sqs": "3.984.0", "@node-minify/core": "^9.0.2", "@node-minify/terser": "^9.0.1", "@tsconfig/node18": "^18.2.7", "aws4fetch": "^1.0.20", "chalk": "^5.6.2", "cookie": "^1.1.1", "esbuild": "0.28.2", "express": "^5.2.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.1.0", "yaml": "^2.9.1" }, "peerDependencies": { "next": ">=15.5.28 <16 || >=16.4.1" }, "bin": { "open-next": "dist/index.js" } }, "sha512-cew0cUgGZF/Kr/C+Z1qvmAdKj4Va5cRho+aB6Ci/5ILd+QDdrR2LVaC/6Qm12KVF37vSLDK7lPDd/KdvvCoNEw=="],
"@opennextjs/aws": ["@opennextjs/aws@4.1.15", "", { "dependencies": { "@ast-grep/napi": "^0.45.3", "@aws-sdk/client-cloudfront": "3.984.0", "@aws-sdk/client-dynamodb": "3.984.0", "@aws-sdk/client-lambda": "3.984.0", "@aws-sdk/client-s3": "3.984.0", "@aws-sdk/client-sqs": "3.984.0", "@node-minify/core": "^9.0.2", "@node-minify/terser": "^9.0.1", "@tsconfig/node18": "^18.2.7", "aws4fetch": "^1.0.20", "chalk": "^5.6.2", "cookie": "^1.1.1", "esbuild": "0.28.2", "express": "^5.2.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.1.0", "yaml": "^2.9.1" }, "peerDependencies": { "diff": "^9.0.0", "next": ">=15.5.29 <16 || >=16.4.5" }, "optionalPeers": ["diff"], "bin": { "open-next": "dist/index.js" } }, "sha512-ulv7rFEAeieD0d/zDMnw4IFPU+zY/atVUhn7ezgFqIurLB5oBYeHCUGlcGk3Pg7uMPeI03SNRjHW1+xL9EwMPw=="],
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.20.12", "", { "dependencies": { "@ast-grep/napi": "^0.45.3", "@dotenvx/dotenvx": "^2.33.0", "@opennextjs/aws": "4.1.11", "ci-info": "^4.2.0", "cloudflare": "^4.4.1", "comment-json": "^4.5.1", "enquirer": "^2.4.1", "glob": "^13.0.6", "ts-tqdm": "^0.8.6", "yargs": "^18.2.0" }, "peerDependencies": { "next": ">=15.5.28 <16 || >=16.4.1", "rclone.js": "^0.6.6", "wrangler": "^4.148.0" }, "optionalPeers": ["rclone.js"], "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-ZU1j7hMuOFaa0RHT/wrPXQKEEqzkkoc3fHaHy0Xrx4K0zZdSXGQbwbSHCnbjPRGXblCuWo+ur6UUkl0o9kVhrg=="],
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.20.16", "", { "dependencies": { "@ast-grep/napi": "^0.45.3", "@dotenvx/dotenvx": "^2.33.0", "@opennextjs/aws": "4.1.15", "ci-info": "^4.2.0", "cloudflare": "^4.4.1", "comment-json": "^4.5.1", "enquirer": "^2.4.1", "glob": "^13.0.6", "ts-tqdm": "^0.8.6", "yargs": "^18.2.0" }, "peerDependencies": { "next": ">=15.5.29 <16 || >=16.4.5", "rclone.js": "^0.6.6", "wrangler": "^4.148.0" }, "optionalPeers": ["rclone.js"], "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-3c1Cwex5e2jXy4o4RZAusy8o9Uz4eowuPCjdVLYEZ/MnkAzN2jh9dOgsRjgu3wjigXuNe8rmPECmOTo+xKQHsw=="],
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
@@ -2636,7 +2637,7 @@
"netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="],
"next": ["next@16.4.3", "", { "dependencies": { "@next/env": "16.4.3", "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.5.23", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.4.3", "@next/swc-darwin-x64": "16.4.3", "@next/swc-linux-arm64-gnu": "16.4.3", "@next/swc-linux-arm64-musl": "16.4.3", "@next/swc-linux-x64-gnu": "16.4.3", "@next/swc-linux-x64-musl": "16.4.3", "@next/swc-win32-arm64-msvc": "16.4.3", "@next/swc-win32-x64-msvc": "16.4.3", "sharp": "^0.35.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "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-OqxKTGi9u80FydydsMNYNjnuje9Au+kvHlt2GZ//ftdpDHm1dxKqAaENoOOTX5wKzn+9vaKUNIfZeyZUuLhCnA=="],
"next": ["next@16.4.5", "", { "dependencies": { "@next/env": "16.4.5", "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.5.23", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.4.5", "@next/swc-darwin-x64": "16.4.5", "@next/swc-linux-arm64-gnu": "16.4.5", "@next/swc-linux-arm64-musl": "16.4.5", "@next/swc-linux-x64-gnu": "16.4.5", "@next/swc-linux-x64-musl": "16.4.5", "@next/swc-win32-arm64-msvc": "16.4.5", "@next/swc-win32-x64-msvc": "16.4.5", "sharp": "^0.35.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "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-rmdfsWosjTUO+6R0S3KokoIk6Kj0sWDRTe86tZHdqaWH/oXq/udX3/4xb+YQi7aWbyEFoglDU54kaifLCdXbBw=="],
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
+3 -2
View File
@@ -67,7 +67,8 @@
"patchedDependencies": {
"decode-named-character-reference@1.0.2": "patches/decode-named-character-reference@1.0.2.patch",
"@vercel/next@4.4.2": "patches/@vercel%2Fnext@4.4.2.patch",
"next@16.4.3": "patches/next@16.4.3.patch",
"mermaid@11.16.1": "patches/mermaid@11.16.1.patch"
"mermaid@11.16.1": "patches/mermaid@11.16.1.patch",
"next@16.4.5": "patches/next@16.4.5.patch",
"@shikijs/langs@3.21.0": "patches/@shikijs%2Flangs@3.21.0.patch"
}
}
+13 -1
View File
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'bun:test';
import { colorScale } from './transformations';
import { colorScale, hexToRgbArray } from './transformations';
describe('colorScale exact base', () => {
it('takes a very light tint as the exact background on step 1 (clean/bold)', () => {
@@ -67,3 +67,15 @@ describe('colorScale exact base', () => {
expect(scale[0]).not.toBe('#F5F3EF');
});
});
describe('hexToRgbArray', () => {
it('parses a 6-digit hex color', () => {
expect(hexToRgbArray('#1a2b3c')).toEqual([26, 43, 60]);
});
it('expands a 3-digit hex color', () => {
expect(hexToRgbArray('#abc')).toEqual([170, 187, 204]);
expect(hexToRgbArray('abc')).toEqual([170, 187, 204]);
expect(hexToRgbArray('#fff')).toEqual([255, 255, 255]);
});
});
+3 -1
View File
@@ -311,7 +311,9 @@ export function hexToRgbArray(hex: string): RGBColor {
const originalHex = hex;
let value = hex.replace('#', '');
if (hex.length === 3) value = value + value;
if (value.length === 3) {
value = [...value].map((digit) => digit + digit).join('');
}
const r = value.substring(0, 2);
const g = value.substring(2, 4);
-10
View File
@@ -283,11 +283,6 @@ const testCases: TestsCase[] = [
contentBaseURL: 'https://vimeo.com',
tests: [{ name: 'Home', url: '/legal' }],
},
{
name: 'help.aikido.dev',
contentBaseURL: 'https://help.aikido.dev',
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
},
{
name: 'doc.demarche.numerique.gouv.fr',
contentBaseURL: 'https://doc.demarche.numerique.gouv.fr',
@@ -589,11 +584,6 @@ const testCases: TestsCase[] = [
contentBaseURL: 'https://help.blotato.com',
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
},
{
name: 'docs.cartographer3d.com',
contentBaseURL: 'https://docs.cartographer3d.com',
tests: [{ name: 'Home', url: '/' }],
},
{
name: 'docs.acestudio.ai',
contentBaseURL: 'https://docs.acestudio.ai',
+3 -3
View File
@@ -20,8 +20,8 @@
"@gitbook/react-openapi": "workspace:*",
"@mermaid-js/mermaid-zenuml": "^0.2.2",
"@modelcontextprotocol/sdk": "1.17.5",
"@opennextjs/aws": "4.1.11",
"@opennextjs/cloudflare": "1.20.12",
"@opennextjs/aws": "4.1.15",
"@opennextjs/cloudflare": "1.20.16",
"@panzoom/panzoom": "^4.6.1",
"@sindresorhus/fnv1a": "^3.1.0",
"@tailwindcss/container-queries": "^0.1.1",
@@ -51,7 +51,7 @@
"micromark-extension-gfm": "^3.0.0",
"motion": "^12.23.24",
"negotiator": "^1.0.0",
"next": "^16.4.3",
"next": "^16.4.5",
"next-themes": "^0.4.6",
"nuqs": "^2.2.3",
"object-hash": "^3.0.0",
@@ -146,6 +146,10 @@ export function AIChatInput(props: {
<Icon icon="message-question" className="size-3.5 opacity-7" />
{t(language, 'ai_chat_context_previous_messages')}
</li>
<li className="flex items-center gap-2">
<Icon icon="lock" className="size-3.5 opacity-7" />
{t(language, 'ai_chat_context_sensitive_info')}
</li>
</ul>
<p>{t(language, 'ai_chat_context_disclaimer')}</p>
</div>
@@ -19,7 +19,7 @@ export function AnnotationPopover(props: { children: React.ReactNode; body: Reac
>
<dfn
data-annotation=""
className="cursor-help underline decoration-dotted decoration-1 underline-offset-2"
className="not-italic cursor-help underline decoration-dotted decoration-1 underline-offset-2"
// oxlint-disable-next-line jsx-a11y/no-noninteractive-tabindex -- we want to be able to focus the definition to open the tooltip
tabIndex={0}
>
@@ -709,6 +709,19 @@ function joinLineContent(line: HighlightLine): string {
return visit(line.tokens);
}
it('keeps the last letter of a bash word before `>` in the same token', async () => {
const tokenContents = async (code: string) => {
const lines = await highlightWithInlines(singleLineBlock('bash', code));
return lines[0]!.tokens.map((t) => (t.type === 'shiki' ? t.token.content : ''));
};
const placeholder = await tokenContents('cli create --name <resource name>');
expect(placeholder.find((c) => c.includes('nam'))).toContain('name');
const fdRedirect = await tokenContents('cmd 2>&1');
expect(fdRedirect.find((c) => c.includes('2'))).toContain('2>&1');
});
function singleLineBlock(syntax: string | undefined, text: string): DocumentBlockCode {
return {
object: 'block',
@@ -137,6 +137,7 @@ export const ar: TranslationLanguage = {
ai_chat_context_pages_youve_read: 'الصفحة التي أنت عليها الآن',
ai_chat_context_info_provided_by_the_site: 'معلومات عنك يوفرها الموقع',
ai_chat_context_previous_messages: 'الرسائل في محادثتك',
ai_chat_context_sensitive_info: 'لا تشارك معلومات شخصية حساسة',
ai_chat_context_disclaimer: 'قد تحتوي إجابات الذكاء الاصطناعي على أخطاء.',
ai_chat_input_placeholder: 'اسأل أو ابحث أو اشرح...',
ai_chat_queued_message: 'سيتم إرسال هذه الرسالة بعد أن ينتهي ${1}',
@@ -141,6 +141,7 @@ export const bg: TranslationLanguage = {
ai_chat_context_pages_youve_read: 'Страницата, на която сте в момента',
ai_chat_context_info_provided_by_the_site: 'Информация за вас, предоставена от сайта',
ai_chat_context_previous_messages: 'Съобщения във вашия разговор',
ai_chat_context_sensitive_info: 'Не споделяйте чувствителна лична информация',
ai_chat_context_disclaimer: 'AI отговорите може да съдържат грешки.',
ai_chat_input_placeholder: 'Попитайте, потърсете или обяснете...',
ai_chat_queued_message: 'Това съобщение ще бъде изпратено, след като ${1} приключи',
@@ -139,6 +139,7 @@ export const cs: TranslationLanguage = {
ai_chat_context_pages_youve_read: 'Stránka, na které se právě nacházíte',
ai_chat_context_info_provided_by_the_site: 'Informace o vás poskytnuté webem',
ai_chat_context_previous_messages: 'Zprávy ve vaší konverzaci',
ai_chat_context_sensitive_info: 'Nesdílejte citlivé osobní údaje',
ai_chat_context_disclaimer: 'Odpovědi AI mohou obsahovat chyby.',
ai_chat_input_placeholder: 'Zeptejte se, hledejte nebo vysvětlete...',
ai_chat_queued_message: 'Tato zpráva se odešle, až ${1} dokončí odpověď',
@@ -138,6 +138,7 @@ export const da: TranslationLanguage = {
ai_chat_context_pages_youve_read: 'Siden du er på nu',
ai_chat_context_info_provided_by_the_site: 'Oplysninger om dig fra webstedet',
ai_chat_context_previous_messages: 'Beskeder i din samtale',
ai_chat_context_sensitive_info: 'Del ikke følsomme personoplysninger',
ai_chat_context_disclaimer: 'AI-svar kan indeholde fejl.',
ai_chat_input_placeholder: 'Spørg, søg eller forklar...',
ai_chat_queued_message: 'Denne besked sendes, når ${1} er færdig',
@@ -145,6 +145,7 @@ export const de: TranslationLanguage = {
ai_chat_context_info_provided_by_the_site:
'Von der Website bereitgestellte Informationen über Sie',
ai_chat_context_previous_messages: 'Nachrichten in Ihrem Gespräch',
ai_chat_context_sensitive_info: 'Teilen Sie keine sensiblen persönlichen Daten',
ai_chat_context_disclaimer: 'KI-Antworten können Fehler enthalten.',
ai_chat_input_placeholder: 'Fragen, suchen oder erklären...',
ai_chat_queued_message: 'Diese Nachricht wird gesendet, sobald ${1} fertig ist',
@@ -143,6 +143,7 @@ export const el: TranslationLanguage = {
ai_chat_context_info_provided_by_the_site:
'Πληροφορίες για εσάς που παρέχονται από τον ιστότοπο',
ai_chat_context_previous_messages: 'Μηνύματα στη συνομιλία σας',
ai_chat_context_sensitive_info: 'Μην κοινοποιείτε ευαίσθητες προσωπικές πληροφορίες',
ai_chat_context_disclaimer: 'Οι απαντήσεις AI μπορεί να περιέχουν λάθη.',
ai_chat_input_placeholder: 'Ρωτήστε, αναζητήστε ή εξηγήστε...',
ai_chat_queued_message: 'Αυτό το μήνυμα θα σταλεί μόλις ολοκληρώσει ${1}',
@@ -137,6 +137,7 @@ export const en = {
ai_chat_context_pages_youve_read: "The page you're on now",
ai_chat_context_info_provided_by_the_site: 'Info about you provided by the site',
ai_chat_context_previous_messages: 'Messages in your conversation',
ai_chat_context_sensitive_info: "Don't share sensitive personal info",
ai_chat_context_disclaimer: 'AI responses may contain mistakes.',
ai_chat_input_placeholder: 'Ask, search, or explain...',
ai_chat_queued_message: 'This message will be sent after ${1} finishes',
@@ -143,6 +143,7 @@ export const es: TranslationLanguage = {
ai_chat_context_pages_youve_read: 'La página en la que estás ahora',
ai_chat_context_info_provided_by_the_site: 'Información sobre ti proporcionada por el sitio',
ai_chat_context_previous_messages: 'Mensajes de tu conversación',
ai_chat_context_sensitive_info: 'No compartas información personal confidencial',
ai_chat_context_disclaimer: 'Las respuestas de IA pueden contener errores.',
ai_chat_input_placeholder: 'Pregunta, busca o explica...',
ai_chat_queued_message: 'Este mensaje se enviará cuando ${1} termine',
@@ -139,6 +139,7 @@ export const et: TranslationLanguage = {
ai_chat_context_pages_youve_read: 'Leht, kus te praegu asute',
ai_chat_context_info_provided_by_the_site: 'Saidi pakutud teave teie kohta',
ai_chat_context_previous_messages: 'Teie vestluse sõnumid',
ai_chat_context_sensitive_info: 'Ärge jagage tundlikku isikuteavet',
ai_chat_context_disclaimer: 'AI vastused võivad sisaldada vigu.',
ai_chat_input_placeholder: 'Küsige, otsige või selgitage...',
ai_chat_queued_message: 'See sõnum saadetakse, kui ${1} on lõpetanud',
@@ -140,6 +140,7 @@ export const fi: TranslationLanguage = {
ai_chat_context_pages_youve_read: 'Sivu, jolla olet nyt',
ai_chat_context_info_provided_by_the_site: 'Sivuston tarjoamat tiedot sinusta',
ai_chat_context_previous_messages: 'Viestit keskustelussasi',
ai_chat_context_sensitive_info: 'Älä jaa arkaluonteisia henkilötietoja',
ai_chat_context_disclaimer: 'AI-vastaukset voivat sisältää virheitä.',
ai_chat_input_placeholder: 'Kysy, hae tai selitä...',
ai_chat_queued_message: 'Tämä viesti lähetetään, kun ${1} on valmis',
@@ -139,6 +139,7 @@ export const fr: TranslationLanguage = {
ai_chat_context_pages_youve_read: 'La page sur laquelle vous êtes actuellement',
ai_chat_context_info_provided_by_the_site: 'Informations vous concernant fournies par le site',
ai_chat_context_previous_messages: 'Messages de votre conversation',
ai_chat_context_sensitive_info: 'Ne partagez pas d’informations personnelles sensibles',
ai_chat_context_disclaimer: 'Les réponses générées peuvent contenir des erreurs.',
ai_chat_input_placeholder: 'Demander, rechercher ou expliquer...',
ai_chat_queued_message: 'Ce message sera envoyé une fois que ${1} aura terminé',
@@ -136,6 +136,7 @@ export const he: TranslationLanguage = {
ai_chat_context_pages_youve_read: 'הדף שאתה נמצא בו כעת',
ai_chat_context_info_provided_by_the_site: 'מידע עליך שסופק על ידי האתר',
ai_chat_context_previous_messages: 'הודעות בשיחה שלך',
ai_chat_context_sensitive_info: 'אל תשתפו מידע אישי רגיש',
ai_chat_context_disclaimer: 'תשובות AI עשויות להכיל טעויות.',
ai_chat_input_placeholder: 'שאל, חפש או הסבר...',
ai_chat_queued_message: 'הודעה זו תישלח לאחר ש-${1} יסיים',
@@ -138,6 +138,7 @@ export const hi: TranslationLanguage = {
ai_chat_context_pages_youve_read: 'वह पृष्ठ जिस पर आप अभी हैं',
ai_chat_context_info_provided_by_the_site: 'साइट द्वारा प्रदान की गई आपके बारे में जानकारी',
ai_chat_context_previous_messages: 'आपकी बातचीत के संदेश',
ai_chat_context_sensitive_info: 'संवेदनशील व्यक्तिगत जानकारी साझा न करें',
ai_chat_context_disclaimer: 'AI उत्तरों में गलतियां हो सकती हैं।',
ai_chat_input_placeholder: 'पूछें, खोजें या समझाएं...',
ai_chat_queued_message: 'यह संदेश ${1} के समाप्त होने के बाद भेजा जाएगा',
@@ -139,6 +139,7 @@ export const hr: TranslationLanguage = {
ai_chat_context_pages_youve_read: 'Stranica na kojoj se trenutno nalazite',
ai_chat_context_info_provided_by_the_site: 'Informacije o vama koje pruža web-mjesto',
ai_chat_context_previous_messages: 'Poruke u vašem razgovoru',
ai_chat_context_sensitive_info: 'Ne dijelite osjetljive osobne podatke',
ai_chat_context_disclaimer: 'AI odgovori mogu sadržavati pogreške.',
ai_chat_input_placeholder: 'Pitajte, pretražite ili objasnite...',
ai_chat_queued_message: 'Ova poruka bit će poslana kada ${1} završi',
@@ -141,6 +141,7 @@ export const hu: TranslationLanguage = {
ai_chat_context_info_provided_by_the_site:
'Az Önről szóló, a webhely által megadott információk',
ai_chat_context_previous_messages: 'Üzenetek a beszélgetésében',
ai_chat_context_sensitive_info: 'Ne osszon meg érzékeny személyes adatokat',
ai_chat_context_disclaimer: 'Az AI-válaszok hibákat tartalmazhatnak.',
ai_chat_input_placeholder: 'Kérdezzen, keressen vagy magyarázzon...',
ai_chat_queued_message: 'Ez az üzenet akkor lesz elküldve, amikor ${1} befejezi',
@@ -138,6 +138,7 @@ export const id: TranslationLanguage = {
ai_chat_context_pages_youve_read: 'Halaman tempat Anda berada sekarang',
ai_chat_context_info_provided_by_the_site: 'Info tentang Anda yang disediakan oleh situs',
ai_chat_context_previous_messages: 'Pesan dalam percakapan Anda',
ai_chat_context_sensitive_info: 'Jangan bagikan informasi pribadi yang sensitif',
ai_chat_context_disclaimer: 'Jawaban AI dapat berisi kesalahan.',
ai_chat_input_placeholder: 'Tanya, cari, atau jelaskan...',
ai_chat_queued_message: 'Pesan ini akan dikirim setelah ${1} selesai',
@@ -142,6 +142,7 @@ export const it: TranslationLanguage = {
ai_chat_context_pages_youve_read: 'La pagina su cui ti trovi ora',
ai_chat_context_info_provided_by_the_site: 'Informazioni su di te fornite dal sito',
ai_chat_context_previous_messages: 'Messaggi nella tua conversazione',
ai_chat_context_sensitive_info: 'Non condividere informazioni personali sensibili',
ai_chat_context_disclaimer: "Le risposte dell'IA possono contenere errori.",
ai_chat_input_placeholder: 'Chiedi, cerca o spiega...',
ai_chat_queued_message: 'Questo messaggio verrà inviato quando ${1} avrà finito',
@@ -139,6 +139,7 @@ export const ja: TranslationLanguage = {
ai_chat_context_pages_youve_read: '現在表示中のページ',
ai_chat_context_info_provided_by_the_site: 'サイトから提供されるあなたに関する情報',
ai_chat_context_previous_messages: '会話内のメッセージ',
ai_chat_context_sensitive_info: '機密性の高い個人情報は共有しないでください',
ai_chat_context_disclaimer: 'AIの回答には誤りが含まれる場合があります。',
ai_chat_input_placeholder: '質問、検索、または説明...',
ai_chat_queued_message: '${1}の応答が完了したら、このメッセージが送信されます',
@@ -139,6 +139,7 @@ export const ko: TranslationLanguage = {
ai_chat_context_pages_youve_read: '현재 보고 있는 페이지',
ai_chat_context_info_provided_by_the_site: '사이트에서 제공하는 회원님에 대한 정보',
ai_chat_context_previous_messages: '대화의 메시지',
ai_chat_context_sensitive_info: '민감한 개인 정보를 공유하지 마세요',
ai_chat_context_disclaimer: 'AI 응답에는 오류가 있을 수 있습니다.',
ai_chat_input_placeholder: '질문, 검색 또는 설명...',
ai_chat_queued_message: '${1}의 응답이 끝나면 이 메시지가 전송됩니다',
@@ -139,6 +139,7 @@ export const lt: TranslationLanguage = {
ai_chat_context_pages_youve_read: 'Puslapis, kuriame šiuo metu esate',
ai_chat_context_info_provided_by_the_site: 'Svetainės pateikta informacija apie jus',
ai_chat_context_previous_messages: 'Pranešimai jūsų pokalbyje',
ai_chat_context_sensitive_info: 'Nebendrinkite jautrios asmeninės informacijos',
ai_chat_context_disclaimer: 'AI atsakymuose gali būti klaidų.',
ai_chat_input_placeholder: 'Klauskite, ieškokite arba paaiškinkite...',
ai_chat_queued_message: 'Ši žinutė bus išsiųsta, kai ${1} baigs',
@@ -138,6 +138,7 @@ export const lv: TranslationLanguage = {
ai_chat_context_pages_youve_read: 'Lapa, kurā pašlaik atrodaties',
ai_chat_context_info_provided_by_the_site: 'Vietnes sniegtā informācija par jums',
ai_chat_context_previous_messages: 'Ziņojumi jūsų sarunā',
ai_chat_context_sensitive_info: 'Nekopīgojiet sensitīvu personisko informāciju',
ai_chat_context_disclaimer: 'AI atbildēs var būt kļūdas.',
ai_chat_input_placeholder: 'Jautājiet, meklējiet vai skaidrojiet...',
ai_chat_queued_message: 'Šī ziņa tiks nosūtīta, kad ${1} būs pabeidzis',
@@ -138,6 +138,7 @@ export const ms: TranslationLanguage = {
ai_chat_context_pages_youve_read: 'Halaman yang anda berada sekarang',
ai_chat_context_info_provided_by_the_site: 'Maklumat tentang anda yang disediakan oleh laman',
ai_chat_context_previous_messages: 'Mesej dalam perbualan anda',
ai_chat_context_sensitive_info: 'Jangan kongsi maklumat peribadi yang sensitif',
ai_chat_context_disclaimer: 'Jawapan AI mungkin mengandungi kesilapan.',
ai_chat_input_placeholder: 'Tanya, cari atau jelaskan...',
ai_chat_queued_message: 'Mesej ini akan dihantar setelah ${1} selesai',
@@ -141,6 +141,7 @@ export const nl: TranslationLanguage = {
ai_chat_context_pages_youve_read: 'De pagina waar je nu op bent',
ai_chat_context_info_provided_by_the_site: 'Informatie over jou verstrekt door de site',
ai_chat_context_previous_messages: 'Berichten in je gesprek',
ai_chat_context_sensitive_info: 'Deel geen gevoelige persoonlijke gegevens',
ai_chat_context_disclaimer: 'AI-antwoorden kunnen fouten bevatten.',
ai_chat_input_placeholder: 'Vraag, zoek of leg uit...',
ai_chat_queued_message: 'Dit bericht wordt verzonden zodra ${1} klaar is',
@@ -141,6 +141,7 @@ export const no: TranslationLanguage = {
ai_chat_context_pages_youve_read: 'Siden du er på nå',
ai_chat_context_info_provided_by_the_site: 'Informasjon om deg gitt av nettstedet',
ai_chat_context_previous_messages: 'Meldinger i samtalen din',
ai_chat_context_sensitive_info: 'Ikke del sensitive personopplysninger',
ai_chat_context_disclaimer: 'AI-svar kan inneholde feil.',
ai_chat_input_placeholder: 'Spør, søk eller forklar...',
ai_chat_queued_message: 'Denne meldingen sendes når ${1} er ferdig',
@@ -140,6 +140,7 @@ export const pl: TranslationLanguage = {
ai_chat_context_pages_youve_read: 'Strona, na której jesteś teraz',
ai_chat_context_info_provided_by_the_site: 'Informacje o Tobie podane przez stronę',
ai_chat_context_previous_messages: 'Wiadomości w Twojej rozmowie',
ai_chat_context_sensitive_info: 'Nie udostępniaj poufnych danych osobowych',
ai_chat_context_disclaimer: 'Odpowiedzi AI mogą zawierać błędy.',
ai_chat_input_placeholder: 'Zapytaj, wyszukaj lub wyjaśnij...',
ai_chat_queued_message: 'Ta wiadomość zostanie wysłana, gdy ${1} zakończy',
@@ -143,6 +143,7 @@ export const pt_br: TranslationLanguage = {
ai_chat_context_pages_youve_read: 'A página em que você está agora',
ai_chat_context_info_provided_by_the_site: 'Informações sobre você fornecidas pelo site',
ai_chat_context_previous_messages: 'Mensagens na sua conversa',
ai_chat_context_sensitive_info: 'Não compartilhe informações pessoais sensíveis',
ai_chat_context_disclaimer: 'Respostas de IA podem conter erros.',
ai_chat_input_placeholder: 'Pergunte, pesquise ou explique...',
ai_chat_queued_message: 'Esta mensagem será enviada quando ${1} terminar',
@@ -140,6 +140,7 @@ export const pt: TranslationLanguage = {
ai_chat_context_pages_youve_read: 'A página em que se encontra agora',
ai_chat_context_info_provided_by_the_site: 'Informações sobre si fornecidas pelo site',
ai_chat_context_previous_messages: 'Mensagens na sua conversa',
ai_chat_context_sensitive_info: 'Não partilhe informações pessoais sensíveis',
ai_chat_context_disclaimer: 'As respostas de IA podem conter erros.',
ai_chat_input_placeholder: 'Pergunte, pesquise ou explique...',
ai_chat_queued_message: 'Esta mensagem será enviada quando ${1} terminar',
@@ -142,6 +142,7 @@ export const ro: TranslationLanguage = {
ai_chat_context_pages_youve_read: 'Pagina pe care te afli acum',
ai_chat_context_info_provided_by_the_site: 'Informații despre tine furnizate de site',
ai_chat_context_previous_messages: 'Mesaje din conversația ta',
ai_chat_context_sensitive_info: 'Nu partajați informații personale sensibile',
ai_chat_context_disclaimer: 'Răspunsurile AI pot conține greșeli.',
ai_chat_input_placeholder: 'Întreabă, caută sau explică...',
ai_chat_queued_message: 'Acest mesaj va fi trimis după ce ${1} termină',
@@ -142,6 +142,7 @@ export const ru: TranslationLanguage = {
ai_chat_context_pages_youve_read: 'Страница, на которой вы сейчас находитесь',
ai_chat_context_info_provided_by_the_site: 'Информация о вас, предоставленная сайтом',
ai_chat_context_previous_messages: 'Сообщения в вашем разговоре',
ai_chat_context_sensitive_info: 'Не передавайте конфиденциальную личную информацию',
ai_chat_context_disclaimer: 'Ответы ИИ могут содержать ошибки.',
ai_chat_input_placeholder: 'Спросите, найдите или объясните...',
ai_chat_queued_message: 'Это сообщение будет отправлено, когда ${1} закончит',
@@ -141,6 +141,7 @@ export const sk: TranslationLanguage = {
ai_chat_context_pages_youve_read: 'Stránka, na ktorej sa práve nachádzate',
ai_chat_context_info_provided_by_the_site: 'Informácie o vás poskytnuté stránkou',
ai_chat_context_previous_messages: 'Správy vo vašej konverzácii',
ai_chat_context_sensitive_info: 'Nezdieľajte citlivé osobné údaje',
ai_chat_context_disclaimer: 'Odpovede AI môžu obsahovať chyby.',
ai_chat_input_placeholder: 'Pýtajte sa, hľadajte alebo vysvetľujte...',
ai_chat_queued_message: 'Táto správa sa odošle, keď ${1} skončí',
@@ -140,6 +140,7 @@ export const sl: TranslationLanguage = {
ai_chat_context_pages_youve_read: 'Stran, na kateri se trenutno nahajate',
ai_chat_context_info_provided_by_the_site: 'Informacije o vas, ki jih zagotavlja spletno mesto',
ai_chat_context_previous_messages: 'Sporočila v vašem pogovoru',
ai_chat_context_sensitive_info: 'Ne delite občutljivih osebnih podatkov',
ai_chat_context_disclaimer: 'Odgovori AI lahko vsebujejo napake.',
ai_chat_input_placeholder: 'Vprašajte, iščite ali razložite...',
ai_chat_queued_message: 'To sporočilo bo poslano, ko ${1} zaključi',
@@ -140,6 +140,7 @@ export const sv: TranslationLanguage = {
ai_chat_context_pages_youve_read: 'Sidan du är på nu',
ai_chat_context_info_provided_by_the_site: 'Information om dig från webbplatsen',
ai_chat_context_previous_messages: 'Meddelanden i din konversation',
ai_chat_context_sensitive_info: 'Dela inte känsliga personuppgifter',
ai_chat_context_disclaimer: 'AI-svar kan innehålla fel.',
ai_chat_input_placeholder: 'Fråga, sök eller förklara...',
ai_chat_queued_message: 'Det här meddelandet skickas när ${1} är klar',
@@ -135,6 +135,7 @@ export const th: TranslationLanguage = {
ai_chat_context_pages_youve_read: 'หน้าที่คุณอยู่ตอนนี้',
ai_chat_context_info_provided_by_the_site: 'ข้อมูลเกี่ยวกับคุณที่เว็บไซต์ให้ไว้',
ai_chat_context_previous_messages: 'ข้อความในการสนทนาของคุณ',
ai_chat_context_sensitive_info: 'โปรดอย่าแชร์ข้อมูลส่วนบุคคลที่ละเอียดอ่อน',
ai_chat_context_disclaimer: 'คำตอบจาก AI อาจมีข้อผิดพลาด',
ai_chat_input_placeholder: 'ถาม ค้นหา หรืออธิบาย...',
ai_chat_queued_message: 'ข้อความนี้จะถูกส่งหลังจากที่ ${1} เสร็จสิ้น',
@@ -138,6 +138,7 @@ export const tr: TranslationLanguage = {
ai_chat_context_pages_youve_read: 'Şu anda bulunduğunuz sayfa',
ai_chat_context_info_provided_by_the_site: 'Site tarafından sağlanan sizinle ilgili bilgiler',
ai_chat_context_previous_messages: 'Konuşmanızdaki mesajlar',
ai_chat_context_sensitive_info: 'Hassas kişisel bilgileri paylaşmayın',
ai_chat_context_disclaimer: 'AI yanıtları hata içerebilir.',
ai_chat_input_placeholder: 'Sor, ara veya açıkla...',
ai_chat_queued_message: 'Bu mesaj, ${1} yanıtını tamamladıktan sonra gönderilecek',
@@ -138,6 +138,7 @@ export const uk: TranslationLanguage = {
ai_chat_context_pages_youve_read: 'Сторінка, на якій ви зараз перебуваєте',
ai_chat_context_info_provided_by_the_site: 'Інформація про вас, надана сайтом',
ai_chat_context_previous_messages: 'Повідомлення у вашій розмові',
ai_chat_context_sensitive_info: 'Не діліться конфіденційною особистою інформацією',
ai_chat_context_disclaimer: 'Відповіді AI можуть містити помилки.',
ai_chat_input_placeholder: 'Запитайте, знайдіть або поясніть...',
ai_chat_queued_message: 'Це повідомлення буде надіслано, коли ${1} завершить',
@@ -138,6 +138,7 @@ export const vi: TranslationLanguage = {
ai_chat_context_pages_youve_read: 'Trang bạn đang ở hiện tại',
ai_chat_context_info_provided_by_the_site: 'Thông tin về bạn do trang web cung cấp',
ai_chat_context_previous_messages: 'Tin nhắn trong cuộc trò chuyện của bạn',
ai_chat_context_sensitive_info: 'Không chia sẻ thông tin cá nhân nhạy cảm',
ai_chat_context_disclaimer: 'Câu trả lời AI có thể chứa lỗi.',
ai_chat_input_placeholder: 'Hỏi, tìm kiếm hoặc giải thích...',
ai_chat_queued_message: 'Tin nhắn này sẽ được gửi sau khi ${1} hoàn tất',
@@ -134,6 +134,7 @@ export const yue: TranslationLanguage = {
ai_chat_context_pages_youve_read: '你而家喺嘅頁面',
ai_chat_context_info_provided_by_the_site: '網站提供嘅關於你嘅資訊',
ai_chat_context_previous_messages: '你對話入面嘅訊息',
ai_chat_context_sensitive_info: '唔好分享敏感個人資料',
ai_chat_context_disclaimer: 'AI 回答可能有錯。',
ai_chat_input_placeholder: '發問、搜尋或說明...',
ai_chat_queued_message: '呢個訊息會喺 ${1} 完成後發送',
@@ -134,6 +134,7 @@ export const zh_tw: TranslationLanguage = {
ai_chat_context_pages_youve_read: '您目前所在的頁面',
ai_chat_context_info_provided_by_the_site: '網站提供的關於您的資訊',
ai_chat_context_previous_messages: '您對話中的訊息',
ai_chat_context_sensitive_info: '請勿分享敏感個人資料',
ai_chat_context_disclaimer: 'AI 回答可能包含錯誤。',
ai_chat_input_placeholder: '提問、搜尋或說明...',
ai_chat_queued_message: '這則訊息將在 ${1} 完成後傳送',
@@ -135,6 +135,7 @@ export const zh: TranslationLanguage = {
ai_chat_context_pages_youve_read: '您当前所在的页面',
ai_chat_context_info_provided_by_the_site: '网站提供的关于您的信息',
ai_chat_context_previous_messages: '您对话中的消息',
ai_chat_context_sensitive_info: '请勿分享敏感个人信息',
ai_chat_context_disclaimer: '人工智能的回答可能包含错误。',
ai_chat_input_placeholder: '询问、搜索或解释...',
ai_chat_queued_message: '此消息将在 ${1} 完成后发送',
+46 -1
View File
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'bun:test';
import { getURLLookup, getURLLookupPathname, normalizeURL } from './urls';
import { getURLLookup, getURLLookupPathname, normalizeRequestURL, normalizeURL } from './urls';
describe('getURLLookupPathname', () => {
const previewRoot = 'https://sites.gitbook.com/preview/site_example/section';
@@ -162,6 +162,51 @@ describe('getURLLookup', () => {
});
});
describe('normalizeRequestURL', () => {
it('does not redirect a normalized URL', () => {
const url = new URL('https://docs.mycompany.com/hello');
expect(normalizeRequestURL(url, 'url-host')).toEqual({ isNormalized: true });
});
it('redirects a non-normalized URL to the normalized one', () => {
const result = normalizeRequestURL(
new URL('https://docs.mycompany.com/hello/tes%74'),
'url-host'
);
expect(result).toBeInstanceOf(Response);
const response = result as Response;
expect(response.status).toBe(302);
expect(response.headers.get('location')).toBe('https://docs.mycompany.com/hello/test');
});
it('does not redirect a non-normalized proxied URL to the internal proxy URL', () => {
const url = new URL(
'https://proxy.gitbook.site/sites/site_abc/getting-started/quickstar%74'
);
expect(normalizeRequestURL(url, 'url-host')).toEqual({ isNormalized: false });
});
it('redirects a non-normalized proxied URL in url mode', () => {
const result = normalizeRequestURL(
new URL('https://proxy.gitbook.site/sites/site_abc/quickstar%74'),
'url'
);
expect(result).toBeInstanceOf(Response);
expect((result as Response).headers.get('location')).toBe(
'https://proxy.gitbook.site/sites/site_abc/quickstart'
);
});
it.each([
'https://docs.mycompany.com/hello/%ZZ',
'https://proxy.gitbook.site/sites/site_abc/%ZZ',
])('returns a 400 for an invalid URL %s', (url) => {
const result = normalizeRequestURL(new URL(url), 'url-host');
expect(result).toBeInstanceOf(Response);
expect((result as Response).status).toBe(400);
});
});
describe('normalizeURL', () => {
it('should remove trailing slashes', () => {
expect(normalizeURL(new URL('https://docs.mycompany.com/hello/'))).toEqual(
+17 -10
View File
@@ -1,5 +1,5 @@
import { joinPath, removeTrailingSlash } from '../paths';
import { isProxyRootRequest } from '../proxy';
import { isProxyRequest, isProxyRootRequest } from '../proxy';
import { DataFetcherError, getExposableError } from './errors';
/**
@@ -105,21 +105,28 @@ export function getURLLookupPathname(
}
/**
* Normalize the URL in a request and redirect if the normalized URL is different from the original one.
* Normalize the URL in a request before the lookup. Returns an error response if it is invalid,
* or a redirect to the normalized URL if it can be built from the request URL.
*/
export function normalizeRequestURL(url: URL): Response | null {
export function normalizeRequestURL(
url: URL,
mode: 'url' | 'url-host'
): Response | { isNormalized: boolean } {
let normalizedURL: URL;
try {
const normalizedURL = normalizeURL(url);
if (normalizedURL.toString() !== url.toString()) {
return Response.redirect(normalizedURL.toString(), 302);
}
return null;
normalizedURL = normalizeURL(url);
} catch (error) {
const sanitized = getExposableError(error);
return new Response(sanitized.message, { status: sanitized.code });
}
const isNormalized = normalizedURL.toString() === url.toString();
// A proxied site's public URL is only known after the lookup, which redirects to the canonical URL.
if (!isNormalized && !(mode !== 'url' && isProxyRequest(url))) {
return Response.redirect(normalizedURL.toString(), 302);
}
return { isNormalized };
}
/**
+10
View File
@@ -50,6 +50,16 @@ describe('extractPagePath', () => {
it('returns empty string when URL equals root base URL', () => {
expect(extractPagePath('https://docs.example.com/', 'https://docs.example.com/')).toBe('');
});
it('extracts path from the markdown URL of a page', () => {
expect(
extractPagePath('https://docs.example.com/api/guides/installation.md', baseURL)
).toBe('guides/installation');
expect(extractPagePath('https://docs.example.com/api/.md', baseURL)).toBe('');
expect(extractPagePath('https://docs.example.com/api/notes.md.html', baseURL)).toBe(
'notes.md.html'
);
});
});
describe('resolveFirstDocument', () => {
+2 -1
View File
@@ -412,7 +412,8 @@ function flattenPages(
* Returns the path segment after the base, or undefined if the URL doesn't match.
*/
export function extractPagePath(url: string, baseURL: string): string | undefined {
const urlPath = getURLPathname(url);
// Pages are also served as Markdown at `<page>.md`, the form llms.txt links to.
const urlPath = getURLPathname(url)?.replace(/\.md$/, '');
const basePath = getURLPathname(baseURL);
if (urlPath === undefined || basePath === undefined) {
+36
View File
@@ -0,0 +1,36 @@
import { describe, expect, it } from 'bun:test';
import {
DISALLOW_ALL_ROBOTS_TXT_LINES,
createRobotsTxtResponse,
isRobotsTxtRequest,
} from './robots';
describe('isRobotsTxtRequest', () => {
it('matches robots.txt at the root of the host', () => {
expect(isRobotsTxtRequest(new URL('https://docs.example.com/robots.txt'))).toBe(true);
});
it('matches robots.txt under a site base path', () => {
expect(isRobotsTxtRequest(new URL('https://example.com/docs/v2/robots.txt'))).toBe(true);
});
it('ignores other paths', () => {
expect(isRobotsTxtRequest(new URL('https://docs.example.com/'))).toBe(false);
expect(isRobotsTxtRequest(new URL('https://docs.example.com/sitemap.xml'))).toBe(false);
expect(isRobotsTxtRequest(new URL('https://docs.example.com/my-robots.txt'))).toBe(false);
expect(isRobotsTxtRequest(new URL('https://docs.example.com/robots.txt/page'))).toBe(false);
});
});
describe('createRobotsTxtResponse', () => {
it('serves a disallow-all robots.txt as plain text', async () => {
const response = createRobotsTxtResponse(DISALLOW_ALL_ROBOTS_TXT_LINES);
expect(response.status).toBe(200);
expect(response.headers.get('content-type')).toBe('text/plain');
expect(await response.text()).toBe(
'User-agent: *\nContent-Signal: ai-train=no, search=no, ai-input=no\nDisallow: /\n'
);
});
});
+18
View File
@@ -0,0 +1,18 @@
import { NextResponse } from 'next/server';
export const DISALLOW_ALL_ROBOTS_TXT_LINES = [
'User-agent: *',
'Content-Signal: ai-train=no, search=no, ai-input=no',
'Disallow: /',
];
export function createRobotsTxtResponse(lines: string[]) {
return new NextResponse(`${lines.join('\n')}\n`, {
headers: { 'Content-Type': 'text/plain' },
});
}
// Matches under any base path, like the signed-in route: a sign-in redirect doesn't tell us the site's base path.
export function isRobotsTxtRequest(siteRequestURL: URL) {
return siteRequestURL.pathname.endsWith('/robots.txt');
}
+17 -4
View File
@@ -55,6 +55,11 @@ import {
parsePreviewCustomizationCookie,
serializePreviewCustomizationCookie,
} from '@/lib/preview';
import {
DISALLOW_ALL_ROBOTS_TXT_LINES,
createRobotsTxtResponse,
isRobotsTxtRequest,
} from '@/lib/robots';
import {
type ResponseCookies,
getPathScopedCookieName,
@@ -169,10 +174,11 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
}
// Normalize URL after extracting the URL from the request to make sure the client is redirected to the proper one
const normalizationResponse = normalizeRequestURL(siteRequestURL);
if (normalizationResponse) {
return normalizationResponse;
const normalization = normalizeRequestURL(siteRequestURL, mode);
if (normalization instanceof Response) {
return normalization;
}
const isNormalizedURL = normalization.isNormalized;
const imagesContextId = getImageResizingContextId(siteRequestURL);
/**
@@ -273,6 +279,12 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
});
}
// Crawlers can't sign in: redirecting them to the login page makes search engines
// report a broken robots.txt and keep crawling, so tell them to stay away instead.
if (siteURLData.target === 'external' && isRobotsTxtRequest(siteRequestURL)) {
return createRobotsTxtResponse(DISALLOW_ALL_ROBOTS_TXT_LINES);
}
// When it is a server action, we cannot just return a redirect response as it may cause CORS issues on redirect.
// For these cases, we return a 303 response with an `X-Action-Redirect` header that the client can handle.
// This is what server actions do when returning a redirect response.
@@ -338,6 +350,7 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
//
// Make sure the URL is clean of any va token after a successful lookup,
// and of any visitor.* params that may have been passed to the URL.
// Proxied requests that were not normalized are also redirected here, to the canonical URL.
//
// We only redirect if the request is not coming from the revalidation worker, as we don't
// want to redirect in that case. It can carry unsigned claims without any token, so we rely
@@ -348,7 +361,7 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
//
const normalizedVisitorURL = normalizeVisitorURL(incomingURL);
if (
normalizedVisitorURL.toString() !== incomingURL.toString() &&
(!isNormalizedURL || normalizedVisitorURL.toString() !== incomingURL.toString()) &&
!isRevalidationRequest(request.headers)
) {
if (visitorToken?.source === 'url') {
+3 -4
View File
@@ -1,6 +1,7 @@
import { SiteVisibility } from '@gitbook/api';
import { type GitBookSiteContext, checkIsRootSiteContext } from '@/lib/context';
import { DISALLOW_ALL_ROBOTS_TXT_LINES, createRobotsTxtResponse } from '@/lib/robots';
import { isSiteIndexable } from '@/lib/seo';
/**
@@ -49,10 +50,8 @@ export async function serveRobotsTxt(context: GitBookSiteContext) {
'',
])
: []),
'User-agent: *',
'Content-Signal: ai-train=no, search=no, ai-input=no',
'Disallow: /',
...DISALLOW_ALL_ROBOTS_TXT_LINES,
];
return new Response(`${lines.join('\n')}\n`, { headers: { 'Content-Type': 'text/plain' } });
return createRobotsTxtResponse(lines);
}
+23
View File
@@ -55,6 +55,29 @@ it(
{ timeout: 15_000 }
);
it(
'should get a page through MCP from its markdown URL',
async () => {
const client = await connectMCPClient(
getContentTestURL(
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-share-links/8tNo6MeXg7CkFMzSSz81/~gitbook/mcp/auth'
)
);
const response = await client.callTool({
name: 'getPage',
arguments: {
url: 'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-share-links/8tNo6MeXg7CkFMzSSz81/3.0/other-page.md',
},
});
expect(response.isError).toBeFalsy();
// @ts-expect-error - response.content is of type unknown
expect(response.content[0]?.text).toContain('# Other Page');
},
{ timeout: 15_000 }
);
describe('MCP on a site behind visitor authentication', () => {
const VA_SITE_URL = 'https://gitbook-open-e2e-sites.gitbook.io/va-site-redirects-fallback';
+11
View File
@@ -0,0 +1,11 @@
diff --git a/dist/shellscript.mjs b/dist/shellscript.mjs
index ccf9c0f62f48a803f0640e758244a950f17ba53e..6956ab14623f71cf5bc88c5c6437502abba7ec0f 100644
--- a/dist/shellscript.mjs
+++ b/dist/shellscript.mjs
@@ -3,3 +3,6 @@ const lang = Object.freeze(JSON.parse("{\"displayName\":\"Shell\",\"name\":\"she
export default [
lang
]
+
+// Upstream keeps the last letter of `name>` out of the word: https://github.com/jeff-hykin/better-shell-syntax/issues/111
+lang.repository.argument_context.patterns[0].match = '[\\t ]*+(?!\\d+>)([^\\t\\n "$\\&-);<>\\\\`|]+)';
@@ -1,5 +1,5 @@
diff --git a/dist/compiled/next-server/app-page.runtime.dev.js b/dist/compiled/next-server/app-page.runtime.dev.js
index 8c24deba180e52b8732b0b98447a14a4c973a1d9..23647ebd4bcb763a383cc2b5a01c4bd9eba504fd 100644
index ee264ffef478ba5dbc51e83d3c4efc9b9fa84ab0..5774304e84f7d33362d4cb37a97a571783a9fa2a 100644
--- a/dist/compiled/next-server/app-page.runtime.dev.js
+++ b/dist/compiled/next-server/app-page.runtime.dev.js
@@ -1,5 +1,5 @@