Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V2: search and ask AI #2889

Merged
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 42 additions & 13 deletions bun.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/gitbook-v2/package.json
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@
"version": "0.1.1",
"private": true,
"dependencies": {
"next": "canary",
"next": "^15.2.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"@gitbook/api": "0.96.1",
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ import {
} from '@/components/SiteLayout';
import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils';
import { GITBOOK_DISABLE_TRACKING } from '@v2/lib/env';
import { getThemeFromMiddleware } from '@v2/lib/middleware';
import { getThemeFromMiddleware, getVisitorAuthTokenFromMiddleware } from '@v2/lib/middleware';

interface SiteDynamicLayoutProps {
params: Promise<RouteLayoutParams>;
@@ -18,13 +18,15 @@ export default async function SiteDynamicLayout({
}: React.PropsWithChildren<SiteDynamicLayoutProps>) {
const context = await getDynamicSiteContext(await params);
const forcedTheme = await getThemeFromMiddleware();
const visitorAuthToken = await getVisitorAuthTokenFromMiddleware();

return (
<CustomizationRootLayout customization={context.customization}>
<SiteLayout
context={context}
forcedTheme={forcedTheme}
withTracking={!GITBOOK_DISABLE_TRACKING}
visitorAuthToken={visitorAuthToken}
>
{children}
</SiteLayout>
Original file line number Diff line number Diff line change
@@ -25,7 +25,11 @@ export default async function SiteStaticLayout({

return (
<CustomizationRootLayout customization={context.customization}>
<SiteLayout context={context} withTracking={!GITBOOK_DISABLE_TRACKING}>
<SiteLayout
context={context}
withTracking={!GITBOOK_DISABLE_TRACKING}
visitorAuthToken={null}
>
{children}
</SiteLayout>
</CustomizationRootLayout>
109 changes: 23 additions & 86 deletions packages/gitbook-v2/src/app/utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { fetchSiteContextByURL } from '@v2/lib/context';
import { createDataFetcher } from '@v2/lib/data';
import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_URL } from '@v2/lib/env';
import { createImageResizer } from '@v2/lib/images';
import { createLinker } from '@v2/lib/links';
import { headers } from 'next/headers';
import {
fetchSiteContextByURL,
fetchSiteContextByURLLookup,
getBaseContext,
} from '@v2/lib/context';
import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';

export type RouteParamMode = 'url-host' | 'url';

@@ -21,66 +21,36 @@ export type RouteParams = RouteLayoutParams & {
/**
* Get the static context when rendering statically a site.
*/
export async function getStaticSiteContext(params: RouteLayoutParams) {
const url = getSiteURLFromParams(params);

const dataFetcher = createDataFetcher();
const { linker, host } = createLinkerFromParams(params);
const context = await fetchSiteContextByURL(
{
dataFetcher,
linker,
},
export function getStaticSiteContext(params: RouteLayoutParams) {
const siteURL = getSiteURLFromParams(params);
return fetchSiteContextByURL(
getBaseContext({
siteURL,
urlMode: getModeFromParams(params.mode),
}),
{
url: url.toString(),
url: siteURL.toString(),
visitorAuthToken: null,
redirectOnError: false,
}
);

context.imageResizer = createImageResizer({
host,
linker: context.linker,
});

return context;
}

/**
* Get the site context when rendering dynamically.
* The context will depend on the request.
*/
export async function getDynamicSiteContext(params: RouteLayoutParams) {
const url = getSiteURLFromParams(params);
const headersSet = await headers();

const dataFetcher = createDataFetcher({
apiToken: headersSet.get('x-gitbook-token') ?? GITBOOK_API_TOKEN,
apiEndpoint: headersSet.get('x-gitbook-api') ?? GITBOOK_API_URL,
});

const { linker, host } = createLinkerFromParams(params);

const context = await fetchSiteContextByURL(
{
dataFetcher,
linker,
},
{
url: url.toString(),
visitorAuthToken: headersSet.get('x-gitbook-visitor-token'),

// TODO: set it only when the token comes from the cookies.
redirectOnError: true,
}
const siteURL = getSiteURLFromParams(params);
const siteURLData = await getSiteURLDataFromMiddleware();

return fetchSiteContextByURLLookup(
getBaseContext({
siteURL,
urlMode: getModeFromParams(params.mode),
}),
siteURLData
);

context.imageResizer = createImageResizer({
host,
linker: context.linker,
});

return context;
}

/**
@@ -91,39 +61,6 @@ export function getPagePathFromParams(params: RouteParams) {
return decoded;
}

function createLinkerFromParams(params: RouteLayoutParams) {
const url = getSiteURLFromParams(params);
const mode = getModeFromParams(params.mode);

if (mode === 'url-host') {
return {
linker: createLinker({
host: url.host,
pathname: '/',
}),
host: url.host,
};
}

const gitbookURL = new URL(GITBOOK_URL);
const linker = createLinker({
protocol: gitbookURL.protocol,
host: gitbookURL.host,
pathname: `/url/${url.host}`,
});

// Create link in the same format for links to other sites/sections.
linker.toLinkForContent = (rawURL: string) => {
const urlObject = new URL(rawURL);
return `/url/${urlObject.host}${urlObject.pathname}`;
};

return {
linker,
host: gitbookURL.host,
};
}

function getSiteURLFromParams(params: RouteLayoutParams) {
const decoded = decodeURIComponent(params.siteURL);
const url = new URL(`https://${decoded}`);
87 changes: 69 additions & 18 deletions packages/gitbook-v2/src/lib/context.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getSiteStructureSections } from '@/lib/sites';
import type {
ChangeRequest,
PublishedSiteContentLookup,
RevisionPage,
RevisionPageDocument,
Site,
@@ -15,8 +16,9 @@ import type {
import { type GitBookDataFetcher, createDataFetcher } from '@v2/lib/data';
import { redirect } from 'next/navigation';
import { assert } from 'ts-essentials';
import type { ImageResizer } from './images';
import { type GitBookSpaceLinker, appendBasePathToLinker } from './links';
import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_URL } from './env';
import { type ImageResizer, createImageResizer } from './images';
import { type GitBookSpaceLinker, createLinker } from './links';

/**
* Generic context when rendering content.
@@ -90,9 +92,6 @@ export type GitBookSiteContext = GitBookSpaceContext & {

/** Scripts to load for the site. */
scripts: SiteIntegrationScript[];

/** Visitor token used to fetch the site */
visitorAuthToken: string | null;
};

/**
@@ -102,6 +101,58 @@ export type GitBookPageContext = (GitBookSpaceContext | GitBookSiteContext) & {
page: RevisionPageDocument;
};

/**
* Get the base context for a request.
*/
export function getBaseContext(input: {
siteURL: URL | string;
urlMode: 'url' | 'url-host';
apiToken?: string | null;
}) {
const url = typeof input.siteURL === 'string' ? new URL(input.siteURL) : input.siteURL;
const urlMode = input.urlMode;

const dataFetcher = createDataFetcher({
apiToken: input.apiToken ?? GITBOOK_API_TOKEN,
apiEndpoint: GITBOOK_API_URL,
});
const gitbookURL = new URL(GITBOOK_URL);

const linker =
urlMode === 'url-host'
? createLinker({
host: url.host,
pathname: url.pathname,
})
: createLinker({
protocol: gitbookURL.protocol,
host: gitbookURL.host,
pathname: `/url/${url.host}${url.pathname}`,
});

if (urlMode === 'url') {
// Create link in the same format for links to other sites/sections.
linker.toLinkForContent = (rawURL: string) => {
const urlObject = new URL(rawURL);
return `/url/${urlObject.host}${urlObject.pathname}`;
};
}

const imageResizer = createImageResizer({
host: urlMode === 'url-host' ? url.host : gitbookURL.host,

// To ensure image resizing work for proxied sites,
// we serve images from the root of the site.
linker: linker,
});

return {
dataFetcher,
linker,
imageResizer,
};
}

/**
* Fetch the context of a site for a given URL and a base context.
*/
@@ -120,15 +171,25 @@ export async function fetchSiteContextByURL(
redirectOnError: input.redirectOnError,
});

return fetchSiteContextByURLLookup(baseContext, data);
}

/**
* Fetch the context of a site using the resolution of a URL
*/
export async function fetchSiteContextByURLLookup(
baseContext: GitBookBaseContext,
data: PublishedSiteContentLookup
): Promise<GitBookSiteContext> {
const { dataFetcher } = baseContext;
if ('redirect' in data) {
redirect(data.redirect);
}

const context = await fetchSiteContextByIds(
return await fetchSiteContextByIds(
{
...baseContext,
dataFetcher: createDataFetcher({
apiEndpoint: dataFetcher.apiEndpoint,
dataFetcher: dataFetcher.withToken({
apiToken: data.apiToken,
}),
},
@@ -141,16 +202,8 @@ export async function fetchSiteContextByURL(
shareKey: data.shareKey,
changeRequest: data.changeRequest,
revision: data.revision,
visitorAuthToken: input.visitorAuthToken,
}
);

const siteContext = {
...context,
linker: appendBasePathToLinker(context.linker, data.basePath),
};

return siteContext;
}

/**
@@ -167,7 +220,6 @@ export async function fetchSiteContextByIds(
shareKey: string | undefined;
changeRequest: string | undefined;
revision: string | undefined;
visitorAuthToken: string | null;
}
): Promise<GitBookSiteContext> {
const { dataFetcher } = baseContext;
@@ -233,7 +285,6 @@ export async function fetchSiteContextByIds(
structure: siteStructure,
sections,
scripts,
visitorAuthToken: ids.visitorAuthToken,
};
}

31 changes: 31 additions & 0 deletions packages/gitbook-v2/src/lib/data/api.ts
Original file line number Diff line number Diff line change
@@ -35,6 +35,17 @@ export function createDataFetcher(input: DataFetcherInput = commonInput): GitBoo
return {
apiEndpoint: input.apiEndpoint,

async api() {
return getAPI(input);
},

withToken({ apiToken }) {
return createDataFetcher({
...input,
apiToken,
});
},

//
// API that are tied to the token
//
@@ -124,6 +135,9 @@ export function createDataFetcher(input: DataFetcherInput = commonInput): GitBoo
spaceId: params.spaceId,
});
},
searchSiteContent(params) {
return searchSiteContent(input, params);
},

//
// API that are not tied to the token
@@ -498,6 +512,23 @@ async function getEmbedByUrl(
return res.data;
}

async function searchSiteContent(
input: DataFetcherInput,
params: Parameters<GitBookDataFetcher['searchSiteContent']>[0]
) {
'use cache';

const { organizationId, siteId, query, scope } = params;

cacheLife('days');

const res = await getAPI(input).orgs.searchSiteContent(organizationId, siteId, {
query,
...scope,
});
return res.data.items;
}

function getAPI(input: DataFetcherInput) {
const { apiEndpoint, apiToken } = input;
const api = new GitBookAPI({
Loading
Oops, something went wrong.
Loading
Oops, something went wrong.