From 2863fe0dc17e2bf20dc8cb4137f72480024cc73a Mon Sep 17 00:00:00 2001
From: Taran Vohra
Date: Mon, 16 Jun 2025 09:59:55 +0530
Subject: [PATCH 1/4] Use `resolvePublishedContentByUrl` instead of the
deprecated resolution endpoint (#3310)
---
packages/gitbook-v2/src/lib/data/lookup.ts | 97 +++++-----------------
packages/gitbook-v2/src/middleware.ts | 20 +----
2 files changed, 24 insertions(+), 93 deletions(-)
diff --git a/packages/gitbook-v2/src/lib/data/lookup.ts b/packages/gitbook-v2/src/lib/data/lookup.ts
index 4c999bd7a..be8c21f1d 100644
--- a/packages/gitbook-v2/src/lib/data/lookup.ts
+++ b/packages/gitbook-v2/src/lib/data/lookup.ts
@@ -1,7 +1,7 @@
import { race, tryCatch } from '@/lib/async';
import { joinPath, joinPathWithBaseURL } from '@/lib/paths';
import { trace } from '@/lib/tracing';
-import type { GitBookAPI, PublishedSiteContentLookup, SiteVisitorPayload } from '@gitbook/api';
+import type { PublishedSiteContentLookup, SiteVisitorPayload } from '@gitbook/api';
import { apiClient } from './api';
import { getExposableError } from './errors';
import type { DataFetcherResponse } from './types';
@@ -18,85 +18,32 @@ interface LookupPublishedContentByUrlInput {
* Lookup a content by its URL using the GitBook resolvePublishedContentByUrl API endpoint.
* To optimize caching, we try multiple lookup alternatives and return the first one that matches.
*/
-export async function resolvePublishedContentByUrl(input: LookupPublishedContentByUrlInput) {
- return lookupPublishedContentByUrl({
- url: input.url,
- fetchLookupAPIResult: ({ url, signal }) => {
- const api = apiClient({ apiToken: input.apiToken });
- return trace(
- {
- operation: 'resolvePublishedContentByUrl',
- name: url,
- },
- () =>
- tryCatch(
- api.urls.resolvePublishedContentByUrl(
- {
- url,
- ...(input.visitorPayload ? { visitor: input.visitorPayload } : {}),
- redirectOnError: input.redirectOnError,
- },
- { signal }
- )
- )
- );
- },
- });
-}
-
-/**
- * Lookup a content by its URL using the GitBook getPublishedContentByUrl API endpoint.
- * To optimize caching, we try multiple lookup alternatives and return the first one that matches.
- *
- * @deprecated use resolvePublishedContentByUrl.
- *
- */
-export async function getPublishedContentByURL(input: LookupPublishedContentByUrlInput) {
- return lookupPublishedContentByUrl({
- url: input.url,
- fetchLookupAPIResult: ({ url, signal }) => {
- const api = apiClient({ apiToken: input.apiToken });
- return trace(
- {
- operation: 'getPublishedContentByURL',
- name: url,
- },
- () =>
- tryCatch(
- api.urls.getPublishedContentByUrl(
- {
- url,
- visitorAuthToken: input.visitorPayload.jwtToken ?? undefined,
- redirectOnError: input.redirectOnError,
- // @ts-expect-error - cacheVersion is not a real query param
- cacheVersion: 'v2',
- },
- { signal }
- )
- )
- );
- },
- });
-}
-
-type TryCatch = ReturnType>;
-
-async function lookupPublishedContentByUrl(input: {
- url: string;
- fetchLookupAPIResult: (args: {
- url: string;
- signal: AbortSignal;
- }) => TryCatch>>;
-}): Promise> {
+export async function lookupPublishedContentByUrl(
+ input: LookupPublishedContentByUrlInput
+): Promise> {
const lookupURL = new URL(input.url);
const url = stripURLSearch(lookupURL);
const lookup = getURLLookupAlternatives(url);
const result = await race(lookup.urls, async (alternative, { signal }) => {
- const callResult = await input.fetchLookupAPIResult({
- url: alternative.url,
- signal,
- });
+ const api = apiClient({ apiToken: input.apiToken });
+ const callResult = await trace(
+ {
+ operation: 'resolvePublishedContentByUrl',
+ name: alternative.url,
+ },
+ () =>
+ tryCatch(
+ api.urls.resolvePublishedContentByUrl(
+ {
+ url: alternative.url,
+ ...(input.visitorPayload ? { visitor: input.visitorPayload } : {}),
+ redirectOnError: input.redirectOnError,
+ },
+ { signal }
+ )
+ )
+ );
if (callResult.error) {
if (alternative.primary) {
diff --git a/packages/gitbook-v2/src/middleware.ts b/packages/gitbook-v2/src/middleware.ts
index 0d57ce27e..d413d6a43 100644
--- a/packages/gitbook-v2/src/middleware.ts
+++ b/packages/gitbook-v2/src/middleware.ts
@@ -16,10 +16,9 @@ import {
import { serveResizedImage } from '@/routes/image';
import {
DataFetcherError,
- getPublishedContentByURL,
getVisitorAuthBasePath,
+ lookupPublishedContentByUrl,
normalizeURL,
- resolvePublishedContentByUrl,
throwIfDataError,
} from '@v2/lib/data';
import { isGitBookAssetsHostURL, isGitBookHostURL } from '@v2/lib/env';
@@ -34,18 +33,6 @@ export const config = {
type URLWithMode = { url: URL; mode: 'url' | 'url-host' };
-/**
- * Temporary list of hosts to test adaptive content using the new resolution API.
- */
-const ADAPTIVE_CONTENT_HOSTS = [
- 'docs.gitbook.com',
- 'paypal.gitbook.com',
- 'adaptive-docs.gitbook-staging.com',
- 'enriched-content-playground.gitbook-staging.io',
- 'docs.testgitbook.com',
- 'launchdarkly-site.gitbook.education',
-];
-
export async function middleware(request: NextRequest) {
try {
const requestURL = new URL(request.url);
@@ -104,11 +91,8 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
});
const withAPIToken = async (apiToken: string | null) => {
- const resolve = ADAPTIVE_CONTENT_HOSTS.includes(siteRequestURL.hostname)
- ? resolvePublishedContentByUrl
- : getPublishedContentByURL;
const siteURLData = await throwIfDataError(
- resolve({
+ lookupPublishedContentByUrl({
url: siteRequestURL.toString(),
visitorPayload: {
jwtToken: visitorToken?.token ?? undefined,
From 42d88da73ce7d0a71e20c60625571bc773443c56 Mon Sep 17 00:00:00 2001
From: Utku Ufuk
Date: Mon, 16 Jun 2025 12:31:15 +0200
Subject: [PATCH 2/4] Fix UX issue about highlighting the search term in search
result sections (#3323)
---
.changeset/afraid-gifts-sparkle.md | 5 +++++
README.md | 10 ++++++++--
.../Search/SearchSectionResultItem.tsx | 17 ++++++++++++-----
3 files changed, 25 insertions(+), 7 deletions(-)
create mode 100644 .changeset/afraid-gifts-sparkle.md
diff --git a/.changeset/afraid-gifts-sparkle.md b/.changeset/afraid-gifts-sparkle.md
new file mode 100644
index 000000000..ec33c1efc
--- /dev/null
+++ b/.changeset/afraid-gifts-sparkle.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Fix UX issue about highlighting the search term in search result sections
diff --git a/README.md b/README.md
index 8a9f5c2fd..9b13767ff 100644
--- a/README.md
+++ b/README.md
@@ -56,13 +56,19 @@ git clone https://github.com/gitbookIO/gitbook.git
bun install
```
-4. Start your local development server.
+4. Run build.
+
+```
+bun build:v2
+```
+
+5. Start your local development server.
```
bun dev:v2
```
-5. Open a published GitBook space in your web browser, prefixing it with `http://localhost:3000/`.
+6. Open a published GitBook space in your web browser, prefixing it with `http://localhost:3000/`.
examples:
diff --git a/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx b/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx
index 960b1eb38..a0f467f80 100644
--- a/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx
+++ b/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx
@@ -66,11 +66,7 @@ export const SearchSectionResultItem = React.forwardRef(function SearchSectionRe
) : null}
- {item.body ? (
-
-
-
- ) : null}
+ {item.body ? highlightQueryInBody(item.body, query) : null}
);
});
+
+function highlightQueryInBody(body: string, query: string) {
+ const idx = body.indexOf(query);
+
+ // Ensure the query to be highlighted is visible in the body.
+ return (
+
+
+
+ );
+}
From 72cd0e59e683ace3d269f0e59338b41c36b88651 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Mon, 16 Jun 2025 13:37:45 +0200
Subject: [PATCH 3/4] Replace withoutConcurrency by a smarter React.cache
(#3325)
---
.changeset/fuzzy-tables-jump.md | 5 +
bun.lock | 3 +
packages/gitbook-v2/package.json | 3 +-
packages/gitbook-v2/src/lib/cache.test.ts | 80 ++
packages/gitbook-v2/src/lib/cache.ts | 59 +
packages/gitbook-v2/src/lib/data/api.ts | 1077 +++++++++----------
packages/gitbook-v2/src/lib/data/memoize.ts | 92 --
7 files changed, 658 insertions(+), 661 deletions(-)
create mode 100644 .changeset/fuzzy-tables-jump.md
create mode 100644 packages/gitbook-v2/src/lib/cache.test.ts
create mode 100644 packages/gitbook-v2/src/lib/cache.ts
delete mode 100644 packages/gitbook-v2/src/lib/data/memoize.ts
diff --git a/.changeset/fuzzy-tables-jump.md b/.changeset/fuzzy-tables-jump.md
new file mode 100644
index 000000000..ada33d1c5
--- /dev/null
+++ b/.changeset/fuzzy-tables-jump.md
@@ -0,0 +1,5 @@
+---
+"gitbook-v2": patch
+---
+
+Optimize performances by using a smarter per-request cache arround data cached functions
diff --git a/bun.lock b/bun.lock
index 3ff6d61c4..b4d2b49d1 100644
--- a/bun.lock
+++ b/bun.lock
@@ -169,6 +169,7 @@
"assert-never": "^1.2.1",
"jwt-decode": "^4.0.0",
"next": "^15.3.2",
+ "object-identity": "^0.1.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"rison": "^0.1.1",
@@ -2438,6 +2439,8 @@
"object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="],
+ "object-identity": ["object-identity@0.1.2", "", {}, "sha512-Px5puVllX5L2aBjbcfXpiG5xXeq6OE8RckryTeP2Zq+0PgYrCGJXmC6LblWgknKSJs11Je2W4U2NOWFj3t/QXQ=="],
+
"object-inspect": ["object-inspect@1.13.2", "", {}, "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g=="],
"object-treeify": ["object-treeify@1.1.33", "", {}, "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A=="],
diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json
index 5723cdbc7..cfa6e71b9 100644
--- a/packages/gitbook-v2/package.json
+++ b/packages/gitbook-v2/package.json
@@ -14,7 +14,8 @@
"react-dom": "^19.0.0",
"rison": "^0.1.1",
"server-only": "^0.0.1",
- "warn-once": "^0.1.1"
+ "warn-once": "^0.1.1",
+ "object-identity": "^0.1.2"
},
"devDependencies": {
"gitbook": "*",
diff --git a/packages/gitbook-v2/src/lib/cache.test.ts b/packages/gitbook-v2/src/lib/cache.test.ts
new file mode 100644
index 000000000..735b1fb06
--- /dev/null
+++ b/packages/gitbook-v2/src/lib/cache.test.ts
@@ -0,0 +1,80 @@
+import { describe, expect, it } from 'bun:test';
+import { withStableRef } from './cache';
+
+describe('withStableRef', () => {
+ it('should return primitive values as is', () => {
+ const toStableRef = withStableRef();
+
+ expect(toStableRef(42)).toBe(42);
+ expect(toStableRef('hello')).toBe('hello');
+ expect(toStableRef(true)).toBe(true);
+ expect(toStableRef(null)).toBe(null);
+ expect(toStableRef(undefined)).toBe(undefined);
+ });
+
+ it('should return the same reference for identical objects', () => {
+ const toStableRef = withStableRef();
+
+ const obj1 = { a: 1, b: 2 };
+ const obj2 = { a: 1, b: 2 };
+
+ const ref1 = toStableRef(obj1);
+ const ref2 = toStableRef(obj2);
+
+ expect(ref1).toBe(ref2);
+ expect(ref1).toBe(obj1);
+ expect(ref1).not.toBe(obj2);
+ });
+
+ it('should return the same reference for identical arrays', () => {
+ const toStableRef = withStableRef();
+
+ const arr1 = [1, 2, 3];
+ const arr2 = [1, 2, 3];
+
+ const ref1 = toStableRef(arr1);
+ const ref2 = toStableRef(arr2);
+
+ expect(ref1).toBe(ref2);
+ expect(ref1).toBe(arr1);
+ expect(ref1).not.toBe(arr2);
+ });
+
+ it('should return the same reference for identical nested objects', () => {
+ const toStableRef = withStableRef();
+
+ const obj1 = { a: { b: 1 }, c: [2, 3] };
+ const obj2 = { a: { b: 1 }, c: [2, 3] };
+
+ const ref1 = toStableRef(obj1);
+ const ref2 = toStableRef(obj2);
+
+ expect(ref1).toBe(ref2);
+ expect(ref1).toBe(obj1);
+ expect(ref1).not.toBe(obj2);
+ });
+
+ it('should return different references for different objects', () => {
+ const toStableRef = withStableRef();
+
+ const obj1 = { a: 1 };
+ const obj2 = { a: 2 };
+
+ const ref1 = toStableRef(obj1);
+ const ref2 = toStableRef(obj2);
+
+ expect(ref1).not.toBe(ref2);
+ });
+
+ it('should maintain reference stability across multiple calls', () => {
+ const toStableRef = withStableRef();
+
+ const obj = { a: 1 };
+ const ref1 = toStableRef(obj);
+ const ref2 = toStableRef(obj);
+ const ref3 = toStableRef(obj);
+
+ expect(ref1).toBe(ref2);
+ expect(ref2).toBe(ref3);
+ });
+});
diff --git a/packages/gitbook-v2/src/lib/cache.ts b/packages/gitbook-v2/src/lib/cache.ts
new file mode 100644
index 000000000..e05753907
--- /dev/null
+++ b/packages/gitbook-v2/src/lib/cache.ts
@@ -0,0 +1,59 @@
+import { identify } from 'object-identity';
+import * as React from 'react';
+
+/**
+ * Equivalent to `React.cache` but with support for non-primitive arguments.
+ * As `React.cache` only uses `Object.is` to compare arguments, it will not work with non-primitive arguments.
+ */
+export function cache(fn: (...args: Args) => Return) {
+ const cached = React.cache(fn);
+
+ return (...args: Args) => {
+ const toStableRef = getWithStableRef();
+ const stableArgs = args.map((value) => {
+ return toStableRef(value);
+ }) as Args;
+ return cached(...stableArgs);
+ };
+}
+
+/**
+ * To ensure memory is garbage collected between each request, we use a per-request cache to store the ref maps.
+ */
+const getWithStableRef = React.cache(withStableRef);
+
+/**
+ * Create a function that converts a value to a stable reference.
+ */
+export function withStableRef(): (value: T) => T {
+ const reverseIndex = new WeakMap