Compare commits

...

14 Commits

Author SHA1 Message Date
Steven H 67c8ed8abf Merge branch 'main' into stevenh/ask-endpoint-prompt 2026-10-01 09:00:31 +01:00
Steven H 05945f4b53 lint 2026-10-01 08:54:22 +01:00
Steven H 7551e7e15a changeset 2026-10-01 08:45:07 +01:00
Steven H d595716d6d improve prompt 2026-10-01 08:44:35 +01:00
Peter White 96325161be Lazy-load Loom embeds so pages with many of them don't stay blank (RND-13147) (#4652) 2026-09-30 14:18:29 +02:00
Steven H d832a54689 prompt update 2026-09-30 10:08:44 +01:00
Steven H 356e1f82f2 Improve the prompt for the Ask endpoint 2026-09-30 10:06:20 +01:00
Surai 9f5290bccc Fix Ask AI submissions when search state is empty (#4651) 2026-09-30 09:50:39 +02:00
Peter White 61992424af Mark text over a background cover in the first frame after hydration (#4650) 2026-09-30 08:21:31 +02:00
Peter White 380af10236 Send previousUrl with site insights events (#4640) 2026-09-29 16:43:14 +00:00
Peter White 0911abc55c Fix page cover image jumping on load (#4648) 2026-09-29 18:11:03 +02:00
Tomek d8dc59b1a9 Improve dark theme contrast for Mermaid edge labels (#4645)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-29 14:02:07 +02:00
Surai d04f8bc05e Update trademark landing page (#4644) 2026-09-29 11:13:52 +02:00
conico974 576a781a8f Bump versions of @opennextjs/aws and @opennextjs/cloudflare (#4643) 2026-09-29 10:51:30 +02:00
21 changed files with 185 additions and 94 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Recolor text over a background page cover in the first frame after hydration.
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix page cover image jumping on load
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix inline Ask AI inputs and buttons doing nothing before search is opened.
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Send the previous page's URL with site insights events so broken links can be traced to the page linking to them.
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix Loom videos staying blank on pages with many Loom embeds by lazy-loading them.
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Improve dark theme contrast for Mermaid edge labels.
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Improve the prompt for agents to ask questions.
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Point the "Powered by GitBook" trademark link to gitbook.com/powered-by.
+6 -7
View File
@@ -129,8 +129,8 @@
"@gitbook/react-openapi": "workspace:*",
"@mermaid-js/mermaid-zenuml": "^0.2.2",
"@modelcontextprotocol/sdk": "1.17.5",
"@opennextjs/aws": "4.1.3",
"@opennextjs/cloudflare": "1.20.5",
"@opennextjs/aws": "4.1.6",
"@opennextjs/cloudflare": "1.20.7",
"@panzoom/panzoom": "^4.6.1",
"@sindresorhus/fnv1a": "^3.1.0",
"@tailwindcss/container-queries": "^0.1.1",
@@ -346,7 +346,6 @@
},
"overrides": {
"@codemirror/state": "6.4.1",
"@opennextjs/aws": "4.1.5",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"axios": "1.8.4",
@@ -356,7 +355,7 @@
},
"catalog": {
"@base-ui/react": "^1.7.0",
"@gitbook/api": "0.202.0",
"@gitbook/api": "0.203.0",
"@scalar/api-client-react": "^1.3.46",
"@tsconfig/node20": "^20.1.6",
"@tsconfig/strictest": "^2.0.6",
@@ -728,7 +727,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.202.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-qhrjEQbNNmCljR0AgP79+BsVh9yelh9TIBg9X3TyYx5a3lJwDiT3sw1ND9DFQSxGHp8hH5w+20KJHn/IDpTbaw=="],
"@gitbook/api": ["@gitbook/api@0.203.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-cSFUMM7cIMTHW9cdMpexGSkHHaXmc506fx+LQE2+KLqce8GlBDU/ElIP4RU0MGtN+SSYF31AUgvPD8ZerBVcMA=="],
"@gitbook/browser-types": ["@gitbook/browser-types@workspace:packages/browser-types"],
@@ -944,9 +943,9 @@
"@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
"@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/aws": ["@opennextjs/aws@4.1.6", "", { "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.26 <16 || >=16.3.6" }, "bin": { "open-next": "dist/index.js" } }, "sha512-J5mzpWo6duetc3ZtcyBxrUqtD6Dwh8msRcMTNQdvpsgTUjEFp03AJmSOnJ/7BKkuF791YTQgjk4iJYIIYLmKqA=="],
"@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=="],
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.20.7", "", { "dependencies": { "@ast-grep/napi": "^0.40.5", "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "4.1.6", "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.26 <16 || >=16.3.6", "rclone.js": "^0.6.6", "wrangler": "^4.125.0" }, "optionalPeers": ["rclone.js"], "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-obZ5l96+MUmGqKd7akJlT9tv0XAvaUDDJj13MyGTVTzA+lbal8kNRoUwei08rtu1Gv+anASAwwMP3dvzcZx+Jw=="],
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
+2 -3
View File
@@ -19,8 +19,7 @@
"react": "catalog:",
"react-dom": "catalog:",
"esbuild": "0.27.3",
"axios": "1.8.4",
"@opennextjs/aws": "4.1.5"
"axios": "1.8.4"
},
"private": true,
"scripts": {
@@ -49,7 +48,7 @@
"@tsconfig/strictest": "^2.0.6",
"@tsconfig/node20": "^20.1.6",
"@base-ui/react": "^1.7.0",
"@gitbook/api": "0.202.0",
"@gitbook/api": "0.203.0",
"@scalar/api-client-react": "^1.3.46",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
+2 -2
View File
@@ -20,8 +20,8 @@
"@gitbook/react-openapi": "workspace:*",
"@mermaid-js/mermaid-zenuml": "^0.2.2",
"@modelcontextprotocol/sdk": "1.17.5",
"@opennextjs/aws": "4.1.3",
"@opennextjs/cloudflare": "1.20.5",
"@opennextjs/aws": "4.1.6",
"@opennextjs/cloudflare": "1.20.7",
"@panzoom/panzoom": "^4.6.1",
"@sindresorhus/fnv1a": "^3.1.0",
"@tailwindcss/container-queries": "^0.1.1",
+7 -3
View File
@@ -119,9 +119,13 @@ export function useAI(): AIContext {
icon: <AISearchIcon />,
open: (query?: string) => {
if (query) {
setSearchState((prev) =>
prev ? { ...prev, query: null, ask: query, open: true } : null
);
setSearchState((prev) => ({
...prev,
query: null,
ask: query,
scope: prev?.scope ?? 'default',
open: true,
}));
}
},
pageAction: false,
@@ -247,11 +247,15 @@ 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,7 +37,10 @@ export async function Embed(props: BlockProps<gitbookAPI.DocumentBlockEmbed>) {
<>
<div
dangerouslySetInnerHTML={{
__html: embed.html,
__html:
context.mode !== 'print' && shouldLazyLoad(block.data.url)
? lazyLoadIframes(embed.html)
: embed.html,
}}
data-visual-test="blackout"
/>
@@ -73,6 +76,23 @@ 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.
*/
@@ -4,6 +4,7 @@ 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';
@@ -75,12 +76,15 @@ 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.
@@ -105,6 +109,7 @@ export function InsightsProvider(props: InsightsProviderProps) {
allEvents.push(
...transformEvents({
url: eventsForPathname.url,
previousUrl: eventsForPathname.previousUrl,
events: eventsForPathname.events,
context: currentContent,
pageContext: eventsForPathname.pageContext,
@@ -154,6 +159,9 @@ 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
@@ -161,6 +169,7 @@ 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 ?? []),
{
@@ -214,6 +223,18 @@ 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.
*/
@@ -240,6 +261,7 @@ function sendEvents(args: { eventUrl: string; events: api.SiteInsightsEvent[] })
*/
function transformEvents(input: {
url: string;
previousUrl: string | null;
events: TrackEventInput<InsightsEventName>[];
context: CurrentContentContext;
pageContext: InsightsEventPageContext;
@@ -258,6 +280,11 @@ 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,
@@ -29,18 +29,21 @@ interface PageCoverImageProps {
export function PageCoverImage(props: PageCoverImageProps) {
const { imgs, y, height, mask } = props;
const { containerRef, objectPositionY, isLoading } = useCoverPosition(imgs, y);
const { objectPositionY, isLoading } = useCoverPosition(imgs, y, {
height,
aspectRatio: PAGE_COVER_SIZE,
});
if (isLoading) {
return (
<div className="h-full w-full overflow-hidden" ref={containerRef}>
<div className="h-full w-full overflow-hidden">
<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" ref={containerRef} style={{ height }}>
<div className="h-full w-full overflow-hidden @container" style={{ height }}>
<img
src={imgs.light.src}
srcSet={imgs.light.srcSet}
@@ -52,7 +55,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:
@@ -73,7 +76,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:
@@ -1,6 +1,5 @@
'use client';
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useResizeObserver } from 'usehooks-ts';
import { useLayoutEffect, useState } from 'react';
interface ImageSize {
width: number;
@@ -22,19 +21,18 @@ interface Images {
}
/**
* Hook to calculate the object position Y percentage for a cover image
* based on the y offset, image dimensions, and container dimensions.
* 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.
*/
export function useCoverPosition(imgs: Images, y: number) {
const containerRef = useRef<HTMLDivElement>(null);
export function useCoverPosition(
imgs: Images,
y: number,
container: { height: number | undefined; aspectRatio: ImageSize }
) {
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.
@@ -68,42 +66,31 @@ export function useCoverPosition(imgs: Images, y: number) {
// 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 {
containerRef,
objectPositionY,
objectPositionY: imageDimensions
? getCoverObjectPositionY(imageDimensions, y, container)
: '50%',
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}))`;
}
@@ -82,7 +82,7 @@ type CoverOverlap =
* `data-over-cover="split"` with the crossing point in `--cover-edge`.
*/
function useMarkTextOverCover() {
React.useEffect(() => {
React.useLayoutEffect(() => {
const root = document.documentElement;
const pageCover = document.querySelector<HTMLElement>('[data-gb-page-cover]');
@@ -150,7 +150,7 @@ function useMarkTextOverCover() {
});
};
scheduleUpdate();
update();
window.addEventListener('scroll', scheduleUpdate, { passive: true });
window.addEventListener('resize', scheduleUpdate, { passive: true });
@@ -19,7 +19,7 @@ export async function Trademark(
const { space } = context;
const language = await getSpaceLanguage(context);
const url = new URL('https://www.gitbook.com');
const url = new URL('https://www.gitbook.com/powered-by');
url.searchParams.set('utm_source', 'content');
url.searchParams.set('utm_medium', 'trademark');
url.searchParams.set('utm_campaign', space.id);
+24
View File
@@ -0,0 +1,24 @@
/**
* Describe the `ask` and `goal` query parameters of the ask endpoint, for agent-facing prompts.
*/
export function renderAskParametersDescription(): string {
return `\`ask\` is the immediate question: it should be specific, self-contained, and written in natural language.
\`goal\` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with \`ask=how do I create an API token\`, a goal like \`build a script that syncs our docs to a CMS\` lets GitBook tailor the answer to that use case.`;
}
/**
* Render the "Querying This Documentation" section of the agent instructions.
* `pageUrl` is the URL of the current page, which the `ask` and `goal` parameters are appended to.
*/
export function renderQueryingDocumentation(options: { pageUrl: string }): string {
const { pageUrl } = options;
return `Perform an HTTP GET request on the following URL with the \`ask\` and \`goal\` query parameters:
\`\`\`
GET ${pageUrl}?ask=<question>&goal=<user_goal>
\`\`\`
${renderAskParametersDescription()}
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.`;
}
+8 -24
View File
@@ -1,6 +1,7 @@
import type { RevisionPageDocument, RevisionPageGroup } from '@gitbook/api';
import { isAIEnabled } from '@/components/utils/isAIChatEnabled';
import { renderQueryingDocumentation } from '@/lib/ask-prompt';
import type { GitBookSiteContext } from '@/lib/context';
import { getExposableError } from '@/lib/data';
import { linkerWithMarkdownPages } from '@/lib/links';
@@ -69,6 +70,11 @@ function renderNotFoundMarkdown(context: GitBookSiteContext, pagePath: string) {
const fullContentUrl = context.linker.toAbsoluteURL(
context.linker.toPathInSite('llms-full.txt')
);
const askPageUrl = context.linker.toAbsoluteURL(
context.linker.toPathForPagePath({
path: similarPages[0]?.path ?? 'docs/example',
})
);
return `# Page Not Found
@@ -85,20 +91,7 @@ If the exact page cannot be found, you can still retrieve the information using
### Option 1 — Ask a question (recommended)
Perform an HTTP GET request on the documentation index with the \`ask\` parameter, and the optional \`goal\` parameter:
\`\`\`
GET ${context.linker.toAbsoluteURL(
context.linker.toPathForPagePath({
path: similarPages[0]?.path ?? 'docs/example',
})
)}?ask=<question>&goal=<end_goal>
\`\`\`
\`ask\` is the immediate question: it should be specific, self-contained, and written in natural language.
\`goal\` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.
${renderQueryingDocumentation({ pageUrl: askPageUrl })}
### Option 2 — Browse the documentation index
@@ -145,16 +138,7 @@ This documentation is published with GitBook. GitBook is the documentation platf
## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.
Perform an HTTP GET request on the current page URL with the \`ask\` query parameter, and the optional \`goal\` query parameter:
\`\`\`
GET ${pageUrl}?ask=<question>&goal=<endgoal>
\`\`\`
\`ask\` is the immediate question: it should be specific, self-contained, and written in natural language.
\`goal\` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.
${renderQueryingDocumentation({ pageUrl })}
Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
`;