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/.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/.changeset/slimy-cows-press.md b/.changeset/slimy-cows-press.md
new file mode 100644
index 000000000..17ec44ca8
--- /dev/null
+++ b/.changeset/slimy-cows-press.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Ignore case while highlighting search results.
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/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
) : null}
- {item.body ? (
-
-
-
- ) : null}
+ {item.body ? highlightQueryInBody(item.body, query) : null}
);
});
+
+function highlightQueryInBody(body: string, query: string) {
+ const idx = body.toLocaleLowerCase().indexOf(query.toLocaleLowerCase());
+
+ // Ensure the query to be highlighted is visible in the body.
+ return (
+
+
+
+ );
+}