mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-10-09 14:52:00 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 078eacce45 | |||
| 9f168ff93b |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Stop the `?ask` query briefly appearing in the search field before an integration assistant opens.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Track which Ask AI entry point opened the chat in ask view analytics.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Recolor text over a background page cover in the first frame after hydration.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Keep links working when their target page is deleted and re-created at the same path, by falling back to the path recorded on the link.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Compress oversized site API tokens in the rewritten URL to stay under URL length limits.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Automatically resolve GitHub and GitLab page links to matching pages in the same published site, including cross-space links imported before their target page was available.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Keep the docs embed open when switching section, variant or language.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"@gitbook/embed": minor
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Add `defaultTab` and `defaultPage` options to the Docs Embed to choose where it opens. `defaultTab` opens the embed on the assistant, search or docs tab. `defaultPage` sets the page the docs tab opens on in place of the site's home page, and the embed opens on that page unless `defaultTab` says otherwise. Both work with the standalone script's `configure`, `frame.configure` and `<GitBookFrame>` props.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix page cover image jumping on load
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Use the DocumentTextColor type defined in API schema.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix inline Ask AI inputs and buttons doing nothing before search is opened.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Track server-side Insights events (markdown, llms.txt, RSS, ask) with the site's public URL instead of the proxy URL.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Send the previous page's URL with site insights events so broken links can be traced to the page linking to them.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix Loom videos staying blank on pages with many Loom embeds by lazy-loading them.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Improve dark theme contrast for Mermaid edge labels.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Improve the prompt for agents to ask questions.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"@gitbook/react-openapi": patch
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix OpenAPI schemas that reference themselves through a nullable union rendering forever when expanded, which crashed PDF exports.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Use the page's tag title, when set, for the HTML `<title>` of published pages.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix previewed customization and theme overrides being lost when navigating between pages inside the site preview. The preview cookie was scoped to the site's canonical base path instead of the preview route it is actually served under, so the browser dropped it on the next in-preview navigation.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Clarify the not-found page on sites with login-protected content: prompt anonymous visitors to log in, and tell logged-in visitors they may not have access.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Scroll the table of contents to the active page after client-side navigation.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Restore the Next.js dev-mode OOM patch that was silently dropped by the 16.3.8 upgrade.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Let search buttons open search limited to the current space.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Send the visitor country to resolvePublishedContentByUrl on dev, preview, staging and selected sites, using the forwarded `x-gitbook-country` header on revalidation requests.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix the first item of a sidebar page group sometimes appearing cut off after navigating.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Point the "Powered by GitBook" trademark link to gitbook.com/powered-by.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Read the production hostnames that receive the visitor country from the `GITBOOK_VISITOR_COUNTRY_HOSTNAMES` build-time env variable.
|
||||
@@ -78,11 +78,6 @@ runs:
|
||||
echo "GITBOOK_DISABLE_INSIGHTS=true" >> .vercel/.env.${{ inputs.environment }}.local
|
||||
echo "--- .vercel/.env.${{ inputs.environment }}.local after inject ---"
|
||||
cat .vercel/.env.${{ inputs.environment }}.local
|
||||
- name: Inject visitor country hostnames
|
||||
if: ${{ env.GITBOOK_VISITOR_COUNTRY_HOSTNAMES != '' }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo "GITBOOK_VISITOR_COUNTRY_HOSTNAMES=$GITBOOK_VISITOR_COUNTRY_HOSTNAMES" >> .vercel/.env.${{ inputs.environment }}.local
|
||||
- name: Inject staging build env vars
|
||||
if: ${{ inputs.environment == 'staging' }}
|
||||
shell: bash
|
||||
|
||||
@@ -10,8 +10,6 @@ jobs:
|
||||
environment:
|
||||
name: 2v-production
|
||||
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
env:
|
||||
GITBOOK_VISITOR_COUNTRY_HOSTNAMES: ${{ vars.GITBOOK_VISITOR_COUNTRY_HOSTNAMES }}
|
||||
outputs:
|
||||
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
steps:
|
||||
@@ -33,8 +31,6 @@ jobs:
|
||||
environment:
|
||||
name: 2c-production
|
||||
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
env:
|
||||
GITBOOK_VISITOR_COUNTRY_HOSTNAMES: ${{ vars.GITBOOK_VISITOR_COUNTRY_HOSTNAMES }}
|
||||
outputs:
|
||||
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
steps:
|
||||
|
||||
@@ -129,8 +129,8 @@
|
||||
"@gitbook/react-openapi": "workspace:*",
|
||||
"@mermaid-js/mermaid-zenuml": "^0.2.2",
|
||||
"@modelcontextprotocol/sdk": "1.17.5",
|
||||
"@opennextjs/aws": "4.1.7",
|
||||
"@opennextjs/cloudflare": "1.20.8",
|
||||
"@opennextjs/aws": "4.1.3",
|
||||
"@opennextjs/cloudflare": "1.20.5",
|
||||
"@panzoom/panzoom": "^4.6.1",
|
||||
"@sindresorhus/fnv1a": "^3.1.0",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
@@ -143,7 +143,6 @@
|
||||
"direction": "^2.0.1",
|
||||
"event-iterator": "^2.0.0",
|
||||
"feed": "^5.1.0",
|
||||
"fflate": "^0.8.3",
|
||||
"flexsearch": "^0.8.212",
|
||||
"image-size": "^2.0.2",
|
||||
"js-cookie": "^3.0.5",
|
||||
@@ -161,7 +160,7 @@
|
||||
"micromark-extension-gfm": "^3.0.0",
|
||||
"motion": "^12.23.24",
|
||||
"negotiator": "^1.0.0",
|
||||
"next": "^16.3.8",
|
||||
"next": "^16.3.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"nuqs": "^2.2.3",
|
||||
"object-hash": "^3.0.0",
|
||||
@@ -343,10 +342,11 @@
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"decode-named-character-reference@1.0.2": "patches/decode-named-character-reference@1.0.2.patch",
|
||||
"next@16.3.8": "patches/next@16.3.8.patch",
|
||||
"next@16.3.6": "patches/next@16.3.6.patch",
|
||||
},
|
||||
"overrides": {
|
||||
"@codemirror/state": "6.4.1",
|
||||
"@opennextjs/aws": "4.1.5",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"axios": "1.8.4",
|
||||
@@ -356,7 +356,7 @@
|
||||
},
|
||||
"catalog": {
|
||||
"@base-ui/react": "^1.7.0",
|
||||
"@gitbook/api": "0.205.0",
|
||||
"@gitbook/api": "0.202.0",
|
||||
"@scalar/api-client-react": "^1.3.46",
|
||||
"@tsconfig/node20": "^20.1.6",
|
||||
"@tsconfig/strictest": "^2.0.6",
|
||||
@@ -728,7 +728,7 @@
|
||||
|
||||
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@7.2.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.2.0" } }, "sha512-6639htZMjEkwskf3J+e6/iar+4cTNM9qhoWuRfj9F3eJD6r7iCzV1SWnQr2Mdv0QT0suuqU8BoJCZUyCtP9R4Q=="],
|
||||
|
||||
"@gitbook/api": ["@gitbook/api@0.205.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-jjkPAhPw6tja1N0ZPKqVsaV+opN8IS7yP1oZHHbcacZ477angPCo9YqiJJYWAl0tQ5OcKTuQbrsqhZSuwKM2Kw=="],
|
||||
"@gitbook/api": ["@gitbook/api@0.202.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-qhrjEQbNNmCljR0AgP79+BsVh9yelh9TIBg9X3TyYx5a3lJwDiT3sw1ND9DFQSxGHp8hH5w+20KJHn/IDpTbaw=="],
|
||||
|
||||
"@gitbook/browser-types": ["@gitbook/browser-types@workspace:packages/browser-types"],
|
||||
|
||||
@@ -892,23 +892,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.3.8", "", {}, "sha512-Al9zqHVV7TJv0eFuOU4U7Lvv74PTih4Ch63sk2xCIpSTkE3udFnaOcnzP2lQVymiL7yS9Cj2iClUXlR3EQ5sEw=="],
|
||||
"@next/env": ["@next/env@16.3.6", "", {}, "sha512-x9Vblze1EbtltQYnNH38xCPWU3TVfBd1eXqA3+w9+BTpedkkdNpAaltXlGQ/nsc1+E0mVTNrtcbX3GoO09zeLQ=="],
|
||||
|
||||
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.3.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2JPRMh2nmQG5CiL7cXGL9AGwnPWJQ//cTtAUCT+w511QHk79SYz3LGv/pc5X643B/WEO0rvu3Yww0hqwt3kgeA=="],
|
||||
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.3.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-E/7GEqaUkt8mk/T8v9lAnrhzR06kdq1ZBkC12F8tAMkdIadwNp3H1KqHynDHrpcTlGCUdq/qu6vUL2aYVyYBdw=="],
|
||||
|
||||
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.3.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-GZtCCOBKJ4leVIT/Th0llWKhD1ca92lzbQiS5R5ON9QkoiFnilFsebDae1JU2a3HWoKMEmEZWGs1AGLavVM72Q=="],
|
||||
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.3.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-yBE893/nDWTlaiBD1p+qgt7NUen4U5R6FXyH0s67Npq1S3E0cVSef1WIXC2xBRgQvwAvJq6DnS6Y6PrY0cy4Ew=="],
|
||||
|
||||
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.3.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-O659ygeQYqneJ1fBKMpFxIFqYkYswu8IAS1OCKK/4f3ZgJJm1dRz4fVJZRi/kLLWjnBKnebOePA4WNv+sV1pVA=="],
|
||||
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.3.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-KJDpjBqBPYlvkivmyrp+Qys6k/7ksbqGQvRVc6ZEGfR+cjQxx+nUkJaWmNZJsmoOrqYNbaXByF8wa0lBwDhB3Q=="],
|
||||
|
||||
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.3.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-dSjKSyWpzxoO1d3DIZZcP4XJcNaKeLmxQMFOiYl5vuBRMmweIqnAhty8tAmRsvTss779cK1FtYnDMj40e4TQlg=="],
|
||||
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.3.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-mqNg2K+hvWskSRb/QM+Ix412DvBsuSF0XV+frTSw5vmoucNnIlynFwKYew8D01bfATErMOM7Bujrf0BA5DRKFA=="],
|
||||
|
||||
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.3.8", "", { "os": "linux", "cpu": "x64" }, "sha512-lbqOuz3RPRcv+o9msNsJw5x4+Y1ZwPTs6vmL6DCf7i0fZfvng/F59wyeDwqHIvV0mK//RBy/jJkZ+nCKsSMXjQ=="],
|
||||
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.3.6", "", { "os": "linux", "cpu": "x64" }, "sha512-nFncBNGAYouRHjRVaITs9beZRfhX4ssVwpnvPIAbkZVH6LtGoAVlH4bJ8Cnf9SOo9bsXgPFer/GdHtEE3JNOkw=="],
|
||||
|
||||
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.3.8", "", { "os": "linux", "cpu": "x64" }, "sha512-+316WswI8ScVgZeUd+1KGaXkHhaYQzCjvH/05TZSpJ8zBizb1a4G7DtO7F12jcBIqMOtsz9ji1t48fmKtzqsGA=="],
|
||||
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.3.6", "", { "os": "linux", "cpu": "x64" }, "sha512-5Mf3cHDGR/Iz0ng2Bj3zUR3p5QS9YK3Hn2QiAfavFmyF48zwThAjpFoiTKNIcOHLYS4zEk+gzyJ/9deQ2ZB8yQ=="],
|
||||
|
||||
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.3.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-ji0gd4kMYUxO+1fJBIbiBVRCjzG/lloiyCccnlebvb1ZJ5qXCPZqYg4Jl1DrrixnWNMKylzgpmMWx0yNDYXlzw=="],
|
||||
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.3.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-0jkJy0C2kbrJWTk4YLa3xk80pVBpx8FCHJym7CnUfDAXe/FWv5qT7SQJbR0KuemyxaEDlEx5WT4VQJoTW+/9Qw=="],
|
||||
|
||||
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.3.8", "", { "os": "win32", "cpu": "x64" }, "sha512-WcTlaKt/TWkh5kUjdJcUmB1XgZ+1c6fz4Y9fDHL73YNSdGaUWjceeWrrlwF0nv19iABYWC4iAq1oX1w4Bn0vfg=="],
|
||||
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.3.6", "", { "os": "win32", "cpu": "x64" }, "sha512-/YXjI1e5OXcZ7YpxRwgP/1jAV/SBKTzeVKqN2mk7mLpcICsyn3Gl5+dIfDTJp70M0ccMhyMMRso4v6mPDCGepg=="],
|
||||
|
||||
"@noble/ciphers": ["@noble/ciphers@1.2.1", "", {}, "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA=="],
|
||||
|
||||
@@ -944,9 +944,9 @@
|
||||
|
||||
"@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
|
||||
|
||||
"@opennextjs/aws": ["@opennextjs/aws@4.1.7", "", { "dependencies": { "@ast-grep/napi": "^0.40.5", "@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": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.3", "aws4fetch": "^1.0.20", "chalk": "^5.6.2", "cookie": "^1.0.2", "esbuild": "0.25.4", "express": "^5.1.0", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.1.0", "yaml": "^2.8.1" }, "peerDependencies": { "next": ">=15.5.27 <16 || >=16.3.8" }, "bin": { "open-next": "dist/index.js" } }, "sha512-ELezSARrTfxp/76cz41eQjki1AV0CuBUKOznU3w/tOk7i87x867RkkYho+gDkog9gpnGzefHdJu463o9VaabNQ=="],
|
||||
"@opennextjs/aws": ["@opennextjs/aws@4.1.5", "", { "dependencies": { "@ast-grep/napi": "^0.40.5", "@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": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.3", "aws4fetch": "^1.0.20", "chalk": "^5.6.2", "cookie": "^1.0.2", "esbuild": "0.25.4", "express": "^5.1.0", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.1.0", "yaml": "^2.8.1" }, "peerDependencies": { "next": ">=15.5.24 <16 || >=16.3.3" }, "bin": { "open-next": "dist/index.js" } }, "sha512-41LNGtS5R5SCfn9zFxHx9QKJIRnmf1uZH+jonHkUMlRKER0bSmUSGjK6GJF7e50XdLXFpTHzaqgRvGmzvr9Btw=="],
|
||||
|
||||
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.20.8", "", { "dependencies": { "@ast-grep/napi": "^0.40.5", "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "4.1.7", "ci-info": "^4.2.0", "cloudflare": "^4.4.1", "comment-json": "^4.5.1", "enquirer": "^2.4.1", "glob": "^12.0.0", "ts-tqdm": "^0.8.6", "yargs": "^18.0.0" }, "peerDependencies": { "next": ">=15.5.27 <16 || >=16.3.8", "rclone.js": "^0.6.6", "wrangler": "^4.125.0" }, "optionalPeers": ["rclone.js"], "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-7rrsBqd234GBBnHITobS0gagT4/fSfHJwm+ogYTyUb7ubg3PGC06biJgL9l4DZabFeu/ucqvml80sBr5torBgg=="],
|
||||
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.20.5", "", { "dependencies": { "@ast-grep/napi": "^0.40.5", "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "4.1.3", "ci-info": "^4.2.0", "cloudflare": "^4.4.1", "comment-json": "^4.5.1", "enquirer": "^2.4.1", "glob": "^12.0.0", "ts-tqdm": "^0.8.6", "yargs": "^18.0.0" }, "peerDependencies": { "next": ">=15.5.24 <16 || >=16.3.3", "rclone.js": "^0.6.6", "wrangler": "^4.125.0" }, "optionalPeers": ["rclone.js"], "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-Y4qCnHTYMa8waEHvf7iqDJuiresuREp0zithSZ8hT33Wn9WyvOUMBM0SWNH961Wv7ynHRdxAJ0ggmnQ3aSdzdg=="],
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
@@ -2118,8 +2118,6 @@
|
||||
|
||||
"feed": ["feed@5.1.0", "", { "dependencies": { "xml-js": "^1.6.11" } }, "sha512-qGNhgYygnefSkAHHrNHqC7p3R8J0/xQDS/cYUud8er/qD9EFGWyCdUDfULHTJQN1d3H3WprzVwMc9MfB4J50Wg=="],
|
||||
|
||||
"fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="],
|
||||
|
||||
"file-entry-cache": ["file-entry-cache@10.0.7", "", { "dependencies": { "flat-cache": "^6.1.7" } }, "sha512-txsf5fu3anp2ff3+gOJJzRImtrtm/oa9tYLN0iTuINZ++EyVR/nRrg2fKYwvG/pXDofcrvvb0scEbX3NyW/COw=="],
|
||||
|
||||
"file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="],
|
||||
@@ -2652,7 +2650,7 @@
|
||||
|
||||
"netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="],
|
||||
|
||||
"next": ["next@16.3.8", "", { "dependencies": { "@next/env": "16.3.8", "@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.3.8", "@next/swc-darwin-x64": "16.3.8", "@next/swc-linux-arm64-gnu": "16.3.8", "@next/swc-linux-arm64-musl": "16.3.8", "@next/swc-linux-x64-gnu": "16.3.8", "@next/swc-linux-x64-musl": "16.3.8", "@next/swc-win32-arm64-msvc": "16.3.8", "@next/swc-win32-x64-msvc": "16.3.8", "sharp": "^0.35.4" }, "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-U7QEZaTini6wKrb8A8hqLLqYQyCetegKjCpJOyxk642vWoMoU1x5PyZCJFvgYgiptA8xc5j/9xYlZFO7w9Sjmw=="],
|
||||
"next": ["next@16.3.6", "", { "dependencies": { "@next/env": "16.3.6", "@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.3.6", "@next/swc-darwin-x64": "16.3.6", "@next/swc-linux-arm64-gnu": "16.3.6", "@next/swc-linux-arm64-musl": "16.3.6", "@next/swc-linux-x64-gnu": "16.3.6", "@next/swc-linux-x64-musl": "16.3.6", "@next/swc-win32-arm64-msvc": "16.3.6", "@next/swc-win32-x64-msvc": "16.3.6", "sharp": "^0.35.4" }, "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-L+otWM/aQbYTx98aZhgEoMb4bZAXx1YVW4UMA/vuCyCoWG5HJyZUili8QAkqzrcC+5///tsz3s0M+SlyB5bLMw=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
|
||||
+4
-3
@@ -19,7 +19,8 @@
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"esbuild": "0.27.3",
|
||||
"axios": "1.8.4"
|
||||
"axios": "1.8.4",
|
||||
"@opennextjs/aws": "4.1.5"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -48,7 +49,7 @@
|
||||
"@tsconfig/strictest": "^2.0.6",
|
||||
"@tsconfig/node20": "^20.1.6",
|
||||
"@base-ui/react": "^1.7.0",
|
||||
"@gitbook/api": "0.205.0",
|
||||
"@gitbook/api": "0.202.0",
|
||||
"@scalar/api-client-react": "^1.3.46",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
@@ -67,6 +68,6 @@
|
||||
"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.3.8": "patches/next@16.3.8.patch"
|
||||
"next@16.3.6": "patches/next@16.3.6.patch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,6 @@ GitBook('configure', {
|
||||
icon: 'assistant' // 'assistant' | 'sparkle' | 'help' | 'book'
|
||||
},
|
||||
tabs: ['assistant', 'search', 'docs'],
|
||||
defaultPage: '/getting-started',
|
||||
actions: [
|
||||
{
|
||||
icon: 'circle-question',
|
||||
@@ -108,7 +107,6 @@ frame.clearChat();
|
||||
// Configure the embed (see Configuration section for all options)
|
||||
frame.configure({
|
||||
tabs: ['assistant', 'search', 'docs'],
|
||||
defaultPage: '/getting-started',
|
||||
actions: [
|
||||
{
|
||||
icon: 'circle-question',
|
||||
@@ -144,7 +142,6 @@ import { GitBookProvider, GitBookFrame } from '@gitbook/embed/react';
|
||||
unsignedClaims: { userId: '123' } // Optional: custom claims for dynamic expressions
|
||||
}}
|
||||
tabs={['assistant', 'search', 'docs']}
|
||||
defaultPage="/getting-started"
|
||||
greeting={{ title: 'Welcome!', subtitle: 'How can I help?' }}
|
||||
assistantName="Support Assistant"
|
||||
suggestions={['What is GitBook?', 'How do I get started?']}
|
||||
@@ -252,33 +249,6 @@ Override which tabs are displayed. Defaults to your site's configuration.
|
||||
tabs: ['assistant', 'search', 'docs']
|
||||
```
|
||||
|
||||
### `defaultTab`
|
||||
|
||||
Available in: Standalone script, NPM package, React components
|
||||
|
||||
The tab the embed opens on. Without it, the embed opens on `defaultPage` if set, otherwise on the assistant (or the docs when the assistant isn't available). The tab must be one of the enabled `tabs`.
|
||||
|
||||
- **Type**: `'assistant' | 'search' | 'docs'`
|
||||
|
||||
```javascript
|
||||
tabs: ['assistant', 'search', 'docs'],
|
||||
defaultTab: 'search'
|
||||
```
|
||||
|
||||
`defaultTab` and `defaultPage` apply when the embed first loads: the standalone widget keeps its place when it is closed and reopened. To move it later, use `navigateToPage` or `navigateToAssistant`.
|
||||
|
||||
### `defaultPage`
|
||||
|
||||
Available in: Standalone script, NPM package, React components
|
||||
|
||||
The page the docs tab opens on, in place of your site's home page. Accepts the same references as `navigateToPage`: the page's path within the site, an absolute path, or its full published URL. Unless `defaultTab` says otherwise, the embed opens on this page.
|
||||
|
||||
- **Type**: `string`
|
||||
|
||||
```javascript
|
||||
defaultPage: '/getting-started/quickstart'
|
||||
```
|
||||
|
||||
### `closeButton`
|
||||
|
||||
Available in: Standalone script, NPM package, React components
|
||||
|
||||
@@ -62,12 +62,6 @@ export type GitBookEmbeddableConfiguration = {
|
||||
/** Tabs to display in the embed (if enabled on the site). */
|
||||
tabs: ('assistant' | 'docs' | 'search')[];
|
||||
|
||||
/** Tab to open the embed on. */
|
||||
defaultTab?: 'assistant' | 'docs' | 'search';
|
||||
|
||||
/** Page to open the docs tab on, instead of the site's home page. */
|
||||
defaultPage?: string;
|
||||
|
||||
/** Additional buttons to be displayed in the header of the GitBook embed. */
|
||||
actions: GitBookEmbeddableActionDefinition[];
|
||||
|
||||
|
||||
@@ -27,8 +27,6 @@ export function GitBookFrame(props: GitBookFrameProps) {
|
||||
suggestions = [],
|
||||
tools = [],
|
||||
tabs = ['assistant', 'search', 'docs'],
|
||||
defaultTab,
|
||||
defaultPage,
|
||||
trademark = true,
|
||||
closeButton = false,
|
||||
assistantName,
|
||||
@@ -52,8 +50,6 @@ export function GitBookFrame(props: GitBookFrameProps) {
|
||||
useEffect(() => {
|
||||
gitbookFrame?.configure({
|
||||
tabs,
|
||||
defaultTab,
|
||||
defaultPage,
|
||||
actions,
|
||||
greeting,
|
||||
suggestions,
|
||||
@@ -69,8 +65,6 @@ export function GitBookFrame(props: GitBookFrameProps) {
|
||||
suggestions,
|
||||
tools,
|
||||
tabs,
|
||||
defaultTab,
|
||||
defaultPage,
|
||||
closeButton,
|
||||
trademark,
|
||||
assistantName,
|
||||
|
||||
@@ -95,8 +95,6 @@ const nextConfig = {
|
||||
GITBOOK_BLOCK_SEARCH_INDEXATION: process.env.GITBOOK_BLOCK_SEARCH_INDEXATION,
|
||||
GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE: process.env.GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE,
|
||||
GITBOOK_DISABLE_INSIGHTS: process.env.GITBOOK_DISABLE_INSIGHTS,
|
||||
GITBOOK_VISITOR_COUNTRY_HOSTNAMES: process.env.GITBOOK_VISITOR_COUNTRY_HOSTNAMES,
|
||||
VERCEL_TARGET_ENV: process.env.VERCEL_TARGET_ENV,
|
||||
|
||||
// Next.js envs
|
||||
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY,
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
"@gitbook/react-openapi": "workspace:*",
|
||||
"@mermaid-js/mermaid-zenuml": "^0.2.2",
|
||||
"@modelcontextprotocol/sdk": "1.17.5",
|
||||
"@opennextjs/aws": "4.1.7",
|
||||
"@opennextjs/cloudflare": "1.20.8",
|
||||
"@opennextjs/aws": "4.1.3",
|
||||
"@opennextjs/cloudflare": "1.20.5",
|
||||
"@panzoom/panzoom": "^4.6.1",
|
||||
"@sindresorhus/fnv1a": "^3.1.0",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
@@ -34,7 +34,6 @@
|
||||
"direction": "^2.0.1",
|
||||
"event-iterator": "^2.0.0",
|
||||
"feed": "^5.1.0",
|
||||
"fflate": "^0.8.3",
|
||||
"flexsearch": "^0.8.212",
|
||||
"image-size": "^2.0.2",
|
||||
"js-cookie": "^3.0.5",
|
||||
@@ -52,7 +51,7 @@
|
||||
"micromark-extension-gfm": "^3.0.0",
|
||||
"motion": "^12.23.24",
|
||||
"negotiator": "^1.0.0",
|
||||
"next": "^16.3.8",
|
||||
"next": "^16.3.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"nuqs": "^2.2.3",
|
||||
"object-hash": "^3.0.0",
|
||||
|
||||
@@ -5,7 +5,6 @@ import rison from 'rison';
|
||||
import type { SiteAPIToken } from '@gitbook/api';
|
||||
|
||||
import { getVisitorAuthClaims, getVisitorAuthClaimsFromToken } from '@/lib/adaptive';
|
||||
import { decompressAPIToken } from '@/lib/api-token-compression';
|
||||
import { type SiteURLData, fetchSiteContextByURLLookup, getBaseContext } from '@/lib/context';
|
||||
import { getDynamicCustomizationSettings } from '@/lib/customization';
|
||||
|
||||
@@ -125,8 +124,7 @@ function getModeFromParams(mode: string): RouteParamMode {
|
||||
export function getSiteURLDataFromParams(params: RouteLayoutParams): SiteURLData {
|
||||
try {
|
||||
const decoded = decodeURIComponent(params.siteData);
|
||||
const data = rison.decode<SiteURLData>(decoded);
|
||||
return { ...data, apiToken: decompressAPIToken(data.apiToken) };
|
||||
return rison.decode(decoded);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Returning 404 after failing to decode site data ${params.siteData}: ${error}`
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { CustomizationAIMode } from '@gitbook/api';
|
||||
import type { GitBookAssistant } from '@gitbook/browser-types';
|
||||
import { Icon, type IconName } from '@gitbook/icons';
|
||||
|
||||
import { type AskAITrigger, useAIChatController, useAIChatState } from '.';
|
||||
import { useAIChatController, useAIChatState } from '.';
|
||||
import { AIChatIcon, AISearchIcon, getAIChatName } from '../AIChat';
|
||||
import { useIntegrationAssistants } from '../Integrations';
|
||||
import { useSetSearchState } from '../Search/useSearch';
|
||||
@@ -26,8 +26,7 @@ export type AIConfig = {
|
||||
};
|
||||
};
|
||||
|
||||
export type Assistant = Omit<GitBookAssistant, 'icon' | 'open'> & {
|
||||
open: (query?: string, trigger?: AskAITrigger) => void;
|
||||
export type Assistant = Omit<GitBookAssistant, 'icon'> & {
|
||||
/**
|
||||
* Unique identifier for the assistant. Generated automatically using Crypto.randomUUID().
|
||||
* @example '123e4567-e89b-12d3-a456-426614174000'
|
||||
@@ -103,8 +102,8 @@ export function useAI(): AIContext {
|
||||
className="size-text-lg"
|
||||
/>
|
||||
),
|
||||
open: (query?: string, trigger?: AskAITrigger) => {
|
||||
chatController.open(trigger);
|
||||
open: (query?: string) => {
|
||||
chatController.open();
|
||||
if (query) {
|
||||
chatController.postMessage({ message: query });
|
||||
}
|
||||
@@ -120,13 +119,9 @@ export function useAI(): AIContext {
|
||||
icon: <AISearchIcon />,
|
||||
open: (query?: string) => {
|
||||
if (query) {
|
||||
setSearchState((prev) => ({
|
||||
...prev,
|
||||
query: null,
|
||||
ask: query,
|
||||
scope: prev?.scope ?? 'default',
|
||||
open: true,
|
||||
}));
|
||||
setSearchState((prev) =>
|
||||
prev ? { ...prev, query: null, ask: query, open: true } : null
|
||||
);
|
||||
}
|
||||
},
|
||||
pageAction: false,
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
type AIStreamResponseToolCallPending,
|
||||
type AIToolCallResult,
|
||||
SiteInsightsDisplayContext,
|
||||
type SiteInsightsEventAskView,
|
||||
} from '@gitbook/api';
|
||||
|
||||
import { getInsightsSession, useTrackEvent } from '../Insights';
|
||||
@@ -61,8 +60,6 @@ export type AIChatStatus =
|
||||
| 'error'
|
||||
| 'confirm';
|
||||
|
||||
export type AskAITrigger = NonNullable<SiteInsightsEventAskView['trigger']>;
|
||||
|
||||
export type AIChatState = {
|
||||
/**
|
||||
* If true, the chat is open.
|
||||
@@ -163,7 +160,7 @@ type AIChatEventListener = (input?: Omit<AIChatEvent, 'type'>) => void;
|
||||
|
||||
export type AIChatController = {
|
||||
/** Open the dialog */
|
||||
open: (trigger?: AskAITrigger) => void;
|
||||
open: () => void;
|
||||
/** Close the dialog */
|
||||
close: () => void;
|
||||
/** Post a message to the session */
|
||||
@@ -305,28 +302,22 @@ export function AIChatProvider(props: {
|
||||
const eventsRef = React.useRef<Map<AIChatEvent['type'], AIChatEventListener[]>>(new Map());
|
||||
|
||||
// Open AI chat and sync with search state
|
||||
const onOpen = React.useCallback(
|
||||
(trigger?: AskAITrigger) => {
|
||||
setIntercomLauncherHidden(true);
|
||||
if (!renderMessageOptions?.asEmbeddable) {
|
||||
trackEvent({ type: 'ask_view', trigger });
|
||||
}
|
||||
const onOpen = React.useCallback(() => {
|
||||
setIntercomLauncherHidden(true);
|
||||
|
||||
const { initialQuery } = globalState.getState();
|
||||
globalState.setState((state) => ({ ...state, opened: true }));
|
||||
const { initialQuery } = globalState.getState();
|
||||
globalState.setState((state) => ({ ...state, opened: true }));
|
||||
|
||||
// Update search state to show ask mode with first message or current ask value
|
||||
setSearchState((prev) => ({
|
||||
ask: prev?.ask ?? initialQuery ?? '',
|
||||
query: prev?.query ?? null,
|
||||
scope: prev?.scope ?? 'default',
|
||||
open: false, // Close search popover when opening chat
|
||||
}));
|
||||
// Update search state to show ask mode with first message or current ask value
|
||||
setSearchState((prev) => ({
|
||||
ask: prev?.ask ?? initialQuery ?? '',
|
||||
query: prev?.query ?? null,
|
||||
scope: prev?.scope ?? 'default',
|
||||
open: false, // Close search popover when opening chat
|
||||
}));
|
||||
|
||||
notify(eventsRef.current.get('open'), {});
|
||||
},
|
||||
[setSearchState, trackEvent, renderMessageOptions?.asEmbeddable]
|
||||
);
|
||||
notify(eventsRef.current.get('open'), {});
|
||||
}, [setSearchState]);
|
||||
|
||||
// Close AI chat and clear ask parameter
|
||||
const onClose = React.useCallback(() => {
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
EmbeddableFrameTitle,
|
||||
} from '../Embeddable/EmbeddableFrame';
|
||||
import { useIsMounted, useNow } from '../hooks';
|
||||
import { useTrackEvent } from '../Insights';
|
||||
import { Button } from '../primitives';
|
||||
import { ScrollContainer } from '../primitives/ScrollContainer';
|
||||
import { SideSheet } from '../primitives/SideSheet';
|
||||
@@ -67,6 +68,16 @@ export function AIChat() {
|
||||
}
|
||||
);
|
||||
|
||||
// Track the view of the AI chat
|
||||
const trackEvent = useTrackEvent();
|
||||
React.useEffect(() => {
|
||||
if (chat.opened) {
|
||||
trackEvent({
|
||||
type: 'ask_view',
|
||||
});
|
||||
}
|
||||
}, [chat.opened, trackEvent]);
|
||||
|
||||
return (
|
||||
<SideSheet
|
||||
side="right"
|
||||
|
||||
@@ -39,7 +39,7 @@ export function AIChatButton(props: {
|
||||
</div>
|
||||
}
|
||||
aria-label={tString(language, 'ai_chat_ask', assistant.label)}
|
||||
onClick={() => assistant.open(undefined, 'site')}
|
||||
onClick={() => assistant.open()}
|
||||
>
|
||||
{showLabel ? t(language, 'ask') : null}
|
||||
</Button>
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ export function AskAIParagraphButton(props: { content: string; className?: Class
|
||||
id: `text-${fnv1a(text, { size: 32 })}`,
|
||||
content: text,
|
||||
});
|
||||
chatController.open('hover');
|
||||
chatController.open();
|
||||
chatController.setDraft(tString(language, 'ai_chat_paragraph_draft'));
|
||||
chatController.focus();
|
||||
};
|
||||
|
||||
@@ -84,7 +84,7 @@ export function AskAITextSelection() {
|
||||
id: `text-${fnv1a(content, { size: 32 })}`,
|
||||
content,
|
||||
});
|
||||
chatController.open('selection');
|
||||
chatController.open();
|
||||
chatController.focus();
|
||||
clear();
|
||||
};
|
||||
|
||||
@@ -247,15 +247,11 @@ async function renderMermaidDiagram(args: {
|
||||
const { source, id, darkMode, mermaidRuntimeURL } = args;
|
||||
const { mermaid } = await loadMermaid(mermaidRuntimeURL);
|
||||
|
||||
// Mermaid's default dark edge label pill only reaches 4.43:1 contrast, below WCAG AA.
|
||||
const themeVariables = darkMode ? { edgeLabelBackground: '#3a3a3a' } : undefined;
|
||||
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
securityLevel: 'strict',
|
||||
darkMode,
|
||||
theme: darkMode ? 'dark' : undefined,
|
||||
themeVariables,
|
||||
});
|
||||
|
||||
const renderContainer = createMermaidRenderContainer();
|
||||
|
||||
@@ -37,10 +37,7 @@ export async function Embed(props: BlockProps<gitbookAPI.DocumentBlockEmbed>) {
|
||||
<>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
context.mode !== 'print' && shouldLazyLoad(block.data.url)
|
||||
? lazyLoadIframes(embed.html)
|
||||
: embed.html,
|
||||
__html: embed.html,
|
||||
}}
|
||||
data-visual-test="blackout"
|
||||
/>
|
||||
@@ -76,23 +73,6 @@ export async function Embed(props: BlockProps<gitbookAPI.DocumentBlockEmbed>) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pages with many Loom embeds exhaust the browser's request budget
|
||||
* (ERR_INSUFFICIENT_RESOURCES) when every player loads at once, leaving some blank.
|
||||
*/
|
||||
function lazyLoadIframes(html: string): string {
|
||||
return html.replace(/<iframe\b(?![^>]*\bloading=)/gi, '<iframe loading="lazy"');
|
||||
}
|
||||
|
||||
function shouldLazyLoad(url: string): boolean {
|
||||
try {
|
||||
const { hostname } = new URL(url);
|
||||
return hostname === 'loom.com' || hostname.endsWith('.loom.com');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an integration block with an unfurl action from the GitBook Embed response.
|
||||
*/
|
||||
|
||||
@@ -13,14 +13,9 @@ const INPUT_SIZE_MAP: Record<NonNullable<ButtonProps['size']>, 'small' | 'medium
|
||||
};
|
||||
|
||||
export function InlineActionButton(
|
||||
props: {
|
||||
action: 'ask' | 'search';
|
||||
query?: string;
|
||||
/** Where a search runs: the whole site, or only the space the visitor is reading. */
|
||||
scope?: 'site' | 'space';
|
||||
} & { buttonProps: ButtonProps } // TODO: Type this properly: Pick<api.DocumentInlineButton, 'action' | 'query'> & { buttonProps: ButtonProps }
|
||||
props: { action: 'ask' | 'search'; query?: string } & { buttonProps: ButtonProps } // TODO: Type this properly: Pick<api.DocumentInlineButton, 'action' | 'query'> & { buttonProps: ButtonProps }
|
||||
) {
|
||||
const { action, query, scope, buttonProps } = props;
|
||||
const { action, query, buttonProps } = props;
|
||||
|
||||
const { assistants } = useAI();
|
||||
const chatState = useAIChatState();
|
||||
@@ -34,7 +29,7 @@ export function InlineActionButton(
|
||||
setSearchState((prev) => ({
|
||||
...prev,
|
||||
ask: null,
|
||||
scope: scope === 'space' ? 'current' : 'default',
|
||||
scope: 'default',
|
||||
query: value ?? query,
|
||||
open: true,
|
||||
}));
|
||||
|
||||
@@ -47,11 +47,6 @@ export function InlineButton(props: InlineProps<api.DocumentInlineButton>) {
|
||||
<InlineActionButton
|
||||
action={inline.data.action.action}
|
||||
query={inline.data.action.query ?? ''}
|
||||
scope={
|
||||
inline.data.action.action === 'search'
|
||||
? inline.data.action.scope
|
||||
: undefined
|
||||
}
|
||||
buttonProps={buttonProps}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -52,8 +52,8 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
|
||||
const anchorElement = (
|
||||
<InlineLinkAnchor
|
||||
href={resolved.href}
|
||||
contentRef={resolved.resolvedRef ?? inline.data.ref}
|
||||
isExternal={(resolved.resolvedRef ?? inline.data.ref).kind === 'url'}
|
||||
contentRef={inline.data.ref}
|
||||
isExternal={inline.data.ref.kind === 'url'}
|
||||
>
|
||||
{inlinesElement}
|
||||
</InlineLinkAnchor>
|
||||
@@ -121,7 +121,7 @@ function InlineLinkTooltipWrapper(props: {
|
||||
|
||||
let breadcrumbs = resolved.ancestors ?? [];
|
||||
const isMailto = resolved.href.startsWith('mailto:');
|
||||
const isExternal = (resolved.resolvedRef ?? inline.data.ref).kind === 'url';
|
||||
const isExternal = inline.data.ref.kind === 'url';
|
||||
const isSamePage = inline.data.ref.kind === 'anchor' && inline.data.ref.page === undefined;
|
||||
|
||||
if (isMailto) {
|
||||
|
||||
@@ -117,8 +117,8 @@ function Color(props: MarkedLeafProps<DocumentMarkColor>) {
|
||||
return (
|
||||
<span
|
||||
className={tcls([
|
||||
textColorToStyle[mark.data.text ?? 'default'],
|
||||
backgroundColorToStyle[mark.data.background ?? 'default'],
|
||||
textColorToStyle[mark.data.text],
|
||||
backgroundColorToStyle[mark.data.background],
|
||||
])}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -61,7 +61,9 @@ export function EmbeddableAIChat(props: EmbeddableAIChatProps) {
|
||||
}
|
||||
|
||||
trackEvent(
|
||||
{ type: 'ask_view', trigger: 'embed' },
|
||||
{
|
||||
type: 'ask_view',
|
||||
},
|
||||
{
|
||||
pageId: null,
|
||||
displayContext: api.SiteInsightsDisplayContext.Embed,
|
||||
|
||||
@@ -97,7 +97,6 @@ export async function EmbeddableDocsPage(
|
||||
) ?? context.siteSpace
|
||||
}
|
||||
siteSpaces={variants.translations}
|
||||
asEmbeddable
|
||||
className="my-1.5 ml-2 self-start"
|
||||
/>
|
||||
) : null}
|
||||
@@ -126,7 +125,6 @@ export async function EmbeddableDocsPage(
|
||||
) ?? context.siteSpace
|
||||
}
|
||||
siteSpaces={variants.translations}
|
||||
asEmbeddable
|
||||
className="max-md:[&_.button-content]:block"
|
||||
/>
|
||||
) : null
|
||||
@@ -139,7 +137,6 @@ export async function EmbeddableDocsPage(
|
||||
context={context}
|
||||
siteSpace={context.siteSpace}
|
||||
siteSpaces={variants.generic}
|
||||
asEmbeddable
|
||||
className="w-full px-3"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -9,7 +9,6 @@ import type { GitBookEmbeddableConfiguration, ParentToFrameMessage } from '@gitb
|
||||
import { integrationsAssistantTools } from '../Integrations';
|
||||
import { Button, LinkContext, type LinkContextType } from '../primitives';
|
||||
import { getChannel } from './channel';
|
||||
import { DEFAULT_EMBEDDABLE_TABS, getEmbeddableLandingTab } from './getEmbeddableLandingTab';
|
||||
import { resolveEmbedPageLink } from './server-actions';
|
||||
import { useAI, useAIChatController } from '@/components/AI';
|
||||
import { isAIChatEnabled } from '@/components/utils/isAIChatEnabled';
|
||||
@@ -24,17 +23,6 @@ const embeddableConfiguration = createStore<GitBookEmbeddableConfiguration>(() =
|
||||
trademark: true,
|
||||
}));
|
||||
|
||||
const docsHome = createStore<{ reference?: string; href?: string }>(() => ({}));
|
||||
|
||||
// Module-level so it survives the layout remounting on a cross-space navigation.
|
||||
let landed = false;
|
||||
|
||||
function resolvePageHref(pagePath: string, baseURL: string): Promise<string> {
|
||||
return resolveEmbedPageLink(pagePath)
|
||||
.then((resolved) => ('href' in resolved ? resolved.href : `${baseURL}/page/${pagePath}`))
|
||||
.catch(() => `${baseURL}/page/${pagePath}`);
|
||||
}
|
||||
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
function log(...data: any[]) {
|
||||
// oxlint-disable-next-line no-console
|
||||
@@ -78,15 +66,6 @@ export function EmbeddableIframeAPI(props: { baseURL: string }) {
|
||||
const { baseURL, router, chatController } = refs.current;
|
||||
const message = payload as ParentToFrameMessage;
|
||||
|
||||
const navigate = (href: Promise<string>) => {
|
||||
const token = ++navToken.current;
|
||||
href.then((href) => {
|
||||
if (navToken.current === token) {
|
||||
refs.current.router.push(href);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
log('[gitbook] received message', message);
|
||||
|
||||
switch (message.type) {
|
||||
@@ -104,43 +83,29 @@ export function EmbeddableIframeAPI(props: { baseURL: string }) {
|
||||
break;
|
||||
}
|
||||
case 'configure': {
|
||||
const { settings } = message;
|
||||
embeddableConfiguration.setState(settings);
|
||||
embeddableConfiguration.setState(message.settings);
|
||||
integrationsAssistantTools.setState({
|
||||
tools: settings.tools,
|
||||
tools: message.settings.tools,
|
||||
});
|
||||
|
||||
const defaultPage =
|
||||
typeof settings.defaultPage === 'string' && settings.defaultPage
|
||||
? settings.defaultPage
|
||||
: undefined;
|
||||
let docsHomeHref: Promise<string> | undefined;
|
||||
if (defaultPage !== docsHome.getState().reference) {
|
||||
docsHome.setState({ reference: defaultPage, href: undefined });
|
||||
if (defaultPage) {
|
||||
docsHomeHref = resolvePageHref(defaultPage, baseURL);
|
||||
docsHomeHref.then((href) => {
|
||||
if (docsHome.getState().reference === defaultPage) {
|
||||
docsHome.setState({ href });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!landed) {
|
||||
landed = true;
|
||||
const tab = getEmbeddableLandingTab(settings);
|
||||
if (tab === 'docs') {
|
||||
navigate(docsHomeHref ?? Promise.resolve(`${baseURL}/page/`));
|
||||
} else if (tab) {
|
||||
navToken.current++;
|
||||
router.push(`${baseURL}/${tab}`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'navigateToPage': {
|
||||
navigate(resolvePageHref(message.pagePath, baseURL));
|
||||
// Resolve the target server-side: on a multi-space site the page may
|
||||
// live in another space/section, whose base must go before
|
||||
// `~gitbook/embed/page`. Fall back to a same-space push on failure.
|
||||
const { pagePath } = message;
|
||||
const token = ++navToken.current;
|
||||
// Ignore the result if a later navigation has since superseded this one.
|
||||
const push = (href: string) => {
|
||||
if (navToken.current === token) {
|
||||
router.push(href);
|
||||
}
|
||||
};
|
||||
resolveEmbedPageLink(pagePath)
|
||||
.then((resolved) =>
|
||||
push('href' in resolved ? resolved.href : `${baseURL}/page/${pagePath}`)
|
||||
)
|
||||
.catch(() => push(`${baseURL}/page/${pagePath}`));
|
||||
break;
|
||||
}
|
||||
case 'navigateToAssistant': {
|
||||
@@ -167,7 +132,7 @@ export function useEmbeddableConfiguration<T = GitBookEmbeddableConfiguration>(
|
||||
|
||||
export function useEmbeddableTabs() {
|
||||
const configuredTabs = useEmbeddableConfiguration((state) => state.tabs);
|
||||
return configuredTabs.length > 0 ? configuredTabs : DEFAULT_EMBEDDABLE_TABS;
|
||||
return configuredTabs.length > 0 ? configuredTabs : ['assistant', 'search', 'docs'];
|
||||
}
|
||||
|
||||
export function useEmbeddableLinkContext() {
|
||||
@@ -235,7 +200,6 @@ export function EmbeddableIframeTabs(props: {
|
||||
const { ref, active = 'assistant', baseURL, siteTitle, onNavigate } = props;
|
||||
const actions = useEmbeddableConfiguration((state) => state.actions);
|
||||
const tabs = useEmbeddableTabs();
|
||||
const docsHomeHref = useStore(docsHome, (state) => state.href);
|
||||
|
||||
const { assistants, config } = useAI();
|
||||
const language = useLanguage();
|
||||
@@ -264,7 +228,7 @@ export function EmbeddableIframeTabs(props: {
|
||||
key: 'docs',
|
||||
label: siteTitle,
|
||||
icon: 'book-open',
|
||||
href: docsHomeHref ?? `${baseURL}/page/`,
|
||||
href: `${baseURL}/page/`,
|
||||
}
|
||||
: null,
|
||||
].filter((tab) => tab !== null);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { CustomizationDefaultThemeMode } from '@gitbook/api';
|
||||
import { SiteInsightsTrademarkPlacement } from '@gitbook/api';
|
||||
|
||||
import { LoadIntegrations } from '../Integrations';
|
||||
import { NavigationLoader } from '../primitives/NavigationLoader';
|
||||
import { SpaceLayoutServerContext } from '../SpaceLayout';
|
||||
import { Trademark } from '../TableOfContents/Trademark';
|
||||
@@ -94,8 +93,6 @@ export async function EmbeddableRootLayout({
|
||||
/>
|
||||
</SpaceLayoutServerContext>
|
||||
</EmbeddableAIContextProvider>
|
||||
{/* Embeds load no integrations; this marks them loaded so search stops waiting. */}
|
||||
<LoadIntegrations />
|
||||
</SiteLayoutClientContexts>
|
||||
</CustomizationRootLayout>
|
||||
);
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
|
||||
import { getEmbeddableLandingTab } from './getEmbeddableLandingTab';
|
||||
|
||||
describe('getEmbeddableLandingTab', () => {
|
||||
it('keeps the embed default when nothing is configured', () => {
|
||||
expect(getEmbeddableLandingTab({})).toBeNull();
|
||||
expect(getEmbeddableLandingTab({ tabs: ['docs', 'search'] })).toBeNull();
|
||||
});
|
||||
|
||||
it('opens on the configured tab', () => {
|
||||
expect(getEmbeddableLandingTab({ defaultTab: 'search' })).toBe('search');
|
||||
expect(
|
||||
getEmbeddableLandingTab({ tabs: ['assistant', 'search', 'docs'], defaultTab: 'search' })
|
||||
).toBe('search');
|
||||
});
|
||||
|
||||
it('opens on the docs when only a default page is set', () => {
|
||||
expect(getEmbeddableLandingTab({ defaultPage: 'getting-started' })).toBe('docs');
|
||||
});
|
||||
|
||||
it('prefers the configured tab over the default page', () => {
|
||||
expect(
|
||||
getEmbeddableLandingTab({ defaultTab: 'assistant', defaultPage: 'getting-started' })
|
||||
).toBe('assistant');
|
||||
});
|
||||
|
||||
it('ignores a tab that is not enabled', () => {
|
||||
expect(getEmbeddableLandingTab({ tabs: ['assistant'], defaultTab: 'search' })).toBeNull();
|
||||
expect(
|
||||
getEmbeddableLandingTab({ tabs: ['assistant', 'search'], defaultPage: 'quickstart' })
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('ignores an unknown tab', () => {
|
||||
// @ts-expect-error - the parent window is plain JS and can send anything.
|
||||
expect(getEmbeddableLandingTab({ defaultTab: 'settings' })).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { GitBookEmbeddableConfiguration } from '@gitbook/embed';
|
||||
|
||||
export type EmbeddableTab = GitBookEmbeddableConfiguration['tabs'][number];
|
||||
|
||||
export const DEFAULT_EMBEDDABLE_TABS: EmbeddableTab[] = ['assistant', 'search', 'docs'];
|
||||
|
||||
export function getEmbeddableLandingTab(
|
||||
settings: Partial<Pick<GitBookEmbeddableConfiguration, 'tabs' | 'defaultTab' | 'defaultPage'>>
|
||||
): EmbeddableTab | null {
|
||||
const tab = settings.defaultTab ?? (settings.defaultPage ? 'docs' : undefined);
|
||||
if (!tab || !DEFAULT_EMBEDDABLE_TABS.includes(tab)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tabs = settings.tabs?.length ? settings.tabs : DEFAULT_EMBEDDABLE_TABS;
|
||||
return tabs.includes(tab) ? tab : null;
|
||||
}
|
||||
@@ -2,10 +2,9 @@ import type { SiteSpace } from '@gitbook/api';
|
||||
import type { IconName } from '@gitbook/icons';
|
||||
|
||||
import type { ButtonProps } from '../primitives';
|
||||
import { getTargetURLForSiteSpace } from '../SiteSections/encodeClientSiteSections';
|
||||
import { SpacesDropdownClient } from './SpacesDropdownClient';
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
import { getLocalizedTitle } from '@/lib/sites';
|
||||
import { getLocalizedTitle, getSiteSpaceURL } from '@/lib/sites';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
// Memoized regex for checking if a string starts with an emoji
|
||||
@@ -21,18 +20,9 @@ export function SpacesDropdown(
|
||||
siteSpace: SiteSpace;
|
||||
siteSpaces: SiteSpace[];
|
||||
className?: string;
|
||||
/** Keep the links inside the embed rather than sending readers to the full site. */
|
||||
asEmbeddable?: boolean;
|
||||
} & ButtonProps
|
||||
) {
|
||||
const {
|
||||
context,
|
||||
siteSpace,
|
||||
siteSpaces,
|
||||
className,
|
||||
asEmbeddable = false,
|
||||
...buttonProps
|
||||
} = props;
|
||||
const { context, siteSpace, siteSpaces, className, ...buttonProps } = props;
|
||||
const currentLanguage = context.locale;
|
||||
|
||||
const dropdownClassName = tcls(
|
||||
@@ -43,7 +33,7 @@ export function SpacesDropdown(
|
||||
const slimSpaces = siteSpaces.map((siteSp) => ({
|
||||
id: siteSp.id,
|
||||
title: getLocalizedTitle(siteSp, currentLanguage),
|
||||
url: getTargetURLForSiteSpace(context, siteSp, asEmbeddable),
|
||||
url: getSiteSpaceURL(context, siteSp),
|
||||
isActive: siteSp.id === siteSpace.id,
|
||||
spaceId: siteSp.space.id,
|
||||
}));
|
||||
@@ -67,10 +57,9 @@ export function TranslationsDropdown(
|
||||
siteSpace: SiteSpace;
|
||||
siteSpaces: SiteSpace[];
|
||||
className?: string;
|
||||
asEmbeddable?: boolean;
|
||||
} & ButtonProps
|
||||
) {
|
||||
const { context, siteSpace, siteSpaces, className, asEmbeddable, ...buttonProps } = props;
|
||||
const { context, siteSpace, siteSpaces, className, ...buttonProps } = props;
|
||||
|
||||
const title = getLocalizedTitle(siteSpace, context.locale);
|
||||
const hasEmojiPrefix = startsWithEmoji(title);
|
||||
@@ -81,7 +70,6 @@ export function TranslationsDropdown(
|
||||
context={context}
|
||||
siteSpace={siteSpace}
|
||||
siteSpaces={siteSpaces}
|
||||
asEmbeddable={asEmbeddable}
|
||||
variant="blank"
|
||||
className={tcls(
|
||||
'bg-transparent lg:max-w-64 max-md:[&_.button-content]:hidden',
|
||||
|
||||
@@ -4,7 +4,6 @@ import * as React from 'react';
|
||||
import { useDebounceCallback, useEventCallback } from 'usehooks-ts';
|
||||
|
||||
import type * as api from '@gitbook/api';
|
||||
import { SiteInsightsDisplayContext } from '@gitbook/api';
|
||||
import { OpenAPIOperationContextProvider } from '@gitbook/react-openapi';
|
||||
|
||||
import { type CurrentContentContext, useCurrentContent } from '../hooks';
|
||||
@@ -76,15 +75,12 @@ export function InsightsProvider(props: InsightsProviderProps) {
|
||||
[pathname: string]:
|
||||
| {
|
||||
url: string;
|
||||
previousUrl: string | null;
|
||||
events: TrackEventInput<InsightsEventName>[];
|
||||
context: CurrentContentContext;
|
||||
pageContext?: InsightsEventPageContext;
|
||||
}
|
||||
| undefined;
|
||||
}>({});
|
||||
// Href of the last page an event was tracked on; `undefined` until the first event.
|
||||
const lastUrlRef = React.useRef<string | null | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Synchronously flush all the pending events.
|
||||
@@ -109,7 +105,6 @@ export function InsightsProvider(props: InsightsProviderProps) {
|
||||
allEvents.push(
|
||||
...transformEvents({
|
||||
url: eventsForPathname.url,
|
||||
previousUrl: eventsForPathname.previousUrl,
|
||||
events: eventsForPathname.events,
|
||||
context: currentContent,
|
||||
pageContext: eventsForPathname.pageContext,
|
||||
@@ -159,9 +154,6 @@ export function InsightsProvider(props: InsightsProviderProps) {
|
||||
) => {
|
||||
const pathname = window.location.pathname;
|
||||
const previous = eventsRef.current[pathname];
|
||||
const lastUrl =
|
||||
lastUrlRef.current === undefined ? getSameOriginReferrer() : lastUrlRef.current;
|
||||
lastUrlRef.current = window.location.href;
|
||||
eventsRef.current[pathname] = {
|
||||
// An explicitly-provided context wins so page-scoped events (e.g. feedback) can
|
||||
// attribute to their page even when the pathname's ambient context has none — such
|
||||
@@ -169,7 +161,6 @@ export function InsightsProvider(props: InsightsProviderProps) {
|
||||
// context keep the stored one.
|
||||
pageContext: ctx ?? previous?.pageContext,
|
||||
url: previous?.url ?? window.location.href,
|
||||
previousUrl: previous ? previous.previousUrl : lastUrl,
|
||||
events: [
|
||||
...(previous?.events ?? []),
|
||||
{
|
||||
@@ -223,18 +214,6 @@ export function useTrackEvent(): TrackEventCallback {
|
||||
return React.useContext(InsightsContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* The referrer when it's another page of this origin, so a full page load (e.g. an absolute link
|
||||
* to a missing page) still records the page it came from.
|
||||
*/
|
||||
function getSameOriginReferrer(): string | null {
|
||||
if (document.referrer === window.location.href || !URL.canParse(document.referrer)) {
|
||||
return null;
|
||||
}
|
||||
const referrer = new URL(document.referrer);
|
||||
return referrer.origin === window.location.origin ? referrer.href : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Post the events to the server.
|
||||
*/
|
||||
@@ -261,7 +240,6 @@ function sendEvents(args: { eventUrl: string; events: api.SiteInsightsEvent[] })
|
||||
*/
|
||||
function transformEvents(input: {
|
||||
url: string;
|
||||
previousUrl: string | null;
|
||||
events: TrackEventInput<InsightsEventName>[];
|
||||
context: CurrentContentContext;
|
||||
pageContext: InsightsEventPageContext;
|
||||
@@ -280,11 +258,6 @@ function transformEvents(input: {
|
||||
|
||||
const location: api.SiteInsightsEventLocation = {
|
||||
url: input.url,
|
||||
// The embed's navigation is between its own tabs, not pages of the site.
|
||||
previousUrl:
|
||||
input.pageContext.displayContext === SiteInsightsDisplayContext.Embed
|
||||
? null
|
||||
: input.previousUrl,
|
||||
siteSection: input.context.siteSectionId ?? null,
|
||||
siteSpace: input.context.siteSpaceId ?? null,
|
||||
space: input.context.spaceId,
|
||||
|
||||
@@ -147,10 +147,6 @@ export function useCustomCookieBanner(): CustomCookieBannerStore {
|
||||
export function LoadIntegrations() {
|
||||
React.useEffect(() => {
|
||||
return whenIntegrationsReady(() => {
|
||||
// Integrations must only receive `load` once, even if this is mounted twice.
|
||||
if (integrationsStore.getState().loaded) {
|
||||
return;
|
||||
}
|
||||
dispatchGitBookIntegrationEvent('load');
|
||||
integrationsStore.setState({ loaded: true });
|
||||
});
|
||||
|
||||
@@ -67,7 +67,7 @@ export function ActionOpenAssistant(props: {
|
||||
href: page.href,
|
||||
});
|
||||
}
|
||||
assistant.open(undefined, 'page');
|
||||
assistant.open();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
type JSONDocument,
|
||||
type RevisionPageDocument,
|
||||
SiteInsightsDisplayContext,
|
||||
} from '@gitbook/api';
|
||||
import type { JSONDocument, RevisionPageDocument, SiteInsightsDisplayContext } from '@gitbook/api';
|
||||
|
||||
import { DocumentView, DocumentViewSkeleton } from '../DocumentView';
|
||||
import { CurrentPageProvider } from '../hooks/useCurrentPage';
|
||||
@@ -131,7 +127,6 @@ export async function PageBody(props: {
|
||||
ancestors={ancestors}
|
||||
withRSSFeed={contentHasUpdates}
|
||||
hasAPIBlocks={hasAPIBlocks}
|
||||
asEmbeddable={insightsDisplayContext === SiteInsightsDisplayContext.Embed}
|
||||
/>
|
||||
{document && !isNodeEmpty(document) ? (
|
||||
<OptionalSuspense
|
||||
|
||||
@@ -29,21 +29,18 @@ interface PageCoverImageProps {
|
||||
|
||||
export function PageCoverImage(props: PageCoverImageProps) {
|
||||
const { imgs, y, height, mask } = props;
|
||||
const { objectPositionY, isLoading } = useCoverPosition(imgs, y, {
|
||||
height,
|
||||
aspectRatio: PAGE_COVER_SIZE,
|
||||
});
|
||||
const { containerRef, objectPositionY, isLoading } = useCoverPosition(imgs, y);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="h-full w-full overflow-hidden">
|
||||
<div className="h-full w-full overflow-hidden" ref={containerRef}>
|
||||
<div className="h-full w-full animate-pulse bg-gradient-to-br from-gray-100 to-gray-200 dark:from-gray-800 dark:to-gray-900" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full w-full overflow-hidden @container" style={{ height }}>
|
||||
<div className="h-full w-full overflow-hidden" ref={containerRef} style={{ height }}>
|
||||
<img
|
||||
src={imgs.light.src}
|
||||
srcSet={imgs.light.srcSet}
|
||||
@@ -55,7 +52,7 @@ export function PageCoverImage(props: PageCoverImageProps) {
|
||||
aspectRatio: height
|
||||
? undefined
|
||||
: `${PAGE_COVER_SIZE.width}/${PAGE_COVER_SIZE.height}`,
|
||||
objectPosition: `50% ${objectPositionY}`,
|
||||
objectPosition: `50% ${objectPositionY}%`,
|
||||
height, // if no height is passed, no height will be set.
|
||||
maskComposite: 'intersect',
|
||||
maskImage:
|
||||
@@ -76,7 +73,7 @@ export function PageCoverImage(props: PageCoverImageProps) {
|
||||
aspectRatio: height
|
||||
? undefined
|
||||
: `${PAGE_COVER_SIZE.width}/${PAGE_COVER_SIZE.height}`,
|
||||
objectPosition: `50% ${objectPositionY}`,
|
||||
objectPosition: `50% ${objectPositionY}%`,
|
||||
height, // if no height is passed, no height will be set.
|
||||
maskComposite: 'intersect',
|
||||
maskImage:
|
||||
|
||||
@@ -19,17 +19,14 @@ import {
|
||||
import { PageAsideToggleButton } from '../PageAside/PageAsideButton';
|
||||
import { PageIcon } from '../PageIcon';
|
||||
import { getPDFURLSearchParams } from '../PDF';
|
||||
import {
|
||||
findBestTargetURL,
|
||||
getTargetURLForSiteSpace,
|
||||
} from '../SiteSections/encodeClientSiteSections';
|
||||
import { findBestTargetURL } from '../SiteSections/encodeClientSiteSections';
|
||||
import { categorizeVariants } from '../SpaceLayout/categorizeVariants';
|
||||
import { BreadcrumbItemDropdown, type BreadcrumbSibling } from './BreadcrumbItemDropdown';
|
||||
import { PageTags } from './PageTags';
|
||||
import type { GitBookSiteContext, SiteStructureNode } from '@/lib/context';
|
||||
import { hasAdaptiveMcpEndpoint } from '@/lib/mcp/endpoints';
|
||||
import { type AncestorRevisionPage, resolveFirstDocument } from '@/lib/pages';
|
||||
import { getLocalizedTitle } from '@/lib/sites';
|
||||
import { getLocalizedTitle, getSiteSpaceURL } from '@/lib/sites';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { getPageRSSURL } from '@/routes/rss';
|
||||
|
||||
@@ -44,10 +41,8 @@ export async function PageHeader(props: {
|
||||
* exactly as-is.
|
||||
*/
|
||||
hasAPIBlocks: boolean;
|
||||
/** Keep the context crumbs' links inside the embed rather than sending readers to the full site. */
|
||||
asEmbeddable: boolean;
|
||||
}) {
|
||||
const { context, page, ancestors, withRSSFeed, hasAPIBlocks, asEmbeddable } = props;
|
||||
const { context, page, ancestors, withRSSFeed, hasAPIBlocks } = props;
|
||||
const { revision, linker } = context;
|
||||
|
||||
const hasAncestors = ancestors.length > 0;
|
||||
@@ -87,10 +82,7 @@ export async function PageHeader(props: {
|
||||
// Section groups have no URL of their own; only sections are navigable. Resolve the
|
||||
// section to the site space matching the current variant so switching sections keeps
|
||||
// the reader on their current variant (same logic as the section tabs).
|
||||
href:
|
||||
node.object === 'site-section'
|
||||
? findBestTargetURL(context, node, asEmbeddable)
|
||||
: undefined,
|
||||
href: node.object === 'site-section' ? findBestTargetURL(context, node) : undefined,
|
||||
label: getLocalizedTitle(node, context.locale),
|
||||
icon: node.icon,
|
||||
siblings: siblings
|
||||
@@ -100,7 +92,7 @@ export async function PageHeader(props: {
|
||||
// Don't offer hidden sections/groups as switch targets.
|
||||
.filter((sibling) => !visibleSectionIds || visibleSectionIds.has(sibling.id))
|
||||
.map((sibling) => {
|
||||
const href = getSectionNodeURL(context, sibling, asEmbeddable);
|
||||
const href = getSectionNodeURL(context, sibling);
|
||||
// Keep siblings whose URL is "" (the site's first page); only drop the ones
|
||||
// with no resolvable URL at all.
|
||||
return href !== undefined
|
||||
@@ -120,7 +112,7 @@ export async function PageHeader(props: {
|
||||
if (currentSiteSpace) {
|
||||
contextCrumbs.push({
|
||||
key: `variant-${currentSiteSpace.id}`,
|
||||
href: getTargetURLForSiteSpace(context, currentSiteSpace, asEmbeddable),
|
||||
href: getSiteSpaceURL(context, currentSiteSpace),
|
||||
label: getLocalizedTitle(currentSiteSpace, context.locale),
|
||||
siblings: [],
|
||||
// Reuse the header's variant switcher rather than plain per-variant links, so each entry
|
||||
@@ -130,7 +122,7 @@ export async function PageHeader(props: {
|
||||
slimSpaces: variantSpaces.map((siteSpace) => ({
|
||||
id: siteSpace.id,
|
||||
title: getLocalizedTitle(siteSpace, context.locale),
|
||||
url: getTargetURLForSiteSpace(context, siteSpace, asEmbeddable),
|
||||
url: getSiteSpaceURL(context, siteSpace),
|
||||
isActive: siteSpace.id === currentSiteSpace.id,
|
||||
spaceId: siteSpace.space.id,
|
||||
})),
|
||||
@@ -456,13 +448,9 @@ function collectSectionNodeIds(list: SiteStructureNode[]): Set<string> {
|
||||
* The URL resolves to the site space matching the current variant (same logic as the section tabs),
|
||||
* so switching sections keeps the reader on their current variant.
|
||||
*/
|
||||
function getSectionNodeURL(
|
||||
context: GitBookSiteContext,
|
||||
node: SectionNode,
|
||||
asEmbeddable: boolean
|
||||
): string | undefined {
|
||||
function getSectionNodeURL(context: GitBookSiteContext, node: SectionNode): string | undefined {
|
||||
const section = findFirstSection(node);
|
||||
return section ? findBestTargetURL(context, section, asEmbeddable) : undefined;
|
||||
return section ? findBestTargetURL(context, section) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
import { useLayoutEffect, useState } from 'react';
|
||||
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useResizeObserver } from 'usehooks-ts';
|
||||
|
||||
interface ImageSize {
|
||||
width: number;
|
||||
@@ -21,18 +22,19 @@ interface Images {
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to compute the CSS object position Y for a cover image, from the y offset and the image
|
||||
* dimensions. The container must set `container-type: inline-size` and be as wide as the image,
|
||||
* since the position is computed against its width.
|
||||
* Hook to calculate the object position Y percentage for a cover image
|
||||
* based on the y offset, image dimensions, and container dimensions.
|
||||
*/
|
||||
export function useCoverPosition(
|
||||
imgs: Images,
|
||||
y: number,
|
||||
container: { height: number | undefined; aspectRatio: ImageSize }
|
||||
) {
|
||||
export function useCoverPosition(imgs: Images, y: number) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [loadedDimensions, setLoadedDimensions] = useState<ImageSize | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(!imgs.light.size && !imgs.dark?.size);
|
||||
|
||||
const container = useResizeObserver({
|
||||
// @ts-expect-error wrong types
|
||||
ref: containerRef,
|
||||
});
|
||||
|
||||
// Load original image dimensions if not provided in `imgs`
|
||||
useLayoutEffect(() => {
|
||||
// Check if we have dimensions from dark (if provided) or else the default light.
|
||||
@@ -66,31 +68,42 @@ export function useCoverPosition(
|
||||
// Check dark first, then light, then loaded dimensions
|
||||
const imageDimensions = imgs.dark?.size ?? imgs.light.size ?? loadedDimensions;
|
||||
|
||||
// Calculate ratio and dimensions similar to useCoverPosition hook
|
||||
const ratio =
|
||||
imageDimensions && container.height && container.width
|
||||
? Math.max(
|
||||
container.width / imageDimensions.width,
|
||||
container.height / imageDimensions.height
|
||||
)
|
||||
: 1;
|
||||
const safeRatio = ratio || 1;
|
||||
|
||||
const scaledHeight =
|
||||
imageDimensions && container.height ? imageDimensions.height * safeRatio : null;
|
||||
const maxOffset =
|
||||
scaledHeight && container.height
|
||||
? Math.max(0, (scaledHeight - container.height) / 2 / safeRatio)
|
||||
: 0;
|
||||
|
||||
// Parse the position between the allowed min/max
|
||||
const objectPositionY = useMemo(() => {
|
||||
if (!container.height || !imageDimensions) {
|
||||
return 50;
|
||||
}
|
||||
|
||||
const scaled = imageDimensions.height * safeRatio;
|
||||
if (scaled <= container.height || maxOffset === 0) {
|
||||
return 50;
|
||||
}
|
||||
|
||||
const clampedOffset = Math.max(-maxOffset, Math.min(maxOffset, y));
|
||||
const relative = (maxOffset - clampedOffset) / (2 * maxOffset);
|
||||
return relative * 100;
|
||||
}, [container.height, imageDimensions, maxOffset, safeRatio, y]);
|
||||
|
||||
return {
|
||||
objectPositionY: imageDimensions
|
||||
? getCoverObjectPositionY(imageDimensions, y, container)
|
||||
: '50%',
|
||||
containerRef,
|
||||
objectPositionY,
|
||||
isLoading: !imageDimensions || isLoading,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset the image `y` natural pixels from centered, clamped so it keeps covering the container.
|
||||
* Expressed in CSS against the container width (`cqw`), so it renders the same on the server as
|
||||
* after hydration, without measuring the container.
|
||||
*/
|
||||
function getCoverObjectPositionY(
|
||||
image: ImageSize,
|
||||
y: number,
|
||||
container: { height: number | undefined; aspectRatio: ImageSize }
|
||||
): string {
|
||||
const containerHeight = container.height
|
||||
? `${container.height}px`
|
||||
: `${(100 * container.aspectRatio.height) / container.aspectRatio.width}cqw`;
|
||||
// Rendered height of the image under `object-fit: cover`.
|
||||
const scaledHeight = `max(${(100 * image.height) / image.width}cqw, ${containerHeight})`;
|
||||
const maxOffset = `(${scaledHeight} - ${containerHeight}) / 2`;
|
||||
const offset = `${scaledHeight} * ${y / image.height}`;
|
||||
|
||||
return `calc(50% + clamp(-1 * ${maxOffset}, ${offset}, ${maxOffset}))`;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import React from 'react';
|
||||
|
||||
import { useAI } from '../AI';
|
||||
import { useTrackEvent } from '../Insights';
|
||||
import { useIntegrationsLoaded } from '../Integrations';
|
||||
import { useBodyLoaded } from '../primitives';
|
||||
import {
|
||||
clearLastSearchQuery,
|
||||
@@ -141,7 +140,6 @@ export function useSearchController(
|
||||
const trackEvent = useTrackEvent();
|
||||
const resultsRef = React.useRef<SearchResultsRef>(null);
|
||||
const isLoaded = useBodyLoaded();
|
||||
const integrationsLoaded = useIntegrationsLoaded();
|
||||
|
||||
const restoredLastQueryForSiteSpaceRef = React.useRef<string | null>(null);
|
||||
React.useEffect(() => {
|
||||
@@ -171,8 +169,6 @@ export function useSearchController(
|
||||
|
||||
const withAI = assistants.length > 0;
|
||||
const withSearchAI = assistants.filter((assistant) => assistant.mode === 'search').length > 0;
|
||||
// Integration assistants register on window load, so wait for it before treating `ask` as a search query.
|
||||
const askIsSearchQuery = withSearchAI || (!withAI && integrationsLoaded);
|
||||
|
||||
// Handle initial ask state on page load, once assistants are ready.
|
||||
// `ask=` should still bootstrap the assistant on the docs site, so we must
|
||||
@@ -207,7 +203,7 @@ export function useSearchController(
|
||||
const query =
|
||||
prev?.query ??
|
||||
getLastSearchQuery(siteSpace.id) ??
|
||||
(askIsSearchQuery ? prev?.ask : null) ??
|
||||
(withSearchAI || !withAI ? prev?.ask : null) ??
|
||||
'';
|
||||
|
||||
return {
|
||||
@@ -221,7 +217,7 @@ export function useSearchController(
|
||||
trackEvent({
|
||||
type: 'search_open',
|
||||
});
|
||||
}, [state?.open, setSearchState, siteSpace.id, trackEvent, withAI, askIsSearchQuery]);
|
||||
}, [state?.open, setSearchState, siteSpace.id, trackEvent, withAI, withSearchAI]);
|
||||
|
||||
const setQuery = React.useCallback(
|
||||
(value: string) => {
|
||||
@@ -265,7 +261,7 @@ export function useSearchController(
|
||||
});
|
||||
|
||||
const searchValue =
|
||||
state?.query ?? (askIsSearchQuery ? state?.ask : null) ?? lastSearchQuery ?? '';
|
||||
state?.query ?? (withSearchAI || !withAI ? state?.ask : null) ?? lastSearchQuery ?? '';
|
||||
const searchResultsId = `search-results-${React.useId()}`;
|
||||
|
||||
// Only clears the remembered last query and stops any in-flight fetch — it must
|
||||
|
||||
@@ -82,7 +82,7 @@ type CoverOverlap =
|
||||
* `data-over-cover="split"` with the crossing point in `--cover-edge`.
|
||||
*/
|
||||
function useMarkTextOverCover() {
|
||||
React.useLayoutEffect(() => {
|
||||
React.useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
const pageCover = document.querySelector<HTMLElement>('[data-gb-page-cover]');
|
||||
|
||||
@@ -150,7 +150,7 @@ function useMarkTextOverCover() {
|
||||
});
|
||||
};
|
||||
|
||||
update();
|
||||
scheduleUpdate();
|
||||
|
||||
window.addEventListener('scroll', scheduleUpdate, { passive: true });
|
||||
window.addEventListener('resize', scheduleUpdate, { passive: true });
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
|
||||
import type { RevisionPageDocument } from '@gitbook/api';
|
||||
|
||||
import { getPageFullTitle } from './title';
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
|
||||
function makeContext(siteTitle: string, sectionTitle = ''): GitBookSiteContext {
|
||||
return {
|
||||
site: { title: siteTitle },
|
||||
visibleSections: sectionTitle
|
||||
? {
|
||||
current: { title: sectionTitle, default: false },
|
||||
list: [{ object: 'site-section' }, { object: 'site-section' }],
|
||||
}
|
||||
: undefined,
|
||||
visibleSiteSpaces: [],
|
||||
} as unknown as GitBookSiteContext;
|
||||
}
|
||||
|
||||
function makePage(title: string, tagTitle?: string): RevisionPageDocument {
|
||||
return { title, tagTitle } as unknown as RevisionPageDocument;
|
||||
}
|
||||
|
||||
describe('getPageFullTitle', () => {
|
||||
it('uses tagTitle as the first segment', () => {
|
||||
expect(getPageFullTitle(makeContext('GitBook'), makePage('Page title', 'SEO title'))).toBe(
|
||||
'SEO title | GitBook'
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to the page title when tagTitle is absent', () => {
|
||||
expect(getPageFullTitle(makeContext('GitBook'), makePage('Page title'))).toBe(
|
||||
'Page title | GitBook'
|
||||
);
|
||||
});
|
||||
|
||||
it('deduplicates the section title against tagTitle', () => {
|
||||
expect(
|
||||
getPageFullTitle(makeContext('GitBook', 'Section'), makePage('Page title', 'Section'))
|
||||
).toBe('Section | GitBook');
|
||||
});
|
||||
|
||||
it('deduplicates the site title against tagTitle', () => {
|
||||
expect(
|
||||
getPageFullTitle(makeContext('SEO title'), makePage('Page title', 'SEO title'))
|
||||
).toBe('SEO title');
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import { notFound, redirect } from 'next/navigation';
|
||||
import {
|
||||
CustomizationDefaultThemeMode,
|
||||
CustomizationHeaderPreset,
|
||||
type RevisionPageDocument,
|
||||
SiteInsightsDisplayContext,
|
||||
type TranslationLanguage,
|
||||
} from '@gitbook/api';
|
||||
@@ -17,7 +18,6 @@ import {
|
||||
getPathnameParam,
|
||||
} from './fetch';
|
||||
import { PageClientLayout } from './PageClientLayout';
|
||||
import { getPageFullTitle } from './title';
|
||||
import { UpdatesFilterProvider } from '@/components/DocumentView/UpdatesFilter';
|
||||
import { UpdatesFilterScript } from '@/components/DocumentView/UpdatesFilterScript';
|
||||
import { PageAside } from '@/components/PageAside';
|
||||
@@ -33,7 +33,11 @@ import { getResizedImageURL } from '@/lib/images';
|
||||
import { getPagePath } from '@/lib/pages';
|
||||
import { resolveContentRef } from '@/lib/references';
|
||||
import { isPageIndexable, isSiteIndexable } from '@/lib/seo';
|
||||
import { getSiteSpacePagePaths, resolveSiteSpaceCustomHomePage } from '@/lib/sites';
|
||||
import {
|
||||
getSiteSpacePagePaths,
|
||||
getSiteStructureTitle,
|
||||
resolveSiteSpaceCustomHomePage,
|
||||
} from '@/lib/sites';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import {
|
||||
generateUpdatesFilterCSS,
|
||||
@@ -41,8 +45,6 @@ import {
|
||||
updatesFilterStyleHref,
|
||||
} from '@/lib/updates';
|
||||
|
||||
export { getPageFullTitle } from './title';
|
||||
|
||||
export type SitePageProps = {
|
||||
context: GitBookSiteContext;
|
||||
pageParams: PagePathParams;
|
||||
@@ -284,11 +286,11 @@ export async function getSitePageData(props: SitePageProps) {
|
||||
if (pathname !== null) {
|
||||
// If the pathname was not normalized, redirect to the normalized version
|
||||
// before trying to resolve the page again
|
||||
redirect(context.linker.toPathForPagePath({ path: pathname }));
|
||||
redirect(context.linker.toPathInSpace(pathname));
|
||||
} else {
|
||||
// If the page is not found and we are in fallback mode, return a redirect to the basepath
|
||||
if (context.isFallback) {
|
||||
redirect(context.linker.toPathForPagePath({ path: '/' }));
|
||||
redirect(context.linker.toPathInSpace('/'));
|
||||
}
|
||||
notFound();
|
||||
}
|
||||
@@ -419,3 +421,20 @@ async function resolvePageMetaLinks(
|
||||
alternates: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the <title> for a page.
|
||||
*/
|
||||
export function getPageFullTitle(context: GitBookSiteContext, page: RevisionPageDocument) {
|
||||
const { site } = context;
|
||||
const siteStructureTitle = getSiteStructureTitle(context);
|
||||
|
||||
return [
|
||||
page.title,
|
||||
// Prevent duplicate titles by comparing against the page title.
|
||||
page.title !== siteStructureTitle ? siteStructureTitle : null, // The first page of a section is often the same as the section title, so we don't need to show it.
|
||||
page.title !== site.title ? site.title : null, // The site title can also be the same as the site title on the site's landing page.
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' | ');
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import { useSetSearchState } from '../Search';
|
||||
import { fetchSiteIndex } from '../Search/site-index';
|
||||
import { SiteAuthLoginButton } from '../SiteAuth/SiteAuthLoginLink';
|
||||
import {
|
||||
useIsLoggedInVisitor,
|
||||
useSiteAdaptiveAuthLoginHref,
|
||||
useSiteIndexURL,
|
||||
useSpaceBasePath,
|
||||
@@ -45,9 +44,6 @@ export function SitePageNotFound() {
|
||||
const setSearchState = useSetSearchState();
|
||||
const { assistants } = useAI();
|
||||
|
||||
const isAuthenticated = useIsLoggedInVisitor();
|
||||
const copy = getNotFoundCopy({ hasAdaptiveLogin: !!adaptiveAuthLoginHref, isAuthenticated });
|
||||
|
||||
// Show the assistant input when a non-search assistant is available (i.e. not just ask-AI).
|
||||
const assistant = assistants.find((candidate) => candidate.mode !== 'search') ?? null;
|
||||
const inputLabel = tString(language, assistant ? 'search_or_ask' : 'search');
|
||||
@@ -128,20 +124,21 @@ export function SitePageNotFound() {
|
||||
'text-tint-strong'
|
||||
)}
|
||||
>
|
||||
{t(language, copy.title)}
|
||||
{t(
|
||||
language,
|
||||
adaptiveAuthLoginHref
|
||||
? 'notfound_adaptive_title'
|
||||
: 'notfound_title'
|
||||
)}
|
||||
</h1>
|
||||
<p className={tcls('text-base', 'text-tint', 'text-center')}>
|
||||
{t(language, copy.message)}
|
||||
<p className={tcls('text-base', 'text-tint')}>
|
||||
{t(
|
||||
language,
|
||||
adaptiveAuthLoginHref ? 'notfound_adaptive' : 'notfound'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{adaptiveAuthLoginHref && isAuthenticated ? (
|
||||
// Re-running login refreshes the visitor's claims, e.g. after a role change.
|
||||
<SiteAuthLoginButton
|
||||
href={adaptiveAuthLoginHref}
|
||||
variant="secondary"
|
||||
label={t(language, 'notfound_adaptive_login_again')}
|
||||
/>
|
||||
) : adaptiveAuthLoginHref ? (
|
||||
{adaptiveAuthLoginHref ? (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<SiteAuthLoginButton
|
||||
href={adaptiveAuthLoginHref}
|
||||
@@ -193,19 +190,6 @@ export function SitePageNotFound() {
|
||||
);
|
||||
}
|
||||
|
||||
function getNotFoundCopy(args: { hasAdaptiveLogin: boolean; isAuthenticated: boolean }) {
|
||||
if (!args.hasAdaptiveLogin) {
|
||||
return { title: 'notfound_title', message: 'notfound' } as const;
|
||||
}
|
||||
if (args.isAuthenticated) {
|
||||
return {
|
||||
title: 'notfound_adaptive_no_access_title',
|
||||
message: 'notfound_adaptive_no_access',
|
||||
} as const;
|
||||
}
|
||||
return { title: 'notfound_adaptive_title', message: 'notfound_adaptive' } as const;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of related pages, with skeleton rows while they load. Renders nothing once we know
|
||||
* there are no suggestions.
|
||||
|
||||
@@ -6,8 +6,7 @@ import type { GitBookSiteContext } from '@/lib/context';
|
||||
|
||||
mock.module('server-only', () => ({}));
|
||||
|
||||
const { fetchPageData, getLowercasePathnameRedirect, resolveMissingPagePath } =
|
||||
await import('./fetch');
|
||||
const { fetchPageData, getLowercasePathnameRedirect } = await import('./fetch');
|
||||
const { normalizeURL } = await import('@/lib/data/urls');
|
||||
|
||||
const page = {
|
||||
@@ -95,70 +94,6 @@ describe('fetchPageData', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveMissingPagePath', () => {
|
||||
function createRedirectContext(options: {
|
||||
siteRedirect?: { target: string; permanent?: boolean };
|
||||
spaceRedirectPageId?: string;
|
||||
}) {
|
||||
const getSiteRedirectBySource = mock(async ({ source }: { source: string }) =>
|
||||
options.siteRedirect && source === '/old'
|
||||
? {
|
||||
data: {
|
||||
target: options.siteRedirect.target,
|
||||
redirect: { permanent: options.siteRedirect.permanent ?? false },
|
||||
},
|
||||
}
|
||||
: { error: { code: 404, message: 'Not found' } }
|
||||
);
|
||||
const getRevisionPageByPath = mock(async () =>
|
||||
options.spaceRedirectPageId
|
||||
? { data: { id: options.spaceRedirectPageId } }
|
||||
: { error: { code: 404, message: 'Not found' } }
|
||||
);
|
||||
|
||||
return {
|
||||
organizationId: 'org-1',
|
||||
site: { id: 'site-1' },
|
||||
space: { id: 'space-1', revision: 'revision-1' },
|
||||
revisionId: 'revision-1',
|
||||
revision: { pages: [page] },
|
||||
linker: {
|
||||
toPathInSpace: (path: string) => path,
|
||||
toRelativePathInSite: (path: string) => path,
|
||||
toLinkForContent: (url: string) => new URL(url).pathname,
|
||||
},
|
||||
dataFetcher: { getSiteRedirectBySource, getRevisionPageByPath },
|
||||
} as unknown as GitBookSiteContext;
|
||||
}
|
||||
|
||||
it('resolves a site redirect', async () => {
|
||||
const context = createRedirectContext({
|
||||
siteRedirect: { target: 'https://docs.example.com/new', permanent: true },
|
||||
});
|
||||
|
||||
expect(await resolveMissingPagePath(context, 'old')).toEqual({
|
||||
type: 'redirect',
|
||||
destination: '/new',
|
||||
permanent: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves a space redirect to a page', async () => {
|
||||
const context = createRedirectContext({ spaceRedirectPageId: page.id });
|
||||
|
||||
expect(await resolveMissingPagePath(context, 'old')).toEqual({
|
||||
type: 'page',
|
||||
page: { page, ancestors: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it('returns undefined when nothing matches', async () => {
|
||||
const context = createRedirectContext({});
|
||||
|
||||
expect(await resolveMissingPagePath(context, 'old')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLowercasePathnameRedirect', () => {
|
||||
it('redirects ASCII paths with uppercase letters', () => {
|
||||
expect(getLowercasePathnameRedirect('Foo/Bar')).toBe('foo/bar');
|
||||
|
||||
@@ -2,14 +2,13 @@ import { permanentRedirect, redirect } from 'next/navigation';
|
||||
|
||||
import {
|
||||
CustomizationPageActionType,
|
||||
type RevisionPageDocument,
|
||||
SITE_REDIRECT_SOURCE_PATH_MAX_LENGTH,
|
||||
SITE_REDIRECT_SOURCE_PATH_PATTERN,
|
||||
} from '@gitbook/api';
|
||||
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
import { getDataOrNull } from '@/lib/data';
|
||||
import { type ResolvedPagePath, resolvePageId } from '@/lib/pages';
|
||||
import { resolvePageId } from '@/lib/pages';
|
||||
import { withLeadingSlash } from '@/lib/paths';
|
||||
import { resolveSiteSpacePagePath } from '@/lib/sites';
|
||||
|
||||
@@ -71,7 +70,7 @@ export async function fetchPageData(context: GitBookSiteContext, params: PagePar
|
||||
* If the path can't be found, we try to resolve it from the API to handle redirects.
|
||||
*/
|
||||
async function resolvePage(context: GitBookSiteContext, params: PagePathParams | PageIdParams) {
|
||||
const { revision } = context;
|
||||
const { organizationId, site, space, revision, shareKey, linker, revisionId } = context;
|
||||
|
||||
if ('pageId' in params) {
|
||||
return resolvePageId(revision.pages, params.pageId);
|
||||
@@ -86,102 +85,72 @@ async function resolvePage(context: GitBookSiteContext, params: PagePathParams |
|
||||
return page;
|
||||
}
|
||||
|
||||
const fallback = await resolveMissingPagePath(context, rawPathname);
|
||||
if (fallback?.type === 'redirect') {
|
||||
return fallback.permanent
|
||||
? permanentRedirect(fallback.destination)
|
||||
: redirect(fallback.destination);
|
||||
}
|
||||
|
||||
return fallback?.page;
|
||||
}
|
||||
|
||||
export type MissingPagePathResolution =
|
||||
| {
|
||||
type: 'redirect';
|
||||
/** Destination as returned by `linker.toLinkForContent` (absolute path or URL). */
|
||||
destination: string;
|
||||
permanent: boolean;
|
||||
}
|
||||
| {
|
||||
type: 'page';
|
||||
page: ResolvedPagePath<RevisionPageDocument>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve a pathname that doesn't match any page of the revision, using site-level and space-level redirects.
|
||||
*/
|
||||
export async function resolveMissingPagePath(
|
||||
context: GitBookSiteContext,
|
||||
rawPathname: string
|
||||
): Promise<MissingPagePathResolution | undefined> {
|
||||
const { organizationId, site, space, revision, shareKey, linker, revisionId } = context;
|
||||
|
||||
// We don't test path that are too long as GitBook doesn't support them and will return a 404 anyway.
|
||||
// API has a limit of less than 512 characters for the source path, so we use the same limit here.
|
||||
if (rawPathname.length >= SITE_REDIRECT_SOURCE_PATH_MAX_LENGTH) {
|
||||
return undefined;
|
||||
}
|
||||
if (rawPathname.length < SITE_REDIRECT_SOURCE_PATH_MAX_LENGTH) {
|
||||
const SITE_REDIRECT_SOURCE_PATH_REGEX = new RegExp(SITE_REDIRECT_SOURCE_PATH_PATTERN);
|
||||
const redirectPathname = withLeadingSlash(rawPathname);
|
||||
// If a page can't be found, we try with the API, in case we have a redirect at site level.
|
||||
const redirectSources = new Set(
|
||||
[
|
||||
// Test the pathname relative to the root
|
||||
// For example hello/world -> section/variant/hello/world
|
||||
linker.toRelativePathInSite(linker.toPathInSpace(redirectPathname)),
|
||||
// Test the pathname relative to the content/space
|
||||
// For example hello/world -> /hello/world
|
||||
redirectPathname,
|
||||
]
|
||||
.map(toSiteRedirectSourceCandidate)
|
||||
.filter((source) => SITE_REDIRECT_SOURCE_PATH_REGEX.test(source))
|
||||
);
|
||||
|
||||
const SITE_REDIRECT_SOURCE_PATH_REGEX = new RegExp(SITE_REDIRECT_SOURCE_PATH_PATTERN);
|
||||
const redirectPathname = withLeadingSlash(rawPathname);
|
||||
// If a page can't be found, we try with the API, in case we have a redirect at site level.
|
||||
const redirectSources = new Set(
|
||||
[
|
||||
// Test the pathname relative to the root
|
||||
// For example hello/world -> section/variant/hello/world
|
||||
linker.toRelativePathInSite(linker.toPathInSpace(redirectPathname)),
|
||||
// Test the pathname relative to the content/space
|
||||
// For example hello/world -> /hello/world
|
||||
redirectPathname,
|
||||
]
|
||||
.map(toSiteRedirectSourceCandidate)
|
||||
.filter((source) => SITE_REDIRECT_SOURCE_PATH_REGEX.test(source))
|
||||
);
|
||||
|
||||
for (const source of redirectSources) {
|
||||
// We try to resolve the site redirect
|
||||
const resolvedSiteRedirect =
|
||||
source.length < SITE_REDIRECT_SOURCE_PATH_MAX_LENGTH &&
|
||||
(await getDataOrNull(
|
||||
context.dataFetcher.getSiteRedirectBySource({
|
||||
organizationId,
|
||||
siteId: site.id,
|
||||
source,
|
||||
siteShareKey: shareKey,
|
||||
})
|
||||
));
|
||||
if (resolvedSiteRedirect) {
|
||||
const isPublicLiveContext =
|
||||
!shareKey &&
|
||||
!context.changeRequest &&
|
||||
!context.preview &&
|
||||
context.revisionId === context.space.revision &&
|
||||
!context.isLoggedInVisitor;
|
||||
return {
|
||||
type: 'redirect',
|
||||
destination: linker.toLinkForContent(resolvedSiteRedirect.target),
|
||||
permanent: Boolean(
|
||||
for (const source of redirectSources) {
|
||||
// We try to resolve the site redirect
|
||||
const resolvedSiteRedirect =
|
||||
source.length < SITE_REDIRECT_SOURCE_PATH_MAX_LENGTH &&
|
||||
(await getDataOrNull(
|
||||
context.dataFetcher.getSiteRedirectBySource({
|
||||
organizationId,
|
||||
siteId: site.id,
|
||||
source,
|
||||
siteShareKey: shareKey,
|
||||
})
|
||||
));
|
||||
if (resolvedSiteRedirect) {
|
||||
const destination = linker.toLinkForContent(resolvedSiteRedirect.target);
|
||||
const isPublicLiveContext =
|
||||
!shareKey &&
|
||||
!context.changeRequest &&
|
||||
!context.preview &&
|
||||
context.revisionId === context.space.revision &&
|
||||
!context.isLoggedInVisitor;
|
||||
if (
|
||||
resolvedSiteRedirect.redirect?.permanent &&
|
||||
!resolvedSiteRedirect.redirect.draft &&
|
||||
isPublicLiveContext
|
||||
),
|
||||
};
|
||||
) {
|
||||
return permanentRedirect(destination);
|
||||
}
|
||||
return redirect(destination);
|
||||
}
|
||||
}
|
||||
|
||||
// If page still can't be found, we try with the API, in case we have a redirect at space level.
|
||||
// We use the raw pathname to handle special/malformed redirects setup by users in the GitSync.
|
||||
// The page rendering will take care of redirecting to a normalized pathname.
|
||||
const resolved = await getDataOrNull(
|
||||
context.dataFetcher.getRevisionPageByPath({
|
||||
spaceId: space.id,
|
||||
revisionId: revisionId,
|
||||
path: rawPathname,
|
||||
})
|
||||
);
|
||||
if (resolved) {
|
||||
return resolvePageId(revision.pages, resolved.id);
|
||||
}
|
||||
}
|
||||
|
||||
// If page still can't be found, we try with the API, in case we have a redirect at space level.
|
||||
// We use the raw pathname to handle special/malformed redirects setup by users in the GitSync.
|
||||
// The page rendering will take care of redirecting to a normalized pathname.
|
||||
const resolved = await getDataOrNull(
|
||||
context.dataFetcher.getRevisionPageByPath({
|
||||
spaceId: space.id,
|
||||
revisionId: revisionId,
|
||||
path: rawPathname,
|
||||
})
|
||||
);
|
||||
const page = resolved ? resolvePageId(revision.pages, resolved.id) : undefined;
|
||||
return page ? { type: 'page', page } : undefined;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { RevisionPageDocument } from '@gitbook/api';
|
||||
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
import { getSiteStructureTitle } from '@/lib/sites';
|
||||
|
||||
/**
|
||||
* Get the <title> for a page.
|
||||
*/
|
||||
export function getPageFullTitle(context: GitBookSiteContext, page: RevisionPageDocument) {
|
||||
const { site } = context;
|
||||
const siteStructureTitle = getSiteStructureTitle(context);
|
||||
const tagTitle = page.tagTitle || page.title;
|
||||
|
||||
return [
|
||||
tagTitle,
|
||||
// The first page of a section is often the same as the section title, so we don't need to show it.
|
||||
tagTitle !== siteStructureTitle ? siteStructureTitle : null,
|
||||
// The site title can also be the same as the page title on the site's landing page.
|
||||
tagTitle !== site.title ? site.title : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' | ');
|
||||
}
|
||||
@@ -201,7 +201,7 @@ function getTargetURLForSection(
|
||||
return getSectionURL(context, section);
|
||||
}
|
||||
|
||||
export function getTargetURLForSiteSpace(
|
||||
function getTargetURLForSiteSpace(
|
||||
context: GitBookSiteContext,
|
||||
siteSpace: SiteSpace,
|
||||
asEmbeddable: boolean
|
||||
|
||||
@@ -72,7 +72,6 @@ export function SpaceLayoutServerContext(props: SpaceLayoutProps) {
|
||||
<SpaceLayoutContextProvider
|
||||
basePath={context.linker.toPathInSpace('')}
|
||||
siteAdaptiveAuthLoginHref={siteAdaptiveAuthLoginHref}
|
||||
isLoggedInVisitor={context.isLoggedInVisitor}
|
||||
siteIndexURL={context.linker.toPathInSite('~gitbook/site-index')}
|
||||
>
|
||||
<AdaptiveVisitorContextProvider
|
||||
|
||||
@@ -5,7 +5,6 @@ import React from 'react';
|
||||
const SpaceLayoutContext = React.createContext({
|
||||
basePath: '',
|
||||
siteAdaptiveAuthLoginHref: null as string | null,
|
||||
isLoggedInVisitor: false,
|
||||
siteIndexURL: '',
|
||||
});
|
||||
|
||||
@@ -16,21 +15,14 @@ export function SpaceLayoutContextProvider(
|
||||
props: React.PropsWithChildren<{
|
||||
basePath: string;
|
||||
siteAdaptiveAuthLoginHref?: string | null;
|
||||
isLoggedInVisitor?: boolean;
|
||||
siteIndexURL: string;
|
||||
}>
|
||||
) {
|
||||
const {
|
||||
basePath,
|
||||
siteAdaptiveAuthLoginHref = null,
|
||||
isLoggedInVisitor = false,
|
||||
siteIndexURL,
|
||||
children,
|
||||
} = props;
|
||||
const { basePath, siteAdaptiveAuthLoginHref = null, siteIndexURL, children } = props;
|
||||
|
||||
const value = React.useMemo(
|
||||
() => ({ basePath, siteAdaptiveAuthLoginHref, isLoggedInVisitor, siteIndexURL }),
|
||||
[basePath, siteAdaptiveAuthLoginHref, isLoggedInVisitor, siteIndexURL]
|
||||
() => ({ basePath, siteAdaptiveAuthLoginHref, siteIndexURL }),
|
||||
[basePath, siteAdaptiveAuthLoginHref, siteIndexURL]
|
||||
);
|
||||
|
||||
return <SpaceLayoutContext.Provider value={value}>{children}</SpaceLayoutContext.Provider>;
|
||||
@@ -58,17 +50,6 @@ export function useSiteAdaptiveAuthLoginHref() {
|
||||
return context.siteAdaptiveAuthLoginHref;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the visitor has a visitor auth token, with or without claims.
|
||||
*/
|
||||
export function useIsLoggedInVisitor() {
|
||||
const context = React.useContext(SpaceLayoutContext);
|
||||
if (!context) {
|
||||
throw new Error('SpaceLayoutContext not found');
|
||||
}
|
||||
return context.isLoggedInVisitor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the URL of the site search index (`~gitbook/site-index`).
|
||||
*/
|
||||
|
||||
@@ -29,8 +29,7 @@ export function PageGroupItem(props: { page: ClientTOCPageGroup; isFirst?: boole
|
||||
<div ref={sentinelRef} className="h-0" aria-hidden="true" />
|
||||
<div
|
||||
className={tcls(
|
||||
// No opacity transition on ::after: in Chrome it leaves stale pixels over the first child after the list scrolls.
|
||||
'-top-4 sticky z-1 after:pointer-events-none after:absolute after:inset-x-0 after:top-full after:h-4 after:bg-linear-to-b after:from-tint-base after:to-transparent',
|
||||
'-top-4 sticky z-1 after:pointer-events-none after:absolute after:inset-x-0 after:top-full after:h-4 after:bg-linear-to-b after:from-tint-base after:to-transparent after:transition-opacity',
|
||||
isSticking ? '' : 'after:opacity-0',
|
||||
'mt-1 pt-2.5 pb-0',
|
||||
'bg-tint-base',
|
||||
|
||||
@@ -134,7 +134,6 @@ export async function TableOfContents(props: {
|
||||
orientation="vertical"
|
||||
contentClassName="flex flex-col p-2 gutter-stable"
|
||||
active="[data-active=true]"
|
||||
followActive
|
||||
leading={{
|
||||
fade: true,
|
||||
button: {
|
||||
|
||||
@@ -19,7 +19,7 @@ export async function Trademark(
|
||||
const { space } = context;
|
||||
const language = await getSpaceLanguage(context);
|
||||
|
||||
const url = new URL('https://www.gitbook.com/powered-by');
|
||||
const url = new URL('https://www.gitbook.com');
|
||||
url.searchParams.set('utm_source', 'content');
|
||||
url.searchParams.set('utm_medium', 'trademark');
|
||||
url.searchParams.set('utm_campaign', space.id);
|
||||
|
||||
@@ -11,7 +11,7 @@ import { tcls } from '@/lib/tailwind';
|
||||
* A container that encapsulates a scrollable area with usability features.
|
||||
* - Faded edges when there is more content than the container can display.
|
||||
* - Buttons to advance the scroll position.
|
||||
* - Auto-scroll to the active item on mount and when it changes.
|
||||
* - Auto-scroll to the active item when it's initially active.
|
||||
*/
|
||||
export type ScrollContainerProps = {
|
||||
children: React.ReactNode;
|
||||
@@ -42,12 +42,6 @@ export type ScrollContainerProps = {
|
||||
/** The ID or ref of the active item to scroll to. */
|
||||
active?: string | React.RefObject<HTMLElement | null>;
|
||||
|
||||
/**
|
||||
* Only scroll to the active item when it is not fully visible, and keep following it
|
||||
* when it changes later (requires `active` to be a selector).
|
||||
*/
|
||||
followActive?: boolean;
|
||||
|
||||
/** Scroll by one page of fully visible direct children instead of one viewport. */
|
||||
scrollByVisibleItems?: boolean;
|
||||
} & React.HTMLAttributes<HTMLDivElement>;
|
||||
@@ -59,7 +53,6 @@ export function ScrollContainer(props: ScrollContainerProps) {
|
||||
contentClassName,
|
||||
orientation,
|
||||
active,
|
||||
followActive = false,
|
||||
scrollByVisibleItems = false,
|
||||
leading = { fade: true, button: true },
|
||||
trailing = { fade: true, button: true },
|
||||
@@ -87,44 +80,8 @@ export function ScrollContainer(props: ScrollContainerProps) {
|
||||
if (!activeItem || !container.contains(activeItem)) {
|
||||
return;
|
||||
}
|
||||
if (followActive && isElementFullyVisibleInContainer(activeItem, container)) {
|
||||
return;
|
||||
}
|
||||
scrollToElementInContainer(activeItem, container);
|
||||
}, [active, followActive]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!followActive || !container || typeof active !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
let frame = 0;
|
||||
// Active items can mount only after a collapsed group expands.
|
||||
const observer = new MutationObserver(() => {
|
||||
cancelAnimationFrame(frame);
|
||||
frame = requestAnimationFrame(() => {
|
||||
for (const activeItem of container.querySelectorAll(active)) {
|
||||
if (!isElementFullyVisibleInContainer(activeItem, container)) {
|
||||
scrollToElementInContainer(activeItem, container, 'smooth');
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(container, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-active'],
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
cancelAnimationFrame(frame);
|
||||
};
|
||||
}, [active, followActive]);
|
||||
}, [active]);
|
||||
|
||||
const scrollFurther = () => {
|
||||
const container = containerRef.current;
|
||||
@@ -378,11 +335,7 @@ function scrollByViewport(
|
||||
/**
|
||||
* Scroll to an element in a container.
|
||||
*/
|
||||
export function scrollToElementInContainer(
|
||||
element: Element,
|
||||
container: HTMLElement,
|
||||
behavior: ScrollBehavior = 'auto'
|
||||
) {
|
||||
function scrollToElementInContainer(element: Element, container: HTMLElement) {
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const rect = element.getBoundingClientRect();
|
||||
|
||||
@@ -397,26 +350,8 @@ export function scrollToElementInContainer(
|
||||
(rect.left - containerRect.left) -
|
||||
container.clientWidth / 2 +
|
||||
rect.width / 2,
|
||||
behavior,
|
||||
// Use 'auto' to avoid additional scroll animations when scrolling to an element
|
||||
// as this may be called during layout/initialization when the page is not fully loaded.
|
||||
behavior: 'auto',
|
||||
});
|
||||
}
|
||||
|
||||
function isElementFullyVisibleInContainer(element: Element, container: HTMLElement) {
|
||||
if (
|
||||
!element.getClientRects().length ||
|
||||
container.clientHeight === 0 ||
|
||||
container.clientWidth === 0
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
|
||||
return (
|
||||
elementRect.top >= containerRect.top &&
|
||||
elementRect.bottom <= containerRect.bottom &&
|
||||
elementRect.left >= containerRect.left &&
|
||||
elementRect.right <= containerRect.right
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,14 +77,10 @@ export const ar: TranslationLanguage = {
|
||||
notfound_title: 'الصفحة غير موجودة',
|
||||
notfound_link: 'يشير هذا الرابط إلى صفحة تمت إزالتها أو لم تعد موجودة.',
|
||||
notfound: 'الصفحة التي تبحث عنها غير موجودة.',
|
||||
notfound_adaptive_title: 'سجّل الدخول لعرض هذه الصفحة',
|
||||
notfound_adaptive: 'قد تتطلب هذه الصفحة تسجيل الدخول. إذا اتبعت رابطًا، فتحقق من صحته.',
|
||||
notfound_adaptive_title: 'الصفحة غير متاحة',
|
||||
notfound_adaptive: 'قد تكون هذه الصفحة موجودة، لكن قد تحتاج إلى تسجيل الدخول للوصول إليها.',
|
||||
notfound_adaptive_login: 'تسجيل الدخول',
|
||||
notfound_adaptive_registration_hint: 'قد يكون التسجيل متاحًا حسب المؤسسة.',
|
||||
notfound_adaptive_no_access_title: 'قد لا تملك صلاحية الوصول إلى هذه الصفحة',
|
||||
notfound_adaptive_no_access:
|
||||
'قد لا تكون هذه الصفحة موجودة، أو قد لا يملك حسابك صلاحية الوصول إليها. إذا اتبعت رابطًا، فتحقق من صحته.',
|
||||
notfound_adaptive_login_again: 'تسجيل الدخول مجددًا',
|
||||
notfound_goto_home: 'الانتقال إلى الصفحة الرئيسية',
|
||||
notfound_suggestions_title: 'ربما تبحث عن',
|
||||
unexpected_error_title: 'حدث خطأ',
|
||||
|
||||
@@ -78,16 +78,12 @@ export const bg: TranslationLanguage = {
|
||||
notfound_title: 'Страницата не е намерена',
|
||||
notfound_link: 'Тази връзка сочи към страница, която е премахната или вече не съществува.',
|
||||
notfound: 'Страницата, която търсите, не съществува.',
|
||||
notfound_adaptive_title: 'Влезте, за да видите тази страница',
|
||||
notfound_adaptive_title: 'Страницата не е налична',
|
||||
notfound_adaptive:
|
||||
'Тази страница може да изисква вход. Ако сте последвали връзка, проверете дали е правилна.',
|
||||
'Тази страница може да съществува, но може да трябва да влезете, за да я достъпите.',
|
||||
notfound_adaptive_login: 'Вход',
|
||||
notfound_adaptive_registration_hint:
|
||||
'Регистрацията може да е възможна в зависимост от организацията.',
|
||||
notfound_adaptive_no_access_title: 'Възможно е да нямате достъп до тази страница',
|
||||
notfound_adaptive_no_access:
|
||||
'Тази страница може да не съществува или профилът ви може да няма достъп до нея. Ако сте последвали връзка, проверете дали е правилна.',
|
||||
notfound_adaptive_login_again: 'Повторен вход',
|
||||
notfound_goto_home: 'Към началната страница',
|
||||
notfound_suggestions_title: 'Може би търсите',
|
||||
unexpected_error_title: 'Възникна грешка',
|
||||
|
||||
@@ -77,15 +77,11 @@ export const cs: TranslationLanguage = {
|
||||
notfound_title: 'Stránka nenalezena',
|
||||
notfound_link: 'Tento odkaz vede na stránku, která byla odstraněna nebo již neexistuje.',
|
||||
notfound: 'Stránka, kterou hledáte, neexistuje.',
|
||||
notfound_adaptive_title: 'Pro zobrazení této stránky se přihlaste',
|
||||
notfound_adaptive_title: 'Stránka není dostupná',
|
||||
notfound_adaptive:
|
||||
'Tato stránka může vyžadovat přihlášení. Pokud jste přešli z odkazu, zkontrolujte, zda je správný.',
|
||||
'Tato stránka může existovat, ale pro přístup se možná budete muset přihlásit.',
|
||||
notfound_adaptive_login: 'Přihlásit se',
|
||||
notfound_adaptive_registration_hint: 'Registrace může být dostupná v závislosti na organizaci.',
|
||||
notfound_adaptive_no_access_title: 'K této stránce možná nemáte přístup',
|
||||
notfound_adaptive_no_access:
|
||||
'Tato stránka možná neexistuje nebo k ní váš účet nemá přístup. Pokud jste přešli z odkazu, zkontrolujte, zda je správný.',
|
||||
notfound_adaptive_login_again: 'Přihlásit se znovu',
|
||||
notfound_goto_home: 'Přejít na domovskou stránku',
|
||||
notfound_suggestions_title: 'Možná hledáte',
|
||||
unexpected_error_title: 'Došlo k chybě',
|
||||
|
||||
@@ -77,15 +77,11 @@ export const da: TranslationLanguage = {
|
||||
notfound_title: 'Siden blev ikke fundet',
|
||||
notfound_link: 'Dette link peger på en side, der er blevet fjernet eller ikke længere findes.',
|
||||
notfound: 'Siden, du leder efter, findes ikke.',
|
||||
notfound_adaptive_title: 'Log ind for at se denne side',
|
||||
notfound_adaptive_title: 'Siden er ikke tilgængelig',
|
||||
notfound_adaptive:
|
||||
'Denne side kræver muligvis, at du logger ind. Hvis du har fulgt et link, så tjek, at det er korrekt.',
|
||||
'Denne side findes muligvis, men du skal muligvis logge ind for at få adgang.',
|
||||
notfound_adaptive_login: 'Log ind',
|
||||
notfound_adaptive_registration_hint: 'Registrering kan være mulig afhængigt af organisationen.',
|
||||
notfound_adaptive_no_access_title: 'Du har muligvis ikke adgang til denne side',
|
||||
notfound_adaptive_no_access:
|
||||
'Denne side findes muligvis ikke, eller din konto har muligvis ikke adgang til den. Hvis du har fulgt et link, så tjek, at det er korrekt.',
|
||||
notfound_adaptive_login_again: 'Log ind igen',
|
||||
notfound_goto_home: 'Gå til forsiden',
|
||||
notfound_suggestions_title: 'Du leder måske efter',
|
||||
unexpected_error_title: 'Der opstod en fejl',
|
||||
|
||||
@@ -80,16 +80,12 @@ export const de: TranslationLanguage = {
|
||||
notfound_link:
|
||||
'Dieser Link verweist auf eine Seite, die entfernt wurde oder nicht mehr existiert.',
|
||||
notfound: 'Die gesuchte Seite existiert nicht.',
|
||||
notfound_adaptive_title: 'Melden Sie sich an, um diese Seite anzuzeigen',
|
||||
notfound_adaptive_title: 'Seite nicht verfügbar',
|
||||
notfound_adaptive:
|
||||
'Für diese Seite ist möglicherweise eine Anmeldung erforderlich. Wenn Sie einem Link gefolgt sind, prüfen Sie, ob er korrekt ist.',
|
||||
'Diese Seite existiert möglicherweise, aber Sie müssen sich eventuell anmelden, um darauf zuzugreifen.',
|
||||
notfound_adaptive_login: 'Anmelden',
|
||||
notfound_adaptive_registration_hint:
|
||||
'Die Registrierung ist je nach Organisation möglicherweise verfügbar.',
|
||||
notfound_adaptive_no_access_title: 'Sie haben möglicherweise keinen Zugriff auf diese Seite',
|
||||
notfound_adaptive_no_access:
|
||||
'Diese Seite existiert möglicherweise nicht, oder Ihr Konto hat keinen Zugriff darauf. Wenn Sie einem Link gefolgt sind, prüfen Sie, ob er korrekt ist.',
|
||||
notfound_adaptive_login_again: 'Erneut anmelden',
|
||||
notfound_goto_home: 'Zur Startseite',
|
||||
notfound_suggestions_title: 'Vielleicht suchen Sie nach',
|
||||
unexpected_error_title: 'Ein Fehler ist aufgetreten',
|
||||
|
||||
@@ -78,16 +78,12 @@ export const el: TranslationLanguage = {
|
||||
notfound_title: 'Η σελίδα δεν βρέθηκε',
|
||||
notfound_link: 'Αυτός ο σύνδεσμος οδηγεί σε μια σελίδα που έχει αφαιρεθεί ή δεν υπάρχει πλέον.',
|
||||
notfound: 'Η σελίδα που αναζητάτε δεν υπάρχει.',
|
||||
notfound_adaptive_title: 'Συνδεθείτε για να δείτε αυτή τη σελίδα',
|
||||
notfound_adaptive_title: 'Η σελίδα δεν είναι διαθέσιμη',
|
||||
notfound_adaptive:
|
||||
'Αυτή η σελίδα ενδέχεται να απαιτεί σύνδεση. Αν ακολουθήσατε έναν σύνδεσμο, ελέγξτε ότι είναι σωστός.',
|
||||
'Αυτή η σελίδα μπορεί να υπάρχει, αλλά ίσως χρειαστεί να συνδεθείτε για πρόσβαση.',
|
||||
notfound_adaptive_login: 'Σύνδεση',
|
||||
notfound_adaptive_registration_hint:
|
||||
'Η εγγραφή ενδέχεται να είναι διαθέσιμη ανάλογα με τον οργανισμό.',
|
||||
notfound_adaptive_no_access_title: 'Ίσως δεν έχετε πρόσβαση σε αυτή τη σελίδα',
|
||||
notfound_adaptive_no_access:
|
||||
'Αυτή η σελίδα ενδέχεται να μην υπάρχει ή ο λογαριασμός σας ενδέχεται να μην έχει πρόσβαση σε αυτήν. Αν ακολουθήσατε έναν σύνδεσμο, ελέγξτε ότι είναι σωστός.',
|
||||
notfound_adaptive_login_again: 'Επανασύνδεση',
|
||||
notfound_goto_home: 'Μετάβαση στην αρχική σελίδα',
|
||||
notfound_suggestions_title: 'Ίσως αναζητάτε',
|
||||
unexpected_error_title: 'Παρουσιάστηκε σφάλμα',
|
||||
|
||||
@@ -75,16 +75,11 @@ export const en = {
|
||||
notfound_title: 'Page not found',
|
||||
notfound_link: 'This link points to a page that has been removed or no longer exists.',
|
||||
notfound: "The page you're looking for doesn't exist.",
|
||||
notfound_adaptive_title: 'Log in to view this page',
|
||||
notfound_adaptive:
|
||||
"This page may require you to log in. If you followed a link, check that it's correct.",
|
||||
notfound_adaptive_title: 'Page unavailable',
|
||||
notfound_adaptive: 'This page may exist, but you may need to log in to access it.',
|
||||
notfound_adaptive_login: 'Log in',
|
||||
notfound_adaptive_registration_hint:
|
||||
'Registration may be available depending on the organization.',
|
||||
notfound_adaptive_no_access_title: 'You may not have access to this page',
|
||||
notfound_adaptive_no_access:
|
||||
"This page may not exist, or your account may not have access to it. If you followed a link, check that it's correct.",
|
||||
notfound_adaptive_login_again: 'Log in again',
|
||||
notfound_goto_home: 'Go to homepage',
|
||||
notfound_suggestions_title: 'You might be looking for',
|
||||
unexpected_error_title: 'An error occurred',
|
||||
|
||||
@@ -79,16 +79,12 @@ export const es: TranslationLanguage = {
|
||||
notfound_title: 'Página no encontrada',
|
||||
notfound_link: 'Este enlace apunta a una página que ha sido eliminada o ya no existe.',
|
||||
notfound: 'La página que buscas no existe.',
|
||||
notfound_adaptive_title: 'Inicia sesión para ver esta página',
|
||||
notfound_adaptive_title: 'Página no disponible',
|
||||
notfound_adaptive:
|
||||
'Es posible que esta página requiera iniciar sesión. Si has seguido un enlace, comprueba que sea correcto.',
|
||||
'Esta página puede existir, pero es posible que debas iniciar sesión para acceder.',
|
||||
notfound_adaptive_login: 'Iniciar sesión',
|
||||
notfound_adaptive_registration_hint:
|
||||
'El registro puede estar disponible según la organización.',
|
||||
notfound_adaptive_no_access_title: 'Es posible que no tengas acceso a esta página',
|
||||
notfound_adaptive_no_access:
|
||||
'Es posible que esta página no exista o que tu cuenta no tenga acceso a ella. Si has seguido un enlace, comprueba que sea correcto.',
|
||||
notfound_adaptive_login_again: 'Volver a iniciar sesión',
|
||||
notfound_goto_home: 'Ir al inicio',
|
||||
notfound_suggestions_title: 'Quizás estés buscando',
|
||||
unexpected_error_title: 'Ocurrió un error',
|
||||
|
||||
@@ -77,16 +77,11 @@ export const et: TranslationLanguage = {
|
||||
notfound_title: 'Lehte ei leitud',
|
||||
notfound_link: 'See link viitab lehele, mis on eemaldatud või mida enam ei eksisteeri.',
|
||||
notfound: 'Lehte, mida otsite, ei eksisteeri.',
|
||||
notfound_adaptive_title: 'Selle lehe vaatamiseks logi sisse',
|
||||
notfound_adaptive:
|
||||
'See leht võib nõuda sisselogimist. Kui järgisid linki, kontrolli, et see oleks õige.',
|
||||
notfound_adaptive_title: 'Leht pole saadaval',
|
||||
notfound_adaptive: 'See leht võib olemas olla, kuid juurdepääsuks võib olla vaja sisse logida.',
|
||||
notfound_adaptive_login: 'Logi sisse',
|
||||
notfound_adaptive_registration_hint:
|
||||
'Registreerimine võib olla saadaval olenevalt organisatsioonist.',
|
||||
notfound_adaptive_no_access_title: 'Sul ei pruugi olla sellele lehele juurdepääsu',
|
||||
notfound_adaptive_no_access:
|
||||
'Seda lehte ei pruugi olla olemas või sinu kontol ei pruugi olla sellele juurdepääsu. Kui järgisid linki, kontrolli, et see oleks õige.',
|
||||
notfound_adaptive_login_again: 'Logi uuesti sisse',
|
||||
notfound_goto_home: 'Mine avalehele',
|
||||
notfound_suggestions_title: 'Võib-olla otsite',
|
||||
unexpected_error_title: 'Ilmnes tõrge',
|
||||
|
||||
@@ -78,16 +78,12 @@ export const fi: TranslationLanguage = {
|
||||
notfound_title: 'Sivua ei löytynyt',
|
||||
notfound_link: 'Tämä linkki osoittaa sivulle, joka on poistettu tai jota ei enää ole.',
|
||||
notfound: 'Etsimääsi sivua ei ole olemassa.',
|
||||
notfound_adaptive_title: 'Kirjaudu sisään nähdäksesi tämän sivun',
|
||||
notfound_adaptive_title: 'Sivu ei ole saatavilla',
|
||||
notfound_adaptive:
|
||||
'Tämä sivu saattaa vaatia kirjautumisen. Jos seurasit linkkiä, tarkista, että se on oikein.',
|
||||
'Tämä sivu voi olla olemassa, mutta sen käyttö voi edellyttää kirjautumista.',
|
||||
notfound_adaptive_login: 'Kirjaudu sisään',
|
||||
notfound_adaptive_registration_hint:
|
||||
'Rekisteröityminen voi olla mahdollista organisaatiosta riippuen.',
|
||||
notfound_adaptive_no_access_title: 'Sinulla ei ehkä ole pääsyä tälle sivulle',
|
||||
notfound_adaptive_no_access:
|
||||
'Tätä sivua ei ehkä ole olemassa, tai tililläsi ei ehkä ole pääsyä siihen. Jos seurasit linkkiä, tarkista, että se on oikein.',
|
||||
notfound_adaptive_login_again: 'Kirjaudu uudelleen',
|
||||
notfound_goto_home: 'Siirry etusivulle',
|
||||
notfound_suggestions_title: 'Saatat etsiä',
|
||||
unexpected_error_title: 'Tapahtui virhe',
|
||||
|
||||
@@ -78,15 +78,11 @@ export const fr: TranslationLanguage = {
|
||||
notfound_title: 'Page introuvable',
|
||||
notfound_link: "Ce lien pointe vers une page qui a été supprimée ou n'existe plus.",
|
||||
notfound: 'La page que vous cherchez n’existe pas.',
|
||||
notfound_adaptive_title: 'Connectez-vous pour afficher cette page',
|
||||
notfound_adaptive_title: 'Page inaccessible',
|
||||
notfound_adaptive:
|
||||
"Cette page peut nécessiter une connexion. Si vous avez suivi un lien, vérifiez qu'il est correct.",
|
||||
'Cette page existe peut-être, mais vous devez peut-être vous connecter pour y accéder.',
|
||||
notfound_adaptive_login: 'Se connecter',
|
||||
notfound_adaptive_registration_hint: "L'inscription peut être disponible selon l'organisation.",
|
||||
notfound_adaptive_no_access_title: "Vous n'avez peut-être pas accès à cette page",
|
||||
notfound_adaptive_no_access:
|
||||
"Cette page n'existe peut-être pas, ou votre compte n'y a peut-être pas accès. Si vous avez suivi un lien, vérifiez qu'il est correct.",
|
||||
notfound_adaptive_login_again: 'Se reconnecter',
|
||||
notfound_goto_home: "Aller à l'accueil",
|
||||
notfound_suggestions_title: 'Vous cherchez peut-être',
|
||||
unexpected_error_title: 'Erreur inattendue',
|
||||
|
||||
@@ -77,14 +77,10 @@ export const he: TranslationLanguage = {
|
||||
notfound_title: 'הדף לא נמצא',
|
||||
notfound_link: 'קישור זה מפנה לדף שהוסר או אינו קיים עוד.',
|
||||
notfound: 'הדף שחיפשת אינו קיים.',
|
||||
notfound_adaptive_title: 'יש להתחבר כדי לצפות בדף זה',
|
||||
notfound_adaptive: 'ייתכן שדף זה דורש התחברות. אם הגעת דרך קישור, יש לוודא שהוא נכון.',
|
||||
notfound_adaptive_title: 'הדף אינו זמין',
|
||||
notfound_adaptive: 'ייתכן שהדף קיים, אך ייתכן שעליך להתחבר כדי לגשת אליו.',
|
||||
notfound_adaptive_login: 'התחברות',
|
||||
notfound_adaptive_registration_hint: 'ייתכן שההרשמה זמינה בהתאם לארגון.',
|
||||
notfound_adaptive_no_access_title: 'ייתכן שאין לך גישה לדף זה',
|
||||
notfound_adaptive_no_access:
|
||||
'ייתכן שדף זה אינו קיים, או שלחשבון שלך אין גישה אליו. אם הגעת דרך קישור, יש לוודא שהוא נכון.',
|
||||
notfound_adaptive_login_again: 'התחברות מחדש',
|
||||
notfound_goto_home: 'מעבר לדף הבית',
|
||||
notfound_suggestions_title: 'אולי אתה מחפש',
|
||||
unexpected_error_title: 'אירעה שגיאה',
|
||||
|
||||
@@ -77,15 +77,10 @@ export const hi: TranslationLanguage = {
|
||||
notfound_title: 'पृष्ठ नहीं मिला',
|
||||
notfound_link: 'यह लिंक ऐसे पृष्ठ पर जाता है जिसे हटा दिया गया है या जो अब मौजूद नहीं है।',
|
||||
notfound: 'आप जिस पृष्ठ को खोज रहे हैं वह मौजूद नहीं है।',
|
||||
notfound_adaptive_title: 'यह पृष्ठ देखने के लिए लॉग इन करें',
|
||||
notfound_adaptive:
|
||||
'इस पृष्ठ के लिए लॉग इन करना आवश्यक हो सकता है। यदि आप किसी लिंक से आए हैं, तो जांच लें कि वह सही है।',
|
||||
notfound_adaptive_title: 'पृष्ठ उपलब्ध नहीं',
|
||||
notfound_adaptive: 'यह पृष्ठ मौजूद हो सकता है, लेकिन इसे देखने के लिए आपको लॉग इन करना पड़ सकता है।',
|
||||
notfound_adaptive_login: 'लॉग इन करें',
|
||||
notfound_adaptive_registration_hint: 'संगठन के आधार पर पंजीकरण उपलब्ध हो सकता है।',
|
||||
notfound_adaptive_no_access_title: 'हो सकता है कि आपके पास इस पृष्ठ की पहुंच न हो',
|
||||
notfound_adaptive_no_access:
|
||||
'हो सकता है कि यह पृष्ठ मौजूद न हो, या आपके खाते के पास इसकी पहुंच न हो। यदि आप किसी लिंक से आए हैं, तो जांच लें कि वह सही है।',
|
||||
notfound_adaptive_login_again: 'फिर से लॉग इन करें',
|
||||
notfound_goto_home: 'मुखपृष्ठ पर जाएं',
|
||||
notfound_suggestions_title: 'शायद आप यह ढूँढ रहे हैं',
|
||||
unexpected_error_title: 'त्रुटि हुई',
|
||||
|
||||
@@ -77,15 +77,11 @@ export const hr: TranslationLanguage = {
|
||||
notfound_title: 'Stranica nije pronađena',
|
||||
notfound_link: 'Ova poveznica vodi na stranicu koja je uklonjena ili više ne postoji.',
|
||||
notfound: 'Stranica koju tražite ne postoji.',
|
||||
notfound_adaptive_title: 'Prijavite se za prikaz ove stranice',
|
||||
notfound_adaptive_title: 'Stranica nije dostupna',
|
||||
notfound_adaptive:
|
||||
'Ova stranica možda zahtijeva prijavu. Ako ste slijedili poveznicu, provjerite je li ispravna.',
|
||||
'Ova stranica možda postoji, ali možda se morate prijaviti da biste joj pristupili.',
|
||||
notfound_adaptive_login: 'Prijava',
|
||||
notfound_adaptive_registration_hint: 'Registracija može biti dostupna ovisno o organizaciji.',
|
||||
notfound_adaptive_no_access_title: 'Možda nemate pristup ovoj stranici',
|
||||
notfound_adaptive_no_access:
|
||||
'Ova stranica možda ne postoji ili vaš račun nema pristup njoj. Ako ste slijedili poveznicu, provjerite je li ispravna.',
|
||||
notfound_adaptive_login_again: 'Ponovna prijava',
|
||||
notfound_goto_home: 'Idi na početnu stranicu',
|
||||
notfound_suggestions_title: 'Možda tražite',
|
||||
unexpected_error_title: 'Došlo je do pogreške',
|
||||
|
||||
@@ -78,15 +78,10 @@ export const hu: TranslationLanguage = {
|
||||
notfound_title: 'Az oldal nem található',
|
||||
notfound_link: 'Ez a hivatkozás egy eltávolított vagy már nem létező oldalra mutat.',
|
||||
notfound: 'A keresett oldal nem létezik.',
|
||||
notfound_adaptive_title: 'Jelentkezzen be az oldal megtekintéséhez',
|
||||
notfound_adaptive:
|
||||
'Ehhez az oldalhoz bejelentkezés szükséges lehet. Ha egy hivatkozást követett, ellenőrizze, hogy helyes-e.',
|
||||
notfound_adaptive_title: 'Az oldal nem érhető el',
|
||||
notfound_adaptive: 'Ez az oldal létezhet, de a hozzáféréshez lehet, hogy be kell jelentkeznie.',
|
||||
notfound_adaptive_login: 'Bejelentkezés',
|
||||
notfound_adaptive_registration_hint: 'A regisztráció a szervezettől függően elérhető lehet.',
|
||||
notfound_adaptive_no_access_title: 'Lehet, hogy nincs hozzáférése ehhez az oldalhoz',
|
||||
notfound_adaptive_no_access:
|
||||
'Lehet, hogy ez az oldal nem létezik, vagy a fiókja nem fér hozzá. Ha egy hivatkozást követett, ellenőrizze, hogy helyes-e.',
|
||||
notfound_adaptive_login_again: 'Újbóli bejelentkezés',
|
||||
notfound_goto_home: 'Ugrás a kezdőlapra',
|
||||
notfound_suggestions_title: 'Talán ezt keresi',
|
||||
unexpected_error_title: 'Hiba történt',
|
||||
|
||||
@@ -77,15 +77,11 @@ export const id: TranslationLanguage = {
|
||||
notfound_title: 'Halaman tidak ditemukan',
|
||||
notfound_link: 'Tautan ini mengarah ke halaman yang telah dihapus atau sudah tidak ada.',
|
||||
notfound: 'Halaman yang Anda cari tidak ada.',
|
||||
notfound_adaptive_title: 'Masuk untuk melihat halaman ini',
|
||||
notfound_adaptive_title: 'Halaman tidak tersedia',
|
||||
notfound_adaptive:
|
||||
'Halaman ini mungkin mengharuskan Anda masuk. Jika Anda mengikuti tautan, periksa apakah tautan tersebut benar.',
|
||||
'Halaman ini mungkin ada, tetapi Anda mungkin perlu masuk untuk mengaksesnya.',
|
||||
notfound_adaptive_login: 'Masuk',
|
||||
notfound_adaptive_registration_hint: 'Pendaftaran mungkin tersedia tergantung pada organisasi.',
|
||||
notfound_adaptive_no_access_title: 'Anda mungkin tidak memiliki akses ke halaman ini',
|
||||
notfound_adaptive_no_access:
|
||||
'Halaman ini mungkin tidak ada, atau akun Anda mungkin tidak memiliki akses ke halaman ini. Jika Anda mengikuti tautan, periksa apakah tautan tersebut benar.',
|
||||
notfound_adaptive_login_again: 'Masuk lagi',
|
||||
notfound_goto_home: 'Buka beranda',
|
||||
notfound_suggestions_title: 'Mungkin Anda mencari',
|
||||
unexpected_error_title: 'Terjadi kesalahan',
|
||||
|
||||
@@ -79,16 +79,12 @@ export const it: TranslationLanguage = {
|
||||
notfound_title: 'Pagina non trovata',
|
||||
notfound_link: 'Questo link punta a una pagina che è stata rimossa o non esiste più.',
|
||||
notfound: 'La pagina che cerchi non esiste.',
|
||||
notfound_adaptive_title: 'Accedi per visualizzare questa pagina',
|
||||
notfound_adaptive_title: 'Pagina non disponibile',
|
||||
notfound_adaptive:
|
||||
"Questa pagina potrebbe richiedere l'accesso. Se hai seguito un link, verifica che sia corretto.",
|
||||
"Questa pagina potrebbe esistere, ma potrebbe essere necessario effettuare l'accesso per visualizzarla.",
|
||||
notfound_adaptive_login: 'Accedi',
|
||||
notfound_adaptive_registration_hint:
|
||||
"La registrazione potrebbe essere disponibile a seconda dell'organizzazione.",
|
||||
notfound_adaptive_no_access_title: 'Potresti non avere accesso a questa pagina',
|
||||
notfound_adaptive_no_access:
|
||||
'Questa pagina potrebbe non esistere, oppure il tuo account potrebbe non avervi accesso. Se hai seguito un link, verifica che sia corretto.',
|
||||
notfound_adaptive_login_again: 'Accedi di nuovo',
|
||||
notfound_goto_home: 'Vai alla home',
|
||||
notfound_suggestions_title: 'Forse stai cercando',
|
||||
unexpected_error_title: 'Si è verificato un errore',
|
||||
|
||||
@@ -78,15 +78,11 @@ export const ja: TranslationLanguage = {
|
||||
notfound_title: 'ページが見つかりません',
|
||||
notfound_link: 'このリンクは、削除されたか、もはや存在しないページを指しています。',
|
||||
notfound: 'お探しのページは存在しません。',
|
||||
notfound_adaptive_title: 'このページを表示するにはログインしてください',
|
||||
notfound_adaptive_title: 'ページにアクセスできません',
|
||||
notfound_adaptive:
|
||||
'このページの閲覧にはログインが必要な場合があります。リンクからアクセスした場合は、リンクが正しいか確認してください。',
|
||||
'このページは存在する可能性がありますが、アクセスするにはログインが必要な場合があります。',
|
||||
notfound_adaptive_login: 'ログイン',
|
||||
notfound_adaptive_registration_hint: '組織によっては登録が可能な場合があります。',
|
||||
notfound_adaptive_no_access_title: 'このページへのアクセス権がない可能性があります',
|
||||
notfound_adaptive_no_access:
|
||||
'このページが存在しないか、お使いのアカウントにアクセス権がない可能性があります。リンクからアクセスした場合は、リンクが正しいか確認してください。',
|
||||
notfound_adaptive_login_again: '再ログイン',
|
||||
notfound_goto_home: 'ホームへ移動',
|
||||
notfound_suggestions_title: 'お探しのページはこちらですか',
|
||||
unexpected_error_title: 'エラーが発生しました',
|
||||
|
||||
@@ -78,15 +78,10 @@ export const ko: TranslationLanguage = {
|
||||
notfound_title: '페이지를 찾을 수 없음',
|
||||
notfound_link: '이 링크는 삭제되었거나 더 이상 존재하지 않는 페이지를 가리킵니다.',
|
||||
notfound: '찾으시는 페이지가 존재하지 않습니다.',
|
||||
notfound_adaptive_title: '이 페이지를 보려면 로그인하세요',
|
||||
notfound_adaptive:
|
||||
'이 페이지는 로그인이 필요할 수 있습니다. 링크를 통해 접속했다면 링크가 올바른지 확인하세요.',
|
||||
notfound_adaptive_title: '페이지에 접근할 수 없음',
|
||||
notfound_adaptive: '이 페이지는 존재할 수 있지만, 접근하려면 로그인해야 할 수 있습니다.',
|
||||
notfound_adaptive_login: '로그인',
|
||||
notfound_adaptive_registration_hint: '조직에 따라 등록이 가능할 수 있습니다.',
|
||||
notfound_adaptive_no_access_title: '이 페이지에 대한 접근 권한이 없을 수 있습니다',
|
||||
notfound_adaptive_no_access:
|
||||
'이 페이지가 존재하지 않거나 계정에 이 페이지에 대한 접근 권한이 없을 수 있습니다. 링크를 통해 접속했다면 링크가 올바른지 확인하세요.',
|
||||
notfound_adaptive_login_again: '다시 로그인',
|
||||
notfound_goto_home: '홈으로 이동',
|
||||
notfound_suggestions_title: '찾고 계신 내용일 수 있어요',
|
||||
unexpected_error_title: '오류가 발생했습니다',
|
||||
|
||||
@@ -77,16 +77,12 @@ export const lt: TranslationLanguage = {
|
||||
notfound_title: 'Puslapis nerastas',
|
||||
notfound_link: 'Ši nuoroda veda į puslapį, kuris buvo pašalintas arba nebeegzistuoja.',
|
||||
notfound: 'Puslapis, kurio ieškote, neegzistuoja.',
|
||||
notfound_adaptive_title: 'Prisijunkite, kad peržiūrėtumėte šį puslapį',
|
||||
notfound_adaptive_title: 'Puslapis nepasiekiamas',
|
||||
notfound_adaptive:
|
||||
'Šiam puslapiui gali reikėti prisijungti. Jei sekėte nuoroda, patikrinkite, ar ji teisinga.',
|
||||
'Šis puslapis gali egzistuoti, bet norint jį pasiekti gali reikėti prisijungti.',
|
||||
notfound_adaptive_login: 'Prisijungti',
|
||||
notfound_adaptive_registration_hint:
|
||||
'Registracija gali būti galima priklausomai nuo organizacijos.',
|
||||
notfound_adaptive_no_access_title: 'Galbūt neturite prieigos prie šio puslapio',
|
||||
notfound_adaptive_no_access:
|
||||
'Šis puslapis gali neegzistuoti arba jūsų paskyra gali neturėti prieigos prie jo. Jei sekėte nuoroda, patikrinkite, ar ji teisinga.',
|
||||
notfound_adaptive_login_again: 'Prisijungti iš naujo',
|
||||
notfound_goto_home: 'Eiti į pagrindinį puslapį',
|
||||
notfound_suggestions_title: 'Galbūt ieškote',
|
||||
unexpected_error_title: 'Įvyko klaida',
|
||||
|
||||
@@ -77,15 +77,10 @@ export const lv: TranslationLanguage = {
|
||||
notfound_title: 'Lapa nav atrasta',
|
||||
notfound_link: 'Šī saite norāda uz lapu, kas ir noņemta vai vairs nepastāv.',
|
||||
notfound: 'Meklētā lapa nepastāv.',
|
||||
notfound_adaptive_title: 'Pierakstieties, lai skatītu šo lapu',
|
||||
notfound_adaptive:
|
||||
'Šai lapai, iespējams, nepieciešams pierakstīties. Ja sekojāt saitei, pārbaudiet, vai tā ir pareiza.',
|
||||
notfound_adaptive_title: 'Lapa nav pieejama',
|
||||
notfound_adaptive: 'Šī lapa var pastāvēt, bet, iespējams, jums jāpierakstās, lai tai piekļūtu.',
|
||||
notfound_adaptive_login: 'Pierakstīties',
|
||||
notfound_adaptive_registration_hint: 'Reģistrācija var būt pieejama atkarībā no organizācijas.',
|
||||
notfound_adaptive_no_access_title: 'Iespējams, jums nav piekļuves šai lapai',
|
||||
notfound_adaptive_no_access:
|
||||
'Šī lapa, iespējams, neeksistē, vai arī jūsu kontam nav piekļuves tai. Ja sekojāt saitei, pārbaudiet, vai tā ir pareiza.',
|
||||
notfound_adaptive_login_again: 'Pierakstīties vēlreiz',
|
||||
notfound_goto_home: 'Doties uz sākumlapu',
|
||||
notfound_suggestions_title: 'Iespējams, meklējat',
|
||||
unexpected_error_title: 'Radās kļūda',
|
||||
|
||||
@@ -77,15 +77,11 @@ export const ms: TranslationLanguage = {
|
||||
notfound_title: 'Halaman tidak ditemui',
|
||||
notfound_link: 'Pautan ini menghala ke halaman yang telah dialih keluar atau tidak lagi wujud.',
|
||||
notfound: 'Halaman yang anda cari tidak wujud.',
|
||||
notfound_adaptive_title: 'Log masuk untuk melihat halaman ini',
|
||||
notfound_adaptive_title: 'Halaman tidak tersedia',
|
||||
notfound_adaptive:
|
||||
'Halaman ini mungkin memerlukan anda log masuk. Jika anda mengikuti pautan, semak sama ada pautan itu betul.',
|
||||
'Halaman ini mungkin wujud, tetapi anda mungkin perlu log masuk untuk mengaksesnya.',
|
||||
notfound_adaptive_login: 'Log masuk',
|
||||
notfound_adaptive_registration_hint: 'Pendaftaran mungkin tersedia bergantung pada organisasi.',
|
||||
notfound_adaptive_no_access_title: 'Anda mungkin tidak mempunyai akses ke halaman ini',
|
||||
notfound_adaptive_no_access:
|
||||
'Halaman ini mungkin tidak wujud, atau akaun anda mungkin tidak mempunyai akses kepadanya. Jika anda mengikuti pautan, semak sama ada pautan itu betul.',
|
||||
notfound_adaptive_login_again: 'Log masuk semula',
|
||||
notfound_goto_home: 'Pergi ke halaman utama',
|
||||
notfound_suggestions_title: 'Mungkin anda mencari',
|
||||
unexpected_error_title: 'Ralat berlaku',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user