From 2a23f1f00365268024234041157f480b8b5a18f9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Greg=20Berg=C3=A9?= <berge.greg@gmail.com>
Date: Wed, 23 Apr 2025 09:31:22 +0200
Subject: [PATCH 001/127] Improve performances of TOC (#3176)

---
 .../SiteSections/SiteSectionList.tsx          |  52 ++--
 .../TableOfContents/PageDocumentItem.tsx      |   4 +-
 .../TableOfContents/PageGroupItem.tsx         |  20 +-
 .../TableOfContents/PageLinkItem.tsx          |   6 +-
 .../components/TableOfContents/PagesList.tsx  |  55 ++--
 .../TableOfContents/TOCScroller.tsx           | 122 ++++----
 .../TableOfContents/TableOfContents.tsx       |  25 +-
 .../TableOfContents/ToggleableLinkItem.tsx    | 294 +++++++++++-------
 .../components/hooks/useCurrentPagePath.ts    |  11 +-
 .../components/hooks/useToggleAnimation.ts    |  41 +--
 10 files changed, 344 insertions(+), 286 deletions(-)

diff --git a/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx b/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx
index a8b83cfb5a..6026296839 100644
--- a/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx
+++ b/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx
@@ -75,13 +75,13 @@ export function SiteSectionListItem(props: {
     const isMounted = useIsMounted();
     React.useEffect(() => {}, [isMounted]); // This updates the useScrollToActiveTOCItem hook once we're mounted, so we can actually scroll to the this item
 
-    const linkRef = React.createRef<HTMLAnchorElement>();
-    useScrollToActiveTOCItem({ linkRef, isActive });
+    const anchorRef = React.createRef<HTMLAnchorElement>();
+    useScrollToActiveTOCItem({ anchorRef, isActive });
 
     return (
         <Link
+            ref={anchorRef}
             href={section.url}
-            ref={linkRef}
             aria-current={isActive && 'page'}
             className={tcls(
                 'group/section-link flex flex-row items-center gap-3 rounded-md straight-corners:rounded-none px-3 py-2 transition-all hover:bg-tint-hover hover:text-tint-strong contrast-more:hover:ring-1 contrast-more:hover:ring-tint',
@@ -121,18 +121,15 @@ export function SiteSectionGroupItem(props: {
 
     const hasDescendants = group.sections.length > 0;
     const isActiveGroup = group.sections.some((section) => section.id === currentSection.id);
-    const [isVisible, setIsVisible] = React.useState(isActiveGroup);
+    const shouldOpen = hasDescendants && isActiveGroup;
+    const [isOpen, setIsOpen] = React.useState(shouldOpen);
 
-    // Update the visibility of the children, if we are navigating to a descendant.
+    // Update the visibility of the children if the group becomes active.
     React.useEffect(() => {
-        if (!hasDescendants) {
-            return;
+        if (shouldOpen) {
+            setIsOpen(shouldOpen);
         }
-
-        setIsVisible((prev) => prev || isActiveGroup);
-    }, [isActiveGroup, hasDescendants]);
-
-    const { show, hide, scope } = useToggleAnimation({ hasDescendants, isVisible });
+    }, [shouldOpen]);
 
     return (
         <>
@@ -141,7 +138,7 @@ export function SiteSectionGroupItem(props: {
                 onClick={(event) => {
                     event.preventDefault();
                     event.stopPropagation();
-                    setIsVisible((prev) => !prev);
+                    setIsOpen((prev) => !prev);
                 }}
                 className={`group/section-link flex w-full flex-row items-center gap-3 rounded-md straight-corners:rounded-none px-3 py-2 text-left transition-all hover:bg-tint-hover hover:text-tint-strong contrast-more:hover:ring-1 contrast-more:hover:ring-tint ${
                     isActiveGroup
@@ -184,7 +181,7 @@ export function SiteSectionGroupItem(props: {
                         'after:h-7',
                         'hover:bg-tint-active',
                         'hover:text-current',
-                        isActiveGroup ? ['hover:bg-tint-hover'] : []
+                        isActiveGroup && 'hover:bg-tint-hover'
                     )}
                 >
                     <Icon
@@ -201,17 +198,13 @@ export function SiteSectionGroupItem(props: {
                             'group-hover:opacity-11',
                             'contrast-more:opacity-11',
 
-                            isVisible ? ['rotate-90'] : ['rotate-0']
+                            isOpen ? 'rotate-90' : 'rotate-0'
                         )}
                     />
                 </span>
             </button>
             {hasDescendants ? (
-                <motion.div
-                    ref={scope}
-                    className={tcls(isVisible ? null : '[&_ul>li]:opacity-1')}
-                    initial={isVisible ? show : hide}
-                >
+                <Descendants isVisible={isOpen}>
                     {group.sections.map((section) => (
                         <SiteSectionListItem
                             section={section}
@@ -220,8 +213,25 @@ export function SiteSectionGroupItem(props: {
                             className="pl-5"
                         />
                     ))}
-                </motion.div>
+                </Descendants>
             ) : null}
         </>
     );
 }
+
+function Descendants(props: {
+    isVisible: boolean;
+    children: React.ReactNode;
+}) {
+    const { isVisible, children } = props;
+    const { show, hide, scope } = useToggleAnimation(isVisible);
+    return (
+        <motion.div
+            ref={scope}
+            className={isVisible ? undefined : '[&_ul>li]:opacity-1'}
+            initial={isVisible ? show : hide}
+        >
+            {children}
+        </motion.div>
+    );
+}
diff --git a/packages/gitbook/src/components/TableOfContents/PageDocumentItem.tsx b/packages/gitbook/src/components/TableOfContents/PageDocumentItem.tsx
index ad7fc513e3..2ff3289d7f 100644
--- a/packages/gitbook/src/components/TableOfContents/PageDocumentItem.tsx
+++ b/packages/gitbook/src/components/TableOfContents/PageDocumentItem.tsx
@@ -20,7 +20,7 @@ export async function PageDocumentItem(props: {
     const href = context.linker.toPathForPage({ pages: rootPages, page });
 
     return (
-        <li className={tcls('flex', 'flex-col')}>
+        <li className="flex flex-col">
             <ToggleableLinkItem
                 href={href}
                 pathname={getPagePath(rootPages, page)}
@@ -52,7 +52,7 @@ export async function PageDocumentItem(props: {
                 }
             >
                 {page.emoji || page.icon ? (
-                    <span className={tcls('flex', 'gap-3', 'items-center')}>
+                    <span className="flex items-center gap-3">
                         <TOCPageIcon page={page} />
                         {page.title}
                     </span>
diff --git a/packages/gitbook/src/components/TableOfContents/PageGroupItem.tsx b/packages/gitbook/src/components/TableOfContents/PageGroupItem.tsx
index 884f6eedac..24ea366a2e 100644
--- a/packages/gitbook/src/components/TableOfContents/PageGroupItem.tsx
+++ b/packages/gitbook/src/components/TableOfContents/PageGroupItem.tsx
@@ -15,27 +15,13 @@ export function PageGroupItem(props: {
     const { rootPages, page, context } = props;
 
     return (
-        <li className={tcls('flex', 'flex-col', 'group/page-group-item')}>
+        <li className="group/page-group-item flex flex-col">
             <div
                 className={tcls(
-                    'flex',
-                    'items-center',
-
-                    'gap-3',
-                    'px-3',
-                    'z-[1]',
-                    'sticky',
-                    '-top-5',
-                    'pt-6',
-                    'group-first/page-group-item:-mt-5',
+                    '-top-5 group-first/page-group-item:-mt-5 sticky z-[1] flex items-center gap-3 px-3 pt-6',
+                    'font-semibold text-xs uppercase tracking-wide',
                     'pb-3', // Add extra padding to make the header fade a bit nicer
                     '-mb-1.5', // Then pull the page items a bit closer, effective bottom padding is 1.5 units / 6px.
-
-                    'text-xs',
-                    'tracking-wide',
-                    'font-semibold',
-                    'uppercase',
-
                     '[mask-image:linear-gradient(rgba(0,0,0,1)_70%,rgba(0,0,0,0))]', // Fade out effect of fixed page items. We want the fade to start past the header, this is a good approximation.
                     'bg-tint-base',
                     'sidebar-filled:bg-tint-subtle',
diff --git a/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx b/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx
index 43c31730c5..636cc514d9 100644
--- a/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx
+++ b/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx
@@ -55,9 +55,9 @@ export async function PageLinkItem(props: { page: RevisionPageLink; context: Git
                         'shrink-0',
                         'text-current',
                         'transition-colors',
-                        '[&>path]:transition-[opacity]',
-                        '[&>path]:[opacity:0.40]',
-                        'group-hover:[&>path]:[opacity:1]'
+                        '[&>path]:transition-opacity',
+                        '[&>path]:opacity-[0.4]',
+                        'group-hover:[&>path]:opacity-11'
                     )}
                 />
             </Link>
diff --git a/packages/gitbook/src/components/TableOfContents/PagesList.tsx b/packages/gitbook/src/components/TableOfContents/PagesList.tsx
index ff92f269b7..ccd7579ec3 100644
--- a/packages/gitbook/src/components/TableOfContents/PagesList.tsx
+++ b/packages/gitbook/src/components/TableOfContents/PagesList.tsx
@@ -1,8 +1,9 @@
-import { type RevisionPage, RevisionPageType } from '@gitbook/api';
+import type { RevisionPage } from '@gitbook/api';
 import type { GitBookSiteContext } from '@v2/lib/context';
 
 import { type ClassValue, tcls } from '@/lib/tailwind';
 
+import assertNever from 'assert-never';
 import { PageDocumentItem } from './PageDocumentItem';
 import { PageGroupItem } from './PageGroupItem';
 import { PageLinkItem } from './PageLinkItem';
@@ -16,41 +17,45 @@ export function PagesList(props: {
     const { rootPages, pages, context, style } = props;
 
     return (
-        <ul className={tcls('flex', 'flex-col', 'gap-y-0.5', style)}>
+        <ul className={tcls('flex flex-col gap-y-0.5', style)}>
             {pages.map((page) => {
-                if (page.type === RevisionPageType.Computed) {
+                if (page.type === 'computed') {
                     throw new Error(
                         'Unexpected computed page, it should have been computed in the API'
                     );
                 }
 
-                if (page.type === RevisionPageType.Link) {
-                    return <PageLinkItem key={page.id} page={page} context={context} />;
-                }
-
                 if (page.hidden) {
                     return null;
                 }
 
-                if (page.type === RevisionPageType.Group) {
-                    return (
-                        <PageGroupItem
-                            key={page.id}
-                            rootPages={rootPages}
-                            page={page}
-                            context={context}
-                        />
-                    );
+                switch (page.type) {
+                    case 'document':
+                        return (
+                            <PageDocumentItem
+                                key={page.id}
+                                rootPages={rootPages}
+                                page={page}
+                                context={context}
+                            />
+                        );
+
+                    case 'link':
+                        return <PageLinkItem key={page.id} page={page} context={context} />;
+
+                    case 'group':
+                        return (
+                            <PageGroupItem
+                                key={page.id}
+                                rootPages={rootPages}
+                                page={page}
+                                context={context}
+                            />
+                        );
+
+                    default:
+                        assertNever(page);
                 }
-
-                return (
-                    <PageDocumentItem
-                        key={page.id}
-                        rootPages={rootPages}
-                        page={page}
-                        context={context}
-                    />
-                );
             })}
         </ul>
     );
diff --git a/packages/gitbook/src/components/TableOfContents/TOCScroller.tsx b/packages/gitbook/src/components/TableOfContents/TOCScroller.tsx
index 9e9332ea1c..492dc9022d 100644
--- a/packages/gitbook/src/components/TableOfContents/TOCScroller.tsx
+++ b/packages/gitbook/src/components/TableOfContents/TOCScroller.tsx
@@ -1,40 +1,64 @@
 'use client';
 
-import React from 'react';
+import React, {
+    useCallback,
+    useEffect,
+    useMemo,
+    useRef,
+    type ComponentPropsWithoutRef,
+} from 'react';
+import { assert } from 'ts-essentials';
 
-import { type ClassValue, tcls } from '@/lib/tailwind';
+interface TOCScrollContainerContextType {
+    onContainerMount: (listener: (element: HTMLDivElement) => void) => () => void;
+}
 
-const TOCScrollContainerRefContext = React.createContext<React.RefObject<HTMLDivElement> | null>(
-    null
-);
+const TOCScrollContainerContext = React.createContext<TOCScrollContainerContextType | null>(null);
 
-function useTOCScrollContainerRefContext() {
-    const ctx = React.useContext(TOCScrollContainerRefContext);
-    if (!ctx) {
-        throw new Error('Context `TOCScrollContainerRefContext` must be used within Provider');
-    }
+function useTOCScrollContainerContext() {
+    const ctx = React.useContext(TOCScrollContainerContext);
+    assert(ctx);
     return ctx;
 }
 
-export function TOCScrollContainer(props: {
-    children: React.ReactNode;
-    className?: ClassValue;
-    style?: React.CSSProperties;
-}) {
-    const { children, className, style } = props;
-    const scrollContainerRef = React.createRef<HTMLDivElement>();
+/**
+ * Table of contents scroll container.
+ */
+export function TOCScrollContainer(props: ComponentPropsWithoutRef<'div'>) {
+    const ref = useRef<HTMLDivElement>(null);
+    const listeners = useRef<((element: HTMLDivElement) => void)[]>([]);
+    const onContainerMount: TOCScrollContainerContextType['onContainerMount'] = useCallback(
+        (listener) => {
+            if (ref.current) {
+                listener(ref.current);
+                return () => {};
+            }
+            listeners.current.push(listener);
+            return () => {
+                listeners.current = listeners.current.filter((l) => l !== listener);
+            };
+        },
+        []
+    );
+    const value: TOCScrollContainerContextType = useMemo(
+        () => ({ onContainerMount }),
+        [onContainerMount]
+    );
+    useEffect(() => {
+        const element = ref.current;
+        if (!element) {
+            return;
+        }
+        listeners.current.forEach((listener) => listener(element));
+        return () => {
+            listeners.current = [];
+        };
+    }, []);
 
     return (
-        <TOCScrollContainerRefContext.Provider value={scrollContainerRef}>
-            <div
-                ref={scrollContainerRef}
-                data-testid="toc-scroll-container"
-                className={tcls(className)}
-                style={style}
-            >
-                {children}
-            </div>
-        </TOCScrollContainerRefContext.Provider>
+        <TOCScrollContainerContext.Provider value={value}>
+            <div ref={ref} data-testid="toc-scroll-container" {...props} />
+        </TOCScrollContainerContext.Provider>
     );
 }
 
@@ -42,39 +66,31 @@ export function TOCScrollContainer(props: {
 const TOC_ITEM_OFFSET = 100;
 
 /**
- * Scrolls the table of contents container to the page item when it becomes active
+ * Scrolls the table of contents container to the page item when it's initially active.
  */
 export function useScrollToActiveTOCItem(props: {
+    anchorRef: React.RefObject<HTMLAnchorElement>;
     isActive: boolean;
-    linkRef: React.RefObject<HTMLAnchorElement>;
 }) {
-    const { isActive, linkRef } = props;
-    const scrollContainerRef = useTOCScrollContainerRefContext();
-    const isScrolled = React.useRef(false);
-    React.useLayoutEffect(() => {
-        if (!isActive) {
-            isScrolled.current = false;
-            return;
-        }
-        if (isScrolled.current) {
-            return;
-        }
-        const tocItem = linkRef.current;
-        const tocContainer = scrollContainerRef.current;
-        if (!tocItem || !tocContainer || !isOutOfView(tocItem, tocContainer)) {
-            return;
+    const { isActive, anchorRef } = props;
+    const isInitialActiveRef = useRef(isActive);
+    const { onContainerMount } = useTOCScrollContainerContext();
+    useEffect(() => {
+        const anchor = anchorRef.current;
+        if (isInitialActiveRef.current && anchor) {
+            return onContainerMount((container) => {
+                if (isOutOfView(anchor, container)) {
+                    container.scrollTo({ top: anchor.offsetTop - TOC_ITEM_OFFSET });
+                }
+            });
         }
-        tocContainer?.scrollTo({
-            top: tocItem.offsetTop - TOC_ITEM_OFFSET,
-        });
-        isScrolled.current = true;
-    }, [isActive, linkRef, scrollContainerRef]);
+    }, [onContainerMount, anchorRef]);
 }
 
-function isOutOfView(tocItem: HTMLElement, tocContainer: HTMLElement) {
-    const tocItemTop = tocItem.offsetTop;
-    const containerTop = tocContainer.scrollTop;
-    const containerBottom = containerTop + tocContainer.clientHeight;
+function isOutOfView(element: HTMLElement, container: HTMLElement) {
+    const tocItemTop = element.offsetTop;
+    const containerTop = container.scrollTop;
+    const containerBottom = containerTop + container.clientHeight;
     return (
         tocItemTop < containerTop + TOC_ITEM_OFFSET ||
         tocItemTop > containerBottom - TOC_ITEM_OFFSET
diff --git a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx
index 9f9c0d85da..05ee56160c 100644
--- a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx
+++ b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx
@@ -66,12 +66,7 @@ export function TableOfContents(props: {
             <div // The actual sidebar, either shown with a filled bg or transparent.
                 className={tcls(
                     'lg:-ms-5',
-                    'overflow-hidden',
-                    'relative',
-
-                    'flex',
-                    'flex-col',
-                    'flex-grow',
+                    'relative flex flex-grow flex-col overflow-hidden',
 
                     'sidebar-filled:bg-tint-subtle',
                     'theme-muted:bg-tint-subtle',
@@ -84,18 +79,12 @@ export function TableOfContents(props: {
                     'straight-corners:rounded-none'
                 )}
             >
-                {innerHeader && <div className={tcls('px-5 *:my-4')}>{innerHeader}</div>}
+                {innerHeader && <div className="px-5 *:my-4">{innerHeader}</div>}
                 <TOCScrollContainer // The scrollview inside the sidebar
                     className={tcls(
-                        'flex',
-                        'flex-grow',
-                        'flex-col',
-
-                        'p-2',
+                        'flex flex-grow flex-col p-2',
                         customization.trademark.enabled && 'lg:pb-20',
-
-                        'overflow-y-auto',
-                        'lg:gutter-stable',
+                        'lg:gutter-stable overflow-y-auto',
                         '[&::-webkit-scrollbar]:bg-transparent',
                         '[&::-webkit-scrollbar-thumb]:bg-transparent',
                         'group-hover:[&::-webkit-scrollbar]:bg-tint-subtle',
@@ -107,11 +96,7 @@ export function TableOfContents(props: {
                         rootPages={pages}
                         pages={pages}
                         context={context}
-                        style={tcls(
-                            'page-no-toc:hidden',
-                            'sidebar-list-line:border-l',
-                            'border-tint-subtle'
-                        )}
+                        style="page-no-toc:hidden border-tint-subtle sidebar-list-line:border-l"
                     />
                     {customization.trademark.enabled ? (
                         <Trademark
diff --git a/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx b/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx
index aa4700482b..38d6674514 100644
--- a/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx
+++ b/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx
@@ -2,12 +2,12 @@
 
 import { Icon } from '@gitbook/icons';
 import { motion } from 'framer-motion';
-import React from 'react';
+import React, { useRef } from 'react';
 
 import { tcls } from '@/lib/tailwind';
 
-import { useCurrentPagePath, useToggleAnimation } from '../hooks';
-import { Link, type LinkInsightsProps } from '../primitives';
+import { useCurrentPagePath } from '../hooks';
+import { Link, type LinkInsightsProps, type LinkProps } from '../primitives';
 import { useScrollToActiveTOCItem } from './TOCScroller';
 
 /**
@@ -24,128 +24,190 @@ export function ToggleableLinkItem(
     const { href, children, descendants, pathname, insights } = props;
 
     const currentPagePath = useCurrentPagePath();
-
     const isActive = currentPagePath === pathname;
-    const hasDescendants = !!descendants;
-    const hasActiveDescendant =
-        hasDescendants && (isActive || currentPagePath.startsWith(`${pathname}/`));
 
-    const [isVisible, setIsVisible] = React.useState(hasActiveDescendant);
+    if (!descendants) {
+        return (
+            <LinkItem href={href} insights={insights} isActive={isActive}>
+                {children}
+            </LinkItem>
+        );
+    }
 
-    // Update the visibility of the children, if we are navigating to a descendant.
-    React.useEffect(() => {
-        if (!hasDescendants) {
-            return;
-        }
+    return (
+        <DescendantsRenderer
+            descendants={descendants}
+            defaultIsOpen={isActive || currentPagePath.startsWith(`${pathname}/`)}
+        >
+            {({ descendants, toggler }) => (
+                <>
+                    <LinkItem href={href} insights={insights} isActive={isActive}>
+                        {children}
+                        {toggler}
+                    </LinkItem>
+                    {descendants}
+                </>
+            )}
+        </DescendantsRenderer>
+    );
+}
 
-        setIsVisible((prev) => prev || hasActiveDescendant);
-    }, [hasActiveDescendant, hasDescendants]);
+function LinkItem(
+    props: Pick<LinkProps, 'href' | 'insights' | 'children'> & {
+        isActive: boolean;
+    }
+) {
+    const { isActive, href, insights, children } = props;
+    const anchorRef = useRef<HTMLAnchorElement>(null);
+    useScrollToActiveTOCItem({ anchorRef, isActive });
+    return (
+        <Link
+            ref={anchorRef}
+            href={href}
+            insights={insights}
+            aria-current={isActive ? 'page' : undefined}
+            className={tcls(
+                'group/toclink relative transition-colors',
+                'flex flex-row justify-between',
+                'rounded-md straight-corners:rounded-none p-1.5 pl-3',
+                'text-balance font-normal text-sm text-tint-strong/7 hover:bg-tint-hover hover:text-tint-strong contrast-more:text-tint-strong',
+                'hover:contrast-more:text-tint-strong hover:contrast-more:ring-1 hover:contrast-more:ring-tint-12',
+                'before:contents[] before:-left-px before:absolute before:inset-y-0',
+                'sidebar-list-line:rounded-l-none sidebar-list-line:before:w-px sidebar-list-default:[&+div_a]:rounded-l-none [&+div_a]:pl-5 sidebar-list-default:[&+div_a]:before:w-px',
+
+                isActive && [
+                    'font-semibold',
+                    'sidebar-list-line:before:w-0.5',
+
+                    'before:bg-primary-solid',
+                    'text-primary-subtle',
+                    'contrast-more:text-primary',
+
+                    'sidebar-list-pill:bg-primary',
+                    '[html.sidebar-list-pill.theme-muted_&]:bg-primary-hover',
+                    '[html.sidebar-list-pill.theme-bold.tint_&]:bg-primary-hover',
+                    '[html.sidebar-filled.sidebar-list-pill.theme-muted_&]:bg-primary',
+                    '[html.sidebar-filled.sidebar-list-pill.theme-bold.tint_&]:bg-primary',
+
+                    'hover:bg-primary-hover',
+                    'hover:text-primary',
+                    'hover:before:bg-primary-solid-hover',
+                    'sidebar-list-pill:hover:bg-primary-hover',
+
+                    'contrast-more:text-primary',
+                    'contrast-more:hover:text-primary-strong',
+                    'contrast-more:bg-primary',
+                    'contrast-more:ring-1',
+                    'contrast-more:ring-primary',
+                    'contrast-more:hover:ring-primary-hover',
+                ]
+            )}
+        >
+            {children}
+        </Link>
+    );
+}
 
-    const { show, hide, scope } = useToggleAnimation({ hasDescendants, isVisible });
+function DescendantsRenderer(props: {
+    defaultIsOpen: boolean;
+    descendants: React.ReactNode;
+    children: (renderProps: {
+        descendants: React.ReactNode;
+        toggler: React.ReactNode;
+    }) => React.ReactNode;
+}) {
+    const { defaultIsOpen, children, descendants } = props;
+    const [isOpen, setIsOpen] = React.useState(defaultIsOpen);
 
-    const linkRef = React.createRef<HTMLAnchorElement>();
-    useScrollToActiveTOCItem({ linkRef, isActive });
+    // Update the visibility of the children if one of the descendants becomes active.
+    React.useEffect(() => {
+        if (defaultIsOpen) {
+            setIsOpen(defaultIsOpen);
+        }
+    }, [defaultIsOpen]);
+
+    return children({
+        toggler: (
+            <Toggler
+                isLinkActive={isOpen}
+                isOpen={isOpen}
+                onToggle={() => {
+                    setIsOpen((prev) => !prev);
+                }}
+            />
+        ),
+        descendants: <Descendants isVisible={isOpen}>{descendants}</Descendants>,
+    });
+}
 
+function Toggler(props: {
+    isLinkActive: boolean;
+    isOpen: boolean;
+    onToggle: () => void;
+}) {
+    const { isLinkActive, isOpen, onToggle } = props;
     return (
-        <>
-            <Link
-                ref={linkRef}
-                href={href}
-                insights={insights}
-                {...(isActive ? { 'aria-current': 'page' } : {})}
+        <span
+            className={tcls(
+                'group',
+                'relative',
+                'rounded-full',
+                'straight-corners:rounded-sm',
+                'w-5',
+                'h-5',
+                'after:grid-area-1-1',
+                'after:absolute',
+                'after:-top-1',
+                'after:grid',
+                'after:-left-1',
+                'after:w-7',
+                'after:h-7',
+                'hover:bg-tint-active',
+                'hover:text-current',
+                isLinkActive && 'hover:bg-tint-hover'
+            )}
+            onClick={(event) => {
+                event.preventDefault();
+                event.stopPropagation();
+                onToggle();
+            }}
+        >
+            <Icon
+                icon="chevron-right"
                 className={tcls(
-                    'group/toclink relative transition-colors',
-                    'flex flex-row justify-between',
-                    'rounded-md straight-corners:rounded-none p-1.5 pl-3',
-                    'text-balance font-normal text-sm text-tint-strong/7 hover:bg-tint-hover hover:text-tint-strong contrast-more:text-tint-strong',
-                    'hover:contrast-more:text-tint-strong hover:contrast-more:ring-1 hover:contrast-more:ring-tint-12',
-                    'before:contents[] before:-left-px before:absolute before:inset-y-0',
-                    'sidebar-list-line:rounded-l-none sidebar-list-line:before:w-px sidebar-list-default:[&+div_a]:rounded-l-none [&+div_a]:pl-5 sidebar-list-default:[&+div_a]:before:w-px',
-
-                    isActive && [
-                        'font-semibold',
-                        'sidebar-list-line:before:w-0.5',
-
-                        'before:bg-primary-solid',
-                        'text-primary-subtle',
-                        'contrast-more:text-primary',
-
-                        'sidebar-list-pill:bg-primary',
-                        '[html.sidebar-list-pill.theme-muted_&]:bg-primary-hover',
-                        '[html.sidebar-list-pill.theme-bold.tint_&]:bg-primary-hover',
-                        '[html.sidebar-filled.sidebar-list-pill.theme-muted_&]:bg-primary',
-                        '[html.sidebar-filled.sidebar-list-pill.theme-bold.tint_&]:bg-primary',
-
-                        'hover:bg-primary-hover',
-                        'hover:text-primary',
-                        'hover:before:bg-primary-solid-hover',
-                        'sidebar-list-pill:hover:bg-primary-hover',
-
-                        'contrast-more:text-primary',
-                        'contrast-more:hover:text-primary-strong',
-                        'contrast-more:bg-primary',
-                        'contrast-more:ring-1',
-                        'contrast-more:ring-primary',
-                        'contrast-more:hover:ring-primary-hover',
-                    ]
+                    'm-1 grid size-3 flex-shrink-0 text-current opacity-6 transition',
+                    'group-hover:opacity-11 contrast-more:opacity-11',
+                    isOpen ? 'rotate-90' : 'rotate-0'
                 )}
-            >
-                {children}
-                {hasDescendants ? (
-                    <span
-                        className={tcls(
-                            'group',
-                            'relative',
-                            'rounded-full',
-                            'straight-corners:rounded-sm',
-                            'w-5',
-                            'h-5',
-                            'after:grid-area-1-1',
-                            'after:absolute',
-                            'after:-top-1',
-                            'after:grid',
-                            'after:-left-1',
-                            'after:w-7',
-                            'after:h-7',
-                            'hover:bg-tint-active',
-                            'hover:text-current',
-                            isActive ? ['hover:bg-tint-hover'] : []
-                        )}
-                        onClick={(event) => {
-                            event.preventDefault();
-                            event.stopPropagation();
-                            setIsVisible((prev) => !prev);
-                        }}
-                    >
-                        <Icon
-                            icon="chevron-right"
-                            className={tcls(
-                                'grid',
-                                'flex-shrink-0',
-                                'size-3',
-                                'm-1',
-                                'transition-[opacity]',
-                                'text-current',
-                                'transition-transform',
-                                'opacity-6',
-                                'group-hover:opacity-11',
-                                'contrast-more:opacity-11',
-
-                                isVisible ? ['rotate-90'] : ['rotate-0']
-                            )}
-                        />
-                    </span>
-                ) : null}
-            </Link>
-            {hasDescendants ? (
-                <motion.div
-                    ref={scope}
-                    className={tcls(isVisible ? null : '[&_ul>li]:opacity-1')}
-                    initial={isVisible ? show : hide}
-                >
-                    {descendants}
-                </motion.div>
-            ) : null}
-        </>
+            />
+        </span>
+    );
+}
+
+const show = {
+    opacity: 1,
+    height: 'auto',
+};
+const hide = {
+    opacity: 0,
+    height: 0,
+    transitionEnd: {
+        display: 'none',
+    },
+};
+
+function Descendants(props: {
+    isVisible: boolean;
+    children: React.ReactNode;
+}) {
+    const { isVisible, children } = props;
+    return (
+        <motion.div
+            className="overflow-hidden"
+            animate={isVisible ? show : hide}
+            initial={isVisible ? show : hide}
+        >
+            {children}
+        </motion.div>
     );
 }
diff --git a/packages/gitbook/src/components/hooks/useCurrentPagePath.ts b/packages/gitbook/src/components/hooks/useCurrentPagePath.ts
index f1f719ddd7..23943e00b0 100644
--- a/packages/gitbook/src/components/hooks/useCurrentPagePath.ts
+++ b/packages/gitbook/src/components/hooks/useCurrentPagePath.ts
@@ -3,6 +3,7 @@
 import { useParams, useSelectedLayoutSegment } from 'next/navigation';
 
 import { removeLeadingSlash } from '@/lib/paths';
+import { useMemo } from 'react';
 
 /**
  * Return the page of the current page being rendered.
@@ -14,9 +15,11 @@ export function useCurrentPagePath() {
     // For V1, we use the selected layout segment.
     const rawActiveSegment = useSelectedLayoutSegment() ?? '';
 
-    if (typeof params.pagePath === 'string') {
-        return removeLeadingSlash(decodeURIComponent(params.pagePath));
-    }
+    return useMemo(() => {
+        if (typeof params.pagePath === 'string') {
+            return removeLeadingSlash(decodeURIComponent(params.pagePath));
+        }
 
-    return decodeURIComponent(rawActiveSegment);
+        return decodeURIComponent(rawActiveSegment);
+    }, [params.pagePath, rawActiveSegment]);
 }
diff --git a/packages/gitbook/src/components/hooks/useToggleAnimation.ts b/packages/gitbook/src/components/hooks/useToggleAnimation.ts
index c028155142..3c9c02fad7 100644
--- a/packages/gitbook/src/components/hooks/useToggleAnimation.ts
+++ b/packages/gitbook/src/components/hooks/useToggleAnimation.ts
@@ -1,7 +1,7 @@
-import { stagger, useAnimate } from 'framer-motion';
-import React from 'react';
+'use client';
 
-import { useIsMounted } from '.';
+import { stagger, useAnimate } from 'framer-motion';
+import React, { useLayoutEffect } from 'react';
 
 const show = {
     opacity: 1,
@@ -19,44 +19,35 @@ const hide = {
 
 const staggerMenuItems = stagger(0.02, { ease: (p) => p ** 2 });
 
-export function useToggleAnimation({
-    hasDescendants,
-    isVisible,
-}: {
-    hasDescendants: boolean;
-    isVisible: boolean;
-}) {
-    const isMounted = useIsMounted();
+export function useToggleAnimation(isVisible: boolean) {
     const [scope, animate] = useAnimate();
+    const previousIsVisibleRef = React.useRef<boolean | undefined>(undefined);
+    useLayoutEffect(() => {
+        previousIsVisibleRef.current = isVisible;
+    });
+    const previousIsVisible = previousIsVisibleRef.current;
 
     // Animate the visibility of the children
     // only after the initial state.
     React.useEffect(() => {
-        if (!isMounted || !hasDescendants) {
+        if (previousIsVisible === undefined || previousIsVisible === isVisible) {
             return;
         }
+
         try {
-            animate(scope.current, isVisible ? show : hide, {
-                duration: 0.1,
-            });
+            animate(scope.current, isVisible ? show : hide, { duration: 0.1 });
 
             const selector = '& > ul > li';
-            if (isVisible)
-                animate(
-                    selector,
-                    { opacity: 1 },
-                    {
-                        delay: staggerMenuItems,
-                    }
-                );
-            else {
+            if (isVisible) {
+                animate(selector, { opacity: 1 }, { delay: staggerMenuItems });
+            } else {
                 animate(selector, { opacity: 0 });
             }
         } catch (error) {
             // The selector can crash in some browsers, we ignore it as the animation is not critical.
             console.error(error);
         }
-    }, [isVisible, isMounted, hasDescendants, animate, scope]);
+    }, [previousIsVisible, isVisible, animate, scope]);
 
     return { show, hide, scope };
 }

From 634e0b43029e85e97b1ef42da66e5a781a08d091 Mon Sep 17 00:00:00 2001
From: Steven H <steven@gitbook.io>
Date: Wed, 23 Apr 2025 12:47:28 +0100
Subject: [PATCH 002/127] Improve error messages around undefined site
 sections. (#3179)

---
 .changeset/soft-planes-talk.md         |  5 +++
 packages/gitbook-v2/src/lib/context.ts | 51 ++++++++++++++++++++------
 2 files changed, 44 insertions(+), 12 deletions(-)
 create mode 100644 .changeset/soft-planes-talk.md

diff --git a/.changeset/soft-planes-talk.md b/.changeset/soft-planes-talk.md
new file mode 100644
index 0000000000..d633d90dcd
--- /dev/null
+++ b/.changeset/soft-planes-talk.md
@@ -0,0 +1,5 @@
+---
+"gitbook-v2": patch
+---
+
+Improve error messages around undefined site sections.
diff --git a/packages/gitbook-v2/src/lib/context.ts b/packages/gitbook-v2/src/lib/context.ts
index d25dd1946e..7ef0f463b9 100644
--- a/packages/gitbook-v2/src/lib/context.ts
+++ b/packages/gitbook-v2/src/lib/context.ts
@@ -19,6 +19,7 @@ import {
     getDataOrNull,
     throwIfDataError,
 } from '@v2/lib/data';
+import assertNever from 'assert-never';
 import { notFound } from 'next/navigation';
 import { assert } from 'ts-essentials';
 import { GITBOOK_URL } from './env';
@@ -242,19 +243,45 @@ export async function fetchSiteContextByIds(
         ? parseSiteSectionsAndGroups(siteStructure, ids.siteSection)
         : null;
 
-    const siteSpace = (
-        siteStructure.type === 'siteSpaces' && siteStructure.structure
-            ? siteStructure.structure
-            : sections?.current.siteSpaces
-    )?.find((siteSpace) => siteSpace.id === ids.siteSpace);
-    if (!siteSpace) {
-        throw new Error('Site space not found');
-    }
+    // Parse the current siteSpace and siteSpaces based on the site structure type.
+    const { siteSpaces, siteSpace }: { siteSpaces: SiteSpace[]; siteSpace: SiteSpace } = (() => {
+        if (siteStructure.type === 'siteSpaces') {
+            const siteSpaces = siteStructure.structure;
+            const siteSpace = siteSpaces.find((siteSpace) => siteSpace.id === ids.siteSpace);
+
+            if (!siteSpace) {
+                throw new Error(
+                    `Site space "${ids.siteSpace}" not found in structure type="siteSpaces"`
+                );
+            }
+
+            return { siteSpaces, siteSpace };
+        }
+
+        if (siteStructure.type === 'sections') {
+            assert(
+                sections,
+                `cannot find site space "${ids.siteSpace}" because parsed sections are missing siteStructure.type="sections" siteSection="${ids.siteSection}"`
+            );
 
-    const siteSpaces =
-        siteStructure.type === 'siteSpaces'
-            ? siteStructure.structure
-            : (sections?.current.siteSpaces ?? []);
+            const currentSection = sections.current;
+            const siteSpaces = currentSection.siteSpaces;
+            const siteSpace = currentSection.siteSpaces.find(
+                (siteSpace) => siteSpace.id === ids.siteSpace
+            );
+
+            if (!siteSpace) {
+                throw new Error(
+                    `Site space "${ids.siteSpace}" not found in structure type="sections" currentSection="${currentSection.id}"`
+                );
+            }
+
+            return { siteSpaces, siteSpace };
+        }
+
+        // @ts-expect-error
+        assertNever(siteStructure, `cannot handle site structure of type ${siteStructure.type}`);
+    })();
 
     const customization = (() => {
         if (ids.siteSpace) {

From 97b7c79bc6a69ca6739b6e593e6f3b5b2f0c3c82 Mon Sep 17 00:00:00 2001
From: Steven H <steven@gitbook.io>
Date: Wed, 23 Apr 2025 15:04:00 +0100
Subject: [PATCH 003/127] Increase logging around caching behaviour causing
 page crashes. (#3182)

---
 .changeset/sweet-schools-flow.md       |  6 ++++++
 packages/gitbook-v2/src/lib/context.ts | 20 +++++++++++++++++++-
 packages/gitbook/src/middleware.ts     |  3 +++
 3 files changed, 28 insertions(+), 1 deletion(-)
 create mode 100644 .changeset/sweet-schools-flow.md

diff --git a/.changeset/sweet-schools-flow.md b/.changeset/sweet-schools-flow.md
new file mode 100644
index 0000000000..6ff44bfa96
--- /dev/null
+++ b/.changeset/sweet-schools-flow.md
@@ -0,0 +1,6 @@
+---
+"gitbook-v2": patch
+"gitbook": patch
+---
+
+Increase logging around caching behaviour causing page crashes.
diff --git a/packages/gitbook-v2/src/lib/context.ts b/packages/gitbook-v2/src/lib/context.ts
index 7ef0f463b9..c85434c91f 100644
--- a/packages/gitbook-v2/src/lib/context.ts
+++ b/packages/gitbook-v2/src/lib/context.ts
@@ -220,6 +220,8 @@ export async function fetchSiteContextByIds(
 ): Promise<GitBookSiteContext> {
     const { dataFetcher } = baseContext;
 
+    const DEBUG = ids.site === 'site_cu2ih';
+
     const [{ site: orgSite, structure: siteStructure, customizations, scripts }, spaceContext] =
         await Promise.all([
             throwIfDataError(
@@ -232,6 +234,22 @@ export async function fetchSiteContextByIds(
             fetchSpaceContextByIds(baseContext, ids),
         ]);
 
+    DEBUG && console.log('ids', JSON.stringify(ids));
+    DEBUG &&
+        console.log(
+            'siteStructure',
+            siteStructure
+                ? JSON.stringify({
+                      type: siteStructure.type,
+                      structure: siteStructure.structure.map((s) => {
+                          // @ts-ignore
+                          const { siteSpaces, urls, ...rest } = s;
+                          return rest;
+                      }),
+                  })
+                : 'null'
+        );
+
     // override the title with the customization title
     // TODO: remove this hack once we have a proper way to handle site customizations
     const site = {
@@ -407,7 +425,7 @@ export function checkIsRootSiteContext(context: GitBookSiteContext): boolean {
 function parseSiteSectionsAndGroups(structure: SiteStructure, siteSectionId: string) {
     const sectionsAndGroups = getSiteStructureSections(structure, { ignoreGroups: false });
     const section = parseCurrentSection(structure, siteSectionId);
-    assert(section, 'A section must be defined when there are multiple sections');
+    assert(section, `couldn't find section "${siteSectionId}" in site structure`);
     return { list: sectionsAndGroups, current: section } satisfies SiteSections;
 }
 
diff --git a/packages/gitbook/src/middleware.ts b/packages/gitbook/src/middleware.ts
index 30f7b44e6a..0e599c4486 100644
--- a/packages/gitbook/src/middleware.ts
+++ b/packages/gitbook/src/middleware.ts
@@ -674,6 +674,9 @@ async function lookupSiteByAPI(
             }
         );
 
+        alternative.url.includes('minas-tirith') &&
+            console.log(`resolved ${alternative.url} -> ${JSON.stringify(data)}`);
+
         if ('error' in data) {
             if (alternative.primary) {
                 // We only return an error for the primary alternative (full URL),

From dd043df00f2619a2448d08b16a4eaa3986574898 Mon Sep 17 00:00:00 2001
From: Steven H <steven@gitbook.io>
Date: Wed, 23 Apr 2025 16:53:33 +0100
Subject: [PATCH 004/127] Revert investigation work around URL caches. (#3183)

---
 .changeset/young-games-talk.md         |  6 ++++++
 packages/gitbook-v2/src/lib/context.ts | 18 ------------------
 packages/gitbook/src/middleware.ts     |  3 ---
 3 files changed, 6 insertions(+), 21 deletions(-)
 create mode 100644 .changeset/young-games-talk.md

diff --git a/.changeset/young-games-talk.md b/.changeset/young-games-talk.md
new file mode 100644
index 0000000000..096dc02060
--- /dev/null
+++ b/.changeset/young-games-talk.md
@@ -0,0 +1,6 @@
+---
+"gitbook": patch
+"gitbook-v2": patch
+---
+
+Revert investigation work around URL caches.
diff --git a/packages/gitbook-v2/src/lib/context.ts b/packages/gitbook-v2/src/lib/context.ts
index c85434c91f..22e25a5990 100644
--- a/packages/gitbook-v2/src/lib/context.ts
+++ b/packages/gitbook-v2/src/lib/context.ts
@@ -220,8 +220,6 @@ export async function fetchSiteContextByIds(
 ): Promise<GitBookSiteContext> {
     const { dataFetcher } = baseContext;
 
-    const DEBUG = ids.site === 'site_cu2ih';
-
     const [{ site: orgSite, structure: siteStructure, customizations, scripts }, spaceContext] =
         await Promise.all([
             throwIfDataError(
@@ -234,22 +232,6 @@ export async function fetchSiteContextByIds(
             fetchSpaceContextByIds(baseContext, ids),
         ]);
 
-    DEBUG && console.log('ids', JSON.stringify(ids));
-    DEBUG &&
-        console.log(
-            'siteStructure',
-            siteStructure
-                ? JSON.stringify({
-                      type: siteStructure.type,
-                      structure: siteStructure.structure.map((s) => {
-                          // @ts-ignore
-                          const { siteSpaces, urls, ...rest } = s;
-                          return rest;
-                      }),
-                  })
-                : 'null'
-        );
-
     // override the title with the customization title
     // TODO: remove this hack once we have a proper way to handle site customizations
     const site = {
diff --git a/packages/gitbook/src/middleware.ts b/packages/gitbook/src/middleware.ts
index 0e599c4486..30f7b44e6a 100644
--- a/packages/gitbook/src/middleware.ts
+++ b/packages/gitbook/src/middleware.ts
@@ -674,9 +674,6 @@ async function lookupSiteByAPI(
             }
         );
 
-        alternative.url.includes('minas-tirith') &&
-            console.log(`resolved ${alternative.url} -> ${JSON.stringify(data)}`);
-
         if ('error' in data) {
             if (alternative.primary) {
                 // We only return an error for the primary alternative (full URL),

From 0353a8186cd8388fd0cc8dcebb748f97b9cde2b8 Mon Sep 17 00:00:00 2001
From: Viktor Renkema <49148610+viktorrenkema@users.noreply.github.com>
Date: Thu, 24 Apr 2025 11:43:20 +0200
Subject: [PATCH 005/127] Have all nodes within a text-cells adhere to
 alignments (#3175)

---
 .../DocumentView/Table/RecordColumnValue.tsx  |  5 +-
 .../DocumentView/Table/ViewGrid.tsx           | 48 +++++++++----------
 .../DocumentView/Table/table.module.css       |  2 +-
 .../components/DocumentView/Table/utils.ts    | 20 ++++++--
 .../src/components/primitives/Button.tsx      |  2 +-
 packages/gitbook/tailwind.config.ts           |  1 +
 6 files changed, 44 insertions(+), 34 deletions(-)

diff --git a/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx b/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx
index d5ddcdc491..aa1bf45b86 100644
--- a/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx
+++ b/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx
@@ -115,7 +115,7 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
                 return <Tag className={tcls(['w-full', verticalAlignment])}>{''}</Tag>;
             }
 
-            const alignment = getColumnAlignment(definition);
+            const horizontalAlignment = `[&_*]:${getColumnAlignment(definition)} ${getColumnAlignment(definition)}`;
 
             return (
                 <Blocks
@@ -130,8 +130,7 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
                         'lg:space-y-3',
                         'leading-normal',
                         verticalAlignment,
-                        alignment === 'right' ? 'text-right' : null,
-                        alignment === 'center' ? 'text-center' : null,
+                        horizontalAlignment,
                     ]}
                     context={context}
                     blockStyle={['w-full', 'max-w-[unset]']}
diff --git a/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx b/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx
index 65bae8e0b8..50aa13565b 100644
--- a/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx
+++ b/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx
@@ -42,32 +42,28 @@ export function ViewGrid(props: TableViewProps<DocumentTableViewGrid>) {
                         )}
                     >
                         <div role="row" className={tcls('flex', 'w-full')}>
-                            {view.columns.map((column) => {
-                                const alignment = getColumnAlignment(block.data.definition[column]);
-                                return (
-                                    <div
-                                        key={column}
-                                        role="columnheader"
-                                        className={tcls(
-                                            styles.columnHeader,
-                                            alignment === 'right' ? 'text-right' : null,
-                                            alignment === 'center' ? 'text-center' : null
-                                        )}
-                                        style={{
-                                            width: getColumnWidth({
-                                                column,
-                                                columnWidths,
-                                                autoSizedColumns,
-                                                fixedColumns,
-                                            }),
-                                            minWidth: columnWidths?.[column] || '100px',
-                                        }}
-                                        title={block.data.definition[column].title}
-                                    >
-                                        {block.data.definition[column].title}
-                                    </div>
-                                );
-                            })}
+                            {view.columns.map((column) => (
+                                <div
+                                    key={column}
+                                    role="columnheader"
+                                    className={tcls(
+                                        styles.columnHeader,
+                                        getColumnAlignment(block.data.definition[column])
+                                    )}
+                                    style={{
+                                        width: getColumnWidth({
+                                            column,
+                                            columnWidths,
+                                            autoSizedColumns,
+                                            fixedColumns,
+                                        }),
+                                        minWidth: columnWidths?.[column] || '100px',
+                                    }}
+                                    title={block.data.definition[column].title}
+                                >
+                                    {block.data.definition[column].title}
+                                </div>
+                            ))}
                         </div>
                     </div>
                 )}
diff --git a/packages/gitbook/src/components/DocumentView/Table/table.module.css b/packages/gitbook/src/components/DocumentView/Table/table.module.css
index 4b602a607c..53065b9d67 100644
--- a/packages/gitbook/src/components/DocumentView/Table/table.module.css
+++ b/packages/gitbook/src/components/DocumentView/Table/table.module.css
@@ -23,7 +23,7 @@
 }
 
 .columnHeader {
-    @apply text-sm font-medium py-2 px-4 text-tint-strong;
+    @apply text-sm font-medium py-2 px-3 text-tint-strong;
 }
 
 .row {
diff --git a/packages/gitbook/src/components/DocumentView/Table/utils.ts b/packages/gitbook/src/components/DocumentView/Table/utils.ts
index 01bd82bcd6..1fc95b357e 100644
--- a/packages/gitbook/src/components/DocumentView/Table/utils.ts
+++ b/packages/gitbook/src/components/DocumentView/Table/utils.ts
@@ -1,4 +1,5 @@
 import type { ContentRef, DocumentTableDefinition, DocumentTableRecord } from '@gitbook/api';
+import assertNever from 'assert-never';
 
 /**
  * Get the value for a column in a record.
@@ -14,11 +15,24 @@ export function getRecordValue<T extends number | string | boolean | string[] |
 /**
  * Get the text alignment for a column.
  */
-export function getColumnAlignment(column: DocumentTableDefinition): 'left' | 'right' | 'center' {
+export function getColumnAlignment(column: DocumentTableDefinition) {
+    const defaultAlignment = 'text-left';
+
     if (column.type === 'text') {
-        return column.textAlignment ?? 'left';
+        switch (column.textAlignment) {
+            case undefined:
+            case 'left':
+                return defaultAlignment;
+            case 'center':
+                return 'text-center';
+            case 'right':
+                return 'text-right';
+            default:
+                assertNever(column.textAlignment);
+        }
     }
-    return 'left';
+
+    return defaultAlignment;
 }
 
 /**
diff --git a/packages/gitbook/src/components/primitives/Button.tsx b/packages/gitbook/src/components/primitives/Button.tsx
index 01d8880077..b7edac7ab0 100644
--- a/packages/gitbook/src/components/primitives/Button.tsx
+++ b/packages/gitbook/src/components/primitives/Button.tsx
@@ -90,7 +90,7 @@ export function Button({
         'contrast-more:hover:ring-2',
         'contrast-more:hover:ring-tint-12',
 
-        'hover:scale-105',
+        'hover:scale-104',
         'active:scale-100',
         'transition-all',
 
diff --git a/packages/gitbook/tailwind.config.ts b/packages/gitbook/tailwind.config.ts
index 7a05bd15fc..2e5b7c359f 100644
--- a/packages/gitbook/tailwind.config.ts
+++ b/packages/gitbook/tailwind.config.ts
@@ -443,6 +443,7 @@ const config: Config = {
             scale: {
                 '98': '0.98',
                 '102': '1.02',
+                '104': '1.04',
             },
         },
         opacity: opacity(),

From 90f0127ada2491b88ef48b754e17c2ba43003c4a Mon Sep 17 00:00:00 2001
From: Johan Preynat <johan.preynat@gmail.com>
Date: Thu, 24 Apr 2025 16:53:23 +0200
Subject: [PATCH 006/127] Use react@19 on v2 and v1 (#3184)

---
 bun.lock                      | 190 +++++++---------------------------
 package.json                  |   6 +-
 packages/gitbook/package.json |   6 +-
 3 files changed, 43 insertions(+), 159 deletions(-)

diff --git a/bun.lock b/bun.lock
index 323725884f..5476639847 100644
--- a/bun.lock
+++ b/bun.lock
@@ -49,7 +49,7 @@
     },
     "packages/gitbook": {
       "name": "gitbook",
-      "version": "0.10.1",
+      "version": "0.11.1",
       "dependencies": {
         "@gitbook/api": "*",
         "@gitbook/cache-do": "workspace:*",
@@ -89,8 +89,8 @@
         "p-map": "^7.0.0",
         "parse-cache-control": "^1.0.1",
         "partial-json": "^0.1.7",
-        "react": "18.3.1",
-        "react-dom": "18.3.1",
+        "react": "^19.0.0",
+        "react-dom": "^19.0.0",
         "react-hotkeys-hook": "^4.4.1",
         "rehype-sanitize": "^6.0.0",
         "rehype-stringify": "^10.0.1",
@@ -111,7 +111,7 @@
       },
       "devDependencies": {
         "@argos-ci/playwright": "^5.0.3",
-        "@cloudflare/next-on-pages": "1.13.7",
+        "@cloudflare/next-on-pages": "1.13.12",
         "@cloudflare/workers-types": "^4.20241230.0",
         "@playwright/test": "^1.51.1",
         "@types/js-cookie": "^3.0.6",
@@ -231,7 +231,7 @@
     },
     "packages/react-openapi": {
       "name": "@gitbook/react-openapi",
-      "version": "1.1.10",
+      "version": "1.2.1",
       "dependencies": {
         "@gitbook/openapi-parser": "workspace:*",
         "@scalar/api-client-react": "^1.2.19",
@@ -260,8 +260,8 @@
   "overrides": {
     "@codemirror/state": "6.4.1",
     "@gitbook/api": "0.111.0",
-    "react": "18.3.1",
-    "react-dom": "18.3.1",
+    "react": "^19.0.0",
+    "react-dom": "^19.0.0",
   },
   "packages": {
     "@ai-sdk/provider": ["@ai-sdk/provider@1.1.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew=="],
@@ -472,7 +472,7 @@
 
     "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.0", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA=="],
 
-    "@cloudflare/next-on-pages": ["@cloudflare/next-on-pages@1.13.7", "", { "dependencies": { "acorn": "^8.8.0", "ast-types": "^0.14.2", "chalk": "^5.2.0", "chokidar": "^3.5.3", "commander": "^11.1.0", "cookie": "^0.5.0", "esbuild": "^0.15.3", "js-yaml": "^4.1.0", "miniflare": "^3.20231218.1", "package-manager-manager": "^0.2.0", "pcre-to-regexp": "^1.1.0", "semver": "^7.5.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20240208.0", "vercel": ">=30.0.0", "wrangler": "^3.28.2" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "next-on-pages": "bin/index.js" } }, "sha512-TSMVy+1fmxzeyykOC9guMEj7G2FgENw1T8V1sIFnah6piaJNBmybdyikPdQSdikP5w6v9eQhBt/TrXDPMDw0dw=="],
+    "@cloudflare/next-on-pages": ["@cloudflare/next-on-pages@1.13.12", "", { "dependencies": { "acorn": "^8.8.0", "ast-types": "^0.14.2", "chalk": "^5.2.0", "chokidar": "^3.5.3", "commander": "^11.1.0", "cookie": "^0.5.0", "esbuild": "^0.15.3", "js-yaml": "^4.1.0", "miniflare": "^3.20231218.1", "package-manager-manager": "^0.2.0", "pcre-to-regexp": "^1.1.0", "semver": "^7.5.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20240208.0", "vercel": ">=30.0.0", "wrangler": "^3.28.2 || ^4.0.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "next-on-pages": "bin/index.js" } }, "sha512-rPy7x9c2+0RDDdJ5o0TeRUwXJ1b7N1epnqF6qKSp5Wz1r9KHOyvaZh1ACoOC6Vu5k9su5WZOgy+8fPLIyrldMQ=="],
 
     "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.3.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.15", "workerd": "^1.20250320.0" }, "optionalPeers": ["workerd"] }, "sha512-Xq57Qd+ADpt6hibcVBO0uLG9zzRgyRhfCUgBT9s+g3+3Ivg5zDyVgLFy40ES1VdNcu8rPNSivm9A+kGP5IVaPg=="],
 
@@ -546,10 +546,6 @@
 
     "@emotion/memoize": ["@emotion/memoize@0.7.4", "", {}, "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="],
 
-    "@esbuild-plugins/node-globals-polyfill": ["@esbuild-plugins/node-globals-polyfill@0.2.3", "", { "peerDependencies": { "esbuild": "*" } }, "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw=="],
-
-    "@esbuild-plugins/node-modules-polyfill": ["@esbuild-plugins/node-modules-polyfill@0.2.2", "", { "dependencies": { "escape-string-regexp": "^4.0.0", "rollup-plugin-node-polyfills": "^0.2.1" }, "peerDependencies": { "esbuild": "*" } }, "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA=="],
-
     "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.24.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA=="],
 
     "@esbuild/android-arm": ["@esbuild/android-arm@0.24.2", "", { "os": "android", "cpu": "arm" }, "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q=="],
@@ -1094,7 +1090,7 @@
 
     "@scalar/object-utils": ["@scalar/object-utils@1.1.13", "", { "dependencies": { "flatted": "^3.3.1", "just-clone": "^6.2.0", "ts-deepmerge": "^7.0.1" } }, "sha512-311eTykIXgOtjCs4VTELj9UMT97jHTWc5qkGNoIzZ5nxjCcvOVe7kDQobIkE8dGT+ybOgHz5qly02Eu7nVHeZQ=="],
 
-    "@scalar/openapi-parser": ["@scalar/openapi-parser@0.10.10", "", { "dependencies": { "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.4.5" } }, "sha512-6MSgvpNKu/anZy96dn8tXQZo1PuDCoeB4m2ZLLDS4vC2zaTnuNBvvQHx+gjwXNKWhTbIVy8bQpYBzlMAYnFNcQ=="],
+    "@scalar/openapi-parser": ["@scalar/openapi-parser@0.10.14", "", { "dependencies": { "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.4.5" } }, "sha512-VXr979NMx6wZ+kpFKor2eyCJZOjyMwcBRc6c4Gc92ZMOC7ZNYqjwbw+Ubh2ELJyP5cWAjOFSrNwtylema0pw5w=="],
 
     "@scalar/openapi-types": ["@scalar/openapi-types@0.1.9", "", {}, "sha512-HQQudOSQBU7ewzfnBW9LhDmBE2XOJgSfwrh5PlUB7zJup/kaRkBGNgV2wMjNz9Af/uztiU/xNrO179FysmUT+g=="],
 
@@ -1390,7 +1386,7 @@
 
     "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
 
-    "acorn": ["acorn@8.12.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg=="],
+    "acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
 
     "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="],
 
@@ -1510,8 +1506,6 @@
 
     "caniuse-lite": ["caniuse-lite@1.0.30001668", "", {}, "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw=="],
 
-    "capnp-ts": ["capnp-ts@0.7.0", "", { "dependencies": { "debug": "^4.3.1", "tslib": "^2.2.0" } }, "sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g=="],
-
     "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
 
     "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
@@ -1564,8 +1558,6 @@
 
     "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
 
-    "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
-
     "configstore": ["configstore@5.0.1", "", { "dependencies": { "dot-prop": "^5.2.0", "graceful-fs": "^4.1.2", "make-dir": "^3.0.0", "unique-string": "^2.0.0", "write-file-atomic": "^3.0.0", "xdg-basedir": "^4.0.0" } }, "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA=="],
 
     "consola": ["consola@3.4.0", "", {}, "sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA=="],
@@ -1756,7 +1748,7 @@
 
     "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
 
-    "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
+    "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
 
     "esniff": ["esniff@2.0.1", "", { "dependencies": { "d": "^1.0.1", "es5-ext": "^0.10.62", "event-emitter": "^0.3.5", "type": "^2.7.2" } }, "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg=="],
 
@@ -2308,8 +2300,6 @@
 
     "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
 
-    "mlly": ["mlly@1.7.4", "", { "dependencies": { "acorn": "^8.14.0", "pathe": "^2.0.1", "pkg-types": "^1.3.0", "ufo": "^1.5.4" } }, "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw=="],
-
     "mnemonist": ["mnemonist@0.38.3", "", { "dependencies": { "obliterator": "^1.6.1" } }, "sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw=="],
 
     "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
@@ -2454,8 +2444,6 @@
 
     "pirates": ["pirates@4.0.6", "", {}, "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg=="],
 
-    "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
-
     "playwright": ["playwright@1.51.1", "", { "dependencies": { "playwright-core": "1.51.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw=="],
 
     "playwright-core": ["playwright-core@1.51.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw=="],
@@ -2518,13 +2506,13 @@
 
     "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
 
-    "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
+    "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
 
     "react-aria": ["react-aria@3.37.0", "", { "dependencies": { "@internationalized/string": "^3.2.5", "@react-aria/breadcrumbs": "^3.5.20", "@react-aria/button": "^3.11.1", "@react-aria/calendar": "^3.7.0", "@react-aria/checkbox": "^3.15.1", "@react-aria/color": "^3.0.3", "@react-aria/combobox": "^3.11.1", "@react-aria/datepicker": "^3.13.0", "@react-aria/dialog": "^3.5.21", "@react-aria/disclosure": "^3.0.1", "@react-aria/dnd": "^3.8.1", "@react-aria/focus": "^3.19.1", "@react-aria/gridlist": "^3.10.1", "@react-aria/i18n": "^3.12.5", "@react-aria/interactions": "^3.23.0", "@react-aria/label": "^3.7.14", "@react-aria/link": "^3.7.8", "@react-aria/listbox": "^3.14.0", "@react-aria/menu": "^3.17.0", "@react-aria/meter": "^3.4.19", "@react-aria/numberfield": "^3.11.10", "@react-aria/overlays": "^3.25.0", "@react-aria/progress": "^3.4.19", "@react-aria/radio": "^3.10.11", "@react-aria/searchfield": "^3.8.0", "@react-aria/select": "^3.15.1", "@react-aria/selection": "^3.22.0", "@react-aria/separator": "^3.4.5", "@react-aria/slider": "^3.7.15", "@react-aria/ssr": "^3.9.7", "@react-aria/switch": "^3.6.11", "@react-aria/table": "^3.16.1", "@react-aria/tabs": "^3.9.9", "@react-aria/tag": "^3.4.9", "@react-aria/textfield": "^3.16.0", "@react-aria/tooltip": "^3.7.11", "@react-aria/utils": "^3.27.0", "@react-aria/visually-hidden": "^3.8.19", "@react-types/shared": "^3.27.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-u3WUEMTcbQFaoHauHO3KhPaBYzEv1o42EdPcLAs05GBw9Q6Axlqwo73UFgMrsc2ElwLAZ4EKpSdWHLo1R5gfiw=="],
 
     "react-aria-components": ["react-aria-components@1.6.0", "", { "dependencies": { "@internationalized/date": "^3.7.0", "@internationalized/string": "^3.2.5", "@react-aria/autocomplete": "3.0.0-alpha.37", "@react-aria/collections": "3.0.0-alpha.7", "@react-aria/color": "^3.0.3", "@react-aria/disclosure": "^3.0.1", "@react-aria/dnd": "^3.8.1", "@react-aria/focus": "^3.19.1", "@react-aria/interactions": "^3.23.0", "@react-aria/live-announcer": "^3.4.1", "@react-aria/menu": "^3.17.0", "@react-aria/toolbar": "3.0.0-beta.12", "@react-aria/tree": "3.0.0-beta.3", "@react-aria/utils": "^3.27.0", "@react-aria/virtualizer": "^4.1.1", "@react-stately/autocomplete": "3.0.0-alpha.0", "@react-stately/color": "^3.8.2", "@react-stately/disclosure": "^3.0.1", "@react-stately/layout": "^4.1.1", "@react-stately/menu": "^3.9.1", "@react-stately/selection": "^3.19.0", "@react-stately/table": "^3.13.1", "@react-stately/utils": "^3.10.5", "@react-stately/virtualizer": "^4.2.1", "@react-types/color": "^3.0.2", "@react-types/form": "^3.7.9", "@react-types/grid": "^3.2.11", "@react-types/shared": "^3.27.0", "@react-types/table": "^3.10.4", "@swc/helpers": "^0.5.0", "client-only": "^0.0.1", "react-aria": "^3.37.0", "react-stately": "^3.35.0", "use-sync-external-store": "^1.2.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-YfG9PUE7XrXtDDAqT4pLTGyYQaiHHTBFdAK/wNgGsypVnQSdzmyYlV3Ty8aHlZJI6hP9RWkbywvosXkU7KcPHg=="],
 
-    "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="],
+    "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
 
     "react-hotkeys-hook": ["react-hotkeys-hook@4.5.1", "", { "peerDependencies": { "react": ">=16.8.1", "react-dom": ">=16.8.1" } }, "sha512-scAEJOh3Irm0g95NIn6+tQVf/OICCjsQsC9NBHfQws/Vxw4sfq1tDQut5fhTEvPraXhu/sHxRd9lOtxzyYuNAg=="],
 
@@ -2598,12 +2586,6 @@
 
     "rollup": ["rollup@3.29.5", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w=="],
 
-    "rollup-plugin-inject": ["rollup-plugin-inject@3.0.2", "", { "dependencies": { "estree-walker": "^0.6.1", "magic-string": "^0.25.3", "rollup-pluginutils": "^2.8.1" } }, "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w=="],
-
-    "rollup-plugin-node-polyfills": ["rollup-plugin-node-polyfills@0.2.1", "", { "dependencies": { "rollup-plugin-inject": "^3.0.0" } }, "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA=="],
-
-    "rollup-pluginutils": ["rollup-pluginutils@2.8.2", "", { "dependencies": { "estree-walker": "^0.6.1" } }, "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ=="],
-
     "router": ["router@2.0.0", "", { "dependencies": { "array-flatten": "3.0.0", "is-promise": "4.0.0", "methods": "~1.1.2", "parseurl": "~1.3.3", "path-to-regexp": "^8.0.0", "setprototypeof": "1.2.0", "utils-merge": "1.0.1" } }, "sha512-dIM5zVoG8xhC6rnSN8uoAgFARwTE7BQs8YwHEvK0VCmfxQXMaOuA1uiR1IPwsW7JyK5iTt7Od/TC9StasS2NPQ=="],
 
     "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
@@ -2612,7 +2594,7 @@
 
     "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
 
-    "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
+    "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
 
     "secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="],
 
@@ -2660,8 +2642,6 @@
 
     "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
 
-    "sourcemap-codec": ["sourcemap-codec@1.4.8", "", {}, "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="],
-
     "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
 
     "spawndamnit": ["spawndamnit@3.0.1", "", { "dependencies": { "cross-spawn": "^7.0.5", "signal-exit": "^4.0.1" } }, "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg=="],
@@ -3558,15 +3538,13 @@
 
     "@changesets/parse/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="],
 
-    "@cloudflare/next-on-pages/chalk": ["chalk@5.3.0", "", {}, "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="],
-
     "@cloudflare/next-on-pages/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
 
     "@cloudflare/next-on-pages/esbuild": ["esbuild@0.15.18", "", { "optionalDependencies": { "@esbuild/android-arm": "0.15.18", "@esbuild/linux-loong64": "0.15.18", "esbuild-android-64": "0.15.18", "esbuild-android-arm64": "0.15.18", "esbuild-darwin-64": "0.15.18", "esbuild-darwin-arm64": "0.15.18", "esbuild-freebsd-64": "0.15.18", "esbuild-freebsd-arm64": "0.15.18", "esbuild-linux-32": "0.15.18", "esbuild-linux-64": "0.15.18", "esbuild-linux-arm": "0.15.18", "esbuild-linux-arm64": "0.15.18", "esbuild-linux-mips64le": "0.15.18", "esbuild-linux-ppc64le": "0.15.18", "esbuild-linux-riscv64": "0.15.18", "esbuild-linux-s390x": "0.15.18", "esbuild-netbsd-64": "0.15.18", "esbuild-openbsd-64": "0.15.18", "esbuild-sunos-64": "0.15.18", "esbuild-windows-32": "0.15.18", "esbuild-windows-64": "0.15.18", "esbuild-windows-arm64": "0.15.18" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q=="],
 
-    "@cloudflare/next-on-pages/miniflare": ["miniflare@3.20241018.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "^8.8.0", "acorn-walk": "^8.2.0", "capnp-ts": "^0.7.0", "exit-hook": "^2.2.1", "glob-to-regexp": "^0.4.1", "stoppable": "^1.1.0", "undici": "^5.28.4", "workerd": "1.20241018.1", "ws": "^8.17.1", "youch": "^3.2.2", "zod": "^3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-g7i5oGAoJOk8+hJp77A5/wAdu7PEvi5hQc+0wzwzjhUNM2I5DHd2Cc29ACPhAe1kIXvCCVkxs3+REF52qnX0aw=="],
+    "@cloudflare/next-on-pages/miniflare": ["miniflare@3.20250214.2", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "stoppable": "1.1.0", "undici": "^5.28.5", "workerd": "1.20250214.0", "ws": "8.18.0", "youch": "3.2.3", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-t+lT4p2lbOcKv4PS3sx1F/wcDAlbEYZCO2VooLp4H7JErWWYIi9yjD3UillC3CGOpiBahVg5nrPCoFltZf6UlA=="],
 
-    "@cloudflare/next-on-pages/wrangler": ["wrangler@3.112.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.3.4", "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@esbuild-plugins/node-modules-polyfill": "0.2.2", "blake3-wasm": "2.1.5", "esbuild": "0.17.19", "miniflare": "3.20250214.2", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.1", "workerd": "1.20250214.0" }, "optionalDependencies": { "fsevents": "~2.3.2", "sharp": "^0.33.5" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20250214.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-PNQWGze3ODlWwG33LPr8kNhbht3eB3L9fogv+fapk2fjaqj0kNweRapkwmvtz46ojcqWzsxmTe4nOC0hIVUfPA=="],
+    "@cloudflare/next-on-pages/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
 
     "@codemirror/lang-html/@codemirror/autocomplete": ["@codemirror/autocomplete@6.18.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-iWHdj/B1ethnHRTwZj+C1obmmuCzquH29EbcKr0qIjA9NfDeBDJ7vs+WOHsFeLeflE4o+dHfYndJloMKHUkWUA=="],
 
@@ -3714,14 +3692,10 @@
 
     "@rollup/pluginutils/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
 
-    "@scalar/api-client/@scalar/openapi-parser": ["@scalar/openapi-parser@0.10.14", "", { "dependencies": { "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.4.5" } }, "sha512-VXr979NMx6wZ+kpFKor2eyCJZOjyMwcBRc6c4Gc92ZMOC7ZNYqjwbw+Ubh2ELJyP5cWAjOFSrNwtylema0pw5w=="],
-
     "@scalar/api-client/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="],
 
     "@scalar/api-client/pretty-ms": ["pretty-ms@8.0.0", "", { "dependencies": { "parse-ms": "^3.0.0" } }, "sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q=="],
 
-    "@scalar/import/@scalar/openapi-parser": ["@scalar/openapi-parser@0.10.14", "", { "dependencies": { "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.4.5" } }, "sha512-VXr979NMx6wZ+kpFKor2eyCJZOjyMwcBRc6c4Gc92ZMOC7ZNYqjwbw+Ubh2ELJyP5cWAjOFSrNwtylema0pw5w=="],
-
     "@scalar/oas-utils/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="],
 
     "@scalar/object-utils/flatted": ["flatted@3.3.1", "", {}, "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw=="],
@@ -3922,8 +3896,6 @@
 
     "@vercel/gatsby-plugin-vercel-builder/fs-extra": ["fs-extra@11.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw=="],
 
-    "@vercel/nft/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
-
     "@vercel/nft/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
 
     "@vercel/nft/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
@@ -3980,8 +3952,6 @@
 
     "cacheable-request/lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="],
 
-    "capnp-ts/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
-
     "codemirror/@codemirror/autocomplete": ["@codemirror/autocomplete@6.18.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-iWHdj/B1ethnHRTwZj+C1obmmuCzquH29EbcKr0qIjA9NfDeBDJ7vs+WOHsFeLeflE4o+dHfYndJloMKHUkWUA=="],
 
     "codemirror/@codemirror/commands": ["@codemirror/commands@6.7.0", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-+cduIZ2KbesDhbykV02K25A5xIVrquSPz4UxxYBemRlAT2aW8dhwUgLDwej7q/RJUHKk4nALYcR1puecDvbdqw=="],
@@ -4032,7 +4002,7 @@
 
     "gaxios/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
 
-    "gitbook-v2/next": ["next@15.3.1-canary.8", "", { "dependencies": { "@next/env": "15.3.1-canary.8", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.3.1-canary.8", "@next/swc-darwin-x64": "15.3.1-canary.8", "@next/swc-linux-arm64-gnu": "15.3.1-canary.8", "@next/swc-linux-arm64-musl": "15.3.1-canary.8", "@next/swc-linux-x64-gnu": "15.3.1-canary.8", "@next/swc-linux-x64-musl": "15.3.1-canary.8", "@next/swc-win32-arm64-msvc": "15.3.1-canary.8", "@next/swc-win32-x64-msvc": "15.3.1-canary.8", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "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-Of5a3BTTIl/iUvL2a9Jh7m7G/H8z4Pj5Vs54CLvcdadokxSNgLOpjzbDgFR8J4PawLx6+MOMy19m9Cvr6EPGug=="],
+    "gitbook-v2/next": ["next@15.4.0-canary.7", "", { "dependencies": { "@next/env": "15.4.0-canary.7", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.0-canary.7", "@next/swc-darwin-x64": "15.4.0-canary.7", "@next/swc-linux-arm64-gnu": "15.4.0-canary.7", "@next/swc-linux-arm64-musl": "15.4.0-canary.7", "@next/swc-linux-x64-gnu": "15.4.0-canary.7", "@next/swc-linux-x64-musl": "15.4.0-canary.7", "@next/swc-win32-arm64-msvc": "15.4.0-canary.7", "@next/swc-win32-x64-msvc": "15.4.0-canary.7", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "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-ZYjT0iu+4osz8XIlr31MuoXaNQKRU75UcwEgNBt93gftoh6tzV2Mebz6sOGeVReYuYUvYlLJJksMBTNcFcPbSA=="],
 
     "global-dirs/ini": ["ini@1.3.7", "", {}, "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ=="],
 
@@ -4070,8 +4040,6 @@
 
     "make-dir/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
 
-    "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
-
     "mdast-util-gfm/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="],
 
     "mdast-util-gfm-footnote/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="],
@@ -4090,8 +4058,6 @@
 
     "micromark/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
 
-    "miniflare/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
-
     "miniflare/undici": ["undici@5.28.5", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA=="],
 
     "miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="],
@@ -4102,8 +4068,6 @@
 
     "minizlib/minipass": ["minipass@2.9.0", "", { "dependencies": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" } }, "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg=="],
 
-    "mlly/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
-
     "next/@swc/helpers": ["@swc/helpers@0.5.5", "", { "dependencies": { "@swc/counter": "^0.1.3", "tslib": "^2.4.0" } }, "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A=="],
 
     "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
@@ -4156,12 +4120,6 @@
 
     "rimraf/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
 
-    "rollup-plugin-inject/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="],
-
-    "rollup-plugin-inject/magic-string": ["magic-string@0.25.9", "", { "dependencies": { "sourcemap-codec": "^1.4.8" } }, "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="],
-
-    "rollup-pluginutils/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="],
-
     "router/is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
 
     "router/path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="],
@@ -4200,10 +4158,10 @@
 
     "terminal-link/supports-hyperlinks": ["supports-hyperlinks@2.3.0", "", { "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" } }, "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA=="],
 
-    "terser/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
-
     "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
 
+    "ts-node/acorn": ["acorn@8.12.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg=="],
+
     "ts-node/acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="],
 
     "ts-node/arg": ["arg@4.1.0", "", {}, "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg=="],
@@ -4650,21 +4608,13 @@
 
     "@cloudflare/next-on-pages/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.15.18", "", { "os": "linux", "cpu": "none" }, "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ=="],
 
-    "@cloudflare/next-on-pages/miniflare/acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="],
-
-    "@cloudflare/next-on-pages/miniflare/workerd": ["workerd@1.20241018.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20241018.1", "@cloudflare/workerd-darwin-arm64": "1.20241018.1", "@cloudflare/workerd-linux-64": "1.20241018.1", "@cloudflare/workerd-linux-arm64": "1.20241018.1", "@cloudflare/workerd-windows-64": "1.20241018.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-JPW2oAbYOnJj1c5boyDOdjl/Yvur45jhVE8lf+I9oxR6myyAvuH2tdXO62kye68jRluJOMUeyssLes+JRwLmaA=="],
-
-    "@cloudflare/next-on-pages/miniflare/zod": ["zod@3.23.8", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="],
-
-    "@cloudflare/next-on-pages/wrangler/@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.3.4", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q=="],
+    "@cloudflare/next-on-pages/miniflare/undici": ["undici@5.28.5", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA=="],
 
-    "@cloudflare/next-on-pages/wrangler/esbuild": ["esbuild@0.17.19", "", { "optionalDependencies": { "@esbuild/android-arm": "0.17.19", "@esbuild/android-arm64": "0.17.19", "@esbuild/android-x64": "0.17.19", "@esbuild/darwin-arm64": "0.17.19", "@esbuild/darwin-x64": "0.17.19", "@esbuild/freebsd-arm64": "0.17.19", "@esbuild/freebsd-x64": "0.17.19", "@esbuild/linux-arm": "0.17.19", "@esbuild/linux-arm64": "0.17.19", "@esbuild/linux-ia32": "0.17.19", "@esbuild/linux-loong64": "0.17.19", "@esbuild/linux-mips64el": "0.17.19", "@esbuild/linux-ppc64": "0.17.19", "@esbuild/linux-riscv64": "0.17.19", "@esbuild/linux-s390x": "0.17.19", "@esbuild/linux-x64": "0.17.19", "@esbuild/netbsd-x64": "0.17.19", "@esbuild/openbsd-x64": "0.17.19", "@esbuild/sunos-x64": "0.17.19", "@esbuild/win32-arm64": "0.17.19", "@esbuild/win32-ia32": "0.17.19", "@esbuild/win32-x64": "0.17.19" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw=="],
+    "@cloudflare/next-on-pages/miniflare/workerd": ["workerd@1.20250214.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20250214.0", "@cloudflare/workerd-darwin-arm64": "1.20250214.0", "@cloudflare/workerd-linux-64": "1.20250214.0", "@cloudflare/workerd-linux-arm64": "1.20250214.0", "@cloudflare/workerd-windows-64": "1.20250214.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-QWcqXZLiMpV12wiaVnb3nLmfs/g4ZsFQq2mX85z546r3AX4CTIkXl0VP50W3CwqLADej3PGYiRDOTelDOwVG1g=="],
 
-    "@cloudflare/next-on-pages/wrangler/miniflare": ["miniflare@3.20250214.2", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "stoppable": "1.1.0", "undici": "^5.28.5", "workerd": "1.20250214.0", "ws": "8.18.0", "youch": "3.2.3", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-t+lT4p2lbOcKv4PS3sx1F/wcDAlbEYZCO2VooLp4H7JErWWYIi9yjD3UillC3CGOpiBahVg5nrPCoFltZf6UlA=="],
+    "@cloudflare/next-on-pages/miniflare/youch": ["youch@3.2.3", "", { "dependencies": { "cookie": "^0.5.0", "mustache": "^4.2.0", "stacktracey": "^2.1.8" } }, "sha512-ZBcWz/uzZaQVdCvfV4uk616Bbpf2ee+F/AvuKDR5EwX/Y4v06xWdtMluqTD7+KlZdM93lLm9gMZYo0sKBS0pgw=="],
 
-    "@cloudflare/next-on-pages/wrangler/unenv": ["unenv@2.0.0-rc.1", "", { "dependencies": { "defu": "^6.1.4", "mlly": "^1.7.4", "ohash": "^1.1.4", "pathe": "^1.1.2", "ufo": "^1.5.4" } }, "sha512-PU5fb40H8X149s117aB4ytbORcCvlASdtF97tfls4BPIyj4PeVxvpSuy1jAptqYHqB0vb2w2sHvzM0XWcp2OKg=="],
-
-    "@cloudflare/next-on-pages/wrangler/workerd": ["workerd@1.20250214.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20250214.0", "@cloudflare/workerd-darwin-arm64": "1.20250214.0", "@cloudflare/workerd-linux-64": "1.20250214.0", "@cloudflare/workerd-linux-arm64": "1.20250214.0", "@cloudflare/workerd-windows-64": "1.20250214.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-QWcqXZLiMpV12wiaVnb3nLmfs/g4ZsFQq2mX85z546r3AX4CTIkXl0VP50W3CwqLADej3PGYiRDOTelDOwVG1g=="],
+    "@cloudflare/next-on-pages/miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="],
 
     "@codemirror/lang-json/@codemirror/language/@codemirror/view": ["@codemirror/view@6.34.1", "", { "dependencies": { "@codemirror/state": "^6.4.0", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-t1zK/l9UiRqwUNPm+pdIT0qzJlzuVckbTEMVNFhfWkGiBQClstzg+78vedCvLSX0xJEZ6lwZbPpnljL7L6iwMQ=="],
 
@@ -4908,23 +4858,23 @@
 
     "gaxios/https-proxy-agent/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
 
-    "gitbook-v2/next/@next/env": ["@next/env@15.3.1-canary.8", "", {}, "sha512-ShZTo0hNhbTRrp7k6oUDSck4Xx4hhfSeLBp35jvGaw1QMZzWYr5v/oc0kEt0bfMdl+833flwKV7kFR3BnrULfg=="],
+    "gitbook-v2/next/@next/env": ["@next/env@15.4.0-canary.7", "", {}, "sha512-q8S7f2lQti3Y3gcAPzE8Pj8y0EwiWHVyyilMzoLbDPXGVfxlQhXLRiFdy2cDkKN4DyjGZWDeehEtw4huvJAa3Q=="],
 
-    "gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.3.1-canary.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZaDynM+pbrnLLlBAxH/CDGp9KN79OFrLcT1ejlWyo86V3SS9Gyqr4nmTuvTevByTTpr1VHReQel8Zbq0Pttu7Q=="],
+    "gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.4.0-canary.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+TMxUu5CAWNe+UFRc47BZAXQxCRqZfVbGyCldddiog4MorvL7kBxSd1qlmrwI73fRRKtXkHIH1TaeItyxzC9rQ=="],
 
-    "gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.3.1-canary.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-BsMR8WqeCDAX8C9RYAO8TI4ttpuqKk2oYMb1+bCrOYi857SMfB4vWD4PWmMvj7mwFXGqrxly4W6CBQlD66A+fg=="],
+    "gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.4.0-canary.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-veXp8lg/X/7O+pG9BDQ3OizFz3B40v29jsvEWj+ULY/W8Z6+dCSd5XPP2M8fG/gKKKA0D6L0CnnM2Mj0RRSUJw=="],
 
-    "gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.3.1-canary.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-xL+K2SW+/46j/KnKNf1gizM1bxwcEaE56eCEG9RPoYS/lfxHLuHcR9O2MlcPI90g/rIN2HXmHeuKRbXbnVy15g=="],
+    "gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.4.0-canary.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-KxNGfW7BO0Z5B9rJyl9p7YVjNrxAhu06mH6h1PSdouZG7YMYpdRCconVXeuBI0PEu6g3ywNrOVxZUk1V6G5u0Q=="],
 
-    "gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.3.1-canary.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-bzXlCUXkjIRsMTb6rr7OsWEQmdO2rZgKijnMGBJzEpa9ROq95VJTF+rdyrLmHuc4fPsAGDn/C18V3E1YOi4ipQ=="],
+    "gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.4.0-canary.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-THgXgmP/cC4DsNwvC6uqB90CebB7Ep1KyZajQL3fYKT5V4SWr46yngKLyoyJVeAYWJH908MrWddf7Ya/Zq7cyg=="],
 
-    "gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.3.1-canary.8", "", { "os": "linux", "cpu": "x64" }, "sha512-snbPQ9th7eoYYMZpNGhEvX4EbqGjjkiXzTEm2F/oDadlZR2wI6egtfQHiZqeczL7XwG3M66hBJ1/U20wdTDtvA=="],
+    "gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.4.0-canary.7", "", { "os": "linux", "cpu": "x64" }, "sha512-kpLB3Jj7fProynQYj2ahFyZlJs0xwm71VzCVrNRu6u7qJGXn6dK5h7+hro8y/y1iqjXWgCLSdxWSHahhWK8XdQ=="],
 
-    "gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.3.1-canary.8", "", { "os": "linux", "cpu": "x64" }, "sha512-5hmcaGazc3w6rg/gbQOrmuw0kKShf4Egs0JlUPop/ZiRDfZO5KlyrGY0hUD+2pLG7Yx3B/fTbj86cHKfQZDMBw=="],
+    "gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.4.0-canary.7", "", { "os": "linux", "cpu": "x64" }, "sha512-rnGAKvl4cWPVV9D+SybWOGijm0VmKXyqQ+IN0A6WDgdlYZAZP0ZnJv/rq7DSvuOh19AXS8UpQc88SelXV/3j3Q=="],
 
-    "gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.3.1-canary.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-t6uwWC/UbQ8CQGyBVbEmpcJC41yAvz2eZZGec9EoO0ZyQ/fbvjOkqfPnf+WcjWifWNeuiWogjN1UBg7YK9IaVA=="],
+    "gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.4.0-canary.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-/PRbn//EuR3UGiquk050gqvjxLliEgGBy1Cx9KkpAT7szaHOBj1mDDQmxMTEhRex4i3YfKGJXWn5mLMCveya6Q=="],
 
-    "gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.3.1-canary.8", "", { "os": "win32", "cpu": "x64" }, "sha512-93gNqVYwlr9bz6Pm5dQnqjpuOEQyvDre0+H39UqS7h2KuFh3sf2MejhAEr2uFEux4mb7TN0PlohaihO7YxJ3fw=="],
+    "gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.4.0-canary.7", "", { "os": "win32", "cpu": "x64" }, "sha512-7a92XL+DlrbWyycCpQjjQMHOrsA0p+VvS7iA2dyi89Xsq0qtOPzFH0Gb56fsjh6M6BQGFhboOSzjmpjlkMTilQ=="],
 
     "gitbook-v2/next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
 
@@ -5114,81 +5064,15 @@
 
     "@babel/highlight/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="],
 
-    "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20241018.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-CRySEzjNRoR8frP5AbtJXd1tgVJa5v7bZon9Dh6nljYlhG+piDv8jvOVEUqF3cXXS+M5aXwr4NlozdMvl5g5mg=="],
-
-    "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20241018.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y63yWJNTgETDFkY3Ony71/k/G1HRDFIhEzwbT+OWmg1Qbsqa4TquHPVFkgv+OJhpmD3HV9gTBcn/M2QJ/+pGmg=="],
-
-    "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20241018.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a2AbSAXNMMiREvN+PwjHdJ5zOzI4+qf8+rb6H/y4HcVbPZN5C2fanxv5Bx7NUHLiMD/W0FrGug1aU+RPUVZC9Q=="],
-
-    "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20241018.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-iCJ7bjD/+zhlp3IWnkiry180DwdNvak/sVoS98pIAS41aR3gJVzE5BCz/2yTWFdCoUVZ5yKJrv1HhSKgQRBIEw=="],
-
-    "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20241018.1", "", { "os": "win32", "cpu": "x64" }, "sha512-qwDVh/KrwEPY82h6tZ1O4BXBAKeGy30BeTr9wvTUVeY9eX/KT73GuEG+ttwiashRfqjOa0Gcqjsfpd913ITFyg=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.17.19", "", { "os": "android", "cpu": "arm" }, "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.17.19", "", { "os": "android", "cpu": "arm64" }, "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.17.19", "", { "os": "android", "cpu": "x64" }, "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.17.19", "", { "os": "darwin", "cpu": "arm64" }, "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.17.19", "", { "os": "darwin", "cpu": "x64" }, "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.17.19", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.17.19", "", { "os": "freebsd", "cpu": "x64" }, "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.17.19", "", { "os": "linux", "cpu": "arm" }, "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.17.19", "", { "os": "linux", "cpu": "arm64" }, "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.17.19", "", { "os": "linux", "cpu": "ia32" }, "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.17.19", "", { "os": "linux", "cpu": "none" }, "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.17.19", "", { "os": "linux", "cpu": "none" }, "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.17.19", "", { "os": "linux", "cpu": "ppc64" }, "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.17.19", "", { "os": "linux", "cpu": "none" }, "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.17.19", "", { "os": "linux", "cpu": "s390x" }, "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.17.19", "", { "os": "linux", "cpu": "x64" }, "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.17.19", "", { "os": "none", "cpu": "x64" }, "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.17.19", "", { "os": "openbsd", "cpu": "x64" }, "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.17.19", "", { "os": "sunos", "cpu": "x64" }, "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.17.19", "", { "os": "win32", "cpu": "arm64" }, "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.17.19", "", { "os": "win32", "cpu": "ia32" }, "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.17.19", "", { "os": "win32", "cpu": "x64" }, "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA=="],
-
-    "@cloudflare/next-on-pages/wrangler/miniflare/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
-
-    "@cloudflare/next-on-pages/wrangler/miniflare/undici": ["undici@5.28.5", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA=="],
-
-    "@cloudflare/next-on-pages/wrangler/miniflare/youch": ["youch@3.2.3", "", { "dependencies": { "cookie": "^0.5.0", "mustache": "^4.2.0", "stacktracey": "^2.1.8" } }, "sha512-ZBcWz/uzZaQVdCvfV4uk616Bbpf2ee+F/AvuKDR5EwX/Y4v06xWdtMluqTD7+KlZdM93lLm9gMZYo0sKBS0pgw=="],
-
-    "@cloudflare/next-on-pages/wrangler/miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="],
-
-    "@cloudflare/next-on-pages/wrangler/unenv/ohash": ["ohash@1.1.4", "", {}, "sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g=="],
-
-    "@cloudflare/next-on-pages/wrangler/unenv/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
-
-    "@cloudflare/next-on-pages/wrangler/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20250214.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cDvvedWDc5zrgDnuXe2qYcz/TwBvzmweO55C7XpPuAWJ9Oqxv81PkdekYxD8mH989aQ/GI5YD0Fe6fDYlM+T3Q=="],
+    "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20250214.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cDvvedWDc5zrgDnuXe2qYcz/TwBvzmweO55C7XpPuAWJ9Oqxv81PkdekYxD8mH989aQ/GI5YD0Fe6fDYlM+T3Q=="],
 
-    "@cloudflare/next-on-pages/wrangler/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250214.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NytCvRveVzu0mRKo+tvZo3d/gCUway3B2ZVqSi/TS6NXDGBYIJo7g6s3BnTLS74kgyzeDOjhu9j/RBJBS809qw=="],
+    "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250214.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NytCvRveVzu0mRKo+tvZo3d/gCUway3B2ZVqSi/TS6NXDGBYIJo7g6s3BnTLS74kgyzeDOjhu9j/RBJBS809qw=="],
 
-    "@cloudflare/next-on-pages/wrangler/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20250214.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pQ7+aHNHj8SiYEs4d/6cNoimE5xGeCMfgU1yfDFtA9YGN9Aj2BITZgOWPec+HW7ZkOy9oWlNrO6EvVjGgB4tbQ=="],
+    "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20250214.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pQ7+aHNHj8SiYEs4d/6cNoimE5xGeCMfgU1yfDFtA9YGN9Aj2BITZgOWPec+HW7ZkOy9oWlNrO6EvVjGgB4tbQ=="],
 
-    "@cloudflare/next-on-pages/wrangler/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20250214.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Vhlfah6Yd9ny1npNQjNgElLIjR6OFdEbuR3LCfbLDCwzWEBFhIf7yC+Tpp/a0Hq7kLz3sLdktaP7xl3PJhyOjA=="],
+    "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20250214.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Vhlfah6Yd9ny1npNQjNgElLIjR6OFdEbuR3LCfbLDCwzWEBFhIf7yC+Tpp/a0Hq7kLz3sLdktaP7xl3PJhyOjA=="],
 
-    "@cloudflare/next-on-pages/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250214.0", "", { "os": "win32", "cpu": "x64" }, "sha512-GMwMyFbkjBKjYJoKDhGX8nuL4Gqe3IbVnVWf2Q6086CValyIknupk5J6uQWGw2EBU3RGO3x4trDXT5WphQJZDQ=="],
+    "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250214.0", "", { "os": "win32", "cpu": "x64" }, "sha512-GMwMyFbkjBKjYJoKDhGX8nuL4Gqe3IbVnVWf2Q6086CValyIknupk5J6uQWGw2EBU3RGO3x4trDXT5WphQJZDQ=="],
 
     "@node-minify/core/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
 
diff --git a/package.json b/package.json
index 736eabe411..fba8d79576 100644
--- a/package.json
+++ b/package.json
@@ -10,9 +10,9 @@
     "packageManager": "bun@1.2.8",
     "overrides": {
         "@codemirror/state": "6.4.1",
-        "react": "18.3.1",
-        "react-dom": "18.3.1",
-        "@gitbook/api": "0.111.0"
+        "@gitbook/api": "0.111.0",
+        "react": "^19.0.0",
+        "react-dom": "^19.0.0"
     },
     "private": true,
     "scripts": {
diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json
index d0abc7512b..6f155974f7 100644
--- a/packages/gitbook/package.json
+++ b/packages/gitbook/package.json
@@ -52,8 +52,8 @@
         "openapi-types": "^12.1.3",
         "p-map": "^7.0.0",
         "parse-cache-control": "^1.0.1",
-        "react": "18.3.1",
-        "react-dom": "18.3.1",
+        "react": "^19.0.0",
+        "react-dom": "^19.0.0",
         "react-hotkeys-hook": "^4.4.1",
         "rehype-sanitize": "^6.0.0",
         "rehype-stringify": "^10.0.1",
@@ -76,7 +76,7 @@
     },
     "devDependencies": {
         "@argos-ci/playwright": "^5.0.3",
-        "@cloudflare/next-on-pages": "1.13.7",
+        "@cloudflare/next-on-pages": "1.13.12",
         "@cloudflare/workers-types": "^4.20241230.0",
         "@playwright/test": "^1.51.1",
         "@types/js-cookie": "^3.0.6",

From 326e28e9b06a3d72879f1196d68bf4563faf63df Mon Sep 17 00:00:00 2001
From: Zeno Kapitein <zeno@gitbook.io>
Date: Thu, 24 Apr 2025 16:53:29 +0200
Subject: [PATCH 007/127] Restyle openapi blocks (#3169)

---
 .changeset/lemon-actors-sing.md               |   6 +
 .../CodeBlock/CodeBlockRenderer.css           |   2 +-
 .../CodeBlock/CodeBlockRenderer.tsx           |   6 +-
 .../DocumentView/CodeBlock/theme.css          |  86 +++++--
 .../components/DocumentView/OpenAPI/style.css | 219 ++++++++++--------
 .../react-openapi/src/OpenAPIDisclosure.tsx   |  10 +-
 .../src/OpenAPIDisclosureGroup.tsx            |  97 ++++----
 .../react-openapi/src/OpenAPIResponse.tsx     |  36 ++-
 packages/react-openapi/src/OpenAPISchema.tsx  | 185 ++++++++-------
 .../react-openapi/src/OpenAPISchemaName.tsx   |   4 +-
 packages/react-openapi/src/StaticSection.tsx  |   2 +-
 .../src/schemas/OpenAPISchemas.tsx            |   7 +-
 packages/react-openapi/src/translations/de.ts |  13 +-
 packages/react-openapi/src/translations/en.ts |  11 +-
 packages/react-openapi/src/translations/es.ts |  13 +-
 packages/react-openapi/src/translations/fr.ts |  15 +-
 packages/react-openapi/src/translations/ja.ts |  11 +-
 packages/react-openapi/src/translations/nl.ts |  11 +-
 packages/react-openapi/src/translations/no.ts |  13 +-
 .../react-openapi/src/translations/pt-br.ts   |  13 +-
 packages/react-openapi/src/translations/zh.ts |  13 +-
 21 files changed, 468 insertions(+), 305 deletions(-)
 create mode 100644 .changeset/lemon-actors-sing.md

diff --git a/.changeset/lemon-actors-sing.md b/.changeset/lemon-actors-sing.md
new file mode 100644
index 0000000000..d212947861
--- /dev/null
+++ b/.changeset/lemon-actors-sing.md
@@ -0,0 +1,6 @@
+---
+"@gitbook/react-openapi": minor
+"gitbook": minor
+---
+
+Design tweaks to code blocks and OpenAPI pages
diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.css b/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.css
index 6a56801796..71d75354eb 100644
--- a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.css
+++ b/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.css
@@ -24,7 +24,7 @@
 }
 
 .highlight-line-number {
-    @apply text-sm text-right pr-3.5 rounded-l pl-2 sticky left-[-3px] bg-gradient-to-r from-80% from-tint to-transparent;
+    @apply text-sm text-right pr-3.5 rounded-l pl-2 sticky left-[-3px] bg-gradient-to-r from-80% from-tint-subtle contrast-more:from-tint-base theme-muted:from-tint-base [html.theme-bold.sidebar-filled_&]:from-tint-base to-transparent;
     @apply before:text-tint before:content-[counter(line)];
 
     .highlight-line.highlighted > & {
diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.tsx b/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.tsx
index f1f8cb8f63..d8f35d90bb 100644
--- a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.tsx
+++ b/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.tsx
@@ -39,7 +39,7 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
         >
             <div className="flex items-center justify-start gap-2 text-sm [grid-area:1/1]">
                 {title ? (
-                    <div className="inline-flex items-center justify-center rounded-t straight-corners:rounded-t-s bg-tint px-3 py-2 text-tint text-xs leading-none tracking-wide">
+                    <div className="relative top-px z-20 inline-flex items-center justify-center rounded-t straight-corners:rounded-t-s border border-tint-subtle border-b-0 bg-tint-subtle theme-muted:bg-tint-base px-3 py-2 text-tint text-xs leading-none tracking-wide contrast-more:border-tint contrast-more:bg-tint-base [html.theme-bold.sidebar-filled_&]:bg-tint-base">
                         {title}
                     </div>
                 ) : null}
@@ -50,8 +50,8 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
             />
             <pre
                 className={tcls(
-                    'hide-scroll relative overflow-auto bg-tint theme-gradient:bg-tint-12/1 ring-tint-subtle [grid-area:2/1]',
-                    'rounded-md straight-corners:rounded-sm',
+                    'hide-scroll relative overflow-auto border border-tint-subtle bg-tint-subtle theme-muted:bg-tint-base [grid-area:2/1] contrast-more:border-tint contrast-more:bg-tint-base [html.theme-bold.sidebar-filled_&]:bg-tint-base',
+                    'rounded-md straight-corners:rounded-sm shadow-sm',
                     title && 'rounded-ss-none'
                 )}
             >
diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/theme.css b/packages/gitbook/src/components/DocumentView/CodeBlock/theme.css
index 22ce3b60e3..21c0fd5791 100644
--- a/packages/gitbook/src/components/DocumentView/CodeBlock/theme.css
+++ b/packages/gitbook/src/components/DocumentView/CodeBlock/theme.css
@@ -1,31 +1,67 @@
 :root {
     --shiki-color-text: theme("colors.tint.11");
-    --shiki-token-constant: #0a6355;
-    --shiki-token-string: #8b6d32;
-    --shiki-token-comment: theme("colors.teal.700/.64");
-    --shiki-token-keyword: theme("colors.pomegranate.600");
-    --shiki-token-parameter: #0a3069;
-    --shiki-token-function: #8250df;
-    --shiki-token-string-expression: #6a4906;
-    --shiki-token-punctuation: theme("colors.pomegranate.700/.92");
-    --shiki-token-link: theme("colors.tint.12");
-    --shiki-token-inserted: #22863a;
-    --shiki-token-deleted: #b31d28;
-    --shiki-token-changed: #8250df;
+    --shiki-token-punctuation: theme("colors.tint.11");
+    --shiki-token-comment: theme("colors.neutral.9/.7");
+    --shiki-token-link: theme("colors.primary.10");
+
+    --shiki-token-constant: theme("colors.warning.10");
+    --shiki-token-string: theme("colors.success.10");
+    --shiki-token-string-expression: theme("colors.success.10");
+    --shiki-token-keyword: theme("colors.danger.10");
+    --shiki-token-parameter: theme("colors.warning.10");
+    --shiki-token-function: theme("colors.primary.10");
+
+    --shiki-token-inserted: theme("colors.success.10");
+    --shiki-token-deleted: theme("colors.danger.10");
+    --shiki-token-changed: theme("colors.tint.12");
+}
+
+@media (prefers-contrast: more) {
+    :root {
+        --shiki-color-text: theme("colors.tint.12");
+        --shiki-token-punctuation: theme("colors.tint.12");
+        --shiki-token-comment: theme("colors.neutral.11");
+        --shiki-token-link: theme("colors.primary.11");
+
+        --shiki-token-constant: theme("colors.warning.11");
+        --shiki-token-string: theme("colors.success.11");
+        --shiki-token-string-expression: theme("colors.success.11");
+        --shiki-token-keyword: theme("colors.danger.11");
+        --shiki-token-parameter: theme("colors.warning.11");
+        --shiki-token-function: theme("colors.primary.11");
+
+        --shiki-token-inserted: theme("colors.success.11");
+        --shiki-token-deleted: theme("colors.danger.11");
+        --shiki-token-changed: theme("colors.tint.12");
+    }
 }
 
 html.dark {
-    --shiki-color-text: theme("colors.tint.11");
-    --shiki-token-constant: #d19a66;
-    --shiki-token-string: theme("colors.pomegranate.300");
-    --shiki-token-comment: theme("colors.teal.300/.64");
-    --shiki-token-keyword: theme("colors.pomegranate.400");
-    --shiki-token-parameter: theme("colors.yellow.500");
-    --shiki-token-function: #56b6c2;
-    --shiki-token-string-expression: theme("colors.tint.11");
-    --shiki-token-punctuation: #acc6ee;
-    --shiki-token-link: theme("colors.pomegranate.400");
-    --shiki-token-inserted: #85e89d;
-    --shiki-token-deleted: #fdaeb7;
-    --shiki-token-changed: #56b6c2;
+    /* Override select colors to have more contrast */
+    --shiki-token-comment: theme("colors.neutral.9");
+
+    --shiki-token-constant: theme("colors.warning.11");
+    --shiki-token-string: theme("colors.success.11");
+    --shiki-token-string-expression: theme("colors.success.11");
+    --shiki-token-keyword: theme("colors.danger.11");
+    --shiki-token-parameter: theme("colors.warning.11");
+    --shiki-token-function: theme("colors.primary.11");
+}
+
+.code-monochrome {
+    --shiki-token-constant: theme("colors.tint.11");
+    --shiki-token-string: theme("colors.tint.12");
+    --shiki-token-string-expression: theme("colors.tint.12");
+    --shiki-token-keyword: theme("colors.primary.10");
+    --shiki-token-parameter: theme("colors.tint.9");
+    --shiki-token-function: theme("colors.primary.9");
+}
+
+html.dark.code-monochrome {
+    --shiki-token-constant: theme("colors.tint.11");
+    --shiki-token-string: theme("colors.tint.12");
+    --shiki-token-string-expression: theme("colors.tint.12");
+    --shiki-token-keyword: theme("colors.primary.11");
+    --shiki-token-parameter: theme("colors.tint.10");
+    --shiki-token-function: theme("colors.primary.10");
 }
diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
index 39cccc4b38..7d08d97f59 100644
--- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
+++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
@@ -169,41 +169,18 @@
     @apply flex flex-col;
 }
 
-.openapi-schema {
+.openapi-schema,
+.openapi-disclosure {
     @apply py-2.5 flex flex-col gap-2;
 }
 
-.openapi-section-body .openapi-schema-properties {
-    @apply divide-y divide-tint-subtle;
-}
-
-.openapi-disclosure-group-panel > .openapi-schema-properties > *:first-child > .openapi-schema {
-    @apply pt-0;
-}
-
-.openapi-responsebody > .openapi-schema-properties > .openapi-schema:last-child {
-    @apply pb-0;
-}
-
-.openapi-responsebody > .openapi-schema-properties > .openapi-schema:only-child {
-    @apply py-0;
-}
-
 .openapi-schema-properties .openapi-schema:last-child {
     @apply border-b-0;
 }
 
-.openapi-schema-properties .openapi-schema-opened {
-    @apply pb-3;
-}
-
-.openapi-schema > .openapi-schema-properties {
-    @apply mt-3;
-}
-
 /* Schema Presentation */
 .openapi-schema-presentation {
-    @apply flex flex-col gap-1.5 font-normal;
+    @apply flex flex-col gap-1 font-normal;
 }
 
 .openapi-schema-properties:last-child {
@@ -221,7 +198,7 @@
 }
 
 .openapi-schema-propertyname {
-    @apply select-all font-mono font-normal text-tint-strong;
+    @apply select-all font-mono font-semibold text-tint-strong;
 }
 
 .openapi-schema-propertyname[data-deprecated="true"] {
@@ -244,6 +221,10 @@
     @apply text-success dark:text-success-subtle/9 text-[0.813rem] lowercase;
 }
 
+.openapi-schema-types {
+    @apply flex items-baseline flex-wrap gap-1;
+}
+
 .openapi-schema-type {
     @apply text-tint select-text text-[0.813rem] font-mono [word-spacing:-0.25rem];
 }
@@ -321,7 +302,7 @@
 .openapi-schema-pattern code,
 .openapi-schema-enum-value code,
 .openapi-schema-default code {
-    @apply py-px px-1 min-w-[1.625rem] text-tint-strong font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint bg-tint rounded text-xs leading-[calc(max(1.20em,1.25rem))] before:!content-none after:!content-none;
+    @apply py-px px-1 min-w-[1.625rem] text-tint-strong font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint-subtle bg-tint rounded text-xs leading-[calc(max(1.20em,1.25rem))] before:!content-none after:!content-none;
 }
 
 /* Authentication */
@@ -373,28 +354,25 @@
 }
 
 .openapi-response-tab-content {
-    @apply overflow-hidden max-w-full flex items-baseline;
-    @apply text-left text-pretty relative leading-[1.125rem] text-tint !font-normal truncate select-text;
+    @apply flex items-baseline truncate grow shrink max-w-max basis-[60%] mr-auto;
+    @apply text-left text-pretty relative leading-tight text-tint select-text;
 }
 
 .openapi-response-description.openapi-markdown {
-    @apply text-left prose-sm text-[0.813rem] text-pretty h-auto relative leading-[1.125rem] text-tint !font-normal truncate select-text prose-strong:font-semibold prose-strong:text-inherit;
+    @apply text-left truncate prose-sm text-sm leading-tight text-tint select-text prose-strong:font-semibold prose-strong:text-inherit;
 }
 
-.openapi-response-description.openapi-markdown::-webkit-scrollbar {
-    display: none;
+.openapi-disclosure-group-trigger[aria-expanded="true"] .openapi-response-tab-content {
+    @apply basis-full;
 }
 
-.openapi-response-description p {
-    @apply truncate max-w-full inline pr-1;
-}
-
-.openapi-response-content-type {
-    @apply text-xs text-tint-8 ml-auto shrink-0;
+.openapi-disclosure-group-trigger[aria-expanded="true"]
+    .openapi-response-description.openapi-markdown {
+    @apply whitespace-normal;
 }
 
 .openapi-response-body {
-    @apply flex flex-col gap-3;
+    @apply flex flex-col;
 }
 
 /* Response Body and Headers */
@@ -411,16 +389,7 @@
     @apply px-3 py-1;
 }
 
-.openapi-responsebody-header-content,
-.openapi-responseheaders-header-content {
-    /* unstyled */
-}
-
 /* Code Sample */
-.openapi-codesample {
-    @apply border rounded-md straight-corners:rounded-none bg-tint border-tint-subtle;
-}
-
 .openapi-codesample-header {
     @apply flex flex-row items-center;
 }
@@ -429,6 +398,12 @@
     @apply flex flex-row items-center gap-2.5;
 }
 
+.openapi-panel-heading,
+.openapi-codesample-header,
+.openapi-response-examples-header {
+    @apply border-b border-tint-subtle;
+}
+
 .openapi-response-examples-header .openapi-select > button {
     @apply max-w-full overflow-hidden shrink pl-0.5 py-0.5;
 }
@@ -503,8 +478,16 @@
 }
 
 /* Panel */
-.openapi-panel {
-    @apply border rounded-md straight-corners:rounded-none bg-tint border-tint-subtle;
+.openapi-panel,
+.openapi-codesample,
+.openapi-response-examples {
+    @apply border rounded-md straight-corners:rounded-none bg-tint-subtle border-tint-subtle shadow-sm;
+}
+
+.openapi-panel pre,
+.openapi-codesample pre,
+.openapi-response-examples pre {
+    @apply bg-transparent border-none rounded-none shadow-none;
 }
 
 .openapi-panel-heading {
@@ -513,12 +496,11 @@
 
 .openapi-panel-body {
     @apply relative;
-    @apply before:w-full before:h-px before:absolute before:bg-tint-6 before:-top-px before:z-10;
 }
 
 .openapi-panel-footer,
 .openapi-codesample-footer {
-    @apply px-3 py-2 pt-2.5 border-t border-tint-subtle text-[0.813rem] text-tint;
+    @apply px-3 py-2 pt-2.5 border-t border-tint-subtle text-[0.813rem] text-tint empty:hidden;
 }
 
 .openapi-panel-footer .openapi-markdown {
@@ -526,10 +508,6 @@
 }
 
 /* Example */
-.openapi-response-examples {
-    @apply border rounded-md straight-corners:rounded-none bg-tint border-tint-subtle;
-}
-
 .openapi-response-examples-header {
     @apply flex flex-row items-center p-2.5;
 }
@@ -551,7 +529,6 @@
 .openapi-response-examples-panel,
 .openapi-codesample-panel {
     @apply flex-1 text-sm relative focus-visible:outline-none;
-    @apply before:w-full before:h-px before:absolute before:bg-tint-6 before:-top-px before:z-10;
 }
 
 .openapi-example-empty {
@@ -703,15 +680,19 @@ body:has(.openapi-select-popover) {
 
 /* Disclosure group */
 .openapi-disclosure-group {
-    @apply border-tint-subtle border-b border-x overflow-auto last:rounded-b-md straight-corners:last:rounded-none first:rounded-t-md straight-corners:first:rounded-none first:border-t relative;
+    @apply border-tint-subtle transition-all border-b border-x overflow-auto last:rounded-b-md straight-corners:last:rounded-none first:rounded-t-md straight-corners:first:rounded-none first:border-t relative;
 }
 
-.openapi-disclosure-group-header {
-    @apply flex flex-row items-baseline justify-between gap-3 relative;
+.openapi-disclosure-group:has(.openapi-disclosure-group-trigger:hover) {
+    @apply bg-tint-subtle;
 }
 
 .openapi-disclosure-group-trigger {
-    @apply flex items-baseline transition-all hover:bg-tint-subtle relative flex-1 gap-2.5 p-3 truncate -outline-offset-1;
+    @apply flex w-full items-baseline gap-3 transition-all relative flex-1 p-3 -outline-offset-1;
+}
+
+.openapi-disclosure-group-label {
+    @apply flex flex-wrap items-baseline gap-x-3 gap-y-1 flex-1 truncate;
 }
 
 .openapi-disclosure-group-trigger:disabled {
@@ -722,10 +703,6 @@ body:has(.openapi-select-popover) {
     @apply invisible;
 }
 
-.openapi-disclosure-group-trigger[aria-expanded="true"] .openapi-response-description {
-    @apply whitespace-normal;
-}
-
 .openapi-disclosure-group-icon > svg {
     @apply size-3 text-tint-subtle transition-all duration-300;
 }
@@ -735,29 +712,20 @@ body:has(.openapi-select-popover) {
 }
 
 .openapi-disclosure-group-panel {
-    @apply p-3 pt-1 transition-all;
+    @apply px-3 transition-all;
 }
 
 .openapi-disclosure-group-trigger[aria-expanded="true"] > .openapi-disclosure-group-icon > svg {
     @apply rotate-90;
 }
 
-.openapi-disclosure-group:hover .openapi-disclosure-group-mediatype,
-.openapi-disclosure-group-mediatype:has(> .openapi-select[data-open="true"]) {
-    @apply opacity-11 visible flex;
-}
-
 .openapi-disclosure-group-mediatype {
-    @apply opacity-0 invisible text-xs transition-opacity duration-300 shrink-0 absolute right-2.5 top-2.5;
-}
-
-.openapi-disclosure-group-mediatype > span {
-    @apply px-1 bg-tint-6 text-tint-12 rounded-full straight-corners:rounded-md;
+    @apply text-[0.625rem] font-mono shrink-0 grow-0 text-tint-subtle contrast-more:text-tint;
 }
 
 /* Disclosure */
 .openapi-schemas-disclosure > .openapi-disclosure-trigger {
-    @apply flex items-center font-mono !w-full transition-all text-tint-strong !text-sm hover:bg-tint-subtle relative flex-1 gap-2.5 p-3 truncate -outline-offset-1;
+    @apply flex items-center font-mono transition-all text-tint-strong !text-sm hover:bg-tint-subtle relative flex-1 gap-2.5 p-3 truncate -outline-offset-1;
 }
 
 .openapi-schemas-disclosure > .openapi-disclosure-trigger,
@@ -765,49 +733,90 @@ body:has(.openapi-select-popover) {
     @apply straight-corners:!rounded-none;
 }
 
+.openapi-disclosure-panel {
+    @apply ml-1.5 pl-3 border-l border-tint-subtle;
+}
+
+.openapi-schema .openapi-schema-properties .openapi-schema {
+    @apply animate-fadeIn [animation-fill-mode:both];
+}
+
 .openapi-schemas-disclosure > .openapi-disclosure-trigger[aria-expanded="true"] > svg {
     @apply rotate-90;
 }
 
 .openapi-disclosure-trigger {
-    @apply transition-all truncate duration-300 max-w-full hover:text-tint-strong rounded-2xl straight-corners:rounded border border-tint-subtle px-2.5 py-1 text-[0.813rem] text-tint flex flex-row items-center gap-1.5 -outline-offset-1;
+    @apply flex flex-row justify-between flex-wrap relative items-start gap-2 text-left -mx-3 px-3 -my-2.5 py-2.5 pr-10;
 }
 
-.openapi-disclosure-trigger span {
-    @apply truncate;
+.openapi-disclosure {
+    @apply -mx-3 px-3 py-2.5 transition-all flex flex-col ring-tint-subtle;
 }
 
-.openapi-disclosure svg {
-    @apply size-3 shrink-0 transition-transform duration-300;
+.openapi-disclosure:not(
+        .openapi-disclosure-group .openapi-disclosure,
+        .openapi-schema-alternatives .openapi-disclosure
+    ) {
+    @apply rounded-xl;
 }
 
-.openapi-disclosure-trigger[aria-expanded="true"] svg {
-    @apply rotate-45;
+.openapi-disclosure:has(> .openapi-disclosure-trigger:hover) {
+    @apply bg-tint-subtle;
 }
 
-.openapi-disclosure-trigger[aria-expanded="true"] {
-    @apply w-full rounded-lg border-b rounded-b-none straight-corners:rounded-b-none;
+.openapi-disclosure:has(> .openapi-disclosure-trigger:hover),
+.openapi-disclosure[data-expanded="true"] {
+    @apply ring-1 shadow-sm;
 }
 
-.openapi-disclosure-trigger[aria-expanded="false"] {
-    @apply w-auto;
+.openapi-disclosure[data-expanded="true"]:not(:first-child) {
+    @apply mt-2;
+}
+.openapi-disclosure[data-expanded="true"]:not(:last-child) {
+    @apply mb-2;
 }
 
-.openapi-disclosure-panel[aria-hidden="false"] {
-    @apply border-b border-x border-tint-subtle rounded-b-lg straight-corners:rounded-b;
+.openapi-disclosure-trigger-label {
+    @apply absolute right-3 px-2 h-5 justify-end shrink-0 ring-tint-subtle truncate text-tint duration-300 transition-all rounded straight-corners:rounded-none flex flex-row gap-1 items-center text-xs;
 }
 
-.openapi-disclosure-panel .openapi-schema {
-    @apply p-2.5;
+.openapi-disclosure-trigger-label span {
+    @apply hidden;
+}
+
+.openapi-disclosure-trigger-label svg {
+    @apply size-3 shrink-0 transition-transform duration-300 text-tint-subtle;
 }
 
-.openapi-disclosure .openapi-schema-properties .openapi-schema:only-child,
-.openapi-disclosure .openapi-schema-properties .openapi-schema:only-child .openapi-schema-name {
-    @apply !m-0;
+.openapi-disclosure-trigger:hover > .openapi-disclosure-trigger-label,
+.openapi-disclosure-trigger[aria-expanded="true"] > .openapi-disclosure-trigger-label {
+    @apply shadow ring-1 bg-tint-base;
 }
 
-.openapi-disclosure .openapi-schema-properties .openapi-schema-enum {
-    @apply pt-0 mt-0;
+.openapi-disclosure-trigger:hover > .openapi-disclosure-trigger-label span,
+.openapi-disclosure-trigger[aria-expanded="true"] > .openapi-disclosure-trigger-label span {
+    @apply block animate-fadeIn;
+}
+
+@media (hover: none) {
+    /* Make button label always visible on non-hover devices like phones */
+    .openapi-disclosure-trigger-label {
+        @apply relative ring-1 bg-tint-base;
+    }
+    .openapi-disclosure-trigger-label span {
+        @apply block;
+    }
+    .openapi-disclosure-trigger {
+        @apply pr-3;
+    }
+}
+
+.openapi-disclosure-trigger[aria-expanded="true"] svg {
+    @apply rotate-45;
+}
+
+.openapi-disclosure-trigger[aria-expanded="false"] {
+    @apply w-auto;
 }
 
 .openapi-section-body.openapi-schema.openapi-schema-root {
@@ -819,6 +828,16 @@ body:has(.openapi-select-popover) {
     @apply p-2.5;
 }
 
+.openapi-schema-alternatives {
+    @apply ml-1.5 pl-3 border-l border-tint-subtle;
+}
+.openapi-schema-alternative {
+    @apply relative;
+}
+.openapi-schema-alternative-separator {
+    @apply p-0.5 tracking-wide leading-none uppercase text-[0.625rem] text-tint-subtle whitespace-nowrap absolute -left-3 -bottom-2.5 -translate-x-1/2 z-10 bg-tint-base border-y border-tint-subtle -rotate-6;
+}
+
 .openapi-tooltip {
     @apply flex items-center gap-1 bg-tint-base border border-tint-subtle text-tint-strong rounded-md straight-corners:rounded-none font-medium px-1.5 py-0.5 shadow-sm text-[13px];
 }
@@ -865,11 +884,11 @@ body:has(.openapi-select-popover) {
 }
 
 @keyframes popover-leave {
-    0% {
+    from {
         opacity: 1;
         transform: translateY(0) scale(1);
     }
-    100% {
+    to {
         opacity: 0;
         transform: translateY(4px) scale(0.95);
     }
diff --git a/packages/react-openapi/src/OpenAPIDisclosure.tsx b/packages/react-openapi/src/OpenAPIDisclosure.tsx
index d84b4523ba..56e663b44b 100644
--- a/packages/react-openapi/src/OpenAPIDisclosure.tsx
+++ b/packages/react-openapi/src/OpenAPIDisclosure.tsx
@@ -9,11 +9,12 @@ import { Button, Disclosure, DisclosurePanel } from 'react-aria-components';
  */
 export function OpenAPIDisclosure(props: {
     icon: React.ReactNode;
+    header: React.ReactNode;
     children: React.ReactNode;
     label: string | ((isExpanded: boolean) => string);
     className?: string;
 }): React.JSX.Element {
-    const { icon, children, label, className } = props;
+    const { icon, header, label, children, className } = props;
     const [isExpanded, setIsExpanded] = useState(false);
 
     return (
@@ -31,8 +32,11 @@ export function OpenAPIDisclosure(props: {
                         : 'none',
                 })}
             >
-                {icon}
-                <span>{typeof label === 'function' ? label(isExpanded) : label}</span>
+                {header}
+                <div className="openapi-disclosure-trigger-label">
+                    <span>{typeof label === 'function' ? label(isExpanded) : label}</span>
+                    {icon}
+                </div>
             </Button>
             <DisclosurePanel className="openapi-disclosure-panel">
                 {isExpanded ? children : null}
diff --git a/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx b/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx
index 7f2a428d5c..27584cfc67 100644
--- a/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx
+++ b/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx
@@ -96,56 +96,57 @@ function DisclosureItem(props: {
 
     return (
         <div className="openapi-disclosure-group" aria-expanded={state.isExpanded}>
-            <div className="openapi-disclosure-group-header">
-                <button
-                    slot="trigger"
-                    ref={triggerRef}
-                    {...mergeProps(buttonProps, focusProps)}
-                    disabled={isDisabled}
-                    style={{
-                        outline: isFocusVisible
-                            ? '2px solid rgb(var(--primary-color-500)/0.4)'
-                            : 'none',
-                    }}
-                    className="openapi-disclosure-group-trigger"
-                >
-                    <div className="openapi-disclosure-group-icon">
-                        {icon || (
-                            <svg viewBox="0 0 24 24" className="openapi-disclosure-group-icon">
-                                <path d="m8.25 4.5 7.5 7.5-7.5 7.5" />
-                            </svg>
-                        )}
-                    </div>
+            <button
+                slot="trigger"
+                ref={triggerRef}
+                {...mergeProps(buttonProps, focusProps)}
+                disabled={isDisabled}
+                style={{
+                    outline: isFocusVisible
+                        ? '2px solid rgb(var(--primary-color-500)/0.4)'
+                        : 'none',
+                }}
+                className="openapi-disclosure-group-trigger"
+            >
+                <div className="openapi-disclosure-group-icon">
+                    {icon || (
+                        <svg viewBox="0 0 24 24" className="openapi-disclosure-group-icon">
+                            <path d="m8.25 4.5 7.5 7.5-7.5 7.5" />
+                        </svg>
+                    )}
+                </div>
 
+                <div className="openapi-disclosure-group-label">
                     {group.label}
-                </button>
-                {group.tabs ? (
-                    <div
-                        className="openapi-disclosure-group-mediatype"
-                        onClick={(e) => e.stopPropagation()}
-                    >
-                        {group.tabs?.length > 1 ? (
-                            <OpenAPISelect
-                                icon={selectIcon}
-                                stateKey={selectStateKey}
-                                onSelectionChange={() => {
-                                    state.expand();
-                                }}
-                                items={group.tabs}
-                                placement="bottom end"
-                            >
-                                {group.tabs.map((tab) => (
-                                    <OpenAPISelectItem key={tab.key} id={tab.key} value={tab}>
-                                        {tab.label}
-                                    </OpenAPISelectItem>
-                                ))}
-                            </OpenAPISelect>
-                        ) : group.tabs[0]?.label ? (
-                            <span>{group.tabs[0].label}</span>
-                        ) : null}
-                    </div>
-                ) : null}
-            </div>
+
+                    {group.tabs ? (
+                        <div
+                            className="openapi-disclosure-group-mediatype"
+                            onClick={(e) => e.stopPropagation()}
+                        >
+                            {group.tabs?.length > 1 ? (
+                                <OpenAPISelect
+                                    icon={selectIcon}
+                                    stateKey={selectStateKey}
+                                    onSelectionChange={() => {
+                                        state.expand();
+                                    }}
+                                    items={group.tabs}
+                                    placement="bottom end"
+                                >
+                                    {group.tabs.map((tab) => (
+                                        <OpenAPISelectItem key={tab.key} id={tab.key} value={tab}>
+                                            {tab.label}
+                                        </OpenAPISelectItem>
+                                    ))}
+                                </OpenAPISelect>
+                            ) : group.tabs[0]?.label ? (
+                                <span>{group.tabs[0].label}</span>
+                            ) : null}
+                        </div>
+                    ) : null}
+                </div>
+            </button>
 
             {state.isExpanded && selectedTab && (
                 <div className="openapi-disclosure-group-panel" ref={panelRef} {...panelProps}>
diff --git a/packages/react-openapi/src/OpenAPIResponse.tsx b/packages/react-openapi/src/OpenAPIResponse.tsx
index de744b7589..3c005ec29b 100644
--- a/packages/react-openapi/src/OpenAPIResponse.tsx
+++ b/packages/react-openapi/src/OpenAPIResponse.tsx
@@ -1,7 +1,9 @@
 import type { OpenAPIV3 } from '@gitbook/openapi-parser';
 import { OpenAPIDisclosure } from './OpenAPIDisclosure';
+import { OpenAPISchemaPresentation } from './OpenAPISchema';
 import { OpenAPISchemaProperties } from './OpenAPISchemaServer';
 import type { OpenAPIClientContext } from './context';
+import { tString } from './translate';
 import { parameterToProperty, resolveDescription } from './utils';
 
 /**
@@ -27,7 +29,31 @@ export function OpenAPIResponse(props: {
     return (
         <div className="openapi-response-body">
             {headers.length > 0 ? (
-                <OpenAPIDisclosure icon={context.icons.plus} label="Headers">
+                <OpenAPIDisclosure
+                    header={
+                        <OpenAPISchemaPresentation
+                            context={context}
+                            property={{
+                                propertyName: tString(context.translation, 'headers'),
+                                schema: {
+                                    type: 'object',
+                                },
+                                required: null,
+                            }}
+                        />
+                    }
+                    icon={context.icons.plus}
+                    label={(isExpanded) =>
+                        tString(
+                            context.translation,
+                            isExpanded ? 'hide' : 'show',
+                            tString(
+                                context.translation,
+                                headers.length === 1 ? 'header' : 'headers'
+                            )
+                        )
+                    }
+                >
                     <OpenAPISchemaProperties
                         properties={headers.map(([name, header]) =>
                             parameterToProperty({ name, ...header })
@@ -40,7 +66,13 @@ export function OpenAPIResponse(props: {
                 <div className="openapi-responsebody">
                     <OpenAPISchemaProperties
                         id={`response-${context.blockKey}`}
-                        properties={[{ schema: mediaType.schema }]}
+                        properties={[
+                            {
+                                schema: mediaType.schema,
+                                propertyName: tString(context.translation, 'response'),
+                                required: null,
+                            },
+                        ]}
                         context={context}
                     />
                 </div>
diff --git a/packages/react-openapi/src/OpenAPISchema.tsx b/packages/react-openapi/src/OpenAPISchema.tsx
index d268b17cf5..8f4c0cd33c 100644
--- a/packages/react-openapi/src/OpenAPISchema.tsx
+++ b/packages/react-openapi/src/OpenAPISchema.tsx
@@ -4,6 +4,7 @@
 
 import type { OpenAPICustomOperationProperties, OpenAPIV3 } from '@gitbook/openapi-parser';
 import { useId } from 'react';
+import type { ComponentPropsWithoutRef } from 'react';
 
 import clsx from 'clsx';
 import { Markdown } from './Markdown';
@@ -19,73 +20,98 @@ import { checkIsReference, resolveDescription, resolveFirstExample } from './uti
 type CircularRefsIds = Map<OpenAPIV3.SchemaObject, string>;
 
 export interface OpenAPISchemaPropertyEntry {
-    propertyName?: string | undefined;
-    required?: boolean | undefined;
+    propertyName?: string;
+    required?: boolean | null;
     schema: OpenAPIV3.SchemaObject;
 }
 
 /**
  * Render a property of an OpenAPI schema.
  */
-function OpenAPISchemaProperty(props: {
-    property: OpenAPISchemaPropertyEntry;
-    context: OpenAPIClientContext;
-    circularRefs: CircularRefsIds;
-    className?: string;
-}) {
-    const { circularRefs: parentCircularRefs, context, className, property } = props;
+function OpenAPISchemaProperty(
+    props: {
+        property: OpenAPISchemaPropertyEntry;
+        context: OpenAPIClientContext;
+        circularRefs: CircularRefsIds;
+        className?: string;
+    } & Omit<ComponentPropsWithoutRef<'div'>, 'property' | 'context' | 'circularRefs' | 'className'>
+) {
+    const { circularRefs: parentCircularRefs, context, className, property, ...rest } = props;
 
     const { schema } = property;
 
     const id = useId();
 
-    return (
-        <div id={id} className={clsx('openapi-schema', className)}>
-            <OpenAPISchemaPresentation context={context} property={property} />
-            {(() => {
-                const circularRefId = parentCircularRefs.get(schema);
-                // Avoid recursing infinitely, and instead render a link to the parent schema
-                if (circularRefId) {
-                    return <OpenAPISchemaCircularRef id={circularRefId} schema={schema} />;
-                }
+    const circularRefId = parentCircularRefs.get(schema);
+    // Avoid recursing infinitely, and instead render a link to the parent schema
+    if (circularRefId) {
+        return <OpenAPISchemaCircularRef id={circularRefId} schema={schema} />;
+    }
+
+    const circularRefs = new Map(parentCircularRefs);
+    circularRefs.set(schema, id);
+
+    const properties = getSchemaProperties(schema);
+
+    const ancestors = new Set(circularRefs.keys());
+    const alternatives = getSchemaAlternatives(schema, ancestors);
+
+    const header = <OpenAPISchemaPresentation context={context} property={property} />;
+    const content = (() => {
+        if (properties?.length) {
+            return (
+                <OpenAPISchemaProperties
+                    properties={properties}
+                    circularRefs={circularRefs}
+                    context={context}
+                />
+            );
+        }
 
-                const circularRefs = new Map(parentCircularRefs);
-                circularRefs.set(schema, id);
-
-                const properties = getSchemaProperties(schema);
-                if (properties?.length) {
-                    return (
-                        <OpenAPIDisclosure
-                            icon={context.icons.plus}
-                            label={(isExpanded) =>
-                                getDisclosureLabel({ schema, isExpanded, context })
-                            }
-                        >
-                            <OpenAPISchemaProperties
-                                properties={properties}
+        if (alternatives) {
+            return (
+                <div className="openapi-schema-alternatives">
+                    {alternatives.map((alternativeSchema, index) => (
+                        <div key={index} className="openapi-schema-alternative">
+                            <OpenAPISchemaAlternative
+                                schema={alternativeSchema}
                                 circularRefs={circularRefs}
                                 context={context}
                             />
-                        </OpenAPIDisclosure>
-                    );
-                }
+                            {index < alternatives.length - 1 ? (
+                                <span className="openapi-schema-alternative-separator">
+                                    {(schema.anyOf || schema.oneOf) &&
+                                        tString(context.translation, 'or')}
+                                    {schema.allOf && tString(context.translation, 'and')}
+                                </span>
+                            ) : null}
+                        </div>
+                    ))}
+                </div>
+            );
+        }
 
-                const ancestors = new Set(circularRefs.keys());
-                const alternatives = getSchemaAlternatives(schema, ancestors);
-
-                if (alternatives) {
-                    return alternatives.map((schema, index) => (
-                        <OpenAPISchemaAlternative
-                            key={index}
-                            schema={schema}
-                            circularRefs={circularRefs}
-                            context={context}
-                        />
-                    ));
-                }
+        return null;
+    })();
+
+    if (properties?.length) {
+        return (
+            <OpenAPIDisclosure
+                icon={context.icons.plus}
+                className={clsx('openapi-schema', className)}
+                header={header}
+                label={(isExpanded) => getDisclosureLabel({ schema, isExpanded, context })}
+                {...rest}
+            >
+                {content}
+            </OpenAPIDisclosure>
+        );
+    }
 
-                return null;
-            })()}
+    return (
+        <div id={id} {...rest} className={clsx('openapi-schema', className)}>
+            {header}
+            {content}
         </div>
     );
 }
@@ -115,6 +141,7 @@ function OpenAPISchemaProperties(props: {
                         circularRefs={circularRefs}
                         property={property}
                         context={context}
+                        style={{ animationDelay: `${index * 0.02}s` }}
                     />
                 );
             })}
@@ -205,34 +232,26 @@ function OpenAPISchemaAlternative(props: {
     context: OpenAPIClientContext;
 }) {
     const { schema, circularRefs, context } = props;
-
-    const description = resolveDescription(schema);
     const properties = getSchemaProperties(schema);
 
-    return (
-        <>
-            {description ? (
-                <Markdown source={description} className="openapi-schema-description" />
-            ) : null}
-            <OpenAPIDisclosure
-                icon={context.icons.plus}
-                label={(isExpanded) => getDisclosureLabel({ schema, isExpanded, context })}
-            >
-                {properties?.length ? (
-                    <OpenAPISchemaProperties
-                        properties={properties}
-                        circularRefs={circularRefs}
-                        context={context}
-                    />
-                ) : (
-                    <OpenAPISchemaProperty
-                        property={{ schema }}
-                        circularRefs={circularRefs}
-                        context={context}
-                    />
-                )}
-            </OpenAPIDisclosure>
-        </>
+    return properties?.length ? (
+        <OpenAPIDisclosure
+            icon={context.icons.plus}
+            header={<OpenAPISchemaPresentation property={{ schema }} context={context} />}
+            label={(isExpanded) => getDisclosureLabel({ schema, isExpanded, context })}
+        >
+            <OpenAPISchemaProperties
+                properties={properties}
+                circularRefs={circularRefs}
+                context={context}
+            />
+        </OpenAPIDisclosure>
+    ) : (
+        <OpenAPISchemaProperty
+            property={{ schema }}
+            circularRefs={circularRefs}
+            context={context}
+        />
     );
 }
 
@@ -293,7 +312,7 @@ function OpenAPISchemaEnum(props: {
 
     return (
         <span className="openapi-schema-enum">
-            Available options:{' '}
+            {tString(context.translation, 'possible_values')}:{' '}
             {enumValues.map((item, index) => (
                 <span key={index} className="openapi-schema-enum-value">
                     <OpenAPICopyButton
@@ -313,7 +332,7 @@ function OpenAPISchemaEnum(props: {
 /**
  * Render the top row of a schema. e.g: name, type, and required status.
  */
-function OpenAPISchemaPresentation(props: {
+export function OpenAPISchemaPresentation(props: {
     property: OpenAPISchemaPropertyEntry;
     context: OpenAPIClientContext;
 }) {
@@ -612,16 +631,14 @@ function getDisclosureLabel(props: {
     if (schema.type === 'array' && !!schema.items) {
         if (schema.items.oneOf) {
             label = tString(context.translation, 'available_items').toLowerCase();
-        }
-        // Fallback to "child attributes" for enums and objects
-        else if (schema.items.enum || schema.items.type === 'object') {
-            label = tString(context.translation, 'child_attributes').toLowerCase();
+        } else if (schema.items.enum || schema.items.type === 'object') {
+            label = tString(context.translation, 'properties').toLowerCase();
         } else {
             label = schema.items.title ?? schema.title ?? getSchemaTitle(schema.items);
         }
     } else {
-        label = schema.title || tString(context.translation, 'child_attributes').toLowerCase();
+        label = schema.title || tString(context.translation, 'properties').toLowerCase();
     }
 
-    return `${isExpanded ? tString(context.translation, 'hide') : tString(context.translation, 'show')} ${label}`;
+    return tString(context.translation, isExpanded ? 'hide' : 'show', label);
 }
diff --git a/packages/react-openapi/src/OpenAPISchemaName.tsx b/packages/react-openapi/src/OpenAPISchemaName.tsx
index 6e230419cd..d24efe3f70 100644
--- a/packages/react-openapi/src/OpenAPISchemaName.tsx
+++ b/packages/react-openapi/src/OpenAPISchemaName.tsx
@@ -6,7 +6,7 @@ import { t, tString } from './translate';
 interface OpenAPISchemaNameProps {
     schema?: OpenAPIV3.SchemaObject;
     propertyName?: string | React.JSX.Element;
-    required?: boolean;
+    required?: boolean | null;
     type?: string;
     context: OpenAPIClientContext;
 }
@@ -43,7 +43,7 @@ export function OpenAPISchemaName(props: OpenAPISchemaNameProps) {
                     {t(context.translation, 'write_only')}
                 </span>
             ) : null}
-            {required ? (
+            {required === null ? null : required ? (
                 <span className="openapi-schema-required">
                     {t(context.translation, 'required')}
                 </span>
diff --git a/packages/react-openapi/src/StaticSection.tsx b/packages/react-openapi/src/StaticSection.tsx
index ffb3a2bba8..b79b20e617 100644
--- a/packages/react-openapi/src/StaticSection.tsx
+++ b/packages/react-openapi/src/StaticSection.tsx
@@ -11,7 +11,7 @@ export function SectionHeader(props: ComponentPropsWithoutRef<'div'>) {
             {...props}
             className={clsx(
                 'openapi-section-header',
-                props.className && `${props.className}-header`
+                props.className ? `${props.className}-header` : undefined
             )}
         />
     );
diff --git a/packages/react-openapi/src/schemas/OpenAPISchemas.tsx b/packages/react-openapi/src/schemas/OpenAPISchemas.tsx
index 2283245fcb..eddb893def 100644
--- a/packages/react-openapi/src/schemas/OpenAPISchemas.tsx
+++ b/packages/react-openapi/src/schemas/OpenAPISchemas.tsx
@@ -9,7 +9,7 @@ import {
     getOpenAPIClientContext,
     resolveOpenAPIContext,
 } from '../context';
-import { t } from '../translate';
+import { t, tString } from '../translate';
 import { getExampleFromSchema } from '../util/example';
 
 /**
@@ -89,7 +89,10 @@ export function OpenAPISchemas(props: {
                         className="openapi-schemas-disclosure"
                         key={name}
                         icon={context.icons.chevronRight}
-                        label={name}
+                        header={name}
+                        label={(isExpanded) =>
+                            tString(context.translation, isExpanded ? 'hide' : 'show')
+                        }
                     >
                         <Section className="openapi-section-schemas">
                             <SectionBody>
diff --git a/packages/react-openapi/src/translations/de.ts b/packages/react-openapi/src/translations/de.ts
index 8fc792e8ea..8a5236c587 100644
--- a/packages/react-openapi/src/translations/de.ts
+++ b/packages/react-openapi/src/translations/de.ts
@@ -18,9 +18,11 @@ export const de = {
     nullable: 'Nullfähig',
     body: 'Rumpf',
     payload: 'Nutzlast',
-    headers: 'Kopfzeilen',
+    headers: 'Header',
+    header: 'Header',
     authorizations: 'Autorisierungen',
     responses: 'Antworten',
+    response: 'Antwort',
     path_parameters: 'Pfadparameter',
     query_parameters: 'Abfrageparameter',
     header_parameters: 'Header-Parameter',
@@ -30,8 +32,11 @@ export const de = {
     success: 'Erfolg',
     redirect: 'Umleitung',
     error: 'Fehler',
-    show: 'Anzeigen',
-    hide: 'Verstecken',
+    show: 'Zeige ${1}',
+    hide: 'Verstecke ${1}',
     available_items: 'Verfügbare Elemente',
-    child_attributes: 'Unterattribute',
+    properties: 'Eigenschaften',
+    or: 'oder',
+    and: 'und',
+    possible_values: 'Mögliche Werte',
 };
diff --git a/packages/react-openapi/src/translations/en.ts b/packages/react-openapi/src/translations/en.ts
index 61f45e9e3f..4276595d54 100644
--- a/packages/react-openapi/src/translations/en.ts
+++ b/packages/react-openapi/src/translations/en.ts
@@ -19,8 +19,10 @@ export const en = {
     body: 'Body',
     payload: 'Payload',
     headers: 'Headers',
+    header: 'Header',
     authorizations: 'Authorizations',
     responses: 'Responses',
+    response: 'Response',
     path_parameters: 'Path parameters',
     query_parameters: 'Query parameters',
     header_parameters: 'Header parameters',
@@ -30,8 +32,11 @@ export const en = {
     success: 'Success',
     redirect: 'Redirect',
     error: 'Error',
-    show: 'Show',
-    hide: 'Hide',
+    show: 'Show ${1}',
+    hide: 'Hide ${1}',
     available_items: 'Available items',
-    child_attributes: 'Child attributes',
+    possible_values: 'Possible values',
+    properties: 'Properties',
+    or: 'or',
+    and: 'and',
 };
diff --git a/packages/react-openapi/src/translations/es.ts b/packages/react-openapi/src/translations/es.ts
index 5faa36c674..f5ac905f6a 100644
--- a/packages/react-openapi/src/translations/es.ts
+++ b/packages/react-openapi/src/translations/es.ts
@@ -18,9 +18,11 @@ export const es = {
     nullable: 'Nulo',
     body: 'Cuerpo',
     payload: 'Caga útil',
-    headers: 'Encabezados',
+    headers: 'Headers',
+    header: 'Header',
     authorizations: 'Autorizaciones',
     responses: 'Respuestas',
+    response: 'Respuesta',
     path_parameters: 'Parámetros de ruta',
     query_parameters: 'Parámetros de consulta',
     header_parameters: 'Parámetros de encabezado',
@@ -30,8 +32,11 @@ export const es = {
     success: 'Éxito',
     redirect: 'Redirección',
     error: 'Error',
-    show: 'Mostrar',
-    hide: 'Ocultar',
+    show: 'Mostrar ${1}',
+    hide: 'Ocultar ${1}',
     available_items: 'Elementos disponibles',
-    child_attributes: 'Atributos secundarios',
+    properties: 'Propiedades',
+    or: 'o',
+    and: 'y',
+    possible_values: 'Valores posibles',
 };
diff --git a/packages/react-openapi/src/translations/fr.ts b/packages/react-openapi/src/translations/fr.ts
index ccaf05b3d9..b2e563c813 100644
--- a/packages/react-openapi/src/translations/fr.ts
+++ b/packages/react-openapi/src/translations/fr.ts
@@ -18,20 +18,25 @@ export const fr = {
     nullable: 'Nullable',
     body: 'Corps',
     payload: 'Charge utile',
-    headers: 'En-têtes',
+    headers: 'Headers',
+    header: 'Header',
     authorizations: 'Autorisations',
     responses: 'Réponses',
+    response: 'Réponse',
     path_parameters: 'Paramètres de chemin',
     query_parameters: 'Paramètres de requête',
-    header_parameters: 'Paramètres d’en-tête',
+    header_parameters: "Paramètres d'en-tête",
     attributes: 'Attributs',
     test_it: 'Tester',
     information: 'Information',
     success: 'Succès',
     redirect: 'Redirection',
     error: 'Erreur',
-    show: 'Afficher',
-    hide: 'Masquer',
+    show: 'Afficher ${1}',
+    hide: 'Masquer ${1}',
     available_items: 'Éléments disponibles',
-    child_attributes: 'Attributs enfants',
+    properties: 'Propriétés',
+    or: 'ou',
+    and: 'et',
+    possible_values: 'Valeurs possibles',
 };
diff --git a/packages/react-openapi/src/translations/ja.ts b/packages/react-openapi/src/translations/ja.ts
index 55b5b2a0a0..e393dd6cbd 100644
--- a/packages/react-openapi/src/translations/ja.ts
+++ b/packages/react-openapi/src/translations/ja.ts
@@ -19,8 +19,10 @@ export const ja = {
     body: '本文',
     payload: 'ペイロード',
     headers: 'ヘッダー',
+    header: 'ヘッダー',
     authorizations: '認可',
     responses: 'レスポンス',
+    response: 'レスポンス',
     path_parameters: 'パスパラメータ',
     query_parameters: 'クエリパラメータ',
     header_parameters: 'ヘッダーパラメータ',
@@ -30,8 +32,11 @@ export const ja = {
     success: '成功',
     redirect: 'リダイレクト',
     error: 'エラー',
-    show: '表示',
-    hide: '非表示',
+    show: '${1}を表示',
+    hide: '${1}を非表示',
     available_items: '利用可能なアイテム',
-    child_attributes: '子属性',
+    properties: 'プロパティ',
+    or: 'または',
+    and: 'かつ',
+    possible_values: '可能な値',
 };
diff --git a/packages/react-openapi/src/translations/nl.ts b/packages/react-openapi/src/translations/nl.ts
index 5186580c78..34867ccf15 100644
--- a/packages/react-openapi/src/translations/nl.ts
+++ b/packages/react-openapi/src/translations/nl.ts
@@ -19,8 +19,10 @@ export const nl = {
     body: 'Body',
     payload: 'Payload',
     headers: 'Headers',
+    header: 'Header',
     authorizations: 'Autorisaties',
     responses: 'Reacties',
+    response: 'Reactie',
     path_parameters: 'Padparameters',
     query_parameters: 'Queryparameters',
     header_parameters: 'Headerparameters',
@@ -30,8 +32,11 @@ export const nl = {
     success: 'Succes',
     redirect: 'Omleiding',
     error: 'Fout',
-    show: 'Toon',
-    hide: 'Verbergen',
+    show: 'Toon ${1}',
+    hide: 'Verberg ${1}',
     available_items: 'Beschikbare items',
-    child_attributes: 'Kindattributen',
+    properties: 'Eigenschappen',
+    or: 'of',
+    and: 'en',
+    possible_values: 'Mogelijke waarden',
 };
diff --git a/packages/react-openapi/src/translations/no.ts b/packages/react-openapi/src/translations/no.ts
index a4efc3cfe7..2701177932 100644
--- a/packages/react-openapi/src/translations/no.ts
+++ b/packages/react-openapi/src/translations/no.ts
@@ -18,9 +18,11 @@ export const no = {
     nullable: 'Kan være null',
     body: 'Brødtekst',
     payload: 'Nyttelast',
-    headers: 'Overskrifter',
+    headers: 'Headers',
+    header: 'Header',
     authorizations: 'Autorisasjoner',
     responses: 'Responser',
+    response: 'Respons',
     path_parameters: 'Sti-parametere',
     query_parameters: 'Forespørselsparametere',
     header_parameters: 'Header-parametere',
@@ -30,8 +32,11 @@ export const no = {
     success: 'Suksess',
     redirect: 'Viderekobling',
     error: 'Feil',
-    show: 'Vis',
-    hide: 'Skjul',
+    show: 'Vis ${1}',
+    hide: 'Skjul ${1}',
     available_items: 'Tilgjengelige elementer',
-    child_attributes: 'Barneattributter',
+    properties: 'Egenskaper',
+    or: 'eller',
+    and: 'og',
+    possible_values: 'Mulige verdier',
 };
diff --git a/packages/react-openapi/src/translations/pt-br.ts b/packages/react-openapi/src/translations/pt-br.ts
index 7c1f86a7c0..00e8ab3c27 100644
--- a/packages/react-openapi/src/translations/pt-br.ts
+++ b/packages/react-openapi/src/translations/pt-br.ts
@@ -18,9 +18,11 @@ export const pt_br = {
     nullable: 'Nulo',
     body: 'Corpo',
     payload: 'Carga útil',
-    headers: 'Cabeçalhos',
+    headers: 'Headers',
+    header: 'Header',
     authorizations: 'Autorizações',
     responses: 'Respostas',
+    response: 'Resposta',
     path_parameters: 'Parâmetros de rota',
     query_parameters: 'Parâmetros de consulta',
     header_parameters: 'Parâmetros de cabeçalho',
@@ -30,8 +32,11 @@ export const pt_br = {
     success: 'Sucesso',
     redirect: 'Redirecionamento',
     error: 'Erro',
-    show: 'Mostrar',
-    hide: 'Ocultar',
+    show: 'Mostrar ${1}',
+    hide: 'Ocultar ${1}',
     available_items: 'Itens disponíveis',
-    child_attributes: 'Atributos filhos',
+    properties: 'Propriedades',
+    or: 'ou',
+    and: 'e',
+    possible_values: 'Valores possíveis',
 };
diff --git a/packages/react-openapi/src/translations/zh.ts b/packages/react-openapi/src/translations/zh.ts
index 414043fd32..f8299d1993 100644
--- a/packages/react-openapi/src/translations/zh.ts
+++ b/packages/react-openapi/src/translations/zh.ts
@@ -18,9 +18,11 @@ export const zh = {
     nullable: '可为 null',
     body: '请求体',
     payload: '有效载荷',
-    headers: '头部信息',
+    headers: '头字段',
+    header: '头部',
     authorizations: '授权',
     responses: '响应',
+    response: '响应',
     path_parameters: '路径参数',
     query_parameters: '查询参数',
     header_parameters: '头参数',
@@ -30,8 +32,11 @@ export const zh = {
     success: '成功',
     redirect: '重定向',
     error: '错误',
-    show: '显示',
-    hide: '隐藏',
+    show: '显示${1}',
+    hide: '隐藏${1}',
     available_items: '可用项',
-    child_attributes: '子属性',
+    properties: '属性',
+    or: '或',
+    and: '和',
+    possible_values: '可能的值',
 };

From 3119066728f79addcf0b1bf5ef8743307199c85f Mon Sep 17 00:00:00 2001
From: Steven H <steven@gitbook.io>
Date: Thu, 24 Apr 2025 16:54:11 +0100
Subject: [PATCH 008/127] Support for reusable content across spaces. (#3173)

---
 .changeset/polite-falcons-agree.md            |   6 +
 bun.lock                                      |   4 +-
 package.json                                  |   2 +-
 .../DocumentView/Integration/contentkit.tsx   |   2 +
 .../DocumentView/ReusableContent.tsx          |  23 +-
 packages/gitbook/src/fonts/custom.test.ts     |  24 ++
 packages/gitbook/src/fonts/custom.ts          |   6 +-
 packages/gitbook/src/lib/api.ts               |   1 +
 packages/gitbook/src/lib/references.tsx       |  47 ++-
 packages/gitbook/src/lib/v1.ts                | 339 +++++++++++-------
 10 files changed, 302 insertions(+), 152 deletions(-)
 create mode 100644 .changeset/polite-falcons-agree.md

diff --git a/.changeset/polite-falcons-agree.md b/.changeset/polite-falcons-agree.md
new file mode 100644
index 0000000000..7743ab7095
--- /dev/null
+++ b/.changeset/polite-falcons-agree.md
@@ -0,0 +1,6 @@
+---
+"gitbook-v2": minor
+"gitbook": minor
+---
+
+Add support for reusable content across spaces.
diff --git a/bun.lock b/bun.lock
index 5476639847..ff44480802 100644
--- a/bun.lock
+++ b/bun.lock
@@ -259,7 +259,7 @@
   },
   "overrides": {
     "@codemirror/state": "6.4.1",
-    "@gitbook/api": "0.111.0",
+    "@gitbook/api": "0.113.0",
     "react": "^19.0.0",
     "react-dom": "^19.0.0",
   },
@@ -624,7 +624,7 @@
 
     "@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="],
 
-    "@gitbook/api": ["@gitbook/api@0.111.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-E5Pk28kPD4p6XNWdwFM9pgDijdByseIZQqcFK+/hoW5tEZa5Yw/plRKJyN1hmwfPL6SKq6Maf0fbIzTQiVXyQQ=="],
+    "@gitbook/api": ["@gitbook/api@0.113.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-PWMeAkdm4bHSl3b5OmtcmskZ6qRkkDhauCPybo8sGnjS03O14YAUtubAQiNCKX/uwbs+yiQ8KRPyeIwn+g42yw=="],
 
     "@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"],
 
diff --git a/package.json b/package.json
index fba8d79576..00f323e2f4 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
     "packageManager": "bun@1.2.8",
     "overrides": {
         "@codemirror/state": "6.4.1",
-        "@gitbook/api": "0.111.0",
+        "@gitbook/api": "0.113.0",
         "react": "^19.0.0",
         "react-dom": "^19.0.0"
     },
diff --git a/packages/gitbook/src/components/DocumentView/Integration/contentkit.tsx b/packages/gitbook/src/components/DocumentView/Integration/contentkit.tsx
index fb692c6824..155077ca25 100644
--- a/packages/gitbook/src/components/DocumentView/Integration/contentkit.tsx
+++ b/packages/gitbook/src/components/DocumentView/Integration/contentkit.tsx
@@ -20,6 +20,8 @@ export const contentKitServerContext: ContentKitServerContext = {
         'link-external': (props) => <Icon icon="arrow-up-right-from-square" {...props} />,
         eye: (props) => <Icon icon="eye" {...props} />,
         lock: (props) => <Icon icon="lock" {...props} />,
+        check: (props) => <Icon icon="check" {...props} />,
+        'check-circle': (props) => <Icon icon="check-circle" {...props} />,
     },
     codeBlock: (props) => {
         return <PlainCodeBlock code={props.code} syntax={props.syntax} />;
diff --git a/packages/gitbook/src/components/DocumentView/ReusableContent.tsx b/packages/gitbook/src/components/DocumentView/ReusableContent.tsx
index e95ffc7545..7e2d28d8d2 100644
--- a/packages/gitbook/src/components/DocumentView/ReusableContent.tsx
+++ b/packages/gitbook/src/components/DocumentView/ReusableContent.tsx
@@ -13,15 +13,28 @@ export async function ReusableContent(props: BlockProps<DocumentBlockReusableCon
         throw new Error('Expected a content context to render a reusable content block');
     }
 
-    const resolved = await resolveContentRef(block.data.ref, context.contentContext);
-    if (!resolved?.reusableContent?.document) {
+    const dataFetcher = block.meta?.token
+        ? context.contentContext.dataFetcher.withToken({ apiToken: block.meta.token })
+        : context.contentContext.dataFetcher;
+
+    const resolved = await resolveContentRef(block.data.ref, {
+        ...context.contentContext,
+        dataFetcher,
+    });
+
+    if (!resolved?.reusableContent) {
+        return null;
+    }
+
+    const reusableContent = resolved.reusableContent.revisionReusableContent;
+    if (!reusableContent.document) {
         return null;
     }
 
     const document = await getDataOrNull(
-        context.contentContext.dataFetcher.getDocument({
-            spaceId: context.contentContext.space.id,
-            documentId: resolved.reusableContent.document,
+        dataFetcher.getDocument({
+            spaceId: resolved.reusableContent.space,
+            documentId: reusableContent.document,
         })
     );
 
diff --git a/packages/gitbook/src/fonts/custom.test.ts b/packages/gitbook/src/fonts/custom.test.ts
index ce2616187e..b430b76241 100644
--- a/packages/gitbook/src/fonts/custom.test.ts
+++ b/packages/gitbook/src/fonts/custom.test.ts
@@ -28,6 +28,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = {
                 ],
             },
         ],
+        permissions: {
+            edit: false,
+        },
     },
 
     multiWeight: {
@@ -81,6 +84,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = {
                 ],
             },
         ],
+        permissions: {
+            edit: false,
+        },
     },
 
     multiSource: {
@@ -99,6 +105,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = {
                 ],
             },
         ],
+        permissions: {
+            edit: false,
+        },
     },
 
     missingFormat: {
@@ -117,6 +126,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = {
                 ],
             },
         ],
+        permissions: {
+            edit: false,
+        },
     },
 
     empty: {
@@ -124,6 +136,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = {
         custom: true,
         fontFamily: 'Empty Font',
         fontFaces: [],
+        permissions: {
+            edit: false,
+        },
     },
 
     specialChars: {
@@ -136,6 +151,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = {
                 sources: [{ url: 'https://example.com/fonts/special.woff2', format: 'woff2' }],
             },
         ],
+        permissions: {
+            edit: false,
+        },
     },
 
     complex: {
@@ -158,6 +176,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = {
                 ],
             },
         ],
+        permissions: {
+            edit: false,
+        },
     },
 
     variousURLs: {
@@ -174,6 +195,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = {
                 ],
             },
         ],
+        permissions: {
+            edit: false,
+        },
     },
 };
 
diff --git a/packages/gitbook/src/fonts/custom.ts b/packages/gitbook/src/fonts/custom.ts
index cb31f771ed..84abdf8b27 100644
--- a/packages/gitbook/src/fonts/custom.ts
+++ b/packages/gitbook/src/fonts/custom.ts
@@ -1,9 +1,9 @@
-import type { CustomizationFontDefinition } from '@gitbook/api';
+import type { CustomizationFontDefinitionInput } from '@gitbook/api';
 
 /**
  * Define the custom font faces and set the --font-custom to the custom font name
  */
-export function generateFontFacesCSS(customFont: CustomizationFontDefinition): string {
+export function generateFontFacesCSS(customFont: CustomizationFontDefinitionInput): string {
     const { fontFaces } = customFont;
 
     // Generate font face declarations for all weights
@@ -45,7 +45,7 @@ export function generateFontFacesCSS(customFont: CustomizationFontDefinition): s
 /**
  * Get a list of font sources to preload (only 400 and 700 weights)
  */
-export function getFontSourcesToPreload(customFont: CustomizationFontDefinition) {
+export function getFontSourcesToPreload(customFont: CustomizationFontDefinitionInput) {
     return customFont.fontFaces.filter(
         (face): face is typeof face & { weight: 400 | 700 } =>
             face.weight === 400 || face.weight === 700
diff --git a/packages/gitbook/src/lib/api.ts b/packages/gitbook/src/lib/api.ts
index 2bb2039bc3..ca3ce3b1f8 100644
--- a/packages/gitbook/src/lib/api.ts
+++ b/packages/gitbook/src/lib/api.ts
@@ -267,6 +267,7 @@ export const getPublishedContentByUrl = cache({
 
             const parsed = parseCacheResponse(response);
 
+            // biome-ignore lint/suspicious/noConsole: log the ttl of the token
             console.log(
                 `Parsed ttl: ${parsed.ttl} at ${Date.now()}, for ${'apiToken' in response.data ? response.data.apiToken : '<no-token>'}`
             );
diff --git a/packages/gitbook/src/lib/references.tsx b/packages/gitbook/src/lib/references.tsx
index ab941e43c5..2ef8c48a6a 100644
--- a/packages/gitbook/src/lib/references.tsx
+++ b/packages/gitbook/src/lib/references.tsx
@@ -41,8 +41,12 @@ export interface ResolvedContentRef {
     file?: RevisionFile;
     /** Page document resolved from the content ref */
     page?: RevisionPageDocument;
-    /** Resolved reusable content, if the ref points to reusable content on a revision. */
-    reusableContent?: RevisionReusableContent;
+    /** Resolved reusable content, if the ref points to reusable content on a revision. Also contains the space and revision used for resolution. */
+    reusableContent?: {
+        revisionReusableContent: RevisionReusableContent;
+        space: string;
+        revision: string;
+    };
     /** Resolve OpenAPI spec filesystem. */
     openAPIFilesystem?: Filesystem;
 }
@@ -231,21 +235,52 @@ export async function resolveContentRef(
         }
 
         case 'reusable-content': {
+            // Figure out which space and revision the reusable content is in.
+            const container: { space: string; revision: string } | null = await (async () => {
+                // without a space on the content ref, or if the space is the same as the current one, we can use the current revision.
+                if (!contentRef.space || contentRef.space === context.space.id) {
+                    return { space: context.space.id, revision: revisionId };
+                }
+
+                const space = await getDataOrNull(
+                    dataFetcher.getSpace({
+                        spaceId: contentRef.space,
+                        shareKey: undefined,
+                    })
+                );
+
+                if (!space) {
+                    return null;
+                }
+
+                return { space: space.id, revision: space.revision };
+            })();
+
+            if (!container) {
+                return null;
+            }
+
             const reusableContent = await getDataOrNull(
                 dataFetcher.getReusableContent({
-                    spaceId: space.id,
-                    revisionId,
+                    spaceId: container.space,
+                    revisionId: container.revision,
                     reusableContentId: contentRef.reusableContent,
                 })
             );
+
             if (!reusableContent) {
                 return null;
             }
+
             return {
-                href: getGitBookAppHref(`/s/${space.id}`),
+                href: getGitBookAppHref(`/s/${container.space}/~/reusable/${reusableContent.id}`),
                 text: reusableContent.title,
                 active: false,
-                reusableContent,
+                reusableContent: {
+                    revisionReusableContent: reusableContent,
+                    space: container.space,
+                    revision: container.revision,
+                },
             };
         }
 
diff --git a/packages/gitbook/src/lib/v1.ts b/packages/gitbook/src/lib/v1.ts
index ea5219816b..eb2accdc24 100644
--- a/packages/gitbook/src/lib/v1.ts
+++ b/packages/gitbook/src/lib/v1.ts
@@ -8,6 +8,7 @@ import type { GitBookDataFetcher } from '@v2/lib/data/types';
 import { createImageResizer } from '@v2/lib/images';
 import { createLinker } from '@v2/lib/links';
 
+import { GitBookAPI } from '@gitbook/api';
 import { DataFetcherError, wrapDataFetcherError } from '@v2/lib/data';
 import { headers } from 'next/headers';
 import {
@@ -30,6 +31,7 @@ import {
     getUserById,
     renderIntegrationUi,
     searchSiteContent,
+    withAPI as withAPIV1,
 } from './api';
 import { getDynamicCustomizationSettings } from './customization';
 import { withLeadingSlash, withTrailingSlash } from './paths';
@@ -58,7 +60,7 @@ export async function getV1BaseContext(): Promise<GitBookBaseContext> {
         return url;
     };
 
-    const dataFetcher = await getDataFetcherV1();
+    const dataFetcher = getDataFetcherV1();
 
     const imageResizer = createImageResizer({
         imagesContextId: host,
@@ -82,77 +84,121 @@ export async function getV1BaseContext(): Promise<GitBookBaseContext> {
  * Try not to use this as much as possible, and instead take the data fetcher from the props.
  * This data fetcher should only be used at the top of the tree.
  */
-async function getDataFetcherV1(): Promise<GitBookDataFetcher> {
+function getDataFetcherV1(apiTokenOverride?: string): GitBookDataFetcher {
+    let apiClient: GitBookAPI | undefined;
+
+    /**
+     * Run a function with the correct API client. If an API token is provided, we
+     * create a new API client with the token. Otherwise, we use the default API client.
+     */
+    async function withAPI<T>(fn: () => Promise<T>): Promise<T> {
+        // No token override - we can use the default API client.
+        if (!apiTokenOverride) {
+            return fn();
+        }
+
+        const client = await api();
+
+        if (!apiClient) {
+            // New client uses same endpoint and user agent as the default client.
+            apiClient = new GitBookAPI({
+                endpoint: client.client.endpoint,
+                authToken: apiTokenOverride,
+                userAgent: client.client.userAgent,
+            });
+        }
+
+        return withAPIV1(
+            {
+                client: apiClient,
+                contextId: client.contextId,
+            },
+            fn
+        );
+    }
+
     const dataFetcher: GitBookDataFetcher = {
         async api() {
-            const result = await api();
-            return result.client;
+            return withAPI(async () => {
+                const result = await api();
+                return result.client;
+            });
         },
 
-        withToken() {
-            // In v1, the token is global and controlled by the middleware.
-            // We don't need to do anything special here.
-            return dataFetcher;
+        withToken({ apiToken }) {
+            return getDataFetcherV1(apiToken);
         },
 
         getUserById(userId) {
-            return wrapDataFetcherError(async () => {
-                const user = await getUserById(userId);
-                if (!user) {
-                    throw new DataFetcherError('User not found', 404);
-                }
-
-                return user;
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(async () => {
+                    const user = await getUserById(userId);
+                    if (!user) {
+                        throw new DataFetcherError('User not found', 404);
+                    }
+
+                    return user;
+                })
+            );
         },
 
         getPublishedContentSite(params) {
-            return wrapDataFetcherError(async () => {
-                return getPublishedContentSite(params);
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(async () => {
+                    return getPublishedContentSite(params);
+                })
+            );
         },
 
         getSpace(params) {
-            return wrapDataFetcherError(async () => {
-                return getSpace(params.spaceId, params.shareKey);
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(async () => {
+                    return getSpace(params.spaceId, params.shareKey);
+                })
+            );
         },
 
         getChangeRequest(params) {
-            return wrapDataFetcherError(async () => {
-                const changeRequest = await getChangeRequest(
-                    params.spaceId,
-                    params.changeRequestId
-                );
-                if (!changeRequest) {
-                    throw new DataFetcherError('Change request not found', 404);
-                }
-
-                return changeRequest;
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(async () => {
+                    const changeRequest = await getChangeRequest(
+                        params.spaceId,
+                        params.changeRequestId
+                    );
+                    if (!changeRequest) {
+                        throw new DataFetcherError('Change request not found', 404);
+                    }
+
+                    return changeRequest;
+                })
+            );
         },
 
         getRevision(params) {
-            return wrapDataFetcherError(async () => {
-                return getRevision(params.spaceId, params.revisionId, {
-                    metadata: params.metadata,
-                });
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(async () => {
+                    return getRevision(params.spaceId, params.revisionId, {
+                        metadata: params.metadata,
+                    });
+                })
+            );
         },
 
         getRevisionFile(params) {
-            return wrapDataFetcherError(async () => {
-                const revisionFile = await getRevisionFile(
-                    params.spaceId,
-                    params.revisionId,
-                    params.fileId
-                );
-                if (!revisionFile) {
-                    throw new DataFetcherError('Revision file not found', 404);
-                }
-
-                return revisionFile;
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(async () => {
+                    const revisionFile = await getRevisionFile(
+                        params.spaceId,
+                        params.revisionId,
+                        params.fileId
+                    );
+                    if (!revisionFile) {
+                        throw new DataFetcherError('Revision file not found', 404);
+                    }
+
+                    return revisionFile;
+                })
+            );
         },
 
         getRevisionPageMarkdown() {
@@ -160,117 +206,140 @@ async function getDataFetcherV1(): Promise<GitBookDataFetcher> {
         },
 
         getDocument(params) {
-            return wrapDataFetcherError(async () => {
-                const document = await getDocument(params.spaceId, params.documentId);
-                if (!document) {
-                    throw new DataFetcherError('Document not found', 404);
-                }
-
-                return document;
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(async () => {
+                    const document = await getDocument(params.spaceId, params.documentId);
+                    if (!document) {
+                        throw new DataFetcherError('Document not found', 404);
+                    }
+
+                    return document;
+                })
+            );
         },
 
         getComputedDocument(params) {
-            return wrapDataFetcherError(() => {
-                return getComputedDocument(
-                    params.organizationId,
-                    params.spaceId,
-                    params.source,
-                    params.seed
-                );
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(() => {
+                    return getComputedDocument(
+                        params.organizationId,
+                        params.spaceId,
+                        params.source,
+                        params.seed
+                    );
+                })
+            );
         },
 
         getRevisionPages(params) {
-            return wrapDataFetcherError(async () => {
-                return getRevisionPages(params.spaceId, params.revisionId, {
-                    metadata: params.metadata,
-                });
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(async () => {
+                    return getRevisionPages(params.spaceId, params.revisionId, {
+                        metadata: params.metadata,
+                    });
+                })
+            );
         },
 
         getRevisionPageByPath(params) {
-            return wrapDataFetcherError(async () => {
-                const revisionPage = await getRevisionPageByPath(
-                    params.spaceId,
-                    params.revisionId,
-                    params.path
-                );
-
-                if (!revisionPage) {
-                    throw new DataFetcherError('Revision page not found', 404);
-                }
-
-                return revisionPage;
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(async () => {
+                    const revisionPage = await getRevisionPageByPath(
+                        params.spaceId,
+                        params.revisionId,
+                        params.path
+                    );
+
+                    if (!revisionPage) {
+                        throw new DataFetcherError('Revision page not found', 404);
+                    }
+
+                    return revisionPage;
+                })
+            );
         },
 
         getReusableContent(params) {
-            return wrapDataFetcherError(async () => {
-                const reusableContent = await getReusableContent(
-                    params.spaceId,
-                    params.revisionId,
-                    params.reusableContentId
-                );
-
-                if (!reusableContent) {
-                    throw new DataFetcherError('Reusable content not found', 404);
-                }
-
-                return reusableContent;
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(async () => {
+                    const reusableContent = await getReusableContent(
+                        params.spaceId,
+                        params.revisionId,
+                        params.reusableContentId
+                    );
+
+                    if (!reusableContent) {
+                        throw new DataFetcherError('Reusable content not found', 404);
+                    }
+
+                    return reusableContent;
+                })
+            );
         },
 
         getLatestOpenAPISpecVersionContent(params) {
-            return wrapDataFetcherError(async () => {
-                const openAPISpecVersionContent = await getLatestOpenAPISpecVersionContent(
-                    params.organizationId,
-                    params.slug
-                );
-
-                if (!openAPISpecVersionContent) {
-                    throw new DataFetcherError('OpenAPI spec version content not found', 404);
-                }
-
-                return openAPISpecVersionContent;
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(async () => {
+                    const openAPISpecVersionContent = await getLatestOpenAPISpecVersionContent(
+                        params.organizationId,
+                        params.slug
+                    );
+
+                    if (!openAPISpecVersionContent) {
+                        throw new DataFetcherError('OpenAPI spec version content not found', 404);
+                    }
+
+                    return openAPISpecVersionContent;
+                })
+            );
         },
 
         getSiteRedirectBySource(params) {
-            return wrapDataFetcherError(async () => {
-                const siteRedirect = await getSiteRedirectBySource(params);
-                if (!siteRedirect) {
-                    throw new DataFetcherError('Site redirect not found', 404);
-                }
-
-                return siteRedirect;
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(async () => {
+                    const siteRedirect = await getSiteRedirectBySource(params);
+                    if (!siteRedirect) {
+                        throw new DataFetcherError('Site redirect not found', 404);
+                    }
+
+                    return siteRedirect;
+                })
+            );
         },
         getEmbedByUrl(params) {
-            return wrapDataFetcherError(() => {
-                return getEmbedByUrlInSpace(params.spaceId, params.url);
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(() => {
+                    return getEmbedByUrlInSpace(params.spaceId, params.url);
+                })
+            );
         },
 
         searchSiteContent(params) {
-            return wrapDataFetcherError(async () => {
-                const { organizationId, siteId, query, cacheBust, scope } = params;
-                const result = await searchSiteContent(
-                    organizationId,
-                    siteId,
-                    query,
-                    scope,
-                    cacheBust
-                );
-                return result.items;
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(async () => {
+                    const { organizationId, siteId, query, cacheBust, scope } = params;
+                    const result = await searchSiteContent(
+                        organizationId,
+                        siteId,
+                        query,
+                        scope,
+                        cacheBust
+                    );
+                    return result.items;
+                })
+            );
         },
 
         renderIntegrationUi(params) {
-            return wrapDataFetcherError(async () => {
-                const result = await renderIntegrationUi(params.integrationName, params.request);
-                return result;
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(async () => {
+                    const result = await renderIntegrationUi(
+                        params.integrationName,
+                        params.request
+                    );
+                    return result;
+                })
+            );
         },
 
         streamAIResponse() {

From 8339e91e2bc532b87eecba3509d35a53b9fae36d Mon Sep 17 00:00:00 2001
From: Steven H <steven@gitbook.io>
Date: Fri, 25 Apr 2025 15:28:09 +0100
Subject: [PATCH 009/127] Fix images and other content refs in reusable content
 across spaces. (#3190)

---
 .changeset/big-clocks-rush.md                 |  5 +++++
 .../DocumentView/ReusableContent.tsx          | 20 +++++++++++++++++--
 packages/gitbook/src/lib/references.tsx       | 10 +++++-----
 3 files changed, 28 insertions(+), 7 deletions(-)
 create mode 100644 .changeset/big-clocks-rush.md

diff --git a/.changeset/big-clocks-rush.md b/.changeset/big-clocks-rush.md
new file mode 100644
index 0000000000..665953599f
--- /dev/null
+++ b/.changeset/big-clocks-rush.md
@@ -0,0 +1,5 @@
+---
+"gitbook": minor
+---
+
+Fix images in reusable content across spaces.
diff --git a/packages/gitbook/src/components/DocumentView/ReusableContent.tsx b/packages/gitbook/src/components/DocumentView/ReusableContent.tsx
index 7e2d28d8d2..bbf2d23f70 100644
--- a/packages/gitbook/src/components/DocumentView/ReusableContent.tsx
+++ b/packages/gitbook/src/components/DocumentView/ReusableContent.tsx
@@ -2,6 +2,7 @@ import type { DocumentBlockReusableContent } from '@gitbook/api';
 
 import { resolveContentRef } from '@/lib/references';
 
+import type { GitBookSpaceContext } from '@v2/lib/context';
 import { getDataOrNull } from '@v2/lib/data';
 import type { BlockProps } from './Block';
 import { UnwrappedBlocks } from './Blocks';
@@ -33,7 +34,7 @@ export async function ReusableContent(props: BlockProps<DocumentBlockReusableCon
 
     const document = await getDataOrNull(
         dataFetcher.getDocument({
-            spaceId: resolved.reusableContent.space,
+            spaceId: resolved.reusableContent.space.id,
             documentId: reusableContent.document,
         })
     );
@@ -42,12 +43,27 @@ export async function ReusableContent(props: BlockProps<DocumentBlockReusableCon
         return null;
     }
 
+    // Create a new context for reusable content block, including
+    // the data fetcher with the token from the block meta and the correct
+    // space and revision pointers.
+    const reusableContentContext: GitBookSpaceContext = {
+        ...context.contentContext,
+        dataFetcher,
+        space: resolved.reusableContent.space,
+        revisionId: resolved.reusableContent.revision,
+        pages: [],
+        shareKey: undefined,
+    };
+
     return (
         <UnwrappedBlocks
             nodes={document.nodes}
             document={document}
             ancestorBlocks={[...ancestorBlocks, block]}
-            context={context}
+            context={{
+                ...context,
+                contentContext: reusableContentContext,
+            }}
         />
     );
 }
diff --git a/packages/gitbook/src/lib/references.tsx b/packages/gitbook/src/lib/references.tsx
index 2ef8c48a6a..7248ab2ded 100644
--- a/packages/gitbook/src/lib/references.tsx
+++ b/packages/gitbook/src/lib/references.tsx
@@ -44,7 +44,7 @@ export interface ResolvedContentRef {
     /** Resolved reusable content, if the ref points to reusable content on a revision. Also contains the space and revision used for resolution. */
     reusableContent?: {
         revisionReusableContent: RevisionReusableContent;
-        space: string;
+        space: Space;
         revision: string;
     };
     /** Resolve OpenAPI spec filesystem. */
@@ -236,10 +236,10 @@ export async function resolveContentRef(
 
         case 'reusable-content': {
             // Figure out which space and revision the reusable content is in.
-            const container: { space: string; revision: string } | null = await (async () => {
+            const container: { space: Space; revision: string } | null = await (async () => {
                 // without a space on the content ref, or if the space is the same as the current one, we can use the current revision.
                 if (!contentRef.space || contentRef.space === context.space.id) {
-                    return { space: context.space.id, revision: revisionId };
+                    return { space: context.space, revision: revisionId };
                 }
 
                 const space = await getDataOrNull(
@@ -253,7 +253,7 @@ export async function resolveContentRef(
                     return null;
                 }
 
-                return { space: space.id, revision: space.revision };
+                return { space, revision: space.revision };
             })();
 
             if (!container) {
@@ -262,7 +262,7 @@ export async function resolveContentRef(
 
             const reusableContent = await getDataOrNull(
                 dataFetcher.getReusableContent({
-                    spaceId: container.space,
+                    spaceId: container.space.id,
                     revisionId: container.revision,
                     reusableContentId: contentRef.reusableContent,
                 })

From 81b4a4db535de78a70a944878b039ac0b4d4fb38 Mon Sep 17 00:00:00 2001
From: Johan Preynat <johan.preynat@gmail.com>
Date: Mon, 28 Apr 2025 12:07:34 +0200
Subject: [PATCH 010/127] Allow /~gitbook/image and /~gitbook/icon despite
 querystring parameters for SEO in robots.txt (#3191)

---
 .vscode/settings.json                 |  1 +
 packages/gitbook/src/routes/robots.ts | 29 +++++++++++++++------------
 2 files changed, 17 insertions(+), 13 deletions(-)

diff --git a/.vscode/settings.json b/.vscode/settings.json
index b9d15df191..8a86e5d5d0 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -9,6 +9,7 @@
     ],
     "tailwindCSS.classAttributes": ["class", "className", "style", ".*Style"],
     "prettier.enable": false,
+    "editor.formatOnSave": true,
     "editor.defaultFormatter": "biomejs.biome",
     "editor.codeActionsOnSave": {
         "source.organizeImports.biome": "explicit",
diff --git a/packages/gitbook/src/routes/robots.ts b/packages/gitbook/src/routes/robots.ts
index 297fc46771..1e1c0a2873 100644
--- a/packages/gitbook/src/routes/robots.ts
+++ b/packages/gitbook/src/routes/robots.ts
@@ -8,20 +8,23 @@ export async function serveRobotsTxt(context: GitBookSiteContext) {
     const { linker } = context;
 
     const isRoot = checkIsRootSiteContext(context);
-    const lines = [
-        'User-agent: *',
-        // Disallow dynamic routes / search queries
-        'Disallow: /*?',
-        ...((await isSiteIndexable(context))
-            ? [
-                  'Allow: /',
-                  `Sitemap: ${linker.toAbsoluteURL(linker.toPathInSpace(isRoot ? '/sitemap.xml' : '/sitemap-pages.xml'))}`,
-              ]
-            : ['Disallow: /']),
-    ];
-    const content = lines.join('\n');
+    const isIndexable = await isSiteIndexable(context);
 
-    return new Response(content, {
+    const lines = isIndexable
+        ? [
+              'User-agent: *',
+              // Allow image resizing and icon generation routes for favicons and search results
+              'Allow: /~gitbook/image?*',
+              'Allow: /~gitbook/icon?*',
+              // Disallow other dynamic routes / search queries
+              'Disallow: /*?',
+              'Allow: /',
+              `Sitemap: ${linker.toAbsoluteURL(linker.toPathInSpace(isRoot ? '/sitemap.xml' : '/sitemap-pages.xml'))}`,
+          ]
+        : ['User-agent: *', 'Disallow: /'];
+
+    const robotsTxt = lines.join('\n');
+    return new Response(robotsTxt, {
         headers: {
             'Content-Type': 'text/plain',
         },

From 580101d04e0e513996e3de61f1c7a5b0882e9fd8 Mon Sep 17 00:00:00 2001
From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com>
Date: Tue, 29 Apr 2025 16:16:58 +0200
Subject: [PATCH 011/127] Schemas disclosure label causing client error (#3192)

---
 .changeset/sharp-falcons-punch.md             |  6 ++++
 .../components/DocumentView/OpenAPI/style.css |  7 +++-
 packages/react-openapi/src/OpenAPISchema.tsx  | 28 ++++-----------
 .../react-openapi/src/getDisclosureLabel.ts   | 25 ++++++++++++++
 .../src/schemas/OpenAPISchemaItem.tsx         | 34 +++++++++++++++++++
 .../src/schemas/OpenAPISchemas.tsx            | 25 +++++---------
 6 files changed, 85 insertions(+), 40 deletions(-)
 create mode 100644 .changeset/sharp-falcons-punch.md
 create mode 100644 packages/react-openapi/src/getDisclosureLabel.ts
 create mode 100644 packages/react-openapi/src/schemas/OpenAPISchemaItem.tsx

diff --git a/.changeset/sharp-falcons-punch.md b/.changeset/sharp-falcons-punch.md
new file mode 100644
index 0000000000..fbcb613276
--- /dev/null
+++ b/.changeset/sharp-falcons-punch.md
@@ -0,0 +1,6 @@
+---
+'@gitbook/react-openapi': patch
+'gitbook': patch
+---
+
+Fix schemas disclosure label causing client error
diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
index 7d08d97f59..e615f7bb1a 100644
--- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
+++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
@@ -755,11 +755,16 @@ body:has(.openapi-select-popover) {
 
 .openapi-disclosure:not(
         .openapi-disclosure-group .openapi-disclosure,
-        .openapi-schema-alternatives .openapi-disclosure
+        .openapi-schema-alternatives .openapi-disclosure,
+        .openapi-schemas-disclosure .openapi-schema.openapi-disclosure
     ) {
     @apply rounded-xl;
 }
 
+.openapi-disclosure .openapi-schemas-disclosure .openapi-schema.openapi-disclosure {
+    @apply !rounded-none;
+}
+
 .openapi-disclosure:has(> .openapi-disclosure-trigger:hover) {
     @apply bg-tint-subtle;
 }
diff --git a/packages/react-openapi/src/OpenAPISchema.tsx b/packages/react-openapi/src/OpenAPISchema.tsx
index 8f4c0cd33c..007c335cf8 100644
--- a/packages/react-openapi/src/OpenAPISchema.tsx
+++ b/packages/react-openapi/src/OpenAPISchema.tsx
@@ -13,6 +13,7 @@ import { OpenAPIDisclosure } from './OpenAPIDisclosure';
 import { OpenAPISchemaName } from './OpenAPISchemaName';
 import type { OpenAPIClientContext } from './context';
 import { retrocycle } from './decycle';
+import { getDisclosureLabel } from './getDisclosureLabel';
 import { stringifyOpenAPI } from './stringifyOpenAPI';
 import { tString } from './translate';
 import { checkIsReference, resolveDescription, resolveFirstExample } from './utils';
@@ -606,6 +607,11 @@ function getSchemaTitle(schema: OpenAPIV3.SchemaObject): string {
         if (schema.format) {
             type += ` · ${schema.format}`;
         }
+
+        // Only add the title if it's an object (no need for the title of a string, number, etc.)
+        if (type === 'object' && schema.title) {
+            type += ` · ${schema.title.replaceAll(' ', '')}`;
+        }
     }
 
     if ('anyOf' in schema) {
@@ -620,25 +626,3 @@ function getSchemaTitle(schema: OpenAPIV3.SchemaObject): string {
 
     return type;
 }
-
-function getDisclosureLabel(props: {
-    schema: OpenAPIV3.SchemaObject;
-    isExpanded: boolean;
-    context: OpenAPIClientContext;
-}) {
-    const { schema, isExpanded, context } = props;
-    let label: string;
-    if (schema.type === 'array' && !!schema.items) {
-        if (schema.items.oneOf) {
-            label = tString(context.translation, 'available_items').toLowerCase();
-        } else if (schema.items.enum || schema.items.type === 'object') {
-            label = tString(context.translation, 'properties').toLowerCase();
-        } else {
-            label = schema.items.title ?? schema.title ?? getSchemaTitle(schema.items);
-        }
-    } else {
-        label = schema.title || tString(context.translation, 'properties').toLowerCase();
-    }
-
-    return tString(context.translation, isExpanded ? 'hide' : 'show', label);
-}
diff --git a/packages/react-openapi/src/getDisclosureLabel.ts b/packages/react-openapi/src/getDisclosureLabel.ts
new file mode 100644
index 0000000000..814113b910
--- /dev/null
+++ b/packages/react-openapi/src/getDisclosureLabel.ts
@@ -0,0 +1,25 @@
+'use client';
+
+import type { OpenAPIV3 } from '@gitbook/openapi-parser';
+import type { OpenAPIClientContext } from './context';
+import { tString } from './translate';
+
+export function getDisclosureLabel(props: {
+    schema: OpenAPIV3.SchemaObject;
+    isExpanded: boolean;
+    context: OpenAPIClientContext;
+}) {
+    const { schema, isExpanded, context } = props;
+    let label: string;
+    if (schema.type === 'array' && !!schema.items) {
+        if (schema.items.oneOf) {
+            label = tString(context.translation, 'available_items').toLowerCase();
+        } else {
+            label = tString(context.translation, 'properties').toLowerCase();
+        }
+    } else {
+        label = tString(context.translation, 'properties').toLowerCase();
+    }
+
+    return tString(context.translation, isExpanded ? 'hide' : 'show', label);
+}
diff --git a/packages/react-openapi/src/schemas/OpenAPISchemaItem.tsx b/packages/react-openapi/src/schemas/OpenAPISchemaItem.tsx
new file mode 100644
index 0000000000..86b7d49e8f
--- /dev/null
+++ b/packages/react-openapi/src/schemas/OpenAPISchemaItem.tsx
@@ -0,0 +1,34 @@
+'use client';
+
+import { SectionBody } from '../StaticSection';
+
+import type { OpenAPIV3 } from '@gitbook/openapi-parser';
+import { OpenAPIDisclosure } from '../OpenAPIDisclosure';
+import { OpenAPIRootSchema } from '../OpenAPISchemaServer';
+import { Section } from '../StaticSection';
+import type { OpenAPIClientContext } from '../context';
+import { getDisclosureLabel } from '../getDisclosureLabel';
+
+export function OpenAPISchemaItem(props: {
+    name: string;
+    schema: OpenAPIV3.SchemaObject;
+    context: OpenAPIClientContext;
+}) {
+    const { schema, context, name } = props;
+
+    return (
+        <OpenAPIDisclosure
+            className="openapi-schemas-disclosure"
+            key={name}
+            icon={context.icons.plus}
+            header={name}
+            label={(isExpanded) => getDisclosureLabel({ schema, isExpanded, context })}
+        >
+            <Section className="openapi-section-schemas">
+                <SectionBody>
+                    <OpenAPIRootSchema schema={schema} context={context} />
+                </SectionBody>
+            </Section>
+        </OpenAPIDisclosure>
+    );
+}
diff --git a/packages/react-openapi/src/schemas/OpenAPISchemas.tsx b/packages/react-openapi/src/schemas/OpenAPISchemas.tsx
index eddb893def..c0700a530a 100644
--- a/packages/react-openapi/src/schemas/OpenAPISchemas.tsx
+++ b/packages/react-openapi/src/schemas/OpenAPISchemas.tsx
@@ -1,16 +1,16 @@
 import type { OpenAPISchema } from '@gitbook/openapi-parser';
 import clsx from 'clsx';
-import { OpenAPIDisclosure } from '../OpenAPIDisclosure';
 import { OpenAPIExample } from '../OpenAPIExample';
 import { OpenAPIRootSchema } from '../OpenAPISchemaServer';
-import { Section, SectionBody, StaticSection } from '../StaticSection';
+import { StaticSection } from '../StaticSection';
 import {
     type OpenAPIContextInput,
     getOpenAPIClientContext,
     resolveOpenAPIContext,
 } from '../context';
-import { t, tString } from '../translate';
+import { t } from '../translate';
 import { getExampleFromSchema } from '../util/example';
+import { OpenAPISchemaItem } from './OpenAPISchemaItem';
 
 /**
  * OpenAPI Schemas component.
@@ -85,21 +85,12 @@ export function OpenAPISchemas(props: {
         <div className={clsx('openapi-schemas', className)}>
             {schemas.map(({ name, schema }) => {
                 return (
-                    <OpenAPIDisclosure
-                        className="openapi-schemas-disclosure"
+                    <OpenAPISchemaItem
                         key={name}
-                        icon={context.icons.chevronRight}
-                        header={name}
-                        label={(isExpanded) =>
-                            tString(context.translation, isExpanded ? 'hide' : 'show')
-                        }
-                    >
-                        <Section className="openapi-section-schemas">
-                            <SectionBody>
-                                <OpenAPIRootSchema schema={schema} context={clientContext} />
-                            </SectionBody>
-                        </Section>
-                    </OpenAPIDisclosure>
+                        name={name}
+                        context={clientContext}
+                        schema={schema}
+                    />
                 );
             })}
         </div>

From ae5f1abc58beb20e659e3855b1c128565b0525dc Mon Sep 17 00:00:00 2001
From: Zeno Kapitein <zeno@gitbook.io>
Date: Tue, 29 Apr 2025 18:30:47 +0200
Subject: [PATCH 012/127] Change `Dropdown`s to use Radix's `DropdownMenu`
 (#3189)

---
 .changeset/twelve-beers-confess.md            |   5 +
 bun.lock                                      | 113 ++++++++++-
 packages/gitbook/e2e/internal.spec.ts         |  45 +++--
 packages/gitbook/package.json                 |   5 +-
 .../src/components/Header/Dropdown.tsx        | 143 --------------
 .../src/components/Header/DropdownMenu.tsx    | 176 ++++++++++++++++++
 .../src/components/Header/HeaderLink.tsx      |  40 ++--
 .../src/components/Header/HeaderLinkMore.tsx  |  67 +++----
 .../src/components/Header/SpacesDropdown.tsx  |  56 +++---
 .../Header/SpacesDropdownMenuItem.tsx         |   2 +-
 10 files changed, 398 insertions(+), 254 deletions(-)
 create mode 100644 .changeset/twelve-beers-confess.md
 delete mode 100644 packages/gitbook/src/components/Header/Dropdown.tsx
 create mode 100644 packages/gitbook/src/components/Header/DropdownMenu.tsx

diff --git a/.changeset/twelve-beers-confess.md b/.changeset/twelve-beers-confess.md
new file mode 100644
index 0000000000..1c70f6880b
--- /dev/null
+++ b/.changeset/twelve-beers-confess.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Change `Dropdown`s to use Radix's `DropdownMenu`
diff --git a/bun.lock b/bun.lock
index ff44480802..d720c44b84 100644
--- a/bun.lock
+++ b/bun.lock
@@ -62,6 +62,7 @@
         "@gitbook/react-math": "workspace:*",
         "@gitbook/react-openapi": "workspace:*",
         "@radix-ui/react-checkbox": "^1.0.4",
+        "@radix-ui/react-dropdown-menu": "^2.1.12",
         "@radix-ui/react-navigation-menu": "^1.2.3",
         "@radix-ui/react-popover": "^1.0.7",
         "@radix-ui/react-tooltip": "^1.1.8",
@@ -818,11 +819,15 @@
 
     "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.4", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XDUI0IVYVSwjMXxM6P4Dfti7AH+Y4oS/TB+sglZ/EXc7cqLwGAmp1NlMrcUjj7ks6R5WTZuWKv44FBbLpwU3sA=="],
 
+    "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.12", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-VJoMs+BWWE7YhzEQyVwvF9n22Eiyr83HotCVrMQzla/OwRovXCgah7AcaEr4hMNj4gJxSdtIbcHGvmJXOoJVHA=="],
+
     "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg=="],
 
     "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA=="],
 
-    "@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="],
+    "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
+
+    "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.4", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.7", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.4", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.4", "@radix-ui/react-portal": "1.1.6", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-roving-focus": "1.1.7", "@radix-ui/react-slot": "1.2.0", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+qYq6LfbiGo97Zz9fioX83HCiIYYFNs8zAsVCMQrIakoNYylIzWuoD/anAD3UzvvR6cnswmfRFJFq/zYYq/k7Q=="],
 
     "@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.4", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-dismissable-layer": "1.1.4", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wUi01RrTDTOoGtjEPHsxlzPtVzVc3R/AZ5wfh0dyqMAqolhHAHvG5iQjBCTi2AjQqa77FWWbA3kE3RkD+bDMgQ=="],
 
@@ -836,6 +841,8 @@
 
     "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.0", "", { "dependencies": { "@radix-ui/react-slot": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw=="],
 
+    "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.4", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-C6oAg451/fQT3EGbWHbCQjYTtbyjNO1uzQgMzwyivcHT3GKNEmu1q3UuREhN+HzHAVtv3ivMVK08QlC+PkYw9Q=="],
+
     "@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw=="],
 
     "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.1.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA=="],
@@ -844,6 +851,8 @@
 
     "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw=="],
 
+    "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
+
     "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw=="],
 
     "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w=="],
@@ -3654,24 +3663,88 @@
 
     "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.1", "", { "dependencies": { "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg=="],
 
+    "@radix-ui/react-dropdown-menu/@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="],
+
+    "@radix-ui/react-dropdown-menu/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
+
+    "@radix-ui/react-dropdown-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+    "@radix-ui/react-dropdown-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.0", "", { "dependencies": { "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw=="],
+
+    "@radix-ui/react-dropdown-menu/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
+
+    "@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
+
+    "@radix-ui/react-menu/@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-j5+WBUdhccJsmH5/H0K6RncjDtoALSEr6jbkaZu+bjw6hOPOhHycr6vEUujl+HBK8kjUfWcoCJXxP6e4lUlMZw=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-r2annK27lIW5w9Ho5NyQgqs0MmgZSTIKXWpVCJaLC1q2kZrZkcqnmHkCHMEmv8XLvsLlurKMPT+kbKkRkm/xVA=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.4", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.4", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3p2Rgm/a1cK0r/UVkx5F/K9v/EplfjAeIFCGOPYPO4lZ0jtg4iSQXt/YGTSLWaf4x7NG6Z4+uKFcylcTZjeqDA=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.6", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XmsIl2z1n/TsYFLIdYam2rmFwf9OC/Sh2avkbmVMDuBZIe7hSpM0cYnWPAo7nHOVx8zTuwDZGByfcqLdnzp3Vw=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.0", "", { "dependencies": { "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
+
+    "@radix-ui/react-menu/react-remove-scroll": ["react-remove-scroll@2.6.3", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ=="],
+
     "@radix-ui/react-navigation-menu/@radix-ui/primitive": ["@radix-ui/primitive@1.1.1", "", {}, "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="],
 
     "@radix-ui/react-navigation-menu/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="],
 
+    "@radix-ui/react-navigation-menu/@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="],
+
     "@radix-ui/react-navigation-menu/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg=="],
 
     "@radix-ui/react-navigation-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.1", "", { "dependencies": { "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg=="],
 
     "@radix-ui/react-popover/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.1", "", { "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ=="],
 
+    "@radix-ui/react-popover/@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="],
+
     "@radix-ui/react-popper/@radix-ui/react-context": ["@radix-ui/react-context@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A=="],
 
+    "@radix-ui/react-roving-focus/@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="],
+
+    "@radix-ui/react-roving-focus/@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg=="],
+
+    "@radix-ui/react-roving-focus/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
+
+    "@radix-ui/react-roving-focus/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+    "@radix-ui/react-roving-focus/@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
+
+    "@radix-ui/react-roving-focus/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.0", "", { "dependencies": { "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw=="],
+
+    "@radix-ui/react-roving-focus/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
+
+    "@radix-ui/react-roving-focus/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
+
     "@radix-ui/react-tooltip/@radix-ui/primitive": ["@radix-ui/primitive@1.1.1", "", {}, "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="],
 
     "@radix-ui/react-tooltip/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="],
 
     "@radix-ui/react-tooltip/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg=="],
 
+    "@radix-ui/react-tooltip/@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="],
+
     "@radix-ui/react-tooltip/@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.2", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-rect": "1.1.0", "@radix-ui/react-use-size": "1.1.0", "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA=="],
 
     "@radix-ui/react-tooltip/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA=="],
@@ -3684,6 +3757,8 @@
 
     "@radix-ui/react-tooltip/@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q=="],
 
+    "@radix-ui/react-use-effect-event/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
+
     "@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.1", "", { "dependencies": { "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg=="],
 
     "@react-aria/focus/clsx": ["clsx@2.0.0", "", {}, "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q=="],
@@ -4692,8 +4767,44 @@
 
     "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="],
 
+    "@radix-ui/react-dropdown-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="],
+
+    "@radix-ui/react-dropdown-menu/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-dismissable-layer/@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-qz+fxrqgNxG0dYew5l7qR3c7wdgRu1XVUHGnGYX7rg5HM4p9SWaRmJwfgR3J0SgyUKayLmzQIun+N6rWRgiRKw=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-popper/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-popper/@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-popper/@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-popper/@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-portal/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-presence/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
+
+    "@radix-ui/react-menu/react-remove-scroll/react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
+
+    "@radix-ui/react-menu/react-remove-scroll/react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
+
+    "@radix-ui/react-menu/react-remove-scroll/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+    "@radix-ui/react-menu/react-remove-scroll/use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
+
+    "@radix-ui/react-menu/react-remove-scroll/use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
+
     "@radix-ui/react-navigation-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="],
 
+    "@radix-ui/react-roving-focus/@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="],
+
+    "@radix-ui/react-roving-focus/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="],
+
+    "@radix-ui/react-roving-focus/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
+
     "@radix-ui/react-tooltip/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg=="],
 
     "@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="],
diff --git a/packages/gitbook/e2e/internal.spec.ts b/packages/gitbook/e2e/internal.spec.ts
index e7875cd4e8..d5e49b705f 100644
--- a/packages/gitbook/e2e/internal.spec.ts
+++ b/packages/gitbook/e2e/internal.spec.ts
@@ -111,24 +111,24 @@ const testCases: TestsCase[] = [
                 name: 'Customized variant titles are displayed',
                 url: '',
                 run: async (page) => {
-                    const spaceDrowpdown = page
+                    const spaceDropdown = page
                         .locator('[data-testid="space-dropdown-button"]')
                         .locator('visible=true');
-                    await spaceDrowpdown.click();
+                    await spaceDropdown.click();
 
                     const variantSelectionDropdown = page.locator(
-                        'css=[data-testid="space-dropdown-button"] + div'
+                        'css=[data-testid="dropdown-menu"]'
                     );
                     // the customized space title
                     await expect(
-                        variantSelectionDropdown.getByRole('link', {
+                        variantSelectionDropdown.getByRole('menuitem', {
                             name: 'Multi-Variants',
                         })
                     ).toBeVisible();
 
                     // the NON-customized space title
                     await expect(
-                        variantSelectionDropdown.getByRole('link', {
+                        variantSelectionDropdown.getByRole('menuitem', {
                             name: 'RFCs',
                         })
                     ).toBeVisible();
@@ -145,14 +145,17 @@ const testCases: TestsCase[] = [
                 url: 'api-multi-versions/reference/api-reference/pets',
                 screenshot: false,
                 run: async (page) => {
-                    const spaceDrowpdown = await page
+                    const spaceDropdown = await page
                         .locator('[data-testid="space-dropdown-button"]')
                         .locator('visible=true');
-                    await spaceDrowpdown.click();
+                    await spaceDropdown.click();
 
+                    const variantSelectionDropdown = page.locator(
+                        'css=[data-testid="dropdown-menu"]'
+                    );
                     // Click the second variant in the dropdown
-                    await page
-                        .getByRole('link', {
+                    await variantSelectionDropdown
+                        .getByRole('menuitem', {
                             name: '2.0',
                         })
                         .click();
@@ -168,14 +171,18 @@ const testCases: TestsCase[] = [
                 url: 'api-multi-versions-share-links/8tNo6MeXg7CkFMzSSz81/reference/api-reference/pets',
                 screenshot: false,
                 run: async (page) => {
-                    const spaceDrowpdown = await page
+                    const spaceDropdown = await page
                         .locator('[data-testid="space-dropdown-button"]')
                         .locator('visible=true');
-                    await spaceDrowpdown.click();
+                    await spaceDropdown.click();
+
+                    const variantSelectionDropdown = page.locator(
+                        'css=[data-testid="dropdown-menu"]'
+                    );
 
                     // Click the second variant in the dropdown
-                    await page
-                        .getByRole('link', {
+                    await variantSelectionDropdown
+                        .getByRole('menuitem', {
                             name: '2.0',
                         })
                         .click();
@@ -205,14 +212,18 @@ const testCases: TestsCase[] = [
                     return `api-multi-versions-va/reference/api-reference/pets?jwt_token=${token}`;
                 },
                 run: async (page) => {
-                    const spaceDrowpdown = await page
+                    const spaceDropdown = await page
                         .locator('[data-testid="space-dropdown-button"]')
                         .locator('visible=true');
-                    await spaceDrowpdown.click();
+                    await spaceDropdown.click();
+
+                    const variantSelectionDropdown = page.locator(
+                        'css=[data-testid="dropdown-menu"]'
+                    );
 
                     // Click the second variant in the dropdown
-                    await page
-                        .getByRole('link', {
+                    await variantSelectionDropdown
+                        .getByRole('menuitem', {
                             name: '2.0',
                         })
                         .click();
diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json
index 6f155974f7..5d0939b249 100644
--- a/packages/gitbook/package.json
+++ b/packages/gitbook/package.json
@@ -27,6 +27,7 @@
         "@gitbook/react-math": "workspace:*",
         "@gitbook/react-openapi": "workspace:*",
         "@radix-ui/react-checkbox": "^1.0.4",
+        "@radix-ui/react-dropdown-menu": "^2.1.12",
         "@radix-ui/react-navigation-menu": "^1.2.3",
         "@radix-ui/react-popover": "^1.0.7",
         "@radix-ui/react-tooltip": "^1.1.8",
@@ -37,6 +38,7 @@
         "assert-never": "^1.2.1",
         "bun-types": "^1.1.20",
         "classnames": "^2.5.1",
+        "event-iterator": "^2.0.0",
         "framer-motion": "^10.16.14",
         "js-cookie": "^3.0.5",
         "jsontoxml": "^1.0.1",
@@ -52,6 +54,7 @@
         "openapi-types": "^12.1.3",
         "p-map": "^7.0.0",
         "parse-cache-control": "^1.0.1",
+        "partial-json": "^0.1.7",
         "react": "^19.0.0",
         "react-dom": "^19.0.0",
         "react-hotkeys-hook": "^4.4.1",
@@ -70,8 +73,6 @@
         "usehooks-ts": "^3.1.0",
         "zod": "^3.24.2",
         "zod-to-json-schema": "^3.24.5",
-        "event-iterator": "^2.0.0",
-        "partial-json": "^0.1.7",
         "zustand": "^5.0.3"
     },
     "devDependencies": {
diff --git a/packages/gitbook/src/components/Header/Dropdown.tsx b/packages/gitbook/src/components/Header/Dropdown.tsx
deleted file mode 100644
index 21b626ac2c..0000000000
--- a/packages/gitbook/src/components/Header/Dropdown.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-import { Icon } from '@gitbook/icons';
-import { type DetailedHTMLProps, type HTMLAttributes, useId } from 'react';
-
-import { type ClassValue, tcls } from '@/lib/tailwind';
-
-import { Link, type LinkInsightsProps } from '../primitives';
-
-export type DropdownButtonProps<E extends HTMLElement = HTMLElement> = Omit<
-    Partial<DetailedHTMLProps<HTMLAttributes<E>, E>>,
-    'ref'
->;
-
-/**
- * Button with a dropdown.
- */
-export function Dropdown<E extends HTMLElement>(props: {
-    /** Content of the button */
-    button: (buttonProps: DropdownButtonProps<E>) => React.ReactNode;
-    /** Content of the dropdown */
-    children: React.ReactNode;
-    /** Custom styles */
-    className?: ClassValue;
-}) {
-    const { button, children, className } = props;
-    const dropdownId = useId();
-
-    return (
-        <div className={tcls('group/dropdown', 'relative flex min-w-0 shrink')}>
-            {button({
-                id: dropdownId,
-                tabIndex: 0,
-                'aria-expanded': true,
-                'aria-haspopup': true,
-            })}
-            <div
-                tabIndex={-1}
-                role="menu"
-                aria-orientation="vertical"
-                aria-labelledby={dropdownId}
-                className={tcls(
-                    'w-52',
-                    'max-h-80',
-                    'flex',
-                    'absolute',
-                    'top-full',
-                    'left-0',
-                    'origin-top-left',
-                    'invisible',
-                    'transition-opacity',
-                    'duration-1000',
-                    'group-hover/dropdown:visible',
-                    'group-focus-within/dropdown:visible',
-                    className
-                )}
-            >
-                <div className="fixed z-50 w-52">
-                    <div
-                        className={tcls(
-                            'mt-2',
-                            'w-full',
-                            'max-h-80',
-                            'bg-tint-base',
-                            'rounded-lg',
-                            'straight-corners:rounded-sm',
-                            'p-2',
-                            'shadow-1xs',
-                            'overflow-auto',
-                            'ring-1',
-                            'ring-tint-subtle',
-                            'focus:outline-none'
-                        )}
-                    >
-                        {children}
-                    </div>
-                </div>
-            </div>
-        </div>
-    );
-}
-
-/**
- * Animated chevron to display in the dropdown button.
- */
-export function DropdownChevron() {
-    return (
-        <Icon
-            icon="chevron-down"
-            className={tcls(
-                'shrink-0',
-                'opacity-6',
-                'size-3',
-                'ms-1',
-                'transition-all',
-                'group-hover/dropdown:opacity-11',
-                'group-focus-within/dropdown:rotate-180'
-            )}
-        />
-    );
-}
-
-/**
- * Group of menu items in a dropdown.
- */
-export function DropdownMenu(props: { children: React.ReactNode }) {
-    const { children } = props;
-
-    return <div className={tcls('flex', 'flex-col', 'gap-1')}>{children}</div>;
-}
-
-/**
- * Menu item in a dropdown.
- */
-export function DropdownMenuItem(
-    props: {
-        href: string | null;
-        active?: boolean;
-        className?: ClassValue;
-        children: React.ReactNode;
-    } & LinkInsightsProps
-) {
-    const { children, active = false, href, className, insights } = props;
-
-    if (href) {
-        return (
-            <Link
-                href={href}
-                insights={insights}
-                className={tcls(
-                    'rounded straight-corners:rounded-sm px-3 py-1 text-sm',
-                    active ? 'bg-primary text-primary-strong' : null,
-                    'hover:bg-tint-hover',
-                    className
-                )}
-            >
-                {children}
-            </Link>
-        );
-    }
-
-    return (
-        <div className={tcls('px-3 py-1 font-medium text-tint text-xs', className)}>{children}</div>
-    );
-}
diff --git a/packages/gitbook/src/components/Header/DropdownMenu.tsx b/packages/gitbook/src/components/Header/DropdownMenu.tsx
new file mode 100644
index 0000000000..8f51e4685c
--- /dev/null
+++ b/packages/gitbook/src/components/Header/DropdownMenu.tsx
@@ -0,0 +1,176 @@
+'use client';
+
+import { Icon } from '@gitbook/icons';
+import type { DetailedHTMLProps, HTMLAttributes } from 'react';
+import { useState } from 'react';
+
+import { type ClassValue, tcls } from '@/lib/tailwind';
+
+import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu';
+
+import { Link, type LinkInsightsProps } from '../primitives';
+
+export type DropdownButtonProps<E extends HTMLElement = HTMLElement> = Omit<
+    Partial<DetailedHTMLProps<HTMLAttributes<E>, E>>,
+    'ref'
+>;
+
+/**
+ * Button with a dropdown.
+ */
+export function DropdownMenu(props: {
+    /** Content of the button */
+    button: React.ReactNode;
+    /** Content of the dropdown */
+    children: React.ReactNode;
+    /** Custom styles */
+    className?: ClassValue;
+    /** Open the dropdown on hover */
+    openOnHover?: boolean;
+}) {
+    const { button, children, className, openOnHover = false } = props;
+    const [hovered, setHovered] = useState(false);
+    const [clicked, setClicked] = useState(false);
+
+    return (
+        <RadixDropdownMenu.Root
+            modal={false}
+            open={openOnHover ? clicked || hovered : clicked}
+            onOpenChange={setClicked}
+        >
+            <RadixDropdownMenu.Trigger
+                asChild
+                onMouseEnter={() => setHovered(true)}
+                onMouseLeave={() => setHovered(false)}
+                onClick={() => (openOnHover ? setClicked(!clicked) : null)}
+                className="group/dropdown"
+            >
+                {button}
+            </RadixDropdownMenu.Trigger>
+
+            <RadixDropdownMenu.Portal>
+                <RadixDropdownMenu.Content
+                    data-testid="dropdown-menu"
+                    hideWhenDetached
+                    collisionPadding={8}
+                    onMouseEnter={() => setHovered(true)}
+                    onMouseLeave={() => setHovered(false)}
+                    align="start"
+                    className="z-40 animate-present pt-2"
+                >
+                    <div
+                        className={tcls(
+                            'flex max-h-80 min-w-40 max-w-[40vw] flex-col gap-1 overflow-auto rounded-lg straight-corners:rounded-sm bg-tint-base p-2 shadow-lg ring-1 ring-tint-subtle sm:min-w-52 sm:max-w-80',
+                            className
+                        )}
+                    >
+                        {children}
+                    </div>
+                </RadixDropdownMenu.Content>
+            </RadixDropdownMenu.Portal>
+        </RadixDropdownMenu.Root>
+    );
+}
+
+/**
+ * Animated chevron to display in the dropdown button.
+ */
+export function DropdownChevron() {
+    return (
+        <Icon
+            icon="chevron-down"
+            className={tcls(
+                'shrink-0',
+                'opacity-6',
+                'size-3',
+                'ms-1',
+                'transition-all',
+                'group-hover/dropdown:opacity-11',
+                'group-data-[state=open]/dropdown:opacity-11',
+                'group-data-[state=open]/dropdown:rotate-180'
+            )}
+        />
+    );
+}
+
+/**
+ * Button with a chevron for use in dropdowns.
+ */
+export function DropdownButton(props: {
+    children: React.ReactNode;
+    className?: ClassValue;
+}) {
+    const { children, className } = props;
+
+    return (
+        <div className={tcls('group/dropdown', 'flex', 'items-center', className)}>
+            {children}
+            <DropdownChevron />
+        </div>
+    );
+}
+
+/**
+ * Menu item in a dropdown.
+ */
+export function DropdownMenuItem(
+    props: {
+        href: string | null;
+        active?: boolean;
+        className?: ClassValue;
+        children: React.ReactNode;
+    } & LinkInsightsProps
+) {
+    const { children, active = false, href, className, insights } = props;
+
+    const itemClassName = tcls(
+        'rounded straight-corners:rounded-sm px-3 py-1 text-sm',
+        active
+            ? 'bg-primary text-primary-strong data-[highlighted]:bg-primary-hover'
+            : 'data-[highlighted]:bg-tint-hover',
+        'focus:outline-none',
+        className
+    );
+
+    if (href) {
+        return (
+            <RadixDropdownMenu.Item asChild>
+                <Link href={href} insights={insights} className={itemClassName}>
+                    {children}
+                </Link>
+            </RadixDropdownMenu.Item>
+        );
+    }
+
+    return (
+        <RadixDropdownMenu.Item
+            className={tcls('px-3 py-1 font-medium text-tint text-xs', className)}
+        >
+            {children}
+        </RadixDropdownMenu.Item>
+    );
+}
+
+export function DropdownSubMenu(props: { children: React.ReactNode; label: React.ReactNode }) {
+    const { children, label } = props;
+
+    return (
+        <RadixDropdownMenu.Sub>
+            <RadixDropdownMenu.SubTrigger className="flex cursor-pointer items-center justify-between rounded straight-corners:rounded-sm px-3 py-1 text-sm focus:outline-none data-[highlighted]:bg-tint-hover">
+                {label}
+                <Icon icon="chevron-right" className="size-3 shrink-0 opacity-6" />
+            </RadixDropdownMenu.SubTrigger>
+            <RadixDropdownMenu.Portal>
+                <RadixDropdownMenu.SubContent
+                    hideWhenDetached
+                    collisionPadding={8}
+                    className="z-40 animate-present"
+                >
+                    <div className="flex max-h-80 min-w-40 max-w-[40vw] flex-col gap-1 overflow-auto rounded-lg straight-corners:rounded-sm bg-tint-base p-2 shadow-lg ring-1 ring-tint-subtle sm:min-w-52 sm:max-w-80">
+                        {children}
+                    </div>
+                </RadixDropdownMenu.SubContent>
+            </RadixDropdownMenu.Portal>
+        </RadixDropdownMenu.Sub>
+    );
+}
diff --git a/packages/gitbook/src/components/Header/HeaderLink.tsx b/packages/gitbook/src/components/Header/HeaderLink.tsx
index 184537c15d..fcfb28c723 100644
--- a/packages/gitbook/src/components/Header/HeaderLink.tsx
+++ b/packages/gitbook/src/components/Header/HeaderLink.tsx
@@ -13,12 +13,11 @@ import { tcls } from '@/lib/tailwind';
 
 import { Button, Link } from '../primitives';
 import {
-    Dropdown,
     type DropdownButtonProps,
     DropdownChevron,
     DropdownMenu,
     DropdownMenuItem,
-} from './Dropdown';
+} from './DropdownMenu';
 
 export async function HeaderLink(props: {
     context: GitBookSiteContext;
@@ -33,21 +32,13 @@ export async function HeaderLink(props: {
 
     if (link.links && link.links.length > 0) {
         return (
-            <Dropdown
+            <DropdownMenu
                 className={`shrink ${customization.styling.search === 'prominent' ? 'right-0 left-auto' : null}`}
-                button={(buttonProps) => {
-                    if (!target || !link.to) {
-                        return (
-                            <HeaderItemDropdown
-                                {...buttonProps}
-                                headerPreset={headerPreset}
-                                title={link.title}
-                            />
-                        );
-                    }
-                    return (
+                button={
+                    !target || !link.to ? (
+                        <HeaderItemDropdown headerPreset={headerPreset} title={link.title} />
+                    ) : (
                         <HeaderLinkNavItem
-                            {...buttonProps}
                             linkTarget={link.to}
                             linkStyle={linkStyle}
                             headerPreset={headerPreset}
@@ -55,15 +46,14 @@ export async function HeaderLink(props: {
                             isDropdown
                             href={target?.href}
                         />
-                    );
-                }}
+                    )
+                }
+                openOnHover={true}
             >
-                <DropdownMenu>
-                    {link.links.map((subLink, index) => (
-                        <SubHeaderLink key={index} {...props} link={subLink} />
-                    ))}
-                </DropdownMenu>
-            </Dropdown>
+                {link.links.map((subLink, index) => (
+                    <SubHeaderLink key={index} {...props} link={subLink} />
+                ))}
+            </DropdownMenu>
         );
     }
 
@@ -157,10 +147,12 @@ function getHeaderLinkClassName(_props: { headerPreset: CustomizationHeaderPrese
 
         'text-tint',
         'links-default:hover:text-primary',
+        'links-default:data-[state=open]:text-primary',
         'links-default:tint:hover:text-tint-strong',
-
+        'links-default:tint:data-[state=open]:text-tint-strong',
         'underline-offset-2',
         'links-accent:hover:underline',
+        'links-accent:data-[state=open]:underline',
         'links-accent:underline-offset-4',
         'links-accent:decoration-primary-subtle',
         'links-accent:decoration-[3px]',
diff --git a/packages/gitbook/src/components/Header/HeaderLinkMore.tsx b/packages/gitbook/src/components/Header/HeaderLinkMore.tsx
index 5d27fc9a8d..7954b80ec4 100644
--- a/packages/gitbook/src/components/Header/HeaderLinkMore.tsx
+++ b/packages/gitbook/src/components/Header/HeaderLinkMore.tsx
@@ -10,7 +10,7 @@ import type React from 'react';
 import { resolveContentRef } from '@/lib/references';
 import { tcls } from '@/lib/tailwind';
 
-import { Dropdown, DropdownChevron, DropdownMenu, DropdownMenuItem } from './Dropdown';
+import { DropdownChevron, DropdownMenu, DropdownMenuItem, DropdownSubMenu } from './DropdownMenu';
 import styles from './headerLinks.module.css';
 
 /**
@@ -23,7 +23,7 @@ export function HeaderLinkMore(props: {
 }) {
     const { label, links, context } = props;
 
-    const renderButton = () => (
+    const renderButton = (
         <button
             type="button"
             className={tcls(
@@ -45,19 +45,18 @@ export function HeaderLinkMore(props: {
 
     return (
         <div className={`${styles.linkEllipsis} z-20 items-center`}>
-            <Dropdown
+            <DropdownMenu
                 button={renderButton}
+                openOnHover={true}
                 className={tcls(
                     'max-md:right-0 max-md:left-auto',
                     context.customization.styling.search === 'prominent' && 'right-0 left-auto'
                 )}
             >
-                <DropdownMenu>
-                    {links.map((link, index) => (
-                        <MoreMenuLink key={index} link={link} context={context} />
-                    ))}
-                </DropdownMenu>
-            </Dropdown>
+                {links.map((link, index) => (
+                    <MoreMenuLink key={index} link={link} context={context} />
+                ))}
+            </DropdownMenu>
         </div>
     );
 }
@@ -70,32 +69,28 @@ async function MoreMenuLink(props: {
 
     const target = link.to ? await resolveContentRef(link.to, context) : null;
 
-    return (
-        <>
-            {'links' in link && link.links.length > 0 && (
-                <hr className="-mx-2 my-1 border-tint border-t first:hidden" />
-            )}
-            <DropdownMenuItem
-                href={target?.href ?? null}
-                insights={
-                    link.to
-                        ? {
-                              type: 'link_click',
-                              link: {
-                                  target: link.to,
-                                  position: SiteInsightsLinkPosition.Header,
-                              },
-                          }
-                        : undefined
-                }
-            >
-                {link.title}
-            </DropdownMenuItem>
-            {'links' in link
-                ? link.links.map((subLink, index) => (
-                      <MoreMenuLink key={index} {...props} link={subLink} />
-                  ))
-                : null}
-        </>
+    return 'links' in link && link.links.length > 0 ? (
+        <DropdownSubMenu label={link.title}>
+            {link.links.map((subLink, index) => {
+                return <MoreMenuLink key={index} {...props} link={subLink} />;
+            })}
+        </DropdownSubMenu>
+    ) : (
+        <DropdownMenuItem
+            href={target?.href ?? null}
+            insights={
+                link.to
+                    ? {
+                          type: 'link_click',
+                          link: {
+                              target: link.to,
+                              position: SiteInsightsLinkPosition.Header,
+                          },
+                      }
+                    : undefined
+            }
+        >
+            {link.title}
+        </DropdownMenuItem>
     );
 }
diff --git a/packages/gitbook/src/components/Header/SpacesDropdown.tsx b/packages/gitbook/src/components/Header/SpacesDropdown.tsx
index 39e81e59f4..9728216b74 100644
--- a/packages/gitbook/src/components/Header/SpacesDropdown.tsx
+++ b/packages/gitbook/src/components/Header/SpacesDropdown.tsx
@@ -3,7 +3,7 @@ import type { SiteSpace } from '@gitbook/api';
 import { tcls } from '@/lib/tailwind';
 
 import type { GitBookSiteContext } from '@v2/lib/context';
-import { Dropdown, DropdownChevron, DropdownMenu } from './Dropdown';
+import { DropdownChevron, DropdownMenu } from './DropdownMenu';
 import { SpacesDropdownMenuItem } from './SpacesDropdownMenuItem';
 
 export function SpacesDropdown(props: {
@@ -16,14 +16,13 @@ export function SpacesDropdown(props: {
     const { linker } = context;
 
     return (
-        <Dropdown
+        <DropdownMenu
             className={tcls(
                 'group-hover/dropdown:invisible', // Prevent hover from opening the dropdown, as it's annoying in this context
                 'group-focus-within/dropdown:group-hover/dropdown:visible' // When the dropdown is already open, it should remain visible when hovered
             )}
-            button={(buttonProps) => (
+            button={
                 <div
-                    {...buttonProps}
                     data-testid="space-dropdown-button"
                     className={tcls(
                         'flex',
@@ -40,25 +39,24 @@ export function SpacesDropdown(props: {
                         'straight-corners:rounded-none',
 
                         'bg-tint-base',
-                        'group-hover/dropdown:bg-tint-base',
-                        'group-focus-within/dropdown:bg-tint-base',
 
                         'text-sm',
                         'text-tint',
-                        'group-hover/dropdown:text-tint-strong',
-                        'group-focus-within/dropdown:text-tint-strong',
+                        'hover:text-tint-strong',
+                        'data-[state=open]:text-tint-strong',
 
                         'ring-1',
                         'ring-tint-subtle',
-                        'group-hover/dropdown:ring-tint-hover',
-                        'group-focus-within/dropdown:ring-tint-hover',
+                        'hover:ring-tint-hover',
+                        'data-[state=open]:ring-tint-hover',
 
                         'contrast-more:bg-tint-base',
                         'contrast-more:ring-1',
-                        'contrast-more:group-hover/dropdown:ring-2',
+                        'contrast-more:hover:ring-2',
+                        'contrast-more:data-[state=open]:ring-2',
                         'contrast-more:ring-tint',
-                        'contrast-more:group-hover/dropdown:ring-tint-hover',
-                        'contrast-more:group-focus-within/dropdown:ring-tint-hover',
+                        'contrast-more:hover:ring-tint-hover',
+                        'contrast-more:data-[state=open]:ring-tint-hover',
 
                         className
                     )}
@@ -66,23 +64,21 @@ export function SpacesDropdown(props: {
                     <span className={tcls('line-clamp-1', 'grow')}>{siteSpace.title}</span>
                     <DropdownChevron />
                 </div>
-            )}
+            }
         >
-            <DropdownMenu>
-                {siteSpaces.map((otherSiteSpace, index) => (
-                    <SpacesDropdownMenuItem
-                        key={`${otherSiteSpace.id}-${index}`}
-                        variantSpace={{
-                            id: otherSiteSpace.id,
-                            title: otherSiteSpace.title,
-                            url: otherSiteSpace.urls.published
-                                ? linker.toLinkForContent(otherSiteSpace.urls.published)
-                                : otherSiteSpace.space.urls.app,
-                        }}
-                        active={otherSiteSpace.id === siteSpace.id}
-                    />
-                ))}
-            </DropdownMenu>
-        </Dropdown>
+            {siteSpaces.map((otherSiteSpace, index) => (
+                <SpacesDropdownMenuItem
+                    key={`${otherSiteSpace.id}-${index}`}
+                    variantSpace={{
+                        id: otherSiteSpace.id,
+                        title: otherSiteSpace.title,
+                        url: otherSiteSpace.urls.published
+                            ? linker.toLinkForContent(otherSiteSpace.urls.published)
+                            : otherSiteSpace.space.urls.app,
+                    }}
+                    active={otherSiteSpace.id === siteSpace.id}
+                />
+            ))}
+        </DropdownMenu>
     );
 }
diff --git a/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx b/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx
index 8c3676ff89..70859abfae 100644
--- a/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx
+++ b/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx
@@ -4,7 +4,7 @@ import type { Space } from '@gitbook/api';
 
 import { joinPath } from '@/lib/paths';
 import { useCurrentPagePath } from '../hooks';
-import { DropdownMenuItem } from './Dropdown';
+import { DropdownMenuItem } from './DropdownMenu';
 
 function useVariantSpaceHref(variantSpaceUrl: string) {
     const currentPathname = useCurrentPagePath();

From 0e201d589983e6a0d5c03d7298c869ac56d29fec Mon Sep 17 00:00:00 2001
From: Zeno Kapitein <zeno@gitbook.io>
Date: Tue, 29 Apr 2025 19:29:32 +0200
Subject: [PATCH 013/127] Add border to filled sidebar on gradient theme
 (#3185)

---
 .changeset/two-chairs-wait.md                                | 5 +++++
 .../src/components/TableOfContents/TableOfContents.tsx       | 3 ++-
 2 files changed, 7 insertions(+), 1 deletion(-)
 create mode 100644 .changeset/two-chairs-wait.md

diff --git a/.changeset/two-chairs-wait.md b/.changeset/two-chairs-wait.md
new file mode 100644
index 0000000000..6ca7941574
--- /dev/null
+++ b/.changeset/two-chairs-wait.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Add border to filled sidebar on gradient theme
diff --git a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx
index 05ee56160c..dcfb287660 100644
--- a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx
+++ b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx
@@ -66,13 +66,14 @@ export function TableOfContents(props: {
             <div // The actual sidebar, either shown with a filled bg or transparent.
                 className={tcls(
                     'lg:-ms-5',
-                    'relative flex flex-grow flex-col overflow-hidden',
+                    'relative flex flex-grow flex-col overflow-hidden border-tint-subtle',
 
                     'sidebar-filled:bg-tint-subtle',
                     'theme-muted:bg-tint-subtle',
                     '[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
                     '[html.sidebar-filled.theme-muted_&]:bg-tint-base',
                     '[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base',
+                    '[html.sidebar-filled.theme-gradient_&]:border',
                     'page-no-toc:!bg-transparent',
 
                     'sidebar-filled:rounded-xl',

From c3f6b8c003998d04ad133fd16dae90772137921e Mon Sep 17 00:00:00 2001
From: Zeno Kapitein <zeno@gitbook.io>
Date: Tue, 29 Apr 2025 19:29:42 +0200
Subject: [PATCH 014/127] Reduce chroma of text layers (#3188)

---
 .changeset/eight-vans-judge.md         |  5 +++++
 packages/colors/src/transformations.ts | 16 +++++++++++++++-
 2 files changed, 20 insertions(+), 1 deletion(-)
 create mode 100644 .changeset/eight-vans-judge.md

diff --git a/.changeset/eight-vans-judge.md b/.changeset/eight-vans-judge.md
new file mode 100644
index 0000000000..c432fde1a3
--- /dev/null
+++ b/.changeset/eight-vans-judge.md
@@ -0,0 +1,5 @@
+---
+"@gitbook/colors": patch
+---
+
+Update chroma ratio per step
diff --git a/packages/colors/src/transformations.ts b/packages/colors/src/transformations.ts
index bf7f770c09..90cb4482eb 100644
--- a/packages/colors/src/transformations.ts
+++ b/packages/colors/src/transformations.ts
@@ -220,7 +220,21 @@ export function colorScale(
             continue;
         }
 
-        const chromaRatio = index === 8 || index === 9 ? 1 : index * 0.05;
+        const chromaRatio = (() => {
+            switch (index) {
+                // Step 9 and 10 have max chroma, meaning they are fully saturated.
+                case 8:
+                case 9:
+                    return 1;
+                // Step 11 and 12 have a reduced chroma
+                case 10:
+                    return 0.4;
+                case 11:
+                    return 0.1;
+                default:
+                    return index * 0.05;
+            }
+        })();
 
         const shade = {
             L: targetL, // Blend lightness

From 89a5816ee41163fc600475345cee8aef1b24d5b9 Mon Sep 17 00:00:00 2001
From: Zeno Kapitein <zeno@gitbook.io>
Date: Tue, 29 Apr 2025 19:29:56 +0200
Subject: [PATCH 015/127] Fix OpenAPI disclosure label ("Show properties")
 misalignment on mobile (#3193)

---
 .changeset/young-poets-cry.md                                | 5 +++++
 .../gitbook/src/components/DocumentView/OpenAPI/style.css    | 2 +-
 2 files changed, 6 insertions(+), 1 deletion(-)
 create mode 100644 .changeset/young-poets-cry.md

diff --git a/.changeset/young-poets-cry.md b/.changeset/young-poets-cry.md
new file mode 100644
index 0000000000..8757b729b9
--- /dev/null
+++ b/.changeset/young-poets-cry.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Fix OpenAPI disclosure label ("Show properties") misalignment on mobile
diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
index e615f7bb1a..f0371016ec 100644
--- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
+++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
@@ -806,7 +806,7 @@ body:has(.openapi-select-popover) {
 @media (hover: none) {
     /* Make button label always visible on non-hover devices like phones */
     .openapi-disclosure-trigger-label {
-        @apply relative ring-1 bg-tint-base;
+        @apply relative ring-1 bg-tint-base right-0;
     }
     .openapi-disclosure-trigger-label span {
         @apply block;

From 2a805cc082610341cf02670374181790957ca424 Mon Sep 17 00:00:00 2001
From: Zeno Kapitein <zeno@gitbook.io>
Date: Tue, 29 Apr 2025 19:30:11 +0200
Subject: [PATCH 016/127] Change OpenAPI schema-optional from `info` to `tint`
 color (#3194)

---
 .changeset/rich-eggs-talk.md                                 | 5 +++++
 .../gitbook/src/components/DocumentView/OpenAPI/style.css    | 2 +-
 2 files changed, 6 insertions(+), 1 deletion(-)
 create mode 100644 .changeset/rich-eggs-talk.md

diff --git a/.changeset/rich-eggs-talk.md b/.changeset/rich-eggs-talk.md
new file mode 100644
index 0000000000..e863378b55
--- /dev/null
+++ b/.changeset/rich-eggs-talk.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Change OpenAPI schema-optional from `info` to `tint` color
diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
index f0371016ec..601885c5d0 100644
--- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
+++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
@@ -210,7 +210,7 @@
 }
 
 .openapi-schema-optional {
-    @apply text-info-subtle text-[0.813rem] lowercase;
+    @apply text-tint-subtle text-[0.813rem] lowercase;
 }
 
 .openapi-schema-readonly {

From c4ebb3fecd50ee737487a47519d2f3a774ae59e5 Mon Sep 17 00:00:00 2001
From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com>
Date: Wed, 30 Apr 2025 11:27:03 +0200
Subject: [PATCH 017/127] Fix openapi-select hover in responses (#3196)

---
 .changeset/breezy-bugs-dream.md                             | 5 +++++
 .../gitbook/src/components/DocumentView/OpenAPI/style.css   | 6 +++++-
 2 files changed, 10 insertions(+), 1 deletion(-)
 create mode 100644 .changeset/breezy-bugs-dream.md

diff --git a/.changeset/breezy-bugs-dream.md b/.changeset/breezy-bugs-dream.md
new file mode 100644
index 0000000000..69d2022873
--- /dev/null
+++ b/.changeset/breezy-bugs-dream.md
@@ -0,0 +1,5 @@
+---
+'gitbook': patch
+---
+
+Fix openapi-select hover in responses
diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
index 601885c5d0..f4757ca2b6 100644
--- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
+++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
@@ -687,6 +687,10 @@ body:has(.openapi-select-popover) {
     @apply bg-tint-subtle;
 }
 
+.openapi-disclosure-group:has(.openapi-disclosure-group-trigger:hover):has(.openapi-select:hover) {
+    @apply !bg-transparent;
+}
+
 .openapi-disclosure-group-trigger {
     @apply flex w-full items-baseline gap-3 transition-all relative flex-1 p-3 -outline-offset-1;
 }
@@ -719,7 +723,7 @@ body:has(.openapi-select-popover) {
     @apply rotate-90;
 }
 
-.openapi-disclosure-group-mediatype {
+.openapi-disclosure-group-mediatype:not(:has(.openapi-select)) {
     @apply text-[0.625rem] font-mono shrink-0 grow-0 text-tint-subtle contrast-more:text-tint;
 }
 

From 42ca7e15804753a12d19e9df5dcbc091aba8ec13 Mon Sep 17 00:00:00 2001
From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com>
Date: Wed, 30 Apr 2025 11:44:39 +0200
Subject: [PATCH 018/127] Fix openapi CR preview (#3197)

---
 .changeset/giant-dolphins-approve.md                      | 6 ++++++
 .../gitbook/src/components/DocumentView/OpenAPI/style.css | 6 +++---
 packages/react-openapi/src/OpenAPIDisclosureGroup.tsx     | 8 ++++----
 3 files changed, 13 insertions(+), 7 deletions(-)
 create mode 100644 .changeset/giant-dolphins-approve.md

diff --git a/.changeset/giant-dolphins-approve.md b/.changeset/giant-dolphins-approve.md
new file mode 100644
index 0000000000..0fa9d77174
--- /dev/null
+++ b/.changeset/giant-dolphins-approve.md
@@ -0,0 +1,6 @@
+---
+'@gitbook/react-openapi': patch
+'gitbook': patch
+---
+
+Fix openapi CR preview
diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
index f4757ca2b6..f3c24cb738 100644
--- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
+++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
@@ -692,18 +692,18 @@ body:has(.openapi-select-popover) {
 }
 
 .openapi-disclosure-group-trigger {
-    @apply flex w-full items-baseline gap-3 transition-all relative flex-1 p-3 -outline-offset-1;
+    @apply flex w-full cursor-pointer items-baseline gap-3 transition-all relative flex-1 p-3 -outline-offset-1;
 }
 
 .openapi-disclosure-group-label {
     @apply flex flex-wrap items-baseline gap-x-3 gap-y-1 flex-1 truncate;
 }
 
-.openapi-disclosure-group-trigger:disabled {
+.openapi-disclosure-group-trigger[aria-disabled="true"] {
     @apply cursor-default hover:bg-inherit;
 }
 
-.openapi-disclosure-group-trigger:disabled .openapi-disclosure-group-icon {
+.openapi-disclosure-group-trigger[aria-disabled="true"] .openapi-disclosure-group-icon {
     @apply invisible;
 }
 
diff --git a/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx b/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx
index 27584cfc67..ecef441b76 100644
--- a/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx
+++ b/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx
@@ -76,7 +76,7 @@ function DisclosureItem(props: {
     });
 
     const panelRef = useRef<HTMLDivElement | null>(null);
-    const triggerRef = useRef<HTMLButtonElement | null>(null);
+    const triggerRef = useRef<HTMLDivElement | null>(null);
     const isDisabled = groupState?.isDisabled || !group.tabs?.length || false;
     const { buttonProps: triggerProps, panelProps } = useDisclosure(
         {
@@ -96,11 +96,11 @@ function DisclosureItem(props: {
 
     return (
         <div className="openapi-disclosure-group" aria-expanded={state.isExpanded}>
-            <button
+            <div
                 slot="trigger"
                 ref={triggerRef}
                 {...mergeProps(buttonProps, focusProps)}
-                disabled={isDisabled}
+                aria-disabled={isDisabled}
                 style={{
                     outline: isFocusVisible
                         ? '2px solid rgb(var(--primary-color-500)/0.4)'
@@ -146,7 +146,7 @@ function DisclosureItem(props: {
                         </div>
                     ) : null}
                 </div>
-            </button>
+            </div>
 
             {state.isExpanded && selectedTab && (
                 <div className="openapi-disclosure-group-panel" ref={panelRef} {...panelProps}>

From 5a69692f543e7ab7ca6bc364557e85a762d5bdae Mon Sep 17 00:00:00 2001
From: Taran Vohra <taranvohra@outlook.com>
Date: Wed, 30 Apr 2025 19:14:09 +0530
Subject: [PATCH 019/127] Temporarily bump getPublishedContentByUrl timeout to
 30s on v1 (#3199)

---
 packages/gitbook/src/lib/api.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/packages/gitbook/src/lib/api.ts b/packages/gitbook/src/lib/api.ts
index ca3ce3b1f8..e04433dc55 100644
--- a/packages/gitbook/src/lib/api.ts
+++ b/packages/gitbook/src/lib/api.ts
@@ -242,6 +242,7 @@ export const getLatestOpenAPISpecVersionContent = cache({
  * Resolve a URL to the content to render.
  */
 export const getPublishedContentByUrl = cache({
+    timeout: 30 * 1000,
     name: 'api.getPublishedContentByUrl.v7',
     tag: (url) => getCacheTagForURL(url),
     get: async (

From 20ebecb114bbf49833887e92c7d740a30d0d1202 Mon Sep 17 00:00:00 2001
From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com>
Date: Fri, 2 May 2025 11:48:15 +0200
Subject: [PATCH 020/127] Missing top-level required OpenAPI alternatives
 (#3207)

---
 .changeset/shy-plums-deny.md                 |  5 +++
 packages/react-openapi/src/OpenAPISchema.tsx | 38 ++++++++++++++++++--
 2 files changed, 41 insertions(+), 2 deletions(-)
 create mode 100644 .changeset/shy-plums-deny.md

diff --git a/.changeset/shy-plums-deny.md b/.changeset/shy-plums-deny.md
new file mode 100644
index 0000000000..22193b58e1
--- /dev/null
+++ b/.changeset/shy-plums-deny.md
@@ -0,0 +1,5 @@
+---
+'@gitbook/react-openapi': patch
+---
+
+Missing top-level required OpenAPI alternatives
diff --git a/packages/react-openapi/src/OpenAPISchema.tsx b/packages/react-openapi/src/OpenAPISchema.tsx
index 007c335cf8..7841b1c0f0 100644
--- a/packages/react-openapi/src/OpenAPISchema.tsx
+++ b/packages/react-openapi/src/OpenAPISchema.tsx
@@ -572,6 +572,9 @@ function flattenAlternatives(
     schemasOrRefs: (OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject)[],
     ancestors: Set<OpenAPIV3.SchemaObject>
 ): OpenAPIV3.SchemaObject[] {
+    // Get the parent schema's required fields from the most recent ancestor
+    const latestAncestor = Array.from(ancestors).pop();
+
     return schemasOrRefs.reduce<OpenAPIV3.SchemaObject[]>((acc, schemaOrRef) => {
         if (checkIsReference(schemaOrRef)) {
             return acc;
@@ -580,16 +583,47 @@ function flattenAlternatives(
         if (schemaOrRef[alternativeType] && !ancestors.has(schemaOrRef)) {
             const schemas = getSchemaAlternatives(schemaOrRef, ancestors);
             if (schemas) {
-                acc.push(...schemas);
+                acc.push(
+                    ...schemas.map((schema) => ({
+                        ...schema,
+                        required: mergeRequiredFields(schema, latestAncestor),
+                    }))
+                );
             }
             return acc;
         }
 
-        acc.push(schemaOrRef);
+        // For direct schemas, handle required fields
+        const schema = {
+            ...schemaOrRef,
+            required: mergeRequiredFields(schemaOrRef, latestAncestor),
+        };
+
+        acc.push(schema);
         return acc;
     }, []);
 }
 
+/**
+ * Merge the required fields of a schema with the required fields of its latest ancestor.
+ */
+function mergeRequiredFields(
+    schemaOrRef: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
+    latestAncestor: OpenAPIV3.SchemaObject | undefined
+) {
+    if (!schemaOrRef.required && !latestAncestor?.required) {
+        return undefined;
+    }
+
+    if (checkIsReference(schemaOrRef)) {
+        return latestAncestor?.required;
+    }
+
+    return Array.from(
+        new Set([...(latestAncestor?.required || []), ...(schemaOrRef.required || [])])
+    );
+}
+
 function getSchemaTitle(schema: OpenAPIV3.SchemaObject): string {
     // Otherwise try to infer a nice title
     let type = 'any';

From f328a41982c85181c884d05632f541060f9f63dc Mon Sep 17 00:00:00 2001
From: Taran Vohra <taranvohra@outlook.com>
Date: Fri, 2 May 2025 16:46:31 +0530
Subject: [PATCH 021/127] Revert getPublishedContentByUrl timeout back to 10s
 (#3208)

---
 packages/gitbook/src/lib/api.ts | 1 -
 1 file changed, 1 deletion(-)

diff --git a/packages/gitbook/src/lib/api.ts b/packages/gitbook/src/lib/api.ts
index e04433dc55..ca3ce3b1f8 100644
--- a/packages/gitbook/src/lib/api.ts
+++ b/packages/gitbook/src/lib/api.ts
@@ -242,7 +242,6 @@ export const getLatestOpenAPISpecVersionContent = cache({
  * Resolve a URL to the content to render.
  */
 export const getPublishedContentByUrl = cache({
-    timeout: 30 * 1000,
     name: 'api.getPublishedContentByUrl.v7',
     tag: (url) => getCacheTagForURL(url),
     get: async (

From f7a34706c7302093f17ebb7bd324923d00a9f5b4 Mon Sep 17 00:00:00 2001
From: Zeno Kapitein <zeno@gitbook.io>
Date: Fri, 2 May 2025 19:41:05 +0200
Subject: [PATCH 022/127] Allow high-contrast input colors to be used as-is
 (#3209)

---
 .changeset/rich-lamps-compare.md       | 5 +++++
 packages/colors/src/transformations.ts | 6 +++++-
 2 files changed, 10 insertions(+), 1 deletion(-)
 create mode 100644 .changeset/rich-lamps-compare.md

diff --git a/.changeset/rich-lamps-compare.md b/.changeset/rich-lamps-compare.md
new file mode 100644
index 0000000000..57b026b7b4
--- /dev/null
+++ b/.changeset/rich-lamps-compare.md
@@ -0,0 +1,5 @@
+---
+"@gitbook/colors": patch
+---
+
+Change lightness check for color step 9 to allow input colors with a higher-than-needed contrast
diff --git a/packages/colors/src/transformations.ts b/packages/colors/src/transformations.ts
index 90cb4482eb..8234a460a9 100644
--- a/packages/colors/src/transformations.ts
+++ b/packages/colors/src/transformations.ts
@@ -214,7 +214,11 @@ export function colorScale(
         const targetL =
             foregroundColor.L * mapping[index] + backgroundColor.L * (1 - mapping[index]);
 
-        if (index === 8 && !mix && Math.abs(baseColor.L - targetL) < 0.2) {
+        if (
+            index === 8 &&
+            !mix &&
+            (darkMode ? targetL - baseColor.L < 0.2 : baseColor.L - targetL < 0.2)
+        ) {
             // Original colour is close enough to target, so let's use the original colour as step 9.
             result.push(hex);
             continue;

From 04999662dbcf04ebf6632b8acee270eec7013078 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Mon, 5 May 2025 09:03:28 +0200
Subject: [PATCH 023/127] Use absolute URLs in sitemap.xml when pointing to
 sitemap-pages.xml (#3210)

---
 .changeset/real-donkeys-invent.md      | 5 +++++
 packages/gitbook/src/routes/sitemap.ts | 2 +-
 2 files changed, 6 insertions(+), 1 deletion(-)
 create mode 100644 .changeset/real-donkeys-invent.md

diff --git a/.changeset/real-donkeys-invent.md b/.changeset/real-donkeys-invent.md
new file mode 100644
index 0000000000..a322af7d72
--- /dev/null
+++ b/.changeset/real-donkeys-invent.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Fix invalid sitemap.xml generated with relative URLs instead of absolute ones
diff --git a/packages/gitbook/src/routes/sitemap.ts b/packages/gitbook/src/routes/sitemap.ts
index 4a64e12387..a5f5e452fb 100644
--- a/packages/gitbook/src/routes/sitemap.ts
+++ b/packages/gitbook/src/routes/sitemap.ts
@@ -141,7 +141,7 @@ function getUrlsFromSiteSpaces(context: GitBookSiteContext, siteSpaces: SiteSpac
         }
         const url = new URL(siteSpace.urls.published);
         url.pathname = joinPath(url.pathname, 'sitemap-pages.xml');
-        return context.linker.toLinkForContent(url.toString());
+        return context.linker.toAbsoluteURL(context.linker.toLinkForContent(url.toString()));
     }, []);
     return urls.filter(filterOutNullable);
 }

From 5d504ffa4c320e8968dc2f036493592a80ddf1cd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Tue, 6 May 2025 10:30:55 +0200
Subject: [PATCH 024/127] Fix resolution of links in reusable content (#3212)

---
 .changeset/moody-maps-chew.md                 |  5 ++++
 .../DocumentView/ReusableContent.tsx          | 23 ++++++++++++-------
 2 files changed, 20 insertions(+), 8 deletions(-)
 create mode 100644 .changeset/moody-maps-chew.md

diff --git a/.changeset/moody-maps-chew.md b/.changeset/moody-maps-chew.md
new file mode 100644
index 0000000000..0c06afc2ad
--- /dev/null
+++ b/.changeset/moody-maps-chew.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Fix resolution of links in reusable contents
diff --git a/packages/gitbook/src/components/DocumentView/ReusableContent.tsx b/packages/gitbook/src/components/DocumentView/ReusableContent.tsx
index bbf2d23f70..ccb66babd4 100644
--- a/packages/gitbook/src/components/DocumentView/ReusableContent.tsx
+++ b/packages/gitbook/src/components/DocumentView/ReusableContent.tsx
@@ -46,14 +46,21 @@ export async function ReusableContent(props: BlockProps<DocumentBlockReusableCon
     // Create a new context for reusable content block, including
     // the data fetcher with the token from the block meta and the correct
     // space and revision pointers.
-    const reusableContentContext: GitBookSpaceContext = {
-        ...context.contentContext,
-        dataFetcher,
-        space: resolved.reusableContent.space,
-        revisionId: resolved.reusableContent.revision,
-        pages: [],
-        shareKey: undefined,
-    };
+    const reusableContentContext: GitBookSpaceContext =
+        context.contentContext.space.id === resolved.reusableContent.space.id
+            ? context.contentContext
+            : {
+                  ...context.contentContext,
+                  dataFetcher,
+                  space: resolved.reusableContent.space,
+                  revisionId: resolved.reusableContent.revision,
+                  // When the reusable content is in a different space, we don't resolve relative links to pages
+                  // as this space might not be part of the current site.
+                  // In the future, we might expand the logic to look up the space from the list of all spaces in the site
+                  // and adapt the relative links to point to the correct variant.
+                  pages: [],
+                  shareKey: undefined,
+              };
 
     return (
         <UnwrappedBlocks

From a3ec2647646bc39cff40f5ec95b89b0f1fdd98bf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Greg=20Berg=C3=A9?= <berge.greg@gmail.com>
Date: Tue, 6 May 2025 15:40:56 +0200
Subject: [PATCH 025/127] Fix Python code sample "null vs None" (#3215)

---
 .changeset/twelve-news-joke.md                  | 5 +++++
 packages/react-openapi/src/code-samples.test.ts | 3 ++-
 packages/react-openapi/src/code-samples.ts      | 5 ++++-
 3 files changed, 11 insertions(+), 2 deletions(-)
 create mode 100644 .changeset/twelve-news-joke.md

diff --git a/.changeset/twelve-news-joke.md b/.changeset/twelve-news-joke.md
new file mode 100644
index 0000000000..3c0d08681f
--- /dev/null
+++ b/.changeset/twelve-news-joke.md
@@ -0,0 +1,5 @@
+---
+"@gitbook/react-openapi": patch
+---
+
+Fix Python code sample "null vs None"
diff --git a/packages/react-openapi/src/code-samples.test.ts b/packages/react-openapi/src/code-samples.test.ts
index 363baf5525..7fd24bb3f8 100644
--- a/packages/react-openapi/src/code-samples.test.ts
+++ b/packages/react-openapi/src/code-samples.test.ts
@@ -415,13 +415,14 @@ describe('python code sample generator', () => {
                 key: 'value',
                 truethy: true,
                 falsey: false,
+                nullish: null,
             },
         };
 
         const output = generator?.generate(input);
 
         expect(output).toBe(
-            'import requests\n\nresponse = requests.get(\n    "https://example.com/path",\n    headers={"Content-Type":"application/json"},\n    data=json.dumps({"key":"value","truethy":True,"falsey":False})\n)\n\ndata = response.json()'
+            'import requests\n\nresponse = requests.get(\n    "https://example.com/path",\n    headers={"Content-Type":"application/json"},\n    data=json.dumps({"key":"value","truethy":True,"falsey":False,"nullish":None})\n)\n\ndata = response.json()'
         );
     });
 
diff --git a/packages/react-openapi/src/code-samples.ts b/packages/react-openapi/src/code-samples.ts
index fe44da11d8..a6c79fd5cf 100644
--- a/packages/react-openapi/src/code-samples.ts
+++ b/packages/react-openapi/src/code-samples.ts
@@ -362,12 +362,15 @@ const BodyGenerators = {
                         return '$$__TRUE__$$';
                     case false:
                         return '$$__FALSE__$$';
+                    case null:
+                        return '$$__NULL__$$';
                     default:
                         return value;
                 }
             })
                 .replaceAll('"$$__TRUE__$$"', 'True')
-                .replaceAll('"$$__FALSE__$$"', 'False');
+                .replaceAll('"$$__FALSE__$$"', 'False')
+                .replaceAll('"$$__NULL__$$"', 'None');
         }
 
         return { body, code, headers };

From 5e975ab95b35ef9e63b049c57e3b0feae174dbb7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Greg=20Berg=C3=A9?= <berge.greg@gmail.com>
Date: Tue, 6 May 2025 15:41:15 +0200
Subject: [PATCH 026/127] Fix code highlighting for HTTP (#3214)

---
 .changeset/lemon-plants-vanish.md                         | 8 ++++++++
 .../src/components/DocumentView/CodeBlock/theme.css       | 6 +++---
 packages/react-openapi/src/code-samples.ts                | 2 +-
 3 files changed, 12 insertions(+), 4 deletions(-)
 create mode 100644 .changeset/lemon-plants-vanish.md

diff --git a/.changeset/lemon-plants-vanish.md b/.changeset/lemon-plants-vanish.md
new file mode 100644
index 0000000000..0887b10bef
--- /dev/null
+++ b/.changeset/lemon-plants-vanish.md
@@ -0,0 +1,8 @@
+---
+"@gitbook/react-openapi": patch
+"gitbook-v2": patch
+"gitbook": patch
+"@gitbook/colors": patch
+---
+
+Fix code highlighting for HTTP
diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/theme.css b/packages/gitbook/src/components/DocumentView/CodeBlock/theme.css
index 21c0fd5791..5e4a73a889 100644
--- a/packages/gitbook/src/components/DocumentView/CodeBlock/theme.css
+++ b/packages/gitbook/src/components/DocumentView/CodeBlock/theme.css
@@ -5,7 +5,7 @@
     --shiki-token-link: theme("colors.primary.10");
 
     --shiki-token-constant: theme("colors.warning.10");
-    --shiki-token-string: theme("colors.success.10");
+    --shiki-token-string: theme("colors.warning.10");
     --shiki-token-string-expression: theme("colors.success.10");
     --shiki-token-keyword: theme("colors.danger.10");
     --shiki-token-parameter: theme("colors.warning.10");
@@ -24,7 +24,7 @@
         --shiki-token-link: theme("colors.primary.11");
 
         --shiki-token-constant: theme("colors.warning.11");
-        --shiki-token-string: theme("colors.success.11");
+        --shiki-token-string: theme("colors.warning.11");
         --shiki-token-string-expression: theme("colors.success.11");
         --shiki-token-keyword: theme("colors.danger.11");
         --shiki-token-parameter: theme("colors.warning.11");
@@ -41,7 +41,7 @@ html.dark {
     --shiki-token-comment: theme("colors.neutral.9");
 
     --shiki-token-constant: theme("colors.warning.11");
-    --shiki-token-string: theme("colors.success.11");
+    --shiki-token-string: theme("colors.warning.11");
     --shiki-token-string-expression: theme("colors.success.11");
     --shiki-token-keyword: theme("colors.danger.11");
     --shiki-token-parameter: theme("colors.warning.11");
diff --git a/packages/react-openapi/src/code-samples.ts b/packages/react-openapi/src/code-samples.ts
index a6c79fd5cf..50c6a9204c 100644
--- a/packages/react-openapi/src/code-samples.ts
+++ b/packages/react-openapi/src/code-samples.ts
@@ -30,7 +30,7 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [
     {
         id: 'http',
         label: 'HTTP',
-        syntax: 'bash',
+        syntax: 'http',
         generate: ({ method, url, headers = {}, body }: CodeSampleInput) => {
             const { host, path } = parseHostAndPath(url);
 

From 3f292065cd4aeded2cd1469fedf76afa5d21d59a Mon Sep 17 00:00:00 2001
From: Taran Vohra <taranvohra@outlook.com>
Date: Wed, 7 May 2025 12:42:28 +0530
Subject: [PATCH 027/127] Update site redirect regex for validation (#3216)

---
 .changeset/tidy-rings-talk.md                     | 6 ++++++
 packages/gitbook/src/components/SitePage/fetch.ts | 6 +++++-
 packages/gitbook/src/lib/api.ts                   | 7 ++++++-
 3 files changed, 17 insertions(+), 2 deletions(-)
 create mode 100644 .changeset/tidy-rings-talk.md

diff --git a/.changeset/tidy-rings-talk.md b/.changeset/tidy-rings-talk.md
new file mode 100644
index 0000000000..75ac0e7f8d
--- /dev/null
+++ b/.changeset/tidy-rings-talk.md
@@ -0,0 +1,6 @@
+---
+"gitbook": patch
+"gitbook-v2": patch
+---
+
+Update the regex for validating site redirect
diff --git a/packages/gitbook/src/components/SitePage/fetch.ts b/packages/gitbook/src/components/SitePage/fetch.ts
index 6ca4e55293..732c36072c 100644
--- a/packages/gitbook/src/components/SitePage/fetch.ts
+++ b/packages/gitbook/src/components/SitePage/fetch.ts
@@ -69,7 +69,11 @@ async function resolvePage(context: GitBookSiteContext, params: PagePathParams |
 
         // If a page still can't be found, we try with the API, in case we have a redirect at site level.
         const redirectPathname = withLeadingSlash(rawPathname);
-        if (/^\/[a-zA-Z0-9-_.\/]+[a-zA-Z0-9-_.]$/.test(redirectPathname)) {
+        if (
+            /^\/(?:[A-Za-z0-9\-._~]|%[0-9A-Fa-f]{2})+(?:\/(?:[A-Za-z0-9\-._~]|%[0-9A-Fa-f]{2})+)*$/.test(
+                redirectPathname
+            )
+        ) {
             const redirectSources = new Set<string>([
                 // Test the pathname relative to the root
                 // For example hello/world -> section/variant/hello/world
diff --git a/packages/gitbook/src/lib/api.ts b/packages/gitbook/src/lib/api.ts
index ca3ce3b1f8..310074450d 100644
--- a/packages/gitbook/src/lib/api.ts
+++ b/packages/gitbook/src/lib/api.ts
@@ -765,7 +765,12 @@ export const getComputedDocument = cache({
  * Mimic the validation done on source server-side to reduce API usage.
  */
 function validateSiteRedirectSource(source: string) {
-    return source.length <= 512 && /^\/[a-zA-Z0-9-_.\\/]+[a-zA-Z0-9-_.]$/.test(source);
+    return (
+        source.length <= 512 &&
+        /^\/(?:[A-Za-z0-9\-._~]|%[0-9A-Fa-f]{2})+(?:\/(?:[A-Za-z0-9\-._~]|%[0-9A-Fa-f]{2})+)*$/.test(
+            source
+        )
+    );
 }
 
 /**

From 373f18f4519496232ad169b083b3f6978c788036 Mon Sep 17 00:00:00 2001
From: Zeno Kapitein <zeno@gitbook.io>
Date: Wed, 7 May 2025 15:57:58 +0200
Subject: [PATCH 028/127] Prevent section group popovers from opening on click
 (#3213)

---
 .changeset/thirty-donkeys-help.md                         | 5 +++++
 .../src/components/SiteSections/SiteSectionTabs.tsx       | 8 +++++++-
 2 files changed, 12 insertions(+), 1 deletion(-)
 create mode 100644 .changeset/thirty-donkeys-help.md

diff --git a/.changeset/thirty-donkeys-help.md b/.changeset/thirty-donkeys-help.md
new file mode 100644
index 0000000000..529b8a9024
--- /dev/null
+++ b/.changeset/thirty-donkeys-help.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Prevent section group popovers from opening on click
diff --git a/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx b/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx
index 2833f9f31a..8496845507 100644
--- a/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx
+++ b/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx
@@ -83,6 +83,12 @@ export function SiteSectionTabs(props: { sections: ClientSiteSections }) {
                                                     )
                                                 }
                                                 asChild
+                                                onClick={(e) => {
+                                                    if (value) {
+                                                        e.preventDefault();
+                                                        e.stopPropagation();
+                                                    }
+                                                }}
                                             >
                                                 <SectionGroupTab
                                                     isActive={isActive}
@@ -180,7 +186,7 @@ const SectionGroupTab = React.forwardRef(function SectionGroupTab(
             ref={ref}
             {...rest}
             className={tcls(
-                'group relative my-2 flex select-none items-center justify-between rounded straight-corners:rounded-none px-3 py-1 transition-colors',
+                'group relative my-2 flex select-none items-center justify-between rounded straight-corners:rounded-none px-3 py-1 transition-colors hover:cursor-default',
                 isActive
                     ? 'text-primary-subtle'
                     : 'text-tint hover:bg-tint-hover hover:text-tint-strong'

From e6ddc0f07fc5e889e8c672c63ad488845f3522b9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Greg=20Berg=C3=A9?= <berge.greg@gmail.com>
Date: Thu, 8 May 2025 08:54:28 +0200
Subject: [PATCH 029/127] Fix sitemap URLs (#3220)

---
 .changeset/healthy-parrots-call.md   | 6 ++++++
 packages/gitbook-v2/src/lib/links.ts | 5 +++++
 2 files changed, 11 insertions(+)
 create mode 100644 .changeset/healthy-parrots-call.md

diff --git a/.changeset/healthy-parrots-call.md b/.changeset/healthy-parrots-call.md
new file mode 100644
index 0000000000..9503e2afb1
--- /dev/null
+++ b/.changeset/healthy-parrots-call.md
@@ -0,0 +1,6 @@
+---
+"gitbook-v2": patch
+"gitbook": patch
+---
+
+Fix URL in sitemap
diff --git a/packages/gitbook-v2/src/lib/links.ts b/packages/gitbook-v2/src/lib/links.ts
index 857345703c..b84565d1e7 100644
--- a/packages/gitbook-v2/src/lib/links.ts
+++ b/packages/gitbook-v2/src/lib/links.ts
@@ -93,6 +93,11 @@ export function createLinker(
         },
 
         toAbsoluteURL(absolutePath: string): string {
+            // If the path is already a full URL, we return it as is.
+            if (URL.canParse(absolutePath)) {
+                return absolutePath;
+            }
+
             if (!servedOn.host) {
                 return absolutePath;
             }

From 47f01edcb80ef868fb42a72d7e52663a49417c18 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Greg=20Berg=C3=A9?= <berge.greg@gmail.com>
Date: Thu, 8 May 2025 09:34:04 +0200
Subject: [PATCH 030/127] Fix E2E icons (#3222)

---
 packages/gitbook/e2e/util.ts | 42 ++++++++++++++++++++----------------
 1 file changed, 23 insertions(+), 19 deletions(-)

diff --git a/packages/gitbook/e2e/util.ts b/packages/gitbook/e2e/util.ts
index c2edad6186..78d7badb71 100644
--- a/packages/gitbook/e2e/util.ts
+++ b/packages/gitbook/e2e/util.ts
@@ -345,19 +345,30 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
  */
 async function waitForIcons(page: Page) {
     await page.waitForFunction(() => {
-        const urlStates: Record<string, 'pending' | 'loaded'> =
-            (window as any).__ICONS_STATES__ || {};
+        const urlStates: Record<
+            string,
+            { state: 'pending'; uri: null } | { state: 'loaded'; uri: string }
+        > = (window as any).__ICONS_STATES__ || {};
         (window as any).__ICONS_STATES__ = urlStates;
 
+        const fetchSvgAsDataUri = async (url: string): Promise<string> => {
+            const response = await fetch(url);
+            if (!response.ok) {
+                throw new Error(`Failed to fetch SVG: ${response.status}`);
+            }
+
+            const svgText = await response.text();
+            const encoded = encodeURIComponent(svgText).replace(/'/g, '%27').replace(/"/g, '%22');
+
+            return `data:image/svg+xml;charset=utf-8,${encoded}`;
+        };
+
         const loadUrl = (url: string) => {
             // Mark the URL as pending.
-            urlStates[url] = 'pending';
-
-            const img = new Image();
-            img.onload = () => {
-                urlStates[url] = 'loaded';
-            };
-            img.src = url;
+            urlStates[url] = { state: 'pending', uri: null };
+            fetchSvgAsDataUri(url).then((uri) => {
+                urlStates[url] = { state: 'loaded', uri };
+            });
         };
 
         const icons = Array.from(document.querySelectorAll('svg.gb-icon'));
@@ -393,18 +404,11 @@ async function waitForIcons(page: Page) {
 
             // If the URL is already queued for loading, we return the state.
             if (urlStates[url]) {
-                if (urlStates[url] === 'loaded') {
+                if (urlStates[url].state === 'loaded') {
                     icon.setAttribute('data-argos-state', 'pending');
-                    const bckMaskImage = icon.style.maskImage;
-                    const bckDisplay = icon.style.display;
-                    icon.style.maskImage = '';
-                    icon.style.display = 'none';
+                    icon.style.maskImage = `url("${urlStates[url].uri}")`;
                     requestAnimationFrame(() => {
-                        icon.style.maskImage = bckMaskImage;
-                        icon.style.display = bckDisplay;
-                        requestAnimationFrame(() => {
-                            icon.setAttribute('data-argos-state', 'loaded');
-                        });
+                        icon.setAttribute('data-argos-state', 'loaded');
                     });
                     return false;
                 }

From cb5598dc19cd82752756a73cb520df554ab9343d Mon Sep 17 00:00:00 2001
From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com>
Date: Thu, 8 May 2025 11:54:46 +0200
Subject: [PATCH 031/127] Handle invalid OpenAPI Responses (#3223)

---
 .changeset/smart-feet-argue.md                |  5 ++
 .../src/OpenAPIResponseExample.tsx            | 70 ++++++++++---------
 .../react-openapi/src/OpenAPIResponses.tsx    |  8 +--
 3 files changed, 45 insertions(+), 38 deletions(-)
 create mode 100644 .changeset/smart-feet-argue.md

diff --git a/.changeset/smart-feet-argue.md b/.changeset/smart-feet-argue.md
new file mode 100644
index 0000000000..079dada9b0
--- /dev/null
+++ b/.changeset/smart-feet-argue.md
@@ -0,0 +1,5 @@
+---
+'@gitbook/react-openapi': patch
+---
+
+Handle invalid OpenAPI Responses
diff --git a/packages/react-openapi/src/OpenAPIResponseExample.tsx b/packages/react-openapi/src/OpenAPIResponseExample.tsx
index 6d1cfdac25..b4b865f505 100644
--- a/packages/react-openapi/src/OpenAPIResponseExample.tsx
+++ b/packages/react-openapi/src/OpenAPIResponseExample.tsx
@@ -6,8 +6,8 @@ import { OpenAPIResponseExampleContent } from './OpenAPIResponseExampleContent';
 import { type OpenAPIContext, getOpenAPIClientContext } from './context';
 import type { OpenAPIOperationData, OpenAPIWebhookData } from './types';
 import { getExampleFromReference, getExamples } from './util/example';
-import { createStateKey, getStatusCodeDefaultLabel } from './utils';
-import { checkIsReference, resolveDescription } from './utils';
+import { createStateKey, getStatusCodeDefaultLabel, resolveDescription } from './utils';
+import { checkIsReference } from './utils';
 
 /**
  * Display an example of the response content.
@@ -41,45 +41,47 @@ export function OpenAPIResponseExample(props: {
         return Number(a) - Number(b);
     });
 
-    const tabs = responses.map(([key, responseObject]) => {
-        const description = resolveDescription(responseObject);
-        const label = description ? (
-            <Markdown source={description} />
-        ) : (
-            getStatusCodeDefaultLabel(key, context)
-        );
+    const tabs = responses
+        .filter(([_, responseObject]) => responseObject && typeof responseObject === 'object')
+        .map(([key, responseObject]) => {
+            const description = resolveDescription(responseObject);
+            const label = description ? (
+                <Markdown source={description} />
+            ) : (
+                getStatusCodeDefaultLabel(key, context)
+            );
 
-        if (checkIsReference(responseObject)) {
-            return {
-                key: key,
-                label,
-                statusCode: key,
-                body: (
-                    <OpenAPIExample
-                        example={getExampleFromReference(responseObject, context)}
-                        context={context}
-                        syntax="json"
-                    />
-                ),
-            };
-        }
+            if (checkIsReference(responseObject)) {
+                return {
+                    key: key,
+                    label,
+                    statusCode: key,
+                    body: (
+                        <OpenAPIExample
+                            example={getExampleFromReference(responseObject, context)}
+                            context={context}
+                            syntax="json"
+                        />
+                    ),
+                };
+            }
+
+            if (!responseObject.content || Object.keys(responseObject.content).length === 0) {
+                return {
+                    key: key,
+                    label,
+                    statusCode: key,
+                    body: <OpenAPIEmptyExample context={context} />,
+                };
+            }
 
-        if (!responseObject.content || Object.keys(responseObject.content).length === 0) {
             return {
                 key: key,
                 label,
                 statusCode: key,
-                body: <OpenAPIEmptyExample context={context} />,
+                body: <OpenAPIResponse context={context} content={responseObject.content} />,
             };
-        }
-
-        return {
-            key: key,
-            label,
-            statusCode: key,
-            body: <OpenAPIResponse context={context} content={responseObject.content} />,
-        };
-    });
+        });
 
     if (tabs.length === 0) {
         return null;
diff --git a/packages/react-openapi/src/OpenAPIResponses.tsx b/packages/react-openapi/src/OpenAPIResponses.tsx
index efee2b3ce8..c4565aa788 100644
--- a/packages/react-openapi/src/OpenAPIResponses.tsx
+++ b/packages/react-openapi/src/OpenAPIResponses.tsx
@@ -20,8 +20,9 @@ export function OpenAPIResponses(props: {
 }) {
     const { responses, context } = props;
 
-    const groups = Object.entries(responses).map(
-        ([statusCode, response]: [string, OpenAPIV3.ResponseObject]) => {
+    const groups = Object.entries(responses)
+        .filter(([_, response]) => response && typeof response === 'object')
+        .map(([statusCode, response]: [string, OpenAPIV3.ResponseObject]) => {
             const tabs = (() => {
                 // If there is no content, but there are headers, we need to show the headers
                 if (
@@ -83,8 +84,7 @@ export function OpenAPIResponses(props: {
                 ),
                 tabs,
             };
-        }
-    );
+        });
 
     const state = useResponseExamplesState(context.blockKey, groups[0]?.key);
 

From 7d7806df30d83be6fd21778c8ab19e151f442749 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Thu, 8 May 2025 18:57:50 +0200
Subject: [PATCH 032/127] Passthrough SVG images in image resizing (#3224)

---
 .changeset/two-lions-tickle.md                |  6 ++++
 .../src/lib/images/checkIsSizableImageURL.ts  | 30 +++++++++++--------
 .../src/lib/images/createImageResizer.ts      |  6 ++--
 .../src/lib/images/resizer/resizeImage.ts     | 18 ++++++-----
 packages/gitbook/src/routes/image.ts          |  3 +-
 5 files changed, 40 insertions(+), 23 deletions(-)
 create mode 100644 .changeset/two-lions-tickle.md

diff --git a/.changeset/two-lions-tickle.md b/.changeset/two-lions-tickle.md
new file mode 100644
index 0000000000..b9fa676033
--- /dev/null
+++ b/.changeset/two-lions-tickle.md
@@ -0,0 +1,6 @@
+---
+"gitbook-v2": minor
+"gitbook": minor
+---
+
+Pass SVG images through image resizing without resizing them to serve them from optimal host.
diff --git a/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts b/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts
index 45754af505..61e3ebbc2b 100644
--- a/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts
+++ b/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts
@@ -1,4 +1,8 @@
-import { checkIsHttpURL } from '@/lib/urls';
+export enum SizableImageAction {
+    Resize = 'resize',
+    Skip = 'skip',
+    Passthrough = 'passthrough',
+}
 
 /**
  * Check if an image URL is resizable.
@@ -6,22 +10,24 @@ import { checkIsHttpURL } from '@/lib/urls';
  * Skip it for SVGs.
  * Skip it for GitBook images (to avoid recursion).
  */
-export function checkIsSizableImageURL(input: string): boolean {
+export function checkIsSizableImageURL(input: string): SizableImageAction {
     if (!URL.canParse(input)) {
-        return false;
-    }
-
-    if (input.includes('/~gitbook/image')) {
-        return false;
+        return SizableImageAction.Skip;
     }
 
     const parsed = new URL(input);
-    if (parsed.pathname.endsWith('.svg') || parsed.pathname.endsWith('.avif')) {
-        return false;
+    if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
+        return SizableImageAction.Skip;
     }
-    if (!checkIsHttpURL(parsed)) {
-        return false;
+    if (parsed.hostname === 'localhost') {
+        return SizableImageAction.Skip;
+    }
+    if (parsed.pathname.includes('/~gitbook/image')) {
+        return SizableImageAction.Skip;
+    }
+    if (parsed.pathname.endsWith('.svg') || parsed.pathname.endsWith('.avif')) {
+        return SizableImageAction.Passthrough;
     }
 
-    return true;
+    return SizableImageAction.Resize;
 }
diff --git a/packages/gitbook-v2/src/lib/images/createImageResizer.ts b/packages/gitbook-v2/src/lib/images/createImageResizer.ts
index 703fdadd62..8507a7aefd 100644
--- a/packages/gitbook-v2/src/lib/images/createImageResizer.ts
+++ b/packages/gitbook-v2/src/lib/images/createImageResizer.ts
@@ -1,7 +1,7 @@
 import 'server-only';
 import { GITBOOK_IMAGE_RESIZE_SIGNING_KEY, GITBOOK_IMAGE_RESIZE_URL } from '../env';
 import type { GitBookLinker } from '../links';
-import { checkIsSizableImageURL } from './checkIsSizableImageURL';
+import { SizableImageAction, checkIsSizableImageURL } from './checkIsSizableImageURL';
 import { getImageSize } from './resizer';
 import { type SignatureVersion, generateImageSignature } from './signatures';
 import type { ImageResizer } from './types';
@@ -24,7 +24,7 @@ export function createImageResizer({
 
     return {
         getResizedImageURL: (urlInput) => {
-            if (!checkIsSizableImageURL(urlInput)) {
+            if (checkIsSizableImageURL(urlInput) === SizableImageAction.Skip) {
                 return null;
             }
 
@@ -64,7 +64,7 @@ export function createImageResizer({
         },
 
         getImageSize: async (input, options) => {
-            if (!checkIsSizableImageURL(input)) {
+            if (checkIsSizableImageURL(input) !== SizableImageAction.Resize) {
                 return null;
             }
 
diff --git a/packages/gitbook-v2/src/lib/images/resizer/resizeImage.ts b/packages/gitbook-v2/src/lib/images/resizer/resizeImage.ts
index 4ed81dcd2b..8e656f3b70 100644
--- a/packages/gitbook-v2/src/lib/images/resizer/resizeImage.ts
+++ b/packages/gitbook-v2/src/lib/images/resizer/resizeImage.ts
@@ -1,7 +1,7 @@
 import 'server-only';
 import assertNever from 'assert-never';
 import { GITBOOK_IMAGE_RESIZE_MODE } from '../../env';
-import { checkIsSizableImageURL } from '../checkIsSizableImageURL';
+import { SizableImageAction, checkIsSizableImageURL } from '../checkIsSizableImageURL';
 import { resizeImageWithCDNCgi } from './cdn-cgi';
 import { resizeImageWithCFFetch } from './cf-fetch';
 import type { CloudflareImageJsonFormat, CloudflareImageOptions } from './types';
@@ -13,7 +13,7 @@ export async function getImageSize(
     input: string,
     defaultSize: Partial<CloudflareImageOptions> = {}
 ): Promise<{ width: number; height: number } | null> {
-    if (!checkIsSizableImageURL(input)) {
+    if (checkIsSizableImageURL(input) !== SizableImageAction.Resize) {
         return null;
     }
 
@@ -48,13 +48,17 @@ export async function resizeImage(
         signal?: AbortSignal;
     }
 ): Promise<Response> {
-    const parsed = new URL(input);
-    if (parsed.protocol === 'data:') {
-        throw new Error('Cannot resize data: URLs');
+    const action = checkIsSizableImageURL(input);
+    if (action === SizableImageAction.Skip) {
+        throw new Error(
+            'Cannot resize this image, this function should have never been called on this url'
+        );
     }
 
-    if (parsed.hostname === 'localhost') {
-        throw new Error('Cannot resize localhost URLs');
+    if (action === SizableImageAction.Passthrough) {
+        return fetch(input, {
+            signal: options.signal,
+        });
     }
 
     switch (GITBOOK_IMAGE_RESIZE_MODE) {
diff --git a/packages/gitbook/src/routes/image.ts b/packages/gitbook/src/routes/image.ts
index 45d590f311..3bc946ed90 100644
--- a/packages/gitbook/src/routes/image.ts
+++ b/packages/gitbook/src/routes/image.ts
@@ -2,6 +2,7 @@ import {
     CURRENT_SIGNATURE_VERSION,
     type CloudflareImageOptions,
     type SignatureVersion,
+    SizableImageAction,
     checkIsSizableImageURL,
     isSignatureVersion,
     parseImageAPIURL,
@@ -40,7 +41,7 @@ export async function serveResizedImage(
     // Check again if the image can be sized, even though we checked when rendering the Image component
     // Otherwise, it's possible to pass just any link to this endpoint and trigger HTML injection on the domain
     // Also prevent infinite redirects.
-    if (!checkIsSizableImageURL(url)) {
+    if (checkIsSizableImageURL(url) === SizableImageAction.Skip) {
         return new Response('Invalid url parameter', { status: 400 });
     }
 

From 56c923c0ed66e91b22d48f4cd304030a5497190e Mon Sep 17 00:00:00 2001
From: spastorelli <spastorelli@users.noreply.github.com>
Date: Fri, 9 May 2025 12:04:20 +0200
Subject: [PATCH 033/127] Adapt site resolution/lookup to pass visitor unsigned
 claims in addition to token (#3202)

Co-authored-by: taranvohra <taranvohra@outlook.com>
---
 bun.lock                                      |  32 ++--
 package.json                                  |   2 +-
 packages/cache-tags/package.json              |   2 +-
 packages/gitbook-v2/package.json              |  14 +-
 packages/gitbook-v2/src/lib/data/lookup.ts    | 115 ++++++++++-----
 packages/gitbook-v2/src/middleware.ts         |  27 +++-
 packages/gitbook/e2e/internal.spec.ts         |   2 +-
 packages/gitbook/package.json                 |   2 +-
 ...visitor-token.test.ts => visitors.test.ts} | 113 +++++++++++++-
 .../src/lib/{visitor-token.ts => visitors.ts} | 139 ++++++++++++++++++
 packages/gitbook/src/middleware.ts            |  24 +--
 packages/react-contentkit/package.json        |   2 +-
 12 files changed, 393 insertions(+), 81 deletions(-)
 rename packages/gitbook/src/lib/{visitor-token.test.ts => visitors.test.ts} (61%)
 rename packages/gitbook/src/lib/{visitor-token.ts => visitors.ts} (70%)

diff --git a/bun.lock b/bun.lock
index d720c44b84..6e27a46b06 100644
--- a/bun.lock
+++ b/bun.lock
@@ -26,7 +26,7 @@
       "name": "@gitbook/cache-tags",
       "version": "0.3.1",
       "dependencies": {
-        "@gitbook/api": "^0.111.0",
+        "@gitbook/api": "^0.115.0",
         "assert-never": "^1.2.1",
       },
       "devDependencies": {
@@ -51,7 +51,7 @@
       "name": "gitbook",
       "version": "0.11.1",
       "dependencies": {
-        "@gitbook/api": "*",
+        "@gitbook/api": "^0.115.0",
         "@gitbook/cache-do": "workspace:*",
         "@gitbook/cache-tags": "workspace:*",
         "@gitbook/colors": "workspace:*",
@@ -143,7 +143,7 @@
       "name": "gitbook-v2",
       "version": "0.2.5",
       "dependencies": {
-        "@gitbook/api": "*",
+        "@gitbook/api": "^0.115.0",
         "@gitbook/cache-tags": "workspace:*",
         "@sindresorhus/fnv1a": "^3.1.0",
         "assert-never": "^1.2.1",
@@ -202,7 +202,7 @@
       "name": "@gitbook/react-contentkit",
       "version": "0.7.0",
       "dependencies": {
-        "@gitbook/api": "*",
+        "@gitbook/api": "^0.115.0",
         "@gitbook/icons": "workspace:*",
         "classnames": "^2.5.1",
       },
@@ -260,7 +260,7 @@
   },
   "overrides": {
     "@codemirror/state": "6.4.1",
-    "@gitbook/api": "0.113.0",
+    "@gitbook/api": "^0.115.0",
     "react": "^19.0.0",
     "react-dom": "^19.0.0",
   },
@@ -625,7 +625,7 @@
 
     "@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="],
 
-    "@gitbook/api": ["@gitbook/api@0.113.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-PWMeAkdm4bHSl3b5OmtcmskZ6qRkkDhauCPybo8sGnjS03O14YAUtubAQiNCKX/uwbs+yiQ8KRPyeIwn+g42yw=="],
+    "@gitbook/api": ["@gitbook/api@0.115.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-Lyj+1WVNnE/Zuuqa/1ZdnUQfUiNE6es89RFK6CJ+Tb36TFwls6mbHKXCZsBwSYyoMYTVK39WQ3Nob6Nw6+TWCA=="],
 
     "@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"],
 
@@ -4077,7 +4077,7 @@
 
     "gaxios/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
 
-    "gitbook-v2/next": ["next@15.4.0-canary.7", "", { "dependencies": { "@next/env": "15.4.0-canary.7", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.0-canary.7", "@next/swc-darwin-x64": "15.4.0-canary.7", "@next/swc-linux-arm64-gnu": "15.4.0-canary.7", "@next/swc-linux-arm64-musl": "15.4.0-canary.7", "@next/swc-linux-x64-gnu": "15.4.0-canary.7", "@next/swc-linux-x64-musl": "15.4.0-canary.7", "@next/swc-win32-arm64-msvc": "15.4.0-canary.7", "@next/swc-win32-x64-msvc": "15.4.0-canary.7", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "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-ZYjT0iu+4osz8XIlr31MuoXaNQKRU75UcwEgNBt93gftoh6tzV2Mebz6sOGeVReYuYUvYlLJJksMBTNcFcPbSA=="],
+    "gitbook-v2/next": ["next@15.4.0-canary.26", "", { "dependencies": { "@next/env": "15.4.0-canary.26", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.0-canary.26", "@next/swc-darwin-x64": "15.4.0-canary.26", "@next/swc-linux-arm64-gnu": "15.4.0-canary.26", "@next/swc-linux-arm64-musl": "15.4.0-canary.26", "@next/swc-linux-x64-gnu": "15.4.0-canary.26", "@next/swc-linux-x64-musl": "15.4.0-canary.26", "@next/swc-win32-arm64-msvc": "15.4.0-canary.26", "@next/swc-win32-x64-msvc": "15.4.0-canary.26", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "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-0lq0x+H4ewc6vXth3S9shrcK3eYl+4wLXQqdboVwBbJe0ykB3+QbGdXFIEICCZsmbAOaii0ag0tzqD3y/vr3bw=="],
 
     "global-dirs/ini": ["ini@1.3.7", "", {}, "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ=="],
 
@@ -4969,23 +4969,23 @@
 
     "gaxios/https-proxy-agent/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
 
-    "gitbook-v2/next/@next/env": ["@next/env@15.4.0-canary.7", "", {}, "sha512-q8S7f2lQti3Y3gcAPzE8Pj8y0EwiWHVyyilMzoLbDPXGVfxlQhXLRiFdy2cDkKN4DyjGZWDeehEtw4huvJAa3Q=="],
+    "gitbook-v2/next/@next/env": ["@next/env@15.4.0-canary.26", "", {}, "sha512-+WeMYRfTZWaosbIAjuNESPVjynDz/NKukoR7mF/u3Wuwr40KgScpxD0IuU0T7XbPfprnaInSKAylufFvrXRh+A=="],
 
-    "gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.4.0-canary.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+TMxUu5CAWNe+UFRc47BZAXQxCRqZfVbGyCldddiog4MorvL7kBxSd1qlmrwI73fRRKtXkHIH1TaeItyxzC9rQ=="],
+    "gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.4.0-canary.26", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HxtmV8Uoai8Z4wAU1tFWzASogAS+xVVP5Z5frbFu0yQ+1ocb9xQTjNqhiD5xPSAU8pNGWasCod8tlTCBzJzHQg=="],
 
-    "gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.4.0-canary.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-veXp8lg/X/7O+pG9BDQ3OizFz3B40v29jsvEWj+ULY/W8Z6+dCSd5XPP2M8fG/gKKKA0D6L0CnnM2Mj0RRSUJw=="],
+    "gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.4.0-canary.26", "", { "os": "darwin", "cpu": "x64" }, "sha512-1MLiD1Bj6xSi5MkkQ8IK7A13KZJG9bzoWqdXT/tveVCinmYrl/zY7z/9dgvG+84gAE6uN4BGjp6f3IxRsvYDBA=="],
 
-    "gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.4.0-canary.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-KxNGfW7BO0Z5B9rJyl9p7YVjNrxAhu06mH6h1PSdouZG7YMYpdRCconVXeuBI0PEu6g3ywNrOVxZUk1V6G5u0Q=="],
+    "gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.4.0-canary.26", "", { "os": "linux", "cpu": "arm64" }, "sha512-cIVgFgOdMDbnPixR/u3ICW60/HlnDbACCb2O+p9+DJj7s1dsN63Cs9qxc9pDJb7tgL0BFPhYcmGeJfd/bZ4h7w=="],
 
-    "gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.4.0-canary.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-THgXgmP/cC4DsNwvC6uqB90CebB7Ep1KyZajQL3fYKT5V4SWr46yngKLyoyJVeAYWJH908MrWddf7Ya/Zq7cyg=="],
+    "gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.4.0-canary.26", "", { "os": "linux", "cpu": "arm64" }, "sha512-o4YS6E3FD2DpZBDvUai9bPMLcpcNZ3THc2BzysSbZeARPiAQuKoudwPJoCpi2t7vajrvczpxBwTPG2uL05ypEA=="],
 
-    "gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.4.0-canary.7", "", { "os": "linux", "cpu": "x64" }, "sha512-kpLB3Jj7fProynQYj2ahFyZlJs0xwm71VzCVrNRu6u7qJGXn6dK5h7+hro8y/y1iqjXWgCLSdxWSHahhWK8XdQ=="],
+    "gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.4.0-canary.26", "", { "os": "linux", "cpu": "x64" }, "sha512-M2/MFrQcPI7Ul5Fq5AOeoARrT0B9SrGiy7BLnPuE7Iai1+xkhfSsxIMF5JeDm/GfJnzcwA2oSvrOg0e7KKdaCA=="],
 
-    "gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.4.0-canary.7", "", { "os": "linux", "cpu": "x64" }, "sha512-rnGAKvl4cWPVV9D+SybWOGijm0VmKXyqQ+IN0A6WDgdlYZAZP0ZnJv/rq7DSvuOh19AXS8UpQc88SelXV/3j3Q=="],
+    "gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.4.0-canary.26", "", { "os": "linux", "cpu": "x64" }, "sha512-p5JpQ7k/1LyBzNZglqA8JJm7GRmadPkTyHoWaqMxhiVdcQHGbjwsiNjjAtMNjetNOXxj8ebxjiBsAt+34Ak1IQ=="],
 
-    "gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.4.0-canary.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-/PRbn//EuR3UGiquk050gqvjxLliEgGBy1Cx9KkpAT7szaHOBj1mDDQmxMTEhRex4i3YfKGJXWn5mLMCveya6Q=="],
+    "gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.4.0-canary.26", "", { "os": "win32", "cpu": "arm64" }, "sha512-FlXIBNOSwnGxxN+HekUfz4Y0n4gPGzqcY3wa3p+5JhzFT7r0oCxMxOdRbs7w8jF5b6uSkWVIQXWFL43F6+8J4g=="],
 
-    "gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.4.0-canary.7", "", { "os": "win32", "cpu": "x64" }, "sha512-7a92XL+DlrbWyycCpQjjQMHOrsA0p+VvS7iA2dyi89Xsq0qtOPzFH0Gb56fsjh6M6BQGFhboOSzjmpjlkMTilQ=="],
+    "gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.4.0-canary.26", "", { "os": "win32", "cpu": "x64" }, "sha512-h9CKrDiEeBof+8IgHStYATYrKVuUt8ggy6429kViWlDbuY6gkuIplf3IRlfpdWAB32I1e4qqUVl/s2xRMgQdqg=="],
 
     "gitbook-v2/next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
 
diff --git a/package.json b/package.json
index 00f323e2f4..447997ab82 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
     "packageManager": "bun@1.2.8",
     "overrides": {
         "@codemirror/state": "6.4.1",
-        "@gitbook/api": "0.113.0",
+        "@gitbook/api": "^0.115.0",
         "react": "^19.0.0",
         "react-dom": "^19.0.0"
     },
diff --git a/packages/cache-tags/package.json b/packages/cache-tags/package.json
index 3d25aa8f48..8850dafbd6 100644
--- a/packages/cache-tags/package.json
+++ b/packages/cache-tags/package.json
@@ -10,7 +10,7 @@
     },
     "version": "0.3.1",
     "dependencies": {
-        "@gitbook/api": "^0.111.0",
+        "@gitbook/api": "^0.115.0",
         "assert-never": "^1.2.1"
     },
     "devDependencies": {
diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json
index 50cf96a0d7..f19b6951d8 100644
--- a/packages/gitbook-v2/package.json
+++ b/packages/gitbook-v2/package.json
@@ -3,17 +3,17 @@
     "version": "0.2.5",
     "private": true,
     "dependencies": {
+        "@gitbook/api": "^0.115.0",
+        "@gitbook/cache-tags": "workspace:*",
+        "@sindresorhus/fnv1a": "^3.1.0",
+        "assert-never": "^1.2.1",
+        "jwt-decode": "^4.0.0",
         "next": "canary",
         "react": "^19.0.0",
         "react-dom": "^19.0.0",
-        "@gitbook/api": "*",
-        "@gitbook/cache-tags": "workspace:*",
-        "@sindresorhus/fnv1a": "^3.1.0",
-        "server-only": "^0.0.1",
-        "warn-once": "^0.1.1",
         "rison": "^0.1.1",
-        "jwt-decode": "^4.0.0",
-        "assert-never": "^1.2.1"
+        "server-only": "^0.0.1",
+        "warn-once": "^0.1.1"
     },
     "devDependencies": {
         "gitbook": "*",
diff --git a/packages/gitbook-v2/src/lib/data/lookup.ts b/packages/gitbook-v2/src/lib/data/lookup.ts
index 498f6e8859..4c999bd7a4 100644
--- a/packages/gitbook-v2/src/lib/data/lookup.ts
+++ b/packages/gitbook-v2/src/lib/data/lookup.ts
@@ -1,55 +1,102 @@
 import { race, tryCatch } from '@/lib/async';
 import { joinPath, joinPathWithBaseURL } from '@/lib/paths';
 import { trace } from '@/lib/tracing';
-import type { PublishedSiteContentLookup } from '@gitbook/api';
+import type { GitBookAPI, PublishedSiteContentLookup, SiteVisitorPayload } from '@gitbook/api';
 import { apiClient } from './api';
 import { getExposableError } from './errors';
 import type { DataFetcherResponse } from './types';
 import { getURLLookupAlternatives, stripURLSearch } from './urls';
 
+interface LookupPublishedContentByUrlInput {
+    url: string;
+    redirectOnError: boolean;
+    apiToken: string | null;
+    visitorPayload: SiteVisitorPayload;
+}
+
+/**
+ * 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 API.
+ * 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: {
+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<T> = ReturnType<typeof tryCatch<T>>;
+
+async function lookupPublishedContentByUrl(input: {
     url: string;
-    visitorAuthToken: string | null;
-    redirectOnError: boolean;
-    apiToken: string | null;
+    fetchLookupAPIResult: (args: {
+        url: string;
+        signal: AbortSignal;
+    }) => TryCatch<Awaited<ReturnType<GitBookAPI['urls']['resolvePublishedContentByUrl']>>>;
 }): Promise<DataFetcherResponse<PublishedSiteContentLookup>> {
     const lookupURL = new URL(input.url);
     const url = stripURLSearch(lookupURL);
     const lookup = getURLLookupAlternatives(url);
 
     const result = await race(lookup.urls, async (alternative, { signal }) => {
-        const api = await apiClient({ apiToken: input.apiToken });
-
-        const callResult = await trace(
-            {
-                operation: 'getPublishedContentByURL',
-                name: alternative.url,
-            },
-            () =>
-                tryCatch(
-                    api.urls.getPublishedContentByUrl(
-                        {
-                            url: alternative.url,
-                            visitorAuthToken: input.visitorAuthToken ?? undefined,
-                            redirectOnError: input.redirectOnError,
-
-                            // As this endpoint is cached by our API, we version the request
-                            // to void getting stale data with missing properties.
-                            // this could be improved by ensuring our API cache layer is versioned
-                            // or invalidated when needed
-                            // @ts-expect-error - cacheVersion is not a real query param
-                            cacheVersion: 'v2',
-                        },
-                        {
-                            signal,
-                        }
-                    )
-                )
-        );
+        const callResult = await input.fetchLookupAPIResult({
+            url: alternative.url,
+            signal,
+        });
 
         if (callResult.error) {
             if (alternative.primary) {
diff --git a/packages/gitbook-v2/src/middleware.ts b/packages/gitbook-v2/src/middleware.ts
index dd983b536a..5cb34052f8 100644
--- a/packages/gitbook-v2/src/middleware.ts
+++ b/packages/gitbook-v2/src/middleware.ts
@@ -10,15 +10,16 @@ import {
     type ResponseCookies,
     getPathScopedCookieName,
     getResponseCookiesForVisitorAuth,
-    getVisitorToken,
+    getVisitorPayload,
     normalizeVisitorAuthURL,
-} from '@/lib/visitor-token';
+} from '@/lib/visitors';
 import { serveResizedImage } from '@/routes/image';
 import {
     DataFetcherError,
     getPublishedContentByURL,
     getVisitorAuthBasePath,
     normalizeURL,
+    resolvePublishedContentByUrl,
     throwIfDataError,
 } from '@v2/lib/data';
 import { isGitBookAssetsHostURL, isGitBookHostURL } from '@v2/lib/env';
@@ -33,6 +34,15 @@ 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',
+    'adaptive-docs.gitbook-staging.com',
+    'enriched-content-playground.gitbook-staging.io',
+];
+
 export async function middleware(request: NextRequest) {
     try {
         const requestURL = new URL(request.url);
@@ -85,17 +95,22 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
     //
     // Detect and extract the visitor authentication token from the request
     //
-    // @ts-ignore - request typing
-    const visitorToken = getVisitorToken({
+    const { visitorToken, unsignedClaims } = getVisitorPayload({
         cookies: request.cookies.getAll(),
         url: siteRequestURL,
     });
 
     const withAPIToken = async (apiToken: string | null) => {
+        const resolve = ADAPTIVE_CONTENT_HOSTS.includes(siteRequestURL.hostname)
+            ? resolvePublishedContentByUrl
+            : getPublishedContentByURL;
         const siteURLData = await throwIfDataError(
-            getPublishedContentByURL({
+            resolve({
                 url: siteRequestURL.toString(),
-                visitorAuthToken: visitorToken?.token ?? null,
+                visitorPayload: {
+                    jwtToken: visitorToken?.token ?? undefined,
+                    unsignedClaims,
+                },
                 // When the visitor auth token is pulled from the cookie, set redirectOnError when calling getPublishedContentByUrl to allow
                 // redirecting when the token is invalid as we could be dealing with stale token stored in the cookie.
                 // For example when the VA backend signature has changed but the token stored in the cookie is not yet expired.
diff --git a/packages/gitbook/e2e/internal.spec.ts b/packages/gitbook/e2e/internal.spec.ts
index d5e49b705f..83181c3c9a 100644
--- a/packages/gitbook/e2e/internal.spec.ts
+++ b/packages/gitbook/e2e/internal.spec.ts
@@ -12,7 +12,7 @@ import {
     VISITOR_TOKEN_COOKIE,
     getVisitorAuthCookieName,
     getVisitorAuthCookieValue,
-} from '@/lib/visitor-token';
+} from '@/lib/visitors';
 
 import { getSiteAPIToken } from '../tests/utils';
 import {
diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json
index 5d0939b249..4294327546 100644
--- a/packages/gitbook/package.json
+++ b/packages/gitbook/package.json
@@ -16,7 +16,7 @@
         "clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math"
     },
     "dependencies": {
-        "@gitbook/api": "*",
+        "@gitbook/api": "^0.115.0",
         "@gitbook/cache-do": "workspace:*",
         "@gitbook/cache-tags": "workspace:*",
         "@gitbook/colors": "workspace:*",
diff --git a/packages/gitbook/src/lib/visitor-token.test.ts b/packages/gitbook/src/lib/visitors.test.ts
similarity index 61%
rename from packages/gitbook/src/lib/visitor-token.test.ts
rename to packages/gitbook/src/lib/visitors.test.ts
index a1f07d2504..82060fcdfa 100644
--- a/packages/gitbook/src/lib/visitor-token.test.ts
+++ b/packages/gitbook/src/lib/visitors.test.ts
@@ -6,7 +6,8 @@ import {
     getVisitorAuthCookieName,
     getVisitorAuthCookieValue,
     getVisitorToken,
-} from './visitor-token';
+    getVisitorUnsignedClaims,
+} from './visitors';
 
 describe('getVisitorAuthToken', () => {
     it('should return the token from the query parameters', () => {
@@ -158,3 +159,113 @@ function assertVisitorAuthCookieValue(
 
     throw new Error('Expected a VisitorAuthCookieValue');
 }
+
+describe('getVisitorUnsignedClaims', () => {
+    it('should merge claims from multiple public cookies', () => {
+        const cookies = [
+            {
+                name: 'gitbook-visitor-public-bucket',
+                value: JSON.stringify({ bucket: { flags: { SITE_AI: true, SITE_PREVIEW: true } } }),
+            },
+            {
+                name: 'gitbook-visitor-public-launchdarkly',
+                value: JSON.stringify({
+                    launchdarkly: { flags: { ALPHA: true, API: true } },
+                }),
+            },
+        ];
+
+        const url = new URL('https://example.com/');
+        const claims = getVisitorUnsignedClaims({ cookies, url });
+
+        expect(claims).toStrictEqual({
+            bucket: { flags: { SITE_AI: true, SITE_PREVIEW: true } },
+            launchdarkly: { flags: { ALPHA: true, API: true } },
+        });
+    });
+
+    it('should parse visitor.* query params with simple types', () => {
+        const url = new URL(
+            'https://example.com/?visitor.isEnterprise=true&visitor.language=fr&visitor.country=fr'
+        );
+
+        const claims = getVisitorUnsignedClaims({ cookies: [], url });
+
+        expect(claims).toStrictEqual({
+            isEnterprise: true,
+            language: 'fr',
+            country: 'fr',
+        });
+    });
+
+    it('should ignore params that do not match visitor.* convention', () => {
+        const url = new URL('https://example.com/?visitor.isEnterprise=true&otherParam=true');
+
+        const claims = getVisitorUnsignedClaims({ cookies: [], url });
+
+        expect(claims).toStrictEqual({
+            isEnterprise: true,
+            // otherParam is not present
+        });
+    });
+
+    it('should support nested query param keys via dot notation', () => {
+        const url = new URL(
+            'https://example.com/?visitor.isEnterprise=true&visitor.flags.ALPHA=true&visitor.flags.API=false'
+        );
+
+        const claims = getVisitorUnsignedClaims({ cookies: [], url });
+
+        expect(claims).toStrictEqual({
+            isEnterprise: true,
+            flags: {
+                ALPHA: true,
+                API: false,
+            },
+        });
+    });
+
+    it('should ignore invalid JSON in cookie values', () => {
+        const cookies = [
+            {
+                name: 'gitbook-visitor-public',
+                value: '{not: "json"}',
+            },
+        ];
+        const url = new URL('https://example.com/');
+        const claims = getVisitorUnsignedClaims({ cookies, url });
+
+        expect(claims).toStrictEqual({});
+    });
+
+    it('should merge claims from cookies and visitor.* query params', () => {
+        const cookies = [
+            {
+                name: 'gitbook-visitor-public',
+                value: JSON.stringify({ role: 'admin', language: 'fr' }),
+            },
+            {
+                name: 'gitbook-visitor-public-bucket',
+                value: JSON.stringify({ bucket: { flags: { SITE_AI: true, SITE_PREVIEW: true } } }),
+            },
+        ];
+        const url = new URL(
+            'https://example.com/?visitor.isEnterprise=true&visitor.flags.ALPHA=true&visitor.flags.API=false'
+        );
+
+        const claims = getVisitorUnsignedClaims({ cookies, url });
+
+        expect(claims).toStrictEqual({
+            role: 'admin',
+            language: 'fr',
+            bucket: {
+                flags: { SITE_AI: true, SITE_PREVIEW: true },
+            },
+            isEnterprise: true,
+            flags: {
+                ALPHA: true,
+                API: false,
+            },
+        });
+    });
+});
diff --git a/packages/gitbook/src/lib/visitor-token.ts b/packages/gitbook/src/lib/visitors.ts
similarity index 70%
rename from packages/gitbook/src/lib/visitor-token.ts
rename to packages/gitbook/src/lib/visitors.ts
index 64f0c97931..c6a7aae986 100644
--- a/packages/gitbook/src/lib/visitor-token.ts
+++ b/packages/gitbook/src/lib/visitors.ts
@@ -4,6 +4,7 @@ import hash from 'object-hash';
 
 const VISITOR_AUTH_PARAM = 'jwt_token';
 export const VISITOR_TOKEN_COOKIE = 'gitbook-visitor-token';
+const VISITOR_UNSIGNED_CLAIMS_PREFIX = 'gitbook-visitor-public';
 
 /**
  * Typing for a cookie, matching the internal type of Next.js.
@@ -30,6 +31,25 @@ type VisitorAuthCookieValue = {
     token: string;
 };
 
+type ClaimPrimitive =
+    | string
+    | number
+    | boolean
+    | null
+    | undefined
+    | { [key: string]: ClaimPrimitive }
+    | ClaimPrimitive[];
+
+/**
+ * The result of a visitor info lookup that can include:
+ *   - a visitor token (JWT)
+ *   - a record of visitor public/unsigned claims (JSON object)
+ */
+export type VisitorPayloadLookup = {
+    visitorToken: VisitorTokenLookup;
+    unsignedClaims: Record<string, ClaimPrimitive>;
+};
+
 /**
  * The result of a visitor token lookup.
  */
@@ -53,6 +73,25 @@ export type VisitorTokenLookup =
     /** Not visitor token was found */
     | undefined;
 
+/**
+ * Get the visitor info for the request including its token and/or unsigned claims when present.
+ */
+export function getVisitorPayload({
+    cookies,
+    url,
+}: {
+    cookies: RequestCookies;
+    url: URL | NextRequest['nextUrl'];
+}): VisitorPayloadLookup {
+    const visitorToken = getVisitorToken({ cookies, url });
+    const unsignedClaims = getVisitorUnsignedClaims({ cookies, url });
+
+    return {
+        visitorToken,
+        unsignedClaims,
+    };
+}
+
 /**
  * Get the visitor token for the request. This token can either be in the
  * query parameters or stored as a cookie.
@@ -82,6 +121,106 @@ export function getVisitorToken({
     }
 }
 
+/**
+ * Get the visitor unsigned/public claims for the request. They can either be in `visitor.` query
+ * parameters or stored in special `gitbook-visitor-public-*` cookies.
+ */
+export function getVisitorUnsignedClaims(args: {
+    cookies: RequestCookies;
+    url: URL | NextRequest['nextUrl'];
+}): Record<string, ClaimPrimitive> {
+    const { cookies, url } = args;
+    const claims: Record<string, ClaimPrimitive> = {};
+
+    for (const cookie of cookies) {
+        if (cookie.name.startsWith(VISITOR_UNSIGNED_CLAIMS_PREFIX)) {
+            try {
+                const parsed = JSON.parse(cookie.value);
+                if (typeof parsed === 'object' && parsed !== null) {
+                    Object.assign(claims, parsed);
+                }
+            } catch (_err) {
+                console.warn(`Invalid JSON in unsigned claim cookie "${cookie.name}"`);
+            }
+        }
+    }
+
+    for (const [key, value] of url.searchParams.entries()) {
+        if (key.startsWith('visitor.')) {
+            const claimPath = key.substring('visitor.'.length);
+            const claimValue = parseVisitorQueryParamValue(value);
+            setVisitorClaimByPath(claims, claimPath, claimValue);
+        }
+    }
+
+    return claims;
+}
+
+/**
+ * Set the value of claims in a claims object at a specific path.
+ */
+function setVisitorClaimByPath(
+    claims: Record<string, ClaimPrimitive>,
+    keyPath: string,
+    value: ClaimPrimitive
+): void {
+    const keys = keyPath.split('.');
+    let current = claims;
+
+    for (let index = 0; index < keys.length; index++) {
+        const key = keys[index];
+
+        if (index === keys.length - 1) {
+            current[key] = value;
+        } else {
+            if (!(key in current) || !isClaimPrimitiveObject(current[key])) {
+                current[key] = {};
+            }
+
+            current = current[key];
+        }
+    }
+}
+
+function isClaimPrimitiveObject(value: unknown): value is Record<string, ClaimPrimitive> {
+    return typeof value === 'object' && value !== null;
+}
+
+/**
+ * Parse the value expected in a `visitor.` URL query parameter.
+ */
+function parseVisitorQueryParamValue(value: string): ClaimPrimitive {
+    if (value === 'true') {
+        return true;
+    }
+
+    if (value === 'false') {
+        return false;
+    }
+
+    if (value === 'null') {
+        return null;
+    }
+
+    if (value === 'undefined') {
+        return undefined;
+    }
+
+    const num = Number(value);
+    if (!Number.isNaN(num) && value.trim() !== '') {
+        return num;
+    }
+
+    try {
+        const parsed = JSON.parse(value);
+        if (typeof parsed === 'object' && parsed !== null) {
+            return parsed;
+        }
+    } catch {}
+
+    return value;
+}
+
 /**
  * Return the lookup result for content served with visitor auth.
  */
diff --git a/packages/gitbook/src/middleware.ts b/packages/gitbook/src/middleware.ts
index 30f7b44e6a..04b1b40dd9 100644
--- a/packages/gitbook/src/middleware.ts
+++ b/packages/gitbook/src/middleware.ts
@@ -24,9 +24,9 @@ import {
     type ResponseCookies,
     type VisitorTokenLookup,
     getResponseCookiesForVisitorAuth,
-    getVisitorToken,
+    getVisitorPayload,
     normalizeVisitorAuthURL,
-} from '@/lib/visitor-token';
+} from '@/lib/visitors';
 
 import { joinPath, withLeadingSlash } from '@/lib/paths';
 import { getProxyModeBasePath } from '@/lib/proxy';
@@ -392,17 +392,17 @@ async function lookupSiteInProxy(request: NextRequest, url: URL): Promise<Lookup
  * When serving multi spaces based on the current URL.
  */
 async function lookupSiteInMultiMode(request: NextRequest, url: URL): Promise<LookupResult> {
-    const visitorAuthToken = getVisitorToken({
+    const { visitorToken } = getVisitorPayload({
         cookies: request.cookies.getAll(),
         url,
     });
-    const lookup = await lookupSiteByAPI(url, visitorAuthToken);
+    const lookup = await lookupSiteByAPI(url, visitorToken);
     return {
         ...lookup,
-        ...('basePath' in lookup && visitorAuthToken
-            ? getLookupResultForVisitorAuth(lookup.basePath, visitorAuthToken)
+        ...('basePath' in lookup && visitorToken
+            ? getLookupResultForVisitorAuth(lookup.basePath, visitorToken)
             : {}),
-        visitorToken: visitorAuthToken?.token,
+        visitorToken: visitorToken?.token,
     };
 }
 
@@ -609,12 +609,12 @@ async function lookupSiteInMultiPathMode(request: NextRequest, url: URL): Promis
     const target = new URL(targetStr);
     target.search = url.search;
 
-    const visitorAuthToken = getVisitorToken({
+    const { visitorToken } = getVisitorPayload({
         cookies: request.cookies.getAll(),
         url: target,
     });
 
-    const lookup = await lookupSiteByAPI(target, visitorAuthToken);
+    const lookup = await lookupSiteByAPI(target, visitorToken);
     if ('error' in lookup) {
         return lookup;
     }
@@ -641,10 +641,10 @@ async function lookupSiteInMultiPathMode(request: NextRequest, url: URL): Promis
         ...lookup,
         siteBasePath: joinPath(target.host, lookup.siteBasePath),
         basePath: joinPath(target.host, lookup.basePath),
-        ...('basePath' in lookup && visitorAuthToken
-            ? getLookupResultForVisitorAuth(lookup.basePath, visitorAuthToken)
+        ...('basePath' in lookup && visitorToken
+            ? getLookupResultForVisitorAuth(lookup.basePath, visitorToken)
             : {}),
-        visitorToken: visitorAuthToken?.token,
+        visitorToken: visitorToken?.token,
     };
 }
 
diff --git a/packages/react-contentkit/package.json b/packages/react-contentkit/package.json
index d2c8bd8ee7..09e6cc8e7c 100644
--- a/packages/react-contentkit/package.json
+++ b/packages/react-contentkit/package.json
@@ -10,7 +10,7 @@
     },
     "dependencies": {
         "classnames": "^2.5.1",
-        "@gitbook/api": "*",
+        "@gitbook/api": "^0.115.0",
         "@gitbook/icons": "workspace:*"
     },
     "peerDependencies": {

From 0c973a32925d6cc58c172d44f02c8656264bbf98 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Greg=20Berg=C3=A9?= <berge.greg@gmail.com>
Date: Fri, 9 May 2025 12:31:52 +0200
Subject: [PATCH 034/127] Always link main logo to the root of the site (#3226)

---
 .changeset/tough-starfishes-grin.md                   | 5 +++++
 packages/gitbook/src/components/Header/HeaderLogo.tsx | 2 +-
 2 files changed, 6 insertions(+), 1 deletion(-)
 create mode 100644 .changeset/tough-starfishes-grin.md

diff --git a/.changeset/tough-starfishes-grin.md b/.changeset/tough-starfishes-grin.md
new file mode 100644
index 0000000000..2f150b59d8
--- /dev/null
+++ b/.changeset/tough-starfishes-grin.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Always link main logo to the root of the site
diff --git a/packages/gitbook/src/components/Header/HeaderLogo.tsx b/packages/gitbook/src/components/Header/HeaderLogo.tsx
index 08a1d2d10a..64c159472f 100644
--- a/packages/gitbook/src/components/Header/HeaderLogo.tsx
+++ b/packages/gitbook/src/components/Header/HeaderLogo.tsx
@@ -20,7 +20,7 @@ export async function HeaderLogo(props: HeaderLogoProps) {
 
     return (
         <Link
-            href={linker.toAbsoluteURL(linker.toPathInSpace(''))}
+            href={linker.toAbsoluteURL(linker.toPathInSite(''))}
             className={tcls('group/headerlogo', 'min-w-0', 'shrink', 'flex', 'items-center')}
         >
             {customization.header.logo ? (

From aed79fd6c36a088e5bbad58f1f63bdb6fc8b37b8 Mon Sep 17 00:00:00 2001
From: Zeno Kapitein <zeno@gitbook.io>
Date: Fri, 9 May 2025 14:45:38 +0200
Subject: [PATCH 035/127] Remove header logo rounding (#3225)

---
 .changeset/fifty-pots-work.md                         | 5 +++++
 packages/gitbook/src/components/Header/HeaderLogo.tsx | 2 --
 2 files changed, 5 insertions(+), 2 deletions(-)
 create mode 100644 .changeset/fifty-pots-work.md

diff --git a/.changeset/fifty-pots-work.md b/.changeset/fifty-pots-work.md
new file mode 100644
index 0000000000..0ba6550278
--- /dev/null
+++ b/.changeset/fifty-pots-work.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Decrease rounding of header logo
diff --git a/packages/gitbook/src/components/Header/HeaderLogo.tsx b/packages/gitbook/src/components/Header/HeaderLogo.tsx
index 64c159472f..72d0b4798b 100644
--- a/packages/gitbook/src/components/Header/HeaderLogo.tsx
+++ b/packages/gitbook/src/components/Header/HeaderLogo.tsx
@@ -48,8 +48,6 @@ export async function HeaderLogo(props: HeaderLogoProps) {
                     ]}
                     priority="high"
                     style={tcls(
-                        'rounded',
-                        'straight-corners:rounded-sm',
                         'overflow-hidden',
                         'shrink',
                         'min-w-0',

From e15757d01fec3ea006b253d580a6d06115da1b7e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Fri, 9 May 2025 15:17:49 +0200
Subject: [PATCH 036/127] Use next@15.3.2 instead of canary (#3227)

---
 .changeset/modern-windows-type.md     |  5 +++++
 .github/workflows/deploy-preview.yaml |  2 --
 bun.lock                              | 22 +++++++++++-----------
 packages/gitbook-v2/package.json      |  2 +-
 4 files changed, 17 insertions(+), 14 deletions(-)
 create mode 100644 .changeset/modern-windows-type.md

diff --git a/.changeset/modern-windows-type.md b/.changeset/modern-windows-type.md
new file mode 100644
index 0000000000..d2135d1c28
--- /dev/null
+++ b/.changeset/modern-windows-type.md
@@ -0,0 +1,5 @@
+---
+"gitbook-v2": patch
+---
+
+Fix crash on Cloudflare by using latest stable version of Next.js instead of canary
diff --git a/.github/workflows/deploy-preview.yaml b/.github/workflows/deploy-preview.yaml
index 156c1cfecc..4b14e3f83d 100644
--- a/.github/workflows/deploy-preview.yaml
+++ b/.github/workflows/deploy-preview.yaml
@@ -200,7 +200,6 @@ jobs:
         name: Visual Testing v2 (Cloudflare)
         needs: deploy-v2-cloudflare
         timeout-minutes: 10
-        if: startsWith(github.head_ref || github.ref_name, 'cloudflare/') || github.ref == 'refs/heads/main'
         steps:
             - name: Checkout
               uses: actions/checkout@v4
@@ -263,7 +262,6 @@ jobs:
         name: Visual Testing Customers v2 (Cloudflare)
         needs: deploy-v2-cloudflare
         timeout-minutes: 8
-        if: startsWith(github.head_ref || github.ref_name, 'cloudflare/') || github.ref == 'refs/heads/main'
         steps:
             - name: Checkout
               uses: actions/checkout@v4
diff --git a/bun.lock b/bun.lock
index 6e27a46b06..46ac2592dd 100644
--- a/bun.lock
+++ b/bun.lock
@@ -148,7 +148,7 @@
         "@sindresorhus/fnv1a": "^3.1.0",
         "assert-never": "^1.2.1",
         "jwt-decode": "^4.0.0",
-        "next": "canary",
+        "next": "^15.3.2",
         "react": "^19.0.0",
         "react-dom": "^19.0.0",
         "rison": "^0.1.1",
@@ -4077,7 +4077,7 @@
 
     "gaxios/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
 
-    "gitbook-v2/next": ["next@15.4.0-canary.26", "", { "dependencies": { "@next/env": "15.4.0-canary.26", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.0-canary.26", "@next/swc-darwin-x64": "15.4.0-canary.26", "@next/swc-linux-arm64-gnu": "15.4.0-canary.26", "@next/swc-linux-arm64-musl": "15.4.0-canary.26", "@next/swc-linux-x64-gnu": "15.4.0-canary.26", "@next/swc-linux-x64-musl": "15.4.0-canary.26", "@next/swc-win32-arm64-msvc": "15.4.0-canary.26", "@next/swc-win32-x64-msvc": "15.4.0-canary.26", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "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-0lq0x+H4ewc6vXth3S9shrcK3eYl+4wLXQqdboVwBbJe0ykB3+QbGdXFIEICCZsmbAOaii0ag0tzqD3y/vr3bw=="],
+    "gitbook-v2/next": ["next@15.3.2", "", { "dependencies": { "@next/env": "15.3.2", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.3.2", "@next/swc-darwin-x64": "15.3.2", "@next/swc-linux-arm64-gnu": "15.3.2", "@next/swc-linux-arm64-musl": "15.3.2", "@next/swc-linux-x64-gnu": "15.3.2", "@next/swc-linux-x64-musl": "15.3.2", "@next/swc-win32-arm64-msvc": "15.3.2", "@next/swc-win32-x64-msvc": "15.3.2", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "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-CA3BatMyHkxZ48sgOCLdVHjFU36N7TF1HhqAHLFOkV6buwZnvMI84Cug8xD56B9mCuKrqXnLn94417GrZ/jjCQ=="],
 
     "global-dirs/ini": ["ini@1.3.7", "", {}, "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ=="],
 
@@ -4969,23 +4969,23 @@
 
     "gaxios/https-proxy-agent/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
 
-    "gitbook-v2/next/@next/env": ["@next/env@15.4.0-canary.26", "", {}, "sha512-+WeMYRfTZWaosbIAjuNESPVjynDz/NKukoR7mF/u3Wuwr40KgScpxD0IuU0T7XbPfprnaInSKAylufFvrXRh+A=="],
+    "gitbook-v2/next/@next/env": ["@next/env@15.3.2", "", {}, "sha512-xURk++7P7qR9JG1jJtLzPzf0qEvqCN0A/T3DXf8IPMKo9/6FfjxtEffRJIIew/bIL4T3C2jLLqBor8B/zVlx6g=="],
 
-    "gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.4.0-canary.26", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HxtmV8Uoai8Z4wAU1tFWzASogAS+xVVP5Z5frbFu0yQ+1ocb9xQTjNqhiD5xPSAU8pNGWasCod8tlTCBzJzHQg=="],
+    "gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2DR6kY/OGcokbnCsjHpNeQblqCZ85/1j6njYSkzRdpLn5At7OkSdmk7WyAmB9G0k25+VgqVZ/u356OSoQZ3z0g=="],
 
-    "gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.4.0-canary.26", "", { "os": "darwin", "cpu": "x64" }, "sha512-1MLiD1Bj6xSi5MkkQ8IK7A13KZJG9bzoWqdXT/tveVCinmYrl/zY7z/9dgvG+84gAE6uN4BGjp6f3IxRsvYDBA=="],
+    "gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ro/fdqaZWL6k1S/5CLv1I0DaZfDVJkWNaUU3un8Lg6m0YENWlDulmIWzV96Iou2wEYyEsZq51mwV8+XQXqMp3w=="],
 
-    "gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.4.0-canary.26", "", { "os": "linux", "cpu": "arm64" }, "sha512-cIVgFgOdMDbnPixR/u3ICW60/HlnDbACCb2O+p9+DJj7s1dsN63Cs9qxc9pDJb7tgL0BFPhYcmGeJfd/bZ4h7w=="],
+    "gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-covwwtZYhlbRWK2HlYX9835qXum4xYZ3E2Mra1mdQ+0ICGoMiw1+nVAn4d9Bo7R3JqSmK1grMq/va+0cdh7bJA=="],
 
-    "gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.4.0-canary.26", "", { "os": "linux", "cpu": "arm64" }, "sha512-o4YS6E3FD2DpZBDvUai9bPMLcpcNZ3THc2BzysSbZeARPiAQuKoudwPJoCpi2t7vajrvczpxBwTPG2uL05ypEA=="],
+    "gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-KQkMEillvlW5Qk5mtGA/3Yz0/tzpNlSw6/3/ttsV1lNtMuOHcGii3zVeXZyi4EJmmLDKYcTcByV2wVsOhDt/zg=="],
 
-    "gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.4.0-canary.26", "", { "os": "linux", "cpu": "x64" }, "sha512-M2/MFrQcPI7Ul5Fq5AOeoARrT0B9SrGiy7BLnPuE7Iai1+xkhfSsxIMF5JeDm/GfJnzcwA2oSvrOg0e7KKdaCA=="],
+    "gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uRBo6THWei0chz+Y5j37qzx+BtoDRFIkDzZjlpCItBRXyMPIg079eIkOCl3aqr2tkxL4HFyJ4GHDes7W8HuAUg=="],
 
-    "gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.4.0-canary.26", "", { "os": "linux", "cpu": "x64" }, "sha512-p5JpQ7k/1LyBzNZglqA8JJm7GRmadPkTyHoWaqMxhiVdcQHGbjwsiNjjAtMNjetNOXxj8ebxjiBsAt+34Ak1IQ=="],
+    "gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-+uxFlPuCNx/T9PdMClOqeE8USKzj8tVz37KflT3Kdbx/LOlZBRI2yxuIcmx1mPNK8DwSOMNCr4ureSet7eyC0w=="],
 
-    "gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.4.0-canary.26", "", { "os": "win32", "cpu": "arm64" }, "sha512-FlXIBNOSwnGxxN+HekUfz4Y0n4gPGzqcY3wa3p+5JhzFT7r0oCxMxOdRbs7w8jF5b6uSkWVIQXWFL43F6+8J4g=="],
+    "gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.3.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-LLTKmaI5cfD8dVzh5Vt7+OMo+AIOClEdIU/TSKbXXT2iScUTSxOGoBhfuv+FU8R9MLmrkIL1e2fBMkEEjYAtPQ=="],
 
-    "gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.4.0-canary.26", "", { "os": "win32", "cpu": "x64" }, "sha512-h9CKrDiEeBof+8IgHStYATYrKVuUt8ggy6429kViWlDbuY6gkuIplf3IRlfpdWAB32I1e4qqUVl/s2xRMgQdqg=="],
+    "gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-aW5B8wOPioJ4mBdMDXkt5f3j8pUr9W8AnlX0Df35uRWNT1Y6RIybxjnSUe+PhM+M1bwgyY8PHLmXZC6zT1o5tA=="],
 
     "gitbook-v2/next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
 
diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json
index f19b6951d8..e78e3e2dfe 100644
--- a/packages/gitbook-v2/package.json
+++ b/packages/gitbook-v2/package.json
@@ -8,7 +8,7 @@
         "@sindresorhus/fnv1a": "^3.1.0",
         "assert-never": "^1.2.1",
         "jwt-decode": "^4.0.0",
-        "next": "canary",
+        "next": "^15.3.2",
         "react": "^19.0.0",
         "react-dom": "^19.0.0",
         "rison": "^0.1.1",

From 778624af00e48390d39782b4f63345d1f013708a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Tue, 13 May 2025 10:52:58 +0200
Subject: [PATCH 037/127] Only resize images with supported extensions (#3229)

---
 .changeset/empty-badgers-happen.md            |  5 ++
 .../lib/images/checkIsSizableImageURL.test.ts | 81 +++++++++++++++++++
 .../src/lib/images/checkIsSizableImageURL.ts  | 16 +++-
 .../src/components/DocumentView/Embed.tsx     | 10 ++-
 packages/gitbook/src/lib/paths.test.ts        | 16 ++++
 packages/gitbook/src/lib/paths.ts             | 12 +++
 6 files changed, 136 insertions(+), 4 deletions(-)
 create mode 100644 .changeset/empty-badgers-happen.md
 create mode 100644 packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.test.ts
 create mode 100644 packages/gitbook/src/lib/paths.test.ts

diff --git a/.changeset/empty-badgers-happen.md b/.changeset/empty-badgers-happen.md
new file mode 100644
index 0000000000..75eaad7165
--- /dev/null
+++ b/.changeset/empty-badgers-happen.md
@@ -0,0 +1,5 @@
+---
+"gitbook-v2": patch
+---
+
+Only resize images with supported extensions.
diff --git a/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.test.ts b/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.test.ts
new file mode 100644
index 0000000000..679f2c5ce7
--- /dev/null
+++ b/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.test.ts
@@ -0,0 +1,81 @@
+import { describe, expect, it } from 'bun:test';
+import { SizableImageAction, checkIsSizableImageURL } from './checkIsSizableImageURL';
+
+describe('checkIsSizableImageURL', () => {
+    it('should return Skip for non-parsable URLs', () => {
+        expect(checkIsSizableImageURL('not a url')).toBe(SizableImageAction.Skip);
+    });
+
+    it('should return Skip for non-http(s) URLs', () => {
+        expect(checkIsSizableImageURL('data:image/png;base64,abc')).toBe(SizableImageAction.Skip);
+        expect(checkIsSizableImageURL('file:///path/to/image.jpg')).toBe(SizableImageAction.Skip);
+    });
+
+    it('should return Skip for localhost URLs', () => {
+        expect(checkIsSizableImageURL('http://localhost:3000/image.jpg')).toBe(
+            SizableImageAction.Skip
+        );
+        expect(checkIsSizableImageURL('https://localhost/image.png')).toBe(SizableImageAction.Skip);
+    });
+
+    it('should return Skip for GitBook image URLs', () => {
+        expect(checkIsSizableImageURL('https://example.com/~gitbook/image/test.jpg')).toBe(
+            SizableImageAction.Skip
+        );
+    });
+
+    it('should return Resize for supported image extensions', () => {
+        expect(checkIsSizableImageURL('https://example.com/image.jpg')).toBe(
+            SizableImageAction.Resize
+        );
+        expect(checkIsSizableImageURL('https://example.com/image.jpeg')).toBe(
+            SizableImageAction.Resize
+        );
+        expect(checkIsSizableImageURL('https://example.com/image.png')).toBe(
+            SizableImageAction.Resize
+        );
+        expect(checkIsSizableImageURL('https://example.com/image.gif')).toBe(
+            SizableImageAction.Resize
+        );
+        expect(checkIsSizableImageURL('https://example.com/image.webp')).toBe(
+            SizableImageAction.Resize
+        );
+    });
+
+    it('should return Resize for URLs without extensions', () => {
+        expect(checkIsSizableImageURL('https://example.com/image')).toBe(SizableImageAction.Resize);
+    });
+
+    it('should return Passthrough for unsupported image extensions', () => {
+        expect(checkIsSizableImageURL('https://example.com/image.svg')).toBe(
+            SizableImageAction.Passthrough
+        );
+        expect(checkIsSizableImageURL('https://example.com/image.bmp')).toBe(
+            SizableImageAction.Passthrough
+        );
+        expect(checkIsSizableImageURL('https://example.com/image.tiff')).toBe(
+            SizableImageAction.Passthrough
+        );
+        expect(checkIsSizableImageURL('https://example.com/image.ico')).toBe(
+            SizableImageAction.Passthrough
+        );
+    });
+
+    it('should handle URLs with query parameters correctly', () => {
+        expect(checkIsSizableImageURL('https://example.com/image.jpg?width=100')).toBe(
+            SizableImageAction.Resize
+        );
+        expect(checkIsSizableImageURL('https://example.com/image.svg?height=200')).toBe(
+            SizableImageAction.Passthrough
+        );
+    });
+
+    it('should be case-insensitive for extensions', () => {
+        expect(checkIsSizableImageURL('https://example.com/image.JPG')).toBe(
+            SizableImageAction.Resize
+        );
+        expect(checkIsSizableImageURL('https://example.com/image.PNG')).toBe(
+            SizableImageAction.Resize
+        );
+    });
+});
diff --git a/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts b/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts
index 61e3ebbc2b..486ea7a69a 100644
--- a/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts
+++ b/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts
@@ -1,9 +1,16 @@
+import { getExtension } from '@/lib/paths';
+
 export enum SizableImageAction {
     Resize = 'resize',
     Skip = 'skip',
     Passthrough = 'passthrough',
 }
 
+/**
+ * https://developers.cloudflare.com/images/transform-images/#supported-input-formats
+ */
+const SUPPORTED_IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
+
 /**
  * Check if an image URL is resizable.
  * Skip it for non-http(s) URLs (data, etc).
@@ -25,9 +32,12 @@ export function checkIsSizableImageURL(input: string): SizableImageAction {
     if (parsed.pathname.includes('/~gitbook/image')) {
         return SizableImageAction.Skip;
     }
-    if (parsed.pathname.endsWith('.svg') || parsed.pathname.endsWith('.avif')) {
-        return SizableImageAction.Passthrough;
+
+    const extension = getExtension(parsed.pathname).toLowerCase();
+    if (!extension || SUPPORTED_IMAGE_EXTENSIONS.includes(extension)) {
+        // If no extension, we consider it resizable.
+        return SizableImageAction.Resize;
     }
 
-    return SizableImageAction.Resize;
+    return SizableImageAction.Passthrough;
 }
diff --git a/packages/gitbook/src/components/DocumentView/Embed.tsx b/packages/gitbook/src/components/DocumentView/Embed.tsx
index 796cdaeee3..17849c1958 100644
--- a/packages/gitbook/src/components/DocumentView/Embed.tsx
+++ b/packages/gitbook/src/components/DocumentView/Embed.tsx
@@ -6,6 +6,7 @@ import { Card } from '@/components/primitives';
 import { tcls } from '@/lib/tailwind';
 
 import { getDataOrNull } from '@v2/lib/data';
+import { Image } from '../utils';
 import type { BlockProps } from './Block';
 import { Caption } from './Caption';
 import { IntegrationBlock } from './Integration';
@@ -52,7 +53,14 @@ export async function Embed(props: BlockProps<gitbookAPI.DocumentBlockEmbed>) {
                 <Card
                     leadingIcon={
                         embed.icon ? (
-                            <img src={embed.icon} className={tcls('w-5', 'h-5')} alt="Logo" />
+                            <Image
+                                src={embed.icon}
+                                className={tcls('w-5', 'h-5')}
+                                alt="Logo"
+                                sources={{ light: { src: embed.icon } }}
+                                sizes={[{ width: 20 }]}
+                                resize={context.contentContext.imageResizer}
+                            />
                         ) : null
                     }
                     href={block.data.url}
diff --git a/packages/gitbook/src/lib/paths.test.ts b/packages/gitbook/src/lib/paths.test.ts
new file mode 100644
index 0000000000..1d418f4988
--- /dev/null
+++ b/packages/gitbook/src/lib/paths.test.ts
@@ -0,0 +1,16 @@
+import { describe, expect, it } from 'bun:test';
+import { getExtension } from './paths';
+
+describe('getExtension', () => {
+    it('should return the extension of a path', () => {
+        expect(getExtension('test.txt')).toBe('.txt');
+    });
+
+    it('should return an empty string if there is no extension', () => {
+        expect(getExtension('test/path/to/file')).toBe('');
+    });
+
+    it('should return the extension of a path with multiple dots', () => {
+        expect(getExtension('test.with.multiple.dots.txt')).toBe('.txt');
+    });
+});
diff --git a/packages/gitbook/src/lib/paths.ts b/packages/gitbook/src/lib/paths.ts
index eff69a569b..ebdf35cd3f 100644
--- a/packages/gitbook/src/lib/paths.ts
+++ b/packages/gitbook/src/lib/paths.ts
@@ -57,3 +57,15 @@ export function withTrailingSlash(pathname: string): string {
 
     return pathname;
 }
+
+/**
+ * Get the extension of a path.
+ */
+export function getExtension(path: string): string {
+    const re = /\.[0-9a-z]+$/i;
+    const match = path.match(re);
+    if (match) {
+        return match[0];
+    }
+    return '';
+}

From c6637b09e248013f30fccff57a3fa640c9b18243 Mon Sep 17 00:00:00 2001
From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com>
Date: Tue, 13 May 2025 22:07:41 +0200
Subject: [PATCH 038/127] Use default value if string/number/boolean in
 generateSchemaExample (#3234)

---
 .changeset/ten-apples-march.md                      | 5 +++++
 packages/react-openapi/src/generateSchemaExample.ts | 8 ++++++++
 2 files changed, 13 insertions(+)
 create mode 100644 .changeset/ten-apples-march.md

diff --git a/.changeset/ten-apples-march.md b/.changeset/ten-apples-march.md
new file mode 100644
index 0000000000..dd13b276c3
--- /dev/null
+++ b/.changeset/ten-apples-march.md
@@ -0,0 +1,5 @@
+---
+'@gitbook/react-openapi': patch
+---
+
+Use default value if string number or boolean in generateSchemaExample
diff --git a/packages/react-openapi/src/generateSchemaExample.ts b/packages/react-openapi/src/generateSchemaExample.ts
index 37695b9fe6..5a038db06b 100644
--- a/packages/react-openapi/src/generateSchemaExample.ts
+++ b/packages/react-openapi/src/generateSchemaExample.ts
@@ -204,6 +204,14 @@ const getExampleFromSchema = (
         return cache(schema, schema.example);
     }
 
+    // Use a default value, if there’s one and it’s a string or number
+    if (
+        schema.default !== undefined &&
+        ['string', 'number', 'boolean'].includes(typeof schema.default)
+    ) {
+        return cache(schema, schema.default);
+    }
+
     // enum: [ 'available', 'pending', 'sold' ]
     if (Array.isArray(schema.enum) && schema.enum.length > 0) {
         return cache(schema, schema.enum[0]);

From 80cb52a2377b10c9be547e31aa6723276d2ac3d3 Mon Sep 17 00:00:00 2001
From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com>
Date: Wed, 14 May 2025 08:05:08 +0200
Subject: [PATCH 039/127] Handle OpenAPI alternatives from schema.items (#3235)

---
 .changeset/silly-gorillas-teach.md           |  5 +++
 packages/react-openapi/src/OpenAPISchema.tsx | 39 +++++++++++++++++---
 2 files changed, 39 insertions(+), 5 deletions(-)
 create mode 100644 .changeset/silly-gorillas-teach.md

diff --git a/.changeset/silly-gorillas-teach.md b/.changeset/silly-gorillas-teach.md
new file mode 100644
index 0000000000..b5691ce68a
--- /dev/null
+++ b/.changeset/silly-gorillas-teach.md
@@ -0,0 +1,5 @@
+---
+'@gitbook/react-openapi': patch
+---
+
+Handle OpenAPI alternatives from schema.items
diff --git a/packages/react-openapi/src/OpenAPISchema.tsx b/packages/react-openapi/src/OpenAPISchema.tsx
index 7841b1c0f0..e9f8c6707b 100644
--- a/packages/react-openapi/src/OpenAPISchema.tsx
+++ b/packages/react-openapi/src/OpenAPISchema.tsx
@@ -80,11 +80,10 @@ function OpenAPISchemaProperty(
                                 context={context}
                             />
                             {index < alternatives.length - 1 ? (
-                                <span className="openapi-schema-alternative-separator">
-                                    {(schema.anyOf || schema.oneOf) &&
-                                        tString(context.translation, 'or')}
-                                    {schema.allOf && tString(context.translation, 'and')}
-                                </span>
+                                <OpenAPISchemaAlternativeSeparator
+                                    schema={schema}
+                                    context={context}
+                                />
                             ) : null}
                         </div>
                     ))}
@@ -256,6 +255,28 @@ function OpenAPISchemaAlternative(props: {
     );
 }
 
+function OpenAPISchemaAlternativeSeparator(props: {
+    schema: OpenAPIV3.SchemaObject;
+    context: OpenAPIClientContext;
+}) {
+    const { schema, context } = props;
+
+    const anyOf = schema.anyOf || schema.items?.anyOf;
+    const oneOf = schema.oneOf || schema.items?.oneOf;
+    const allOf = schema.allOf || schema.items?.allOf;
+
+    if (!anyOf && !oneOf && !allOf) {
+        return null;
+    }
+
+    return (
+        <span className="openapi-schema-alternative-separator">
+            {(anyOf || oneOf) && tString(context.translation, 'or')}
+            {allOf && tString(context.translation, 'and')}
+        </span>
+    );
+}
+
 /**
  * Render a circular reference to a schema.
  */
@@ -457,6 +478,14 @@ export function getSchemaAlternatives(
     schema: OpenAPIV3.SchemaObject,
     ancestors: Set<OpenAPIV3.SchemaObject> = new Set()
 ): OpenAPIV3.SchemaObject[] | null {
+    // Search for alternatives in the items property if it exists
+    if (
+        schema.items &&
+        ('oneOf' in schema.items || 'allOf' in schema.items || 'anyOf' in schema.items)
+    ) {
+        return getSchemaAlternatives(schema.items, ancestors);
+    }
+
     const alternatives:
         | [AlternativeType, (OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject)[]]
         | null = (() => {

From 08382ce62f9bf59abb7ed093bed99b14b7fd7d8f Mon Sep 17 00:00:00 2001
From: Claire Chabas <claire.chabas@gmail.com>
Date: Wed, 14 May 2025 11:10:09 +0200
Subject: [PATCH 040/127] Improve tabs link support (#3211)

---
 .../DocumentView/HashLinkButton.tsx           | 58 +++++++++++++++++
 .../src/components/DocumentView/Heading.tsx   | 50 +++-----------
 .../DocumentView/Tabs/DynamicTabs.tsx         | 65 ++++++++++++-------
 .../src/components/DocumentView/Tabs/Tabs.tsx |  5 +-
 .../src/components/DocumentView/spacing.ts    |  5 ++
 5 files changed, 120 insertions(+), 63 deletions(-)
 create mode 100644 packages/gitbook/src/components/DocumentView/HashLinkButton.tsx

diff --git a/packages/gitbook/src/components/DocumentView/HashLinkButton.tsx b/packages/gitbook/src/components/DocumentView/HashLinkButton.tsx
new file mode 100644
index 0000000000..02f528c459
--- /dev/null
+++ b/packages/gitbook/src/components/DocumentView/HashLinkButton.tsx
@@ -0,0 +1,58 @@
+import { type ClassValue, tcls } from '@/lib/tailwind';
+import type { DocumentBlockHeading, DocumentBlockTabs } from '@gitbook/api';
+import { Icon } from '@gitbook/icons';
+import { getBlockTextStyle } from './spacing';
+
+/**
+ * A hash icon which adds the block or active block item's ID in the URL hash.
+ * The button needs to be wrapped in a container with `hashLinkButtonWrapperStyles`.
+ */
+export const hashLinkButtonWrapperStyles = tcls('relative', 'group/hash');
+
+export function HashLinkButton(props: {
+    id: string;
+    block: DocumentBlockTabs | DocumentBlockHeading;
+    label?: string;
+    className?: ClassValue;
+    iconClassName?: ClassValue;
+}) {
+    const { id, block, className, iconClassName, label = 'Direct link to block' } = props;
+    const textStyle = getBlockTextStyle(block);
+    return (
+        <div
+            className={tcls(
+                'relative',
+                'hash',
+                'grid',
+                'grid-area-1-1',
+                'h-[1em]',
+                'border-0',
+                'opacity-0',
+                'group-hover/hash:opacity-[0]',
+                'group-focus/hash:opacity-[0]',
+                'md:group-hover/hash:md:opacity-[1]',
+                'md:group-focus/hash:md:opacity-[1]',
+                className
+            )}
+        >
+            <a
+                href={`#${id}`}
+                aria-label={label}
+                className={tcls('inline-flex', 'h-full', 'items-start', textStyle.lineHeight)}
+            >
+                <Icon
+                    icon="hashtag"
+                    className={tcls(
+                        'size-3',
+                        'self-center',
+                        'transition-colors',
+                        'text-transparent',
+                        'group-hover/hash:text-tint-subtle',
+                        'contrast-more:group-hover/hash:text-tint-strong',
+                        iconClassName
+                    )}
+                />
+            </a>
+        </div>
+    );
+}
diff --git a/packages/gitbook/src/components/DocumentView/Heading.tsx b/packages/gitbook/src/components/DocumentView/Heading.tsx
index cb11038321..0de49e623f 100644
--- a/packages/gitbook/src/components/DocumentView/Heading.tsx
+++ b/packages/gitbook/src/components/DocumentView/Heading.tsx
@@ -1,9 +1,9 @@
 import type { DocumentBlockHeading } from '@gitbook/api';
-import { Icon } from '@gitbook/icons';
 
 import { tcls } from '@/lib/tailwind';
 
 import type { BlockProps } from './Block';
+import { HashLinkButton, hashLinkButtonWrapperStyles } from './HashLinkButton';
 import { Inlines } from './Inlines';
 import { getBlockTextStyle } from './spacing';
 
@@ -23,50 +23,20 @@ export function Heading(props: BlockProps<DocumentBlockHeading>) {
             className={tcls(
                 textStyle.textSize,
                 'heading',
-                'group',
-                'relative',
                 'grid',
                 'scroll-m-12',
+                hashLinkButtonWrapperStyles,
                 style
             )}
         >
-            <div
-                className={tcls(
-                    'hash',
-                    'grid',
-                    'grid-area-1-1',
-                    'relative',
-                    '-ml-6',
-                    'w-7',
-                    'border-0',
-                    'opacity-0',
-                    'group-hover:opacity-[0]',
-                    'group-focus:opacity-[0]',
-                    'md:group-hover:md:opacity-[1]',
-                    'md:group-focus:md:opacity-[1]',
-                    textStyle.marginTop
-                )}
-            >
-                <a
-                    href={`#${id}`}
-                    aria-label="Direct link to heading"
-                    className={tcls('inline-flex', 'h-full', 'items-start', textStyle.lineHeight)}
-                >
-                    <Icon
-                        icon="hashtag"
-                        className={tcls(
-                            'w-3.5',
-                            'h-[1em]',
-                            'mt-0.5',
-                            'transition-colors',
-                            'text-transparent',
-                            'group-hover:text-tint-subtle',
-                            'contrast-more:group-hover:text-tint-strong',
-                            'lg:w-4'
-                        )}
-                    />
-                </a>
-            </div>
+            <HashLinkButton
+                id={id}
+                block={block}
+                className={tcls('-ml-6', textStyle.anchorButtonMarginTop)}
+                iconClassName={tcls('size-4')}
+                label="Direct link to heading"
+            />
+
             <div
                 className={tcls(
                     'grid-area-1-1',
diff --git a/packages/gitbook/src/components/DocumentView/Tabs/DynamicTabs.tsx b/packages/gitbook/src/components/DocumentView/Tabs/DynamicTabs.tsx
index 38afbcf086..e4465b3032 100644
--- a/packages/gitbook/src/components/DocumentView/Tabs/DynamicTabs.tsx
+++ b/packages/gitbook/src/components/DocumentView/Tabs/DynamicTabs.tsx
@@ -5,6 +5,8 @@ import React, { useCallback, useMemo } from 'react';
 import { useHash, useIsMounted } from '@/components/hooks';
 import * as storage from '@/lib/local-storage';
 import { type ClassValue, tcls } from '@/lib/tailwind';
+import type { DocumentBlockTabs } from '@gitbook/api';
+import { HashLinkButton, hashLinkButtonWrapperStyles } from '../HashLinkButton';
 
 interface TabsState {
     activeIds: {
@@ -68,9 +70,10 @@ export function DynamicTabs(
     props: TabsInput & {
         tabsBody: React.ReactNode[];
         style: ClassValue;
+        block: DocumentBlockTabs;
     }
 ) {
-    const { id, tabs, tabsBody, style } = props;
+    const { id, block, tabs, tabsBody, style } = props;
 
     const hash = useHash();
     const [tabsState, setTabsState] = useTabsState();
@@ -146,8 +149,8 @@ export function DynamicTabs(
                 'ring-inset',
                 'ring-tint-subtle',
                 'flex',
-                'overflow-hidden',
                 'flex-col',
+                'overflow-hidden',
                 style
             )}
         >
@@ -165,16 +168,14 @@ export function DynamicTabs(
                 )}
             >
                 {tabs.map((tab) => (
-                    <button
+                    <div
                         key={tab.id}
-                        role="tab"
-                        aria-selected={active.id === tab.id}
-                        aria-controls={getTabPanelId(tab.id)}
-                        id={getTabButtonId(tab.id)}
-                        onClick={() => {
-                            onSelectTab(tab);
-                        }}
                         className={tcls(
+                            hashLinkButtonWrapperStyles,
+                            'flex',
+                            'items-center',
+                            'gap-3.5',
+
                             //prev from active-tab
                             '[&:has(+_.active-tab)]:rounded-br-md',
 
@@ -184,14 +185,6 @@ export function DynamicTabs(
                             //next from active-tab
                             '[.active-tab_+_:after]:rounded-br-md',
 
-                            'inline-block',
-                            'text-sm',
-                            'px-3.5',
-                            'py-2',
-                            'transition-[color]',
-                            'font-[500]',
-                            'relative',
-
                             'after:transition-colors',
                             'after:border-r',
                             'after:absolute',
@@ -202,14 +195,16 @@ export function DynamicTabs(
                             'after:h-[70%]',
                             'after:w-[1px]',
 
+                            'px-3.5',
+                            'py-2',
+
                             'last:after:border-transparent',
 
                             'text-tint',
                             'bg-tint-12/1',
                             'hover:text-tint-strong',
-
-                            'truncate',
                             'max-w-full',
+                            'truncate',
 
                             active.id === tab.id
                                 ? [
@@ -224,8 +219,34 @@ export function DynamicTabs(
                                 : null
                         )}
                     >
-                        {tab.title}
-                    </button>
+                        <button
+                            type="button"
+                            role="tab"
+                            aria-selected={active.id === tab.id}
+                            aria-controls={getTabPanelId(tab.id)}
+                            id={getTabButtonId(tab.id)}
+                            onClick={() => {
+                                onSelectTab(tab);
+                            }}
+                            className={tcls(
+                                'inline-block',
+                                'text-sm',
+                                'transition-[color]',
+                                'font-[500]',
+                                'relative',
+                                'max-w-full',
+                                'truncate'
+                            )}
+                        >
+                            {tab.title}
+                        </button>
+
+                        <HashLinkButton
+                            id={getTabButtonId(tab.id)}
+                            block={block}
+                            label="Direct link to tab"
+                        />
+                    </div>
                 ))}
             </div>
             {tabs.map((tab, index) => (
diff --git a/packages/gitbook/src/components/DocumentView/Tabs/Tabs.tsx b/packages/gitbook/src/components/DocumentView/Tabs/Tabs.tsx
index 4d5a0d555a..d1ab244852 100644
--- a/packages/gitbook/src/components/DocumentView/Tabs/Tabs.tsx
+++ b/packages/gitbook/src/components/DocumentView/Tabs/Tabs.tsx
@@ -39,6 +39,7 @@ export function Tabs(props: BlockProps<DocumentBlockTabs>) {
                     <DynamicTabs
                         key={tab.id}
                         id={block.key!}
+                        block={block}
                         tabs={[tab]}
                         tabsBody={[tabsBody[index]]}
                         style={style}
@@ -48,5 +49,7 @@ export function Tabs(props: BlockProps<DocumentBlockTabs>) {
         );
     }
 
-    return <DynamicTabs id={block.key!} tabs={tabs} tabsBody={tabsBody} style={style} />;
+    return (
+        <DynamicTabs id={block.key!} block={block} tabs={tabs} tabsBody={tabsBody} style={style} />
+    );
 }
diff --git a/packages/gitbook/src/components/DocumentView/spacing.ts b/packages/gitbook/src/components/DocumentView/spacing.ts
index a11db3546c..1c21a8b17d 100644
--- a/packages/gitbook/src/components/DocumentView/spacing.ts
+++ b/packages/gitbook/src/components/DocumentView/spacing.ts
@@ -10,6 +10,8 @@ export function getBlockTextStyle(block: DocumentBlock): {
     lineHeight: string;
     /** Tailwind class for the margin top (mt-*) */
     marginTop?: string;
+    /** Tailwind class for the margin top to apply on the anchor link button */
+    anchorButtonMarginTop?: string;
 } {
     switch (block.type) {
         case 'paragraph':
@@ -22,18 +24,21 @@ export function getBlockTextStyle(block: DocumentBlock): {
                 textSize: 'text-3xl font-semibold',
                 lineHeight: 'leading-tight',
                 marginTop: 'mt-[1em]',
+                anchorButtonMarginTop: 'mt-[1.05em]',
             };
         case 'heading-2':
             return {
                 textSize: 'text-2xl font-semibold',
                 lineHeight: 'leading-snug',
                 marginTop: 'mt-[0.75em]',
+                anchorButtonMarginTop: 'mt-[0.9em]',
             };
         case 'heading-3':
             return {
                 textSize: 'text-xl font-semibold',
                 lineHeight: 'leading-snug',
                 marginTop: 'mt-[0.5em]',
+                anchorButtonMarginTop: 'mt-[0.65em]',
             };
         case 'divider':
             return {

From cc37e2ae15ea5896ea28e4401229d458014f00a5 Mon Sep 17 00:00:00 2001
From: spastorelli <spastorelli@users.noreply.github.com>
Date: Wed, 14 May 2025 14:16:13 +0200
Subject: [PATCH 041/127] Persist visitor params values in public visitor
 session cookie (#3231)

---
 packages/gitbook-v2/src/middleware.ts     | 12 +++--
 packages/gitbook/src/lib/visitors.test.ts | 42 +++++++++++++----
 packages/gitbook/src/lib/visitors.ts      | 55 +++++++++++++++++++----
 packages/gitbook/src/middleware.ts        |  6 +--
 4 files changed, 93 insertions(+), 22 deletions(-)

diff --git a/packages/gitbook-v2/src/middleware.ts b/packages/gitbook-v2/src/middleware.ts
index 5cb34052f8..916d53d61d 100644
--- a/packages/gitbook-v2/src/middleware.ts
+++ b/packages/gitbook-v2/src/middleware.ts
@@ -10,7 +10,7 @@ import {
     type ResponseCookies,
     getPathScopedCookieName,
     getResponseCookiesForVisitorAuth,
-    getVisitorPayload,
+    getVisitorData,
     normalizeVisitorAuthURL,
 } from '@/lib/visitors';
 import { serveResizedImage } from '@/routes/image';
@@ -95,7 +95,7 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
     //
     // Detect and extract the visitor authentication token from the request
     //
-    const { visitorToken, unsignedClaims } = getVisitorPayload({
+    const { visitorToken, unsignedClaims, visitorParamsCookie } = getVisitorData({
         cookies: request.cookies.getAll(),
         url: siteRequestURL,
     });
@@ -121,7 +121,13 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
                 apiToken,
             })
         );
-        const cookies: ResponseCookies = [];
+
+        const cookies: ResponseCookies = visitorParamsCookie
+            ? [
+                  // If visitor.* params were passed to the site URL, include a session cookie to persist these params across navigation.
+                  visitorParamsCookie,
+              ]
+            : [];
 
         //
         // Handle redirects
diff --git a/packages/gitbook/src/lib/visitors.test.ts b/packages/gitbook/src/lib/visitors.test.ts
index 82060fcdfa..4337371fa6 100644
--- a/packages/gitbook/src/lib/visitors.test.ts
+++ b/packages/gitbook/src/lib/visitors.test.ts
@@ -178,7 +178,7 @@ describe('getVisitorUnsignedClaims', () => {
         const url = new URL('https://example.com/');
         const claims = getVisitorUnsignedClaims({ cookies, url });
 
-        expect(claims).toStrictEqual({
+        expect(claims.all).toStrictEqual({
             bucket: { flags: { SITE_AI: true, SITE_PREVIEW: true } },
             launchdarkly: { flags: { ALPHA: true, API: true } },
         });
@@ -191,7 +191,12 @@ describe('getVisitorUnsignedClaims', () => {
 
         const claims = getVisitorUnsignedClaims({ cookies: [], url });
 
-        expect(claims).toStrictEqual({
+        expect(claims.all).toStrictEqual({
+            isEnterprise: true,
+            language: 'fr',
+            country: 'fr',
+        });
+        expect(claims.fromVisitorParams).toStrictEqual({
             isEnterprise: true,
             language: 'fr',
             country: 'fr',
@@ -203,10 +208,13 @@ describe('getVisitorUnsignedClaims', () => {
 
         const claims = getVisitorUnsignedClaims({ cookies: [], url });
 
-        expect(claims).toStrictEqual({
+        expect(claims.all).toStrictEqual({
             isEnterprise: true,
             // otherParam is not present
         });
+        expect(claims.fromVisitorParams).toStrictEqual({
+            isEnterprise: true,
+        });
     });
 
     it('should support nested query param keys via dot notation', () => {
@@ -216,7 +224,14 @@ describe('getVisitorUnsignedClaims', () => {
 
         const claims = getVisitorUnsignedClaims({ cookies: [], url });
 
-        expect(claims).toStrictEqual({
+        expect(claims.all).toStrictEqual({
+            isEnterprise: true,
+            flags: {
+                ALPHA: true,
+                API: false,
+            },
+        });
+        expect(claims.fromVisitorParams).toStrictEqual({
             isEnterprise: true,
             flags: {
                 ALPHA: true,
@@ -235,7 +250,7 @@ describe('getVisitorUnsignedClaims', () => {
         const url = new URL('https://example.com/');
         const claims = getVisitorUnsignedClaims({ cookies, url });
 
-        expect(claims).toStrictEqual({});
+        expect(claims.all).toStrictEqual({});
     });
 
     it('should merge claims from cookies and visitor.* query params', () => {
@@ -250,16 +265,27 @@ describe('getVisitorUnsignedClaims', () => {
             },
         ];
         const url = new URL(
-            'https://example.com/?visitor.isEnterprise=true&visitor.flags.ALPHA=true&visitor.flags.API=false'
+            'https://example.com/?visitor.isEnterprise=true&visitor.flags.ALPHA=true&visitor.flags.API=false&visitor.bucket.flags.HELLO=false'
         );
 
         const claims = getVisitorUnsignedClaims({ cookies, url });
 
-        expect(claims).toStrictEqual({
+        expect(claims.all).toStrictEqual({
             role: 'admin',
             language: 'fr',
             bucket: {
-                flags: { SITE_AI: true, SITE_PREVIEW: true },
+                flags: { HELLO: false, SITE_AI: true, SITE_PREVIEW: true },
+            },
+            isEnterprise: true,
+            flags: {
+                ALPHA: true,
+                API: false,
+            },
+        });
+
+        expect(claims.fromVisitorParams).toStrictEqual({
+            bucket: {
+                flags: { HELLO: false },
             },
             isEnterprise: true,
             flags: {
diff --git a/packages/gitbook/src/lib/visitors.ts b/packages/gitbook/src/lib/visitors.ts
index c6a7aae986..7ae970666b 100644
--- a/packages/gitbook/src/lib/visitors.ts
+++ b/packages/gitbook/src/lib/visitors.ts
@@ -41,13 +41,15 @@ type ClaimPrimitive =
     | ClaimPrimitive[];
 
 /**
- * The result of a visitor info lookup that can include:
+ * The result of a visitor data lookup that can include:
  *   - a visitor token (JWT)
  *   - a record of visitor public/unsigned claims (JSON object)
+ *   - a session cookie response to persist any visitor query params across navigations.
  */
-export type VisitorPayloadLookup = {
+export type VisitorDataLookup = {
     visitorToken: VisitorTokenLookup;
     unsignedClaims: Record<string, ClaimPrimitive>;
+    visitorParamsCookie: ResponseCookie | undefined;
 };
 
 /**
@@ -74,21 +76,26 @@ export type VisitorTokenLookup =
     | undefined;
 
 /**
- * Get the visitor info for the request including its token and/or unsigned claims when present.
+ * Get the visitor data for the request potentially including:
+ *   - a JWT token that may contain signed claims or can be used for VA authentication.
+ *   - a record of the unsigned claims passed via a cookie or visitor.* params.
+ *   - a session cookie response that is used to persist any visitor.* params that were passed via the site URL.
  */
-export function getVisitorPayload({
+export function getVisitorData({
     cookies,
     url,
 }: {
     cookies: RequestCookies;
     url: URL | NextRequest['nextUrl'];
-}): VisitorPayloadLookup {
+}): VisitorDataLookup {
     const visitorToken = getVisitorToken({ cookies, url });
     const unsignedClaims = getVisitorUnsignedClaims({ cookies, url });
+    const visitorParamsCookie = getResponseCookieForVisitorParams(unsignedClaims.fromVisitorParams);
 
     return {
         visitorToken,
-        unsignedClaims,
+        unsignedClaims: unsignedClaims.all,
+        visitorParamsCookie,
     };
 }
 
@@ -128,9 +135,19 @@ export function getVisitorToken({
 export function getVisitorUnsignedClaims(args: {
     cookies: RequestCookies;
     url: URL | NextRequest['nextUrl'];
-}): Record<string, ClaimPrimitive> {
+}): {
+    /**
+     * The unsigned claims coming from both `gitbook-visitor-public` cookies and `visitor.*` query params.
+     */
+    all: Record<string, ClaimPrimitive>;
+    /**
+     * The unsigned claims from the `visitor.*` query params.
+     */
+    fromVisitorParams: Record<string, ClaimPrimitive>;
+} {
     const { cookies, url } = args;
     const claims: Record<string, ClaimPrimitive> = {};
+    const searchParamsClaims: Record<string, ClaimPrimitive> = {};
 
     for (const cookie of cookies) {
         if (cookie.name.startsWith(VISITOR_UNSIGNED_CLAIMS_PREFIX)) {
@@ -149,11 +166,13 @@ export function getVisitorUnsignedClaims(args: {
         if (key.startsWith('visitor.')) {
             const claimPath = key.substring('visitor.'.length);
             const claimValue = parseVisitorQueryParamValue(value);
+
             setVisitorClaimByPath(claims, claimPath, claimValue);
+            setVisitorClaimByPath(searchParamsClaims, claimPath, claimValue);
         }
     }
 
-    return claims;
+    return { all: claims, fromVisitorParams: searchParamsClaims };
 }
 
 /**
@@ -221,6 +240,26 @@ function parseVisitorQueryParamValue(value: string): ClaimPrimitive {
     return value;
 }
 
+/**
+ * Returns to cookie response to use in order to persist visitor params that were passed to the URL.
+ */
+function getResponseCookieForVisitorParams(
+    visitorParamsClaims: Record<string, ClaimPrimitive>
+): ResponseCookie | undefined {
+    if (Object.keys(visitorParamsClaims).length === 0) {
+        return undefined;
+    }
+
+    return {
+        name: VISITOR_UNSIGNED_CLAIMS_PREFIX,
+        value: JSON.stringify(visitorParamsClaims),
+        options: {
+            sameSite: process.env.NODE_ENV === 'production' ? 'none' : undefined,
+            secure: process.env.NODE_ENV === 'production',
+        },
+    };
+}
+
 /**
  * Return the lookup result for content served with visitor auth.
  */
diff --git a/packages/gitbook/src/middleware.ts b/packages/gitbook/src/middleware.ts
index 04b1b40dd9..3e5f682ccc 100644
--- a/packages/gitbook/src/middleware.ts
+++ b/packages/gitbook/src/middleware.ts
@@ -24,7 +24,7 @@ import {
     type ResponseCookies,
     type VisitorTokenLookup,
     getResponseCookiesForVisitorAuth,
-    getVisitorPayload,
+    getVisitorData,
     normalizeVisitorAuthURL,
 } from '@/lib/visitors';
 
@@ -392,7 +392,7 @@ async function lookupSiteInProxy(request: NextRequest, url: URL): Promise<Lookup
  * When serving multi spaces based on the current URL.
  */
 async function lookupSiteInMultiMode(request: NextRequest, url: URL): Promise<LookupResult> {
-    const { visitorToken } = getVisitorPayload({
+    const { visitorToken } = getVisitorData({
         cookies: request.cookies.getAll(),
         url,
     });
@@ -609,7 +609,7 @@ async function lookupSiteInMultiPathMode(request: NextRequest, url: URL): Promis
     const target = new URL(targetStr);
     target.search = url.search;
 
-    const { visitorToken } = getVisitorPayload({
+    const { visitorToken } = getVisitorData({
         cookies: request.cookies.getAll(),
         url: target,
     });

From 95a1f652f9df864f52467b84fb5a85e62936e2fb Mon Sep 17 00:00:00 2001
From: Zeno Kapitein <zeno@gitbook.io>
Date: Thu, 15 May 2025 13:24:10 +0200
Subject: [PATCH 042/127] Better print layouts: wrap code blocks & force table
 column auto-sizing (#3236)

---
 .changeset/rare-radios-thank.md                              | 5 +++++
 .../components/DocumentView/CodeBlock/CodeBlockRenderer.tsx  | 2 +-
 .../gitbook/src/components/DocumentView/Table/RecordRow.tsx  | 4 ++--
 .../gitbook/src/components/DocumentView/Table/ViewGrid.tsx   | 4 ++--
 4 files changed, 10 insertions(+), 5 deletions(-)
 create mode 100644 .changeset/rare-radios-thank.md

diff --git a/.changeset/rare-radios-thank.md b/.changeset/rare-radios-thank.md
new file mode 100644
index 0000000000..0fb143ce42
--- /dev/null
+++ b/.changeset/rare-radios-thank.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Better print layouts: wrap code blocks & force table column auto-sizing
diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.tsx b/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.tsx
index d8f35d90bb..0ad885a444 100644
--- a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.tsx
+++ b/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.tsx
@@ -58,7 +58,7 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
                 <code
                     id={id}
                     className={tcls(
-                        'inline-grid min-w-full grid-cols-[auto_1fr] p-2 [count-reset:line]',
+                        'inline-grid min-w-full grid-cols-[auto_1fr] p-2 [count-reset:line] print:whitespace-pre-wrap',
                         withWrap && 'whitespace-pre-wrap'
                     )}
                 >
diff --git a/packages/gitbook/src/components/DocumentView/Table/RecordRow.tsx b/packages/gitbook/src/components/DocumentView/Table/RecordRow.tsx
index e39f386cc5..b09760ceb2 100644
--- a/packages/gitbook/src/components/DocumentView/Table/RecordRow.tsx
+++ b/packages/gitbook/src/components/DocumentView/Table/RecordRow.tsx
@@ -15,14 +15,14 @@ export function RecordRow(
         fixedColumns: string[];
     }
 ) {
-    const { view, autoSizedColumns, fixedColumns, block } = props;
+    const { view, autoSizedColumns, fixedColumns, block, context } = props;
 
     return (
         <div className={styles.row} role="row">
             {view.columns.map((column) => {
                 const columnWidth = getColumnWidth({
                     column,
-                    columnWidths: view.columnWidths,
+                    columnWidths: context.mode === 'print' ? undefined : view.columnWidths,
                     autoSizedColumns,
                     fixedColumns,
                 });
diff --git a/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx b/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx
index 50aa13565b..9f2b98ba44 100644
--- a/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx
+++ b/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx
@@ -13,10 +13,10 @@ import { getColumnAlignment } from './utils';
      3. Auto-size is turned off without setting a width, we then default to a fixed width of 100px
 */
 export function ViewGrid(props: TableViewProps<DocumentTableViewGrid>) {
-    const { block, view, records, style } = props;
+    const { block, view, records, style, context } = props;
 
     /* Calculate how many columns are auto-sized vs fixed width */
-    const columnWidths = view.columnWidths;
+    const columnWidths = context.mode === 'print' ? undefined : view.columnWidths;
     const autoSizedColumns = view.columns.filter((column) => !columnWidths?.[column]);
     const fixedColumns = view.columns.filter((column) => columnWidths?.[column]);
 

From 3460b78b45942e2edb44c82ad90207ac8173e57d Mon Sep 17 00:00:00 2001
From: Addison <42930383+addisonschultz@users.noreply.github.com>
Date: Fri, 16 May 2025 07:29:53 -0500
Subject: [PATCH 043/127] Update docs.gitbook.com to gitbook.com/docs (#3187)

Co-authored-by: Tal Gluck <talagluck@gmail.com>
Co-authored-by: Taran Vohra <taranvohra@outlook.com>
---
 .github/CONTRIBUTING.md                                       | 4 ++--
 .github/workflows/deploy-preview.yaml                         | 2 +-
 README.md                                                     | 4 ++--
 packages/gitbook/CHANGELOG.md                                 | 2 +-
 packages/gitbook/e2e/internal.spec.ts                         | 2 +-
 packages/gitbook/e2e/util.ts                                  | 1 +
 .../src/components/SiteLayout/RocketLoaderDetector.tsx        | 2 +-
 packages/gitbook/tests/pagespeed-testing.ts                   | 4 ++--
 8 files changed, 11 insertions(+), 10 deletions(-)

diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index a65357a23d..b93b518961 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -63,9 +63,9 @@ To start your local version of GitBook, run the command `bun dev`.
 
 When running the development server, published GitBook sites can be rendered through your local version at `http://localhost:3000/`.
 
-For example, our published docs can be viewed using the local version by visiting `http://localhost:3000/docs.gitbook.com` after running the development server.
+For example, our published docs can be viewed using the local version by visiting `http://localhost:3000/gitbook.com/docs` after running the development server.
 
-You can visit any published GitBook site behind your development server. Please make sure your site is [published publicly](https://docs.gitbook.com/published-documentation/publish-your-content-as-a-docs-site) to ensure you can view the site correctly in your development version.
+You can visit any published GitBook site behind your development server. Please make sure your site is [published publicly](https://gitbook.com/docs/published-documentation/publish-your-content-as-a-docs-site) to ensure you can view the site correctly in your development version.
 
 ### Commit your update
 
diff --git a/.github/workflows/deploy-preview.yaml b/.github/workflows/deploy-preview.yaml
index 4b14e3f83d..7d8eec6109 100644
--- a/.github/workflows/deploy-preview.yaml
+++ b/.github/workflows/deploy-preview.yaml
@@ -152,7 +152,7 @@ jobs:
 
                       | Site | `v1` | `2v` | `2c` |
                       | --- | --- | --- | --- |
-                      | GitBook | [${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/docs.gitbook.com](${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/docs.gitbook.com) | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/docs.gitbook.com](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/docs.gitbook.com) | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/docs.gitbook.com](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/docs.gitbook.com) |
+                      | GitBook | [${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/gitbook.com/docs](${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/gitbook.com/docs) | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.com/docs](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.com/docs) | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.com/docs](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.com/docs) |
                       | E2E | [${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/gitbook.gitbook.io/test-gitbook-open) | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open) | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open) |
                   edit-mode: replace
     visual-testing-v1:
diff --git a/README.md b/README.md
index 5a055a7a75..f9702f88e3 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 <h1 align="center">GitBook</h1>
 
 <p align="center">
-  <a href="https://docs.gitbook.com/">Docs</a> - <a href="https://github.com/GitbookIO/community">Community</a> - <a href="https://developer.gitbook.com/">Developer Docs</a> - <a href="https://changelog.gitbook.com/">Changelog</a> - <a href="https://github.com/GitbookIO/gitbook/issues/new?assignees=&labels=bug&template=bug_report.md">Bug reports</a> 
+  <a href="https://gitbook.com/docs/">Docs</a> - <a href="https://github.com/GitbookIO/community">Community</a> - <a href="https://developer.gitbook.com/">Developer Docs</a> - <a href="https://changelog.gitbook.com/">Changelog</a> - <a href="https://github.com/GitbookIO/gitbook/issues/new?assignees=&labels=bug&template=bug_report.md">Bug reports</a> 
 </p>
 
 <p align="center">
@@ -66,7 +66,7 @@ bun dev:v2
 
 examples:
 
--   http://localhost:3000/url/docs.gitbook.com
+-   http://localhost:3000/url/gitbook.com/docs
 -   http://localhost:3000/url/open-source.gitbook.io/midjourney
 
 Any published GitBook site can be accessed through your local development instance, and any updates you make to the codebase will be reflected in your browser.
diff --git a/packages/gitbook/CHANGELOG.md b/packages/gitbook/CHANGELOG.md
index dc285100ff..21003be437 100644
--- a/packages/gitbook/CHANGELOG.md
+++ b/packages/gitbook/CHANGELOG.md
@@ -564,7 +564,7 @@
 - 4cbcc5b: Rollback of scalar modal while fixing perf issue
 - 3996110: Optimize images rendered in community ads
 - 133c3e7: Update design of Checkbox to be more consistent and readable
-- 5096f7f: Disable KV cache for docs.gitbook.com as a test, also disable it for change-request to improve consistency
+- 5096f7f: Disable KV cache for gitbook.com/docs as a test, also disable it for change-request to improve consistency
 - 0f1565c: Add optional env `GITBOOK_INTEGRATIONS_HOST` to configure the host serving the integrations
 - 2ff7ed1: Fix table of contents being visible on mobile when disabled at the page level
 - b075f0f: Fix accessibility of the table of contents by using `aria-current` instead of `aria-selected`
diff --git a/packages/gitbook/e2e/internal.spec.ts b/packages/gitbook/e2e/internal.spec.ts
index 83181c3c9a..a70a7e5d8d 100644
--- a/packages/gitbook/e2e/internal.spec.ts
+++ b/packages/gitbook/e2e/internal.spec.ts
@@ -269,7 +269,7 @@ const testCases: TestsCase[] = [
     },
     {
         name: 'GitBook',
-        contentBaseURL: 'https://docs.gitbook.com',
+        contentBaseURL: 'https://gitbook.com/docs/',
         tests: [
             {
                 name: 'Home',
diff --git a/packages/gitbook/e2e/util.ts b/packages/gitbook/e2e/util.ts
index 78d7badb71..61113f8264 100644
--- a/packages/gitbook/e2e/util.ts
+++ b/packages/gitbook/e2e/util.ts
@@ -168,6 +168,7 @@ export function runTestCases(testCases: TestsCase[]) {
                               new URL(testEntryPathname, testCase.contentBaseURL).toString()
                           )
                         : getTestURL(testEntryPathname);
+
                     if (testEntry.cookies) {
                         await context.addCookies(
                             testEntry.cookies.map((cookie) => ({
diff --git a/packages/gitbook/src/components/SiteLayout/RocketLoaderDetector.tsx b/packages/gitbook/src/components/SiteLayout/RocketLoaderDetector.tsx
index 096024089e..cc689bbef1 100644
--- a/packages/gitbook/src/components/SiteLayout/RocketLoaderDetector.tsx
+++ b/packages/gitbook/src/components/SiteLayout/RocketLoaderDetector.tsx
@@ -18,7 +18,7 @@ export function RocketLoaderDetector(props: { nonce?: string }) {
             alert.className = 'p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 mt-8 mx-8';
             alert.innerHTML = \`
               <strong>Error in site configuration:</strong>
-                It looks like \${window.location.hostname} has been incorrectly configured in Cloudflare. This may lead to unexpected behavior or issues with the page loading. If you are the owner of this site, please refer to <a href="https://docs.gitbook.com/published-documentation/custom-domain/configure-dns#are-you-using-cloudflare" class="underline">GitBook's documentation</a> for steps to fix the problem.
+                It looks like \${window.location.hostname} has been incorrectly configured in Cloudflare. This may lead to unexpected behavior or issues with the page loading. If you are the owner of this site, please refer to <a href="https://gitbook.com/docs/published-documentation/custom-domain/configure-dns#are-you-using-cloudflare" class="underline">GitBook's documentation</a> for steps to fix the problem.
               \`;
 
             document.body.prepend(alert);
diff --git a/packages/gitbook/tests/pagespeed-testing.ts b/packages/gitbook/tests/pagespeed-testing.ts
index 94f48d4201..07f1f01b3d 100644
--- a/packages/gitbook/tests/pagespeed-testing.ts
+++ b/packages/gitbook/tests/pagespeed-testing.ts
@@ -12,13 +12,13 @@ interface Test {
 // and to be able to see the results, and only catch major regressions.
 const tests: Array<Test> = [
     {
-        url: 'https://docs.gitbook.com',
+        url: 'https://gitbook.com/docs',
         strategy: 'desktop',
         threshold: 60,
     },
 
     {
-        url: 'https://docs.gitbook.com',
+        url: 'https://gitbook.com/docs',
         strategy: 'mobile',
         threshold: 30,
     },

From 2c85a302fc7e99d3fb8a7aad9ea215f3e1c7a79c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Fri, 16 May 2025 13:58:35 +0100
Subject: [PATCH 044/127] Update opennext and leverage "use cache" on CF
 (#3204)

Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
---
 bun.lock                                  |    6 +-
 package.json                              |    2 +-
 packages/gitbook-v2/open-next.config.ts   |   25 +-
 packages/gitbook-v2/package.json          |    2 +-
 packages/gitbook-v2/src/lib/data/api.ts   | 1381 ++++++---------------
 packages/gitbook-v2/wrangler.jsonc        |    6 +-
 packages/gitbook/src/lib/openapi/fetch.ts |   16 +-
 packages/gitbook/src/lib/tracing.ts       |    6 +-
 8 files changed, 437 insertions(+), 1007 deletions(-)

diff --git a/bun.lock b/bun.lock
index 46ac2592dd..a7d865c06d 100644
--- a/bun.lock
+++ b/bun.lock
@@ -145,6 +145,7 @@
       "dependencies": {
         "@gitbook/api": "^0.115.0",
         "@gitbook/cache-tags": "workspace:*",
+        "@opennextjs/cloudflare": "https://pkg.pr.new/@opennextjs/cloudflare@666",
         "@sindresorhus/fnv1a": "^3.1.0",
         "assert-never": "^1.2.1",
         "jwt-decode": "^4.0.0",
@@ -156,7 +157,6 @@
         "warn-once": "^0.1.1",
       },
       "devDependencies": {
-        "@opennextjs/cloudflare": "^1.0.0-beta.3",
         "@types/rison": "^0.0.9",
         "gitbook": "*",
         "postcss": "^8",
@@ -791,9 +791,9 @@
 
     "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
 
-    "@opennextjs/aws": ["@opennextjs/aws@3.5.7", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.19.2", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-YjyHJrkIHI7YwQRCp8GjDOudu86oOc1RiwxvBBpPHrplsS18H4ZmkzGggAKhK6B4myGsJQ/q9kNP2TraoZiNzg=="],
+    "@opennextjs/aws": ["@opennextjs/aws@3.6.1", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.19.2", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-RYU9K58vEUPXqc3pZO6kr9vBy1MmJZFQZLe0oXBskC005oGju/m4e3DCCP4eZ/Q/HdYQXCoqNXgSGi8VCAYgew=="],
 
-    "@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.0.0-beta.3", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.5.7", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^3.114.3 || ^4.7.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-qKBXQZhUeQ+iGvfJeF7PO30g59LHnPOlRVZd77zxwn6Uc9C+c0LSwo8N28XRIWyQPkY007rKk9pSIxOrP4MHtQ=="],
+    "@opennextjs/cloudflare": ["@opennextjs/cloudflare@https://pkg.pr.new/@opennextjs/cloudflare@666", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "^3.6.1", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.14.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }],
 
     "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
 
diff --git a/package.json b/package.json
index 447997ab82..5398e52f9f 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
         "turbo": "^2.5.0",
         "vercel": "^39.3.0"
     },
-    "packageManager": "bun@1.2.8",
+    "packageManager": "bun@1.2.11",
     "overrides": {
         "@codemirror/state": "6.4.1",
         "@gitbook/api": "^0.115.0",
diff --git a/packages/gitbook-v2/open-next.config.ts b/packages/gitbook-v2/open-next.config.ts
index 8f4d389da6..4ff4f74e88 100644
--- a/packages/gitbook-v2/open-next.config.ts
+++ b/packages/gitbook-v2/open-next.config.ts
@@ -3,16 +3,27 @@ import r2IncrementalCache from '@opennextjs/cloudflare/overrides/incremental-cac
 import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache';
 import doQueue from '@opennextjs/cloudflare/overrides/queue/do-queue';
 import doShardedTagCache from '@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache';
+import {
+    softTagFilter,
+    withFilter,
+} from '@opennextjs/cloudflare/overrides/tag-cache/tag-cache-filter';
 
 export default defineCloudflareConfig({
     incrementalCache: withRegionalCache(r2IncrementalCache, { mode: 'long-lived' }),
-    tagCache: doShardedTagCache({
-        baseShardSize: 12,
-        regionalCache: true,
-        shardReplication: {
-            numberOfSoftReplicas: 2,
-            numberOfHardReplicas: 1,
-        },
+    tagCache: withFilter({
+        tagCache: doShardedTagCache({
+            baseShardSize: 12,
+            regionalCache: true,
+            shardReplication: {
+                numberOfSoftReplicas: 2,
+                numberOfHardReplicas: 1,
+            },
+        }),
+        // We don't use `revalidatePath`, so we filter out soft tags
+        filterFn: softTagFilter,
     }),
     queue: doQueue,
+
+    // Performance improvements as we don't use PPR
+    enableCacheInterception: true,
 });
diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json
index e78e3e2dfe..646bfb4719 100644
--- a/packages/gitbook-v2/package.json
+++ b/packages/gitbook-v2/package.json
@@ -5,6 +5,7 @@
     "dependencies": {
         "@gitbook/api": "^0.115.0",
         "@gitbook/cache-tags": "workspace:*",
+        "@opennextjs/cloudflare": "https://pkg.pr.new/@opennextjs/cloudflare@666",
         "@sindresorhus/fnv1a": "^3.1.0",
         "assert-never": "^1.2.1",
         "jwt-decode": "^4.0.0",
@@ -17,7 +18,6 @@
     },
     "devDependencies": {
         "gitbook": "*",
-        "@opennextjs/cloudflare": "^1.0.0-beta.3",
         "@types/rison": "^0.0.9",
         "tailwindcss": "^3.4.0",
         "postcss": "^8"
diff --git a/packages/gitbook-v2/src/lib/data/api.ts b/packages/gitbook-v2/src/lib/data/api.ts
index 20e495656e..7a2d03d16d 100644
--- a/packages/gitbook-v2/src/lib/data/api.ts
+++ b/packages/gitbook-v2/src/lib/data/api.ts
@@ -7,14 +7,8 @@ import {
     type RenderIntegrationUI,
 } from '@gitbook/api';
 import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-tags';
-import {
-    GITBOOK_API_TOKEN,
-    GITBOOK_API_URL,
-    GITBOOK_RUNTIME,
-    GITBOOK_USER_AGENT,
-} from '@v2/lib/env';
+import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env';
 import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache';
-import { unstable_cache } from 'next/cache';
 import { getCloudflareContext, getCloudflareRequestGlobal } from './cloudflare';
 import { DataFetcherError, wrapDataFetcherError } from './errors';
 import { withCacheKey, withoutConcurrentExecution } from './memoize';
@@ -28,16 +22,13 @@ interface DataFetcherInput {
 }
 
 /**
- * Revalidation profile for the cache.
- * Based on https://nextjs.org/docs/app/api-reference/functions/cacheLife#default-cache-profiles
+ * Options to pass to the `fetch` call to disable the Next data-cache when wrapped in `use cache`.
  */
-enum RevalidationProfile {
-    minutes = 60,
-    hours = 60 * 60,
-    days = 60 * 60 * 24,
-    weeks = 60 * 60 * 24 * 7,
-    max = 60 * 60 * 24 * 30,
-}
+export const noCacheFetchOptions: Partial<RequestInit> = {
+    next: {
+        revalidate: 0,
+    },
+};
 
 /**
  * Create a data fetcher using an API token.
@@ -206,589 +197,301 @@ export function createDataFetcher(
     };
 }
 
-/*
- * For the following functions, we:
- * - Wrap them with `withCacheKey` to compute a cache key from the function arguments ONCE (to be performant)
- * - Pass the cache key to `unstable_cache` to ensure the cache is not tied to closures
- * - Call the uncached function in a `withoutConcurrentExecution` wrapper to prevent concurrent executions
- *
- * Important:
- * - Only the function inside the `unstable_cache` is wrapped in `withoutConcurrentExecution` as Next.js needs to call
- *   the return of `unstable_cache` to identify the tags.
- */
-
 const getUserById = withCacheKey(
     withoutConcurrentExecution(
         getCloudflareRequestGlobal,
-        async (cacheKey, input: DataFetcherInput, params: { userId: string }) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return getUserByIdUseCache(input, params);
-            }
-
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
-                async () => {
-                    return getUserByIdUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.days,
-                    tags: [],
-                }
-            );
-
-            return uncached();
+        async (_, input: DataFetcherInput, params: { userId: string }) => {
+            'use cache';
+            return trace(`getUserById(${params.userId})`, async () => {
+                return wrapDataFetcherError(async () => {
+                    const api = apiClient(input);
+                    const res = await api.users.getUserById(params.userId, {
+                        ...noCacheFetchOptions,
+                    });
+                    cacheTag(...getCacheTagsFromResponse(res));
+                    cacheLife('days');
+                    return res.data;
+                });
+            });
         }
     )
 );
 
-const getUserByIdUseCache = async (input: DataFetcherInput, params: { userId: string }) => {
-    'use cache';
-    return getUserByIdUncached(input, params, true);
-};
-
-const getUserByIdUncached = async (
-    input: DataFetcherInput,
-    params: { userId: string },
-    withUseCache = false
-) => {
-    return trace(`getUserById.uncached(${params.userId})`, async () => {
-        return wrapDataFetcherError(async () => {
-            const api = apiClient(input);
-            const res = await api.users.getUserById(params.userId);
-            if (withUseCache) {
-                cacheTag(...getCacheTagsFromResponse(res));
-                cacheLife('days');
-            }
-            return res.data;
-        });
-    });
-};
-
 const getSpace = withCacheKey(
     withoutConcurrentExecution(
         getCloudflareRequestGlobal,
         async (
-            cacheKey,
+            _,
             input: DataFetcherInput,
             params: { spaceId: string; shareKey: string | undefined }
         ) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return getSpaceUseCache(input, params);
-            }
-
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
-                async () => {
-                    return getSpaceUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.days,
-                    tags: [
-                        getCacheTag({
-                            tag: 'space',
-                            space: params.spaceId,
-                        }),
-                    ],
-                }
+            'use cache';
+            cacheTag(
+                getCacheTag({
+                    tag: 'space',
+                    space: params.spaceId,
+                })
             );
 
-            return uncached();
+            return trace(`getSpace(${params.spaceId}, ${params.shareKey})`, async () => {
+                return wrapDataFetcherError(async () => {
+                    const api = apiClient(input);
+                    const res = await api.spaces.getSpaceById(
+                        params.spaceId,
+                        {
+                            shareKey: params.shareKey,
+                        },
+                        {
+                            ...noCacheFetchOptions,
+                        }
+                    );
+                    cacheTag(...getCacheTagsFromResponse(res));
+                    cacheLife('days');
+                    return res.data;
+                });
+            });
         }
     )
 );
 
-const getSpaceUseCache = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; shareKey: string | undefined }
-) => {
-    'use cache';
-    return getSpaceUncached(input, params, true);
-};
-
-const getSpaceUncached = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; shareKey: string | undefined },
-    withUseCache = false
-) => {
-    if (withUseCache) {
-        cacheTag(
-            getCacheTag({
-                tag: 'space',
-                space: params.spaceId,
-            })
-        );
-    }
-
-    return trace(`getSpace.uncached(${params.spaceId}, ${params.shareKey})`, async () => {
-        return wrapDataFetcherError(async () => {
-            const api = apiClient(input);
-            const res = await api.spaces.getSpaceById(params.spaceId, {
-                shareKey: params.shareKey,
-            });
-
-            if (withUseCache) {
-                cacheTag(...getCacheTagsFromResponse(res));
-                cacheLife('days');
-            }
-            return res.data;
-        });
-    });
-};
-
 const getChangeRequest = withCacheKey(
     withoutConcurrentExecution(
         getCloudflareRequestGlobal,
         async (
-            cacheKey,
+            _,
             input: DataFetcherInput,
             params: { spaceId: string; changeRequestId: string }
         ) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return getChangeRequestUseCache(input, params);
-            }
+            'use cache';
+            cacheTag(
+                getCacheTag({
+                    tag: 'change-request',
+                    space: params.spaceId,
+                    changeRequest: params.changeRequestId,
+                })
+            );
 
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
+            return trace(
+                `getChangeRequest(${params.spaceId}, ${params.changeRequestId})`,
                 async () => {
-                    return getChangeRequestUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.minutes * 5,
-                    tags: [
-                        getCacheTag({
-                            tag: 'change-request',
-                            space: params.spaceId,
-                            changeRequest: params.changeRequestId,
-                        }),
-                    ],
+                    return wrapDataFetcherError(async () => {
+                        const api = apiClient(input);
+                        const res = await api.spaces.getChangeRequestById(
+                            params.spaceId,
+                            params.changeRequestId,
+                            {
+                                ...noCacheFetchOptions,
+                            }
+                        );
+                        cacheTag(...getCacheTagsFromResponse(res));
+                        cacheLife('minutes');
+                        return res.data;
+                    });
                 }
             );
-
-            return uncached();
         }
     )
 );
 
-const getChangeRequestUseCache = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; changeRequestId: string }
-) => {
-    'use cache';
-    return getChangeRequestUncached(input, params, true);
-};
-
-const getChangeRequestUncached = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; changeRequestId: string },
-    withUseCache = false
-) => {
-    if (withUseCache) {
-        cacheTag(
-            getCacheTag({
-                tag: 'change-request',
-                space: params.spaceId,
-                changeRequest: params.changeRequestId,
-            })
-        );
-    }
-
-    return trace(
-        `getChangeRequest.uncached(${params.spaceId}, ${params.changeRequestId})`,
-        async () => {
-            return wrapDataFetcherError(async () => {
-                const api = apiClient(input);
-                const res = await api.spaces.getChangeRequestById(
-                    params.spaceId,
-                    params.changeRequestId
-                );
-                if (withUseCache) {
-                    cacheTag(...getCacheTagsFromResponse(res));
-                    cacheLife('minutes');
-                }
-                return res.data;
-            });
-        }
-    );
-};
-
 const getRevision = withCacheKey(
     withoutConcurrentExecution(
         getCloudflareRequestGlobal,
         async (
-            cacheKey,
+            _,
             input: DataFetcherInput,
             params: { spaceId: string; revisionId: string; metadata: boolean }
         ) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return getRevisionUseCache(input, params);
-            }
-
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
-                async () => {
-                    return getRevisionUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.max,
-                    tags: [],
-                }
-            );
-
-            return uncached();
+            'use cache';
+            return trace(`getRevision(${params.spaceId}, ${params.revisionId})`, async () => {
+                return wrapDataFetcherError(async () => {
+                    const api = apiClient(input);
+                    const res = await api.spaces.getRevisionById(
+                        params.spaceId,
+                        params.revisionId,
+                        {
+                            metadata: params.metadata,
+                        },
+                        {
+                            ...noCacheFetchOptions,
+                        }
+                    );
+                    cacheTag(...getCacheTagsFromResponse(res));
+                    cacheLife('max');
+                    return res.data;
+                });
+            });
         }
     )
 );
 
-const getRevisionUseCache = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; revisionId: string; metadata: boolean }
-) => {
-    'use cache';
-    return getRevisionUncached(input, params, true);
-};
-
-const getRevisionUncached = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; revisionId: string; metadata: boolean },
-    withUseCache = false
-) => {
-    return trace(`getRevision.uncached(${params.spaceId}, ${params.revisionId})`, async () => {
-        return wrapDataFetcherError(async () => {
-            const api = apiClient(input);
-            const res = await api.spaces.getRevisionById(params.spaceId, params.revisionId, {
-                metadata: params.metadata,
-            });
-            if (withUseCache) {
-                cacheTag(...getCacheTagsFromResponse(res));
-                cacheLife('max');
-            }
-            return res.data;
-        });
-    });
-};
-
 const getRevisionPages = withCacheKey(
     withoutConcurrentExecution(
         getCloudflareRequestGlobal,
         async (
-            cacheKey,
+            _,
             input: DataFetcherInput,
             params: { spaceId: string; revisionId: string; metadata: boolean }
         ) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return getRevisionPagesUseCache(input, params);
-            }
-
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
-                async () => {
-                    return getRevisionPagesUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.max,
-                    tags: [],
-                }
-            );
-
-            return uncached();
+            'use cache';
+            return trace(`getRevisionPages(${params.spaceId}, ${params.revisionId})`, async () => {
+                return wrapDataFetcherError(async () => {
+                    const api = apiClient(input);
+                    const res = await api.spaces.listPagesInRevisionById(
+                        params.spaceId,
+                        params.revisionId,
+                        {
+                            metadata: params.metadata,
+                        },
+                        {
+                            ...noCacheFetchOptions,
+                        }
+                    );
+                    cacheTag(...getCacheTagsFromResponse(res));
+                    cacheLife('max');
+                    return res.data.pages;
+                });
+            });
         }
     )
 );
 
-const getRevisionPagesUseCache = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; revisionId: string; metadata: boolean }
-) => {
-    'use cache';
-    return getRevisionPagesUncached(input, params, true);
-};
-
-const getRevisionPagesUncached = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; revisionId: string; metadata: boolean },
-    withUseCache = false
-) => {
-    return trace(`getRevisionPages.uncached(${params.spaceId}, ${params.revisionId})`, async () => {
-        return wrapDataFetcherError(async () => {
-            const api = apiClient(input);
-            const res = await api.spaces.listPagesInRevisionById(
-                params.spaceId,
-                params.revisionId,
-                {
-                    metadata: params.metadata,
-                }
-            );
-            if (withUseCache) {
-                cacheTag(...getCacheTagsFromResponse(res));
-                cacheLife('max');
-            }
-            return res.data.pages;
-        });
-    });
-};
-
 const getRevisionFile = withCacheKey(
     withoutConcurrentExecution(
         getCloudflareRequestGlobal,
         async (
-            cacheKey,
+            _,
             input: DataFetcherInput,
             params: { spaceId: string; revisionId: string; fileId: string }
         ) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return getRevisionFileUseCache(input, params);
-            }
-
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
+            'use cache';
+            return trace(
+                `getRevisionFile(${params.spaceId}, ${params.revisionId}, ${params.fileId})`,
                 async () => {
-                    return getRevisionFileUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.max,
-                    tags: [],
+                    return wrapDataFetcherError(async () => {
+                        const api = apiClient(input);
+                        const res = await api.spaces.getFileInRevisionById(
+                            params.spaceId,
+                            params.revisionId,
+                            params.fileId,
+                            {},
+                            {
+                                ...noCacheFetchOptions,
+                            }
+                        );
+                        cacheTag(...getCacheTagsFromResponse(res));
+                        cacheLife('max');
+                        return res.data;
+                    });
                 }
             );
-
-            return uncached();
         }
     )
 );
 
-const getRevisionFileUseCache = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; revisionId: string; fileId: string }
-) => {
-    'use cache';
-    return getRevisionFileUncached(input, params, true);
-};
-
-const getRevisionFileUncached = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; revisionId: string; fileId: string },
-    withUseCache = false
-) => {
-    return trace(
-        `getRevisionFile.uncached(${params.spaceId}, ${params.revisionId}, ${params.fileId})`,
-        async () => {
-            return wrapDataFetcherError(async () => {
-                const api = apiClient(input);
-                const res = await api.spaces.getFileInRevisionById(
-                    params.spaceId,
-                    params.revisionId,
-                    params.fileId,
-                    {}
-                );
-                if (withUseCache) {
-                    cacheTag(...getCacheTagsFromResponse(res));
-                    cacheLife('max');
-                }
-                return res.data;
-            });
-        }
-    );
-};
-
 const getRevisionPageMarkdown = withCacheKey(
     withoutConcurrentExecution(
         getCloudflareRequestGlobal,
         async (
-            cacheKey,
+            _,
             input: DataFetcherInput,
             params: { spaceId: string; revisionId: string; pageId: string }
         ) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return getRevisionPageMarkdownUseCache(input, params);
-            }
-
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
+            'use cache';
+            return trace(
+                `getRevisionPageMarkdown(${params.spaceId}, ${params.revisionId}, ${params.pageId})`,
                 async () => {
-                    return getRevisionPageMarkdownUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.max,
-                    tags: [],
+                    return wrapDataFetcherError(async () => {
+                        const api = apiClient(input);
+                        const res = await api.spaces.getPageInRevisionById(
+                            params.spaceId,
+                            params.revisionId,
+                            params.pageId,
+                            {
+                                format: 'markdown',
+                            },
+                            {
+                                ...noCacheFetchOptions,
+                            }
+                        );
+
+                        cacheTag(...getCacheTagsFromResponse(res));
+                        cacheLife('max');
+
+                        if (!('markdown' in res.data)) {
+                            throw new DataFetcherError('Page is not a document', 404);
+                        }
+                        return res.data.markdown;
+                    });
                 }
             );
-
-            return uncached();
         }
     )
 );
 
-const getRevisionPageMarkdownUseCache = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; revisionId: string; pageId: string }
-) => {
-    'use cache';
-    return getRevisionPageMarkdownUncached(input, params, true);
-};
-
-const getRevisionPageMarkdownUncached = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; revisionId: string; pageId: string },
-    withUseCache = false
-) => {
-    return trace(
-        `getRevisionPageMarkdown.uncached(${params.spaceId}, ${params.revisionId}, ${params.pageId})`,
-        async () => {
-            return wrapDataFetcherError(async () => {
-                const api = apiClient(input);
-                const res = await api.spaces.getPageInRevisionById(
-                    params.spaceId,
-                    params.revisionId,
-                    params.pageId,
-                    {
-                        format: 'markdown',
-                    }
-                );
-
-                if (withUseCache) {
-                    cacheTag(...getCacheTagsFromResponse(res));
-                    cacheLife('max');
-                }
-
-                if (!('markdown' in res.data)) {
-                    throw new DataFetcherError('Page is not a document', 404);
-                }
-                return res.data.markdown;
-            });
-        }
-    );
-};
-
 const getRevisionPageByPath = withCacheKey(
     withoutConcurrentExecution(
         getCloudflareRequestGlobal,
         async (
-            cacheKey,
+            _,
             input: DataFetcherInput,
             params: { spaceId: string; revisionId: string; path: string }
         ) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return getRevisionPageByPathUseCache(input, params);
-            }
-
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
+            'use cache';
+            return trace(
+                `getRevisionPageByPath(${params.spaceId}, ${params.revisionId}, ${params.path})`,
                 async () => {
-                    return getRevisionPageByPathUncached(input, params);
-                },
-                [cacheKey, 'v2'],
-                {
-                    revalidate: RevalidationProfile.max,
-                    tags: [],
+                    const encodedPath = encodeURIComponent(params.path);
+                    return wrapDataFetcherError(async () => {
+                        const api = apiClient(input);
+                        const res = await api.spaces.getPageInRevisionByPath(
+                            params.spaceId,
+                            params.revisionId,
+                            encodedPath,
+                            {},
+                            {
+                                ...noCacheFetchOptions,
+                            }
+                        );
+                        cacheTag(...getCacheTagsFromResponse(res));
+                        cacheLife('max');
+                        return res.data;
+                    });
                 }
             );
-
-            return uncached();
         }
     )
 );
 
-const getRevisionPageByPathUseCache = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; revisionId: string; path: string }
-) => {
-    'use cache';
-    return getRevisionPageByPathUncached(input, params, true);
-};
-
-const getRevisionPageByPathUncached = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; revisionId: string; path: string },
-    withUseCache = false
-) => {
-    return trace(
-        `getRevisionPageByPath.uncached(${params.spaceId}, ${params.revisionId}, ${params.path})`,
-        async () => {
-            const encodedPath = encodeURIComponent(params.path);
-            return wrapDataFetcherError(async () => {
-                const api = apiClient(input);
-                const res = await api.spaces.getPageInRevisionByPath(
-                    params.spaceId,
-                    params.revisionId,
-                    encodedPath,
-                    {}
-                );
-                if (withUseCache) {
-                    cacheTag(...getCacheTagsFromResponse(res));
-                    cacheLife('max');
-                }
-                return res.data;
-            });
-        }
-    );
-};
-
 const getDocument = withCacheKey(
     withoutConcurrentExecution(
         getCloudflareRequestGlobal,
-        async (
-            cacheKey,
-            input: DataFetcherInput,
-            params: { spaceId: string; documentId: string }
-        ) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return getDocumentUseCache(input, params);
-            }
-
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
-                async () => {
-                    return getDocumentUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.max,
-                    tags: [],
-                }
-            );
-
-            return uncached();
+        async (_, input: DataFetcherInput, params: { spaceId: string; documentId: string }) => {
+            'use cache';
+            return trace(`getDocument(${params.spaceId}, ${params.documentId})`, async () => {
+                return wrapDataFetcherError(async () => {
+                    const api = apiClient(input);
+                    const res = await api.spaces.getDocumentById(
+                        params.spaceId,
+                        params.documentId,
+                        {},
+                        {
+                            ...noCacheFetchOptions,
+                        }
+                    );
+                    cacheTag(...getCacheTagsFromResponse(res));
+                    cacheLife('max');
+                    return res.data;
+                });
+            });
         }
     )
 );
 
-const getDocumentUseCache = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; documentId: string }
-) => {
-    'use cache';
-    return getDocumentUncached(input, params, true);
-};
-
-const getDocumentUncached = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; documentId: string },
-    withUseCache = false
-) => {
-    return trace(`getDocument.uncached(${params.spaceId}, ${params.documentId})`, async () => {
-        return wrapDataFetcherError(async () => {
-            const api = apiClient(input);
-            const res = await api.spaces.getDocumentById(params.spaceId, params.documentId, {});
-            if (withUseCache) {
-                cacheTag(...getCacheTagsFromResponse(res));
-                cacheLife('max');
-            }
-            return res.data;
-        });
-    });
-};
-
 const getComputedDocument = withCacheKey(
     withoutConcurrentExecution(
         getCloudflareRequestGlobal,
         async (
-            cacheKey,
+            _,
             input: DataFetcherInput,
             params: {
                 spaceId: string;
@@ -797,309 +500,157 @@ const getComputedDocument = withCacheKey(
                 seed: string;
             }
         ) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return getComputedDocumentUseCache(input, params);
-            }
+            'use cache';
+            cacheTag(
+                ...getComputedContentSourceCacheTags(
+                    {
+                        spaceId: params.spaceId,
+                        organizationId: params.organizationId,
+                    },
+                    params.source
+                )
+            );
 
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
+            return trace(
+                `getComputedDocument(${params.spaceId}, ${params.organizationId}, ${params.source.type}, ${params.seed})`,
                 async () => {
-                    return getComputedDocumentUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.max,
-                    tags: getComputedContentSourceCacheTags(
-                        {
-                            spaceId: params.spaceId,
-                            organizationId: params.organizationId,
-                        },
-                        params.source
-                    ),
+                    return wrapDataFetcherError(async () => {
+                        const api = apiClient(input);
+                        const res = await api.spaces.getComputedDocument(
+                            params.spaceId,
+                            {
+                                source: params.source,
+                                seed: params.seed,
+                            },
+                            {},
+                            {
+                                ...noCacheFetchOptions,
+                            }
+                        );
+                        cacheTag(...getCacheTagsFromResponse(res));
+                        cacheLife('max');
+                        return res.data;
+                    });
                 }
             );
-
-            return uncached();
         }
     )
 );
 
-const getComputedDocumentUseCache = async (
-    input: DataFetcherInput,
-    params: {
-        spaceId: string;
-        organizationId: string;
-        source: ComputedContentSource;
-        seed: string;
-    }
-) => {
-    'use cache';
-    return getComputedDocumentUncached(input, params, true);
-};
-
-const getComputedDocumentUncached = async (
-    input: DataFetcherInput,
-    params: {
-        spaceId: string;
-        organizationId: string;
-        source: ComputedContentSource;
-        seed: string;
-    },
-    withUseCache = false
-) => {
-    if (withUseCache) {
-        cacheTag(
-            ...getComputedContentSourceCacheTags(
-                {
-                    spaceId: params.spaceId,
-                    organizationId: params.organizationId,
-                },
-                params.source
-            )
-        );
-    }
-
-    return trace(
-        `getComputedDocument.uncached(${params.spaceId}, ${params.organizationId}, ${params.source.type}, ${params.seed})`,
-        async () => {
-            return wrapDataFetcherError(async () => {
-                const api = apiClient(input);
-                const res = await api.spaces.getComputedDocument(params.spaceId, {
-                    source: params.source,
-                    seed: params.seed,
-                });
-                if (withUseCache) {
-                    cacheTag(...getCacheTagsFromResponse(res));
-                    cacheLife('max');
-                }
-                return res.data;
-            });
-        }
-    );
-};
-
 const getReusableContent = withCacheKey(
     withoutConcurrentExecution(
         getCloudflareRequestGlobal,
         async (
-            cacheKey,
+            _,
             input: DataFetcherInput,
             params: { spaceId: string; revisionId: string; reusableContentId: string }
         ) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return getReusableContentUseCache(input, params);
-            }
-
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
+            'use cache';
+            return trace(
+                `getReusableContent(${params.spaceId}, ${params.revisionId}, ${params.reusableContentId})`,
                 async () => {
-                    return getReusableContentUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.max,
-                    tags: [],
+                    return wrapDataFetcherError(async () => {
+                        const api = apiClient(input);
+                        const res = await api.spaces.getReusableContentInRevisionById(
+                            params.spaceId,
+                            params.revisionId,
+                            params.reusableContentId,
+                            {},
+                            {
+                                ...noCacheFetchOptions,
+                            }
+                        );
+                        cacheTag(...getCacheTagsFromResponse(res));
+                        cacheLife('max');
+                        return res.data;
+                    });
                 }
             );
-
-            return uncached();
         }
     )
 );
 
-const getReusableContentUseCache = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; revisionId: string; reusableContentId: string }
-) => {
-    'use cache';
-    return getReusableContentUncached(input, params, true);
-};
-
-const getReusableContentUncached = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; revisionId: string; reusableContentId: string },
-    withUseCache = false
-) => {
-    return trace(
-        `getReusableContent.uncached(${params.spaceId}, ${params.revisionId}, ${params.reusableContentId})`,
-        async () => {
-            return wrapDataFetcherError(async () => {
-                const api = apiClient(input);
-                const res = await api.spaces.getReusableContentInRevisionById(
-                    params.spaceId,
-                    params.revisionId,
-                    params.reusableContentId
-                );
-                if (withUseCache) {
-                    cacheTag(...getCacheTagsFromResponse(res));
-                    cacheLife('max');
-                }
-                return res.data;
-            });
-        }
-    );
-};
-
 const getLatestOpenAPISpecVersionContent = withCacheKey(
     withoutConcurrentExecution(
         getCloudflareRequestGlobal,
-        async (
-            cacheKey,
-            input: DataFetcherInput,
-            params: { organizationId: string; slug: string }
-        ) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return getLatestOpenAPISpecVersionContentUseCache(input, params);
-            }
+        async (_, input: DataFetcherInput, params: { organizationId: string; slug: string }) => {
+            'use cache';
+            cacheTag(
+                getCacheTag({
+                    tag: 'openapi',
+                    organization: params.organizationId,
+                    openAPISpec: params.slug,
+                })
+            );
 
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
+            return trace(
+                `getLatestOpenAPISpecVersionContent(${params.organizationId}, ${params.slug})`,
                 async () => {
-                    return getLatestOpenAPISpecVersionContentUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.max,
-                    tags: [
-                        getCacheTag({
-                            tag: 'openapi',
-                            organization: params.organizationId,
-                            openAPISpec: params.slug,
-                        }),
-                    ],
+                    return wrapDataFetcherError(async () => {
+                        const api = apiClient(input);
+                        const res = await api.orgs.getLatestOpenApiSpecVersionContent(
+                            params.organizationId,
+                            params.slug,
+                            {
+                                ...noCacheFetchOptions,
+                            }
+                        );
+                        cacheTag(...getCacheTagsFromResponse(res));
+                        cacheLife('max');
+                        return res.data;
+                    });
                 }
             );
-
-            return uncached();
         }
     )
 );
 
-const getLatestOpenAPISpecVersionContentUseCache = async (
-    input: DataFetcherInput,
-    params: { organizationId: string; slug: string }
-) => {
-    'use cache';
-    return getLatestOpenAPISpecVersionContentUncached(input, params, true);
-};
-
-const getLatestOpenAPISpecVersionContentUncached = async (
-    input: DataFetcherInput,
-    params: { organizationId: string; slug: string },
-    withUseCache = false
-) => {
-    if (withUseCache) {
-        cacheTag(
-            getCacheTag({
-                tag: 'openapi',
-                organization: params.organizationId,
-                openAPISpec: params.slug,
-            })
-        );
-    }
-
-    return trace(
-        `getLatestOpenAPISpecVersionContent.uncached(${params.organizationId}, ${params.slug})`,
-        async () => {
-            return wrapDataFetcherError(async () => {
-                const api = apiClient(input);
-                const res = await api.orgs.getLatestOpenApiSpecVersionContent(
-                    params.organizationId,
-                    params.slug
-                );
-                if (withUseCache) {
-                    cacheTag(...getCacheTagsFromResponse(res));
-                    cacheLife('max');
-                }
-                return res.data;
-            });
-        }
-    );
-};
-
 const getPublishedContentSite = withCacheKey(
     withoutConcurrentExecution(
         getCloudflareRequestGlobal,
         async (
-            cacheKey,
+            _,
             input: DataFetcherInput,
             params: { organizationId: string; siteId: string; siteShareKey: string | undefined }
         ) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return getPublishedContentSiteUseCache(input, params);
-            }
+            'use cache';
+            cacheTag(
+                getCacheTag({
+                    tag: 'site',
+                    site: params.siteId,
+                })
+            );
 
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
+            return trace(
+                `getPublishedContentSite(${params.organizationId}, ${params.siteId}, ${params.siteShareKey})`,
                 async () => {
-                    return getPublishedContentSiteUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.days,
-                    tags: [
-                        getCacheTag({
-                            tag: 'site',
-                            site: params.siteId,
-                        }),
-                    ],
+                    return wrapDataFetcherError(async () => {
+                        const api = apiClient(input);
+                        const res = await api.orgs.getPublishedContentSite(
+                            params.organizationId,
+                            params.siteId,
+                            {
+                                shareKey: params.siteShareKey,
+                            },
+                            {
+                                ...noCacheFetchOptions,
+                            }
+                        );
+                        cacheTag(...getCacheTagsFromResponse(res));
+                        cacheLife('days');
+                        return res.data;
+                    });
                 }
             );
-
-            return uncached();
         }
     )
 );
 
-const getPublishedContentSiteUseCache = async (
-    input: DataFetcherInput,
-    params: { organizationId: string; siteId: string; siteShareKey: string | undefined }
-) => {
-    'use cache';
-    return getPublishedContentSiteUncached(input, params, true);
-};
-
-const getPublishedContentSiteUncached = async (
-    input: DataFetcherInput,
-    params: { organizationId: string; siteId: string; siteShareKey: string | undefined },
-    withUseCache = false
-) => {
-    if (withUseCache) {
-        cacheTag(
-            getCacheTag({
-                tag: 'site',
-                site: params.siteId,
-            })
-        );
-    }
-
-    return trace(
-        `getPublishedContentSite.uncached(${params.organizationId}, ${params.siteId}, ${params.siteShareKey})`,
-        async () => {
-            return wrapDataFetcherError(async () => {
-                const api = apiClient(input);
-                const res = await api.orgs.getPublishedContentSite(
-                    params.organizationId,
-                    params.siteId,
-                    {
-                        shareKey: params.siteShareKey,
-                    }
-                );
-                if (withUseCache) {
-                    cacheTag(...getCacheTagsFromResponse(res));
-                    cacheLife('days');
-                }
-                return res.data;
-            });
-        }
-    );
-};
-
 const getSiteRedirectBySource = withCacheKey(
     withoutConcurrentExecution(
         getCloudflareRequestGlobal,
         async (
-            cacheKey,
+            _,
             input: DataFetcherInput,
             params: {
                 organizationId: string;
@@ -1108,293 +659,169 @@ const getSiteRedirectBySource = withCacheKey(
                 source: string;
             }
         ) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return getSiteRedirectBySourceUseCache(input, params);
-            }
+            'use cache';
+            cacheTag(
+                getCacheTag({
+                    tag: 'site',
+                    site: params.siteId,
+                })
+            );
 
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
+            return trace(
+                `getSiteRedirectBySource(${params.organizationId}, ${params.siteId}, ${params.siteShareKey}, ${params.source})`,
                 async () => {
-                    return getSiteRedirectBySourceUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.days,
-                    tags: [
-                        getCacheTag({
-                            tag: 'site',
-                            site: params.siteId,
-                        }),
-                    ],
+                    return wrapDataFetcherError(async () => {
+                        const api = apiClient(input);
+                        const res = await api.orgs.getSiteRedirectBySource(
+                            params.organizationId,
+                            params.siteId,
+                            {
+                                shareKey: params.siteShareKey,
+                                source: params.source,
+                            },
+                            {
+                                ...noCacheFetchOptions,
+                            }
+                        );
+                        cacheTag(...getCacheTagsFromResponse(res));
+                        cacheLife('days');
+                        return res.data;
+                    });
                 }
             );
-
-            return uncached();
         }
     )
 );
 
-const getSiteRedirectBySourceUseCache = async (
-    input: DataFetcherInput,
-    params: {
-        organizationId: string;
-        siteId: string;
-        siteShareKey: string | undefined;
-        source: string;
-    }
-) => {
-    'use cache';
-    return getSiteRedirectBySourceUncached(input, params, true);
-};
-
-const getSiteRedirectBySourceUncached = async (
-    input: DataFetcherInput,
-    params: {
-        organizationId: string;
-        siteId: string;
-        siteShareKey: string | undefined;
-        source: string;
-    },
-    withUseCache = false
-) => {
-    if (withUseCache) {
-        cacheTag(
-            getCacheTag({
-                tag: 'site',
-                site: params.siteId,
-            })
-        );
-    }
-
-    return trace(
-        `getSiteRedirectBySource.uncached(${params.organizationId}, ${params.siteId}, ${params.siteShareKey}, ${params.source})`,
-        async () => {
-            return wrapDataFetcherError(async () => {
-                const api = apiClient(input);
-                const res = await api.orgs.getSiteRedirectBySource(
-                    params.organizationId,
-                    params.siteId,
-                    {
-                        shareKey: params.siteShareKey,
-                        source: params.source,
-                    }
-                );
-                if (withUseCache) {
-                    cacheTag(...getCacheTagsFromResponse(res));
-                    cacheLife('days');
-                }
-                return res.data;
-            });
-        }
-    );
-};
-
 const getEmbedByUrl = withCacheKey(
     withoutConcurrentExecution(
         getCloudflareRequestGlobal,
-        async (cacheKey, input: DataFetcherInput, params: { spaceId: string; url: string }) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return getEmbedByUrlUseCache(input, params);
-            }
-
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
-                async () => {
-                    return getEmbedByUrlUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.weeks,
-                    tags: [],
-                }
+        async (_, input: DataFetcherInput, params: { spaceId: string; url: string }) => {
+            'use cache';
+            cacheTag(
+                getCacheTag({
+                    tag: 'space',
+                    space: params.spaceId,
+                })
             );
 
-            return uncached();
+            return trace(`getEmbedByUrl(${params.spaceId}, ${params.url})`, async () => {
+                return wrapDataFetcherError(async () => {
+                    const api = apiClient(input);
+                    const res = await api.spaces.getEmbedByUrlInSpace(
+                        params.spaceId,
+                        {
+                            url: params.url,
+                        },
+                        {
+                            ...noCacheFetchOptions,
+                        }
+                    );
+                    cacheTag(...getCacheTagsFromResponse(res));
+                    cacheLife('weeks');
+                    return res.data;
+                });
+            });
         }
     )
 );
 
-const getEmbedByUrlUseCache = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; url: string }
-) => {
-    'use cache';
-    return getEmbedByUrlUncached(input, params, true);
-};
-
-const getEmbedByUrlUncached = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; url: string },
-    withUseCache = false
-) => {
-    if (withUseCache) {
-        cacheTag(
-            getCacheTag({
-                tag: 'space',
-                space: params.spaceId,
-            })
-        );
-    }
-
-    return trace(`getEmbedByUrl.uncached(${params.spaceId}, ${params.url})`, async () => {
-        return wrapDataFetcherError(async () => {
-            const api = apiClient(input);
-            const res = await api.spaces.getEmbedByUrlInSpace(params.spaceId, {
-                url: params.url,
-            });
-            if (withUseCache) {
-                cacheTag(...getCacheTagsFromResponse(res));
-                cacheLife('weeks');
-            }
-            return res.data;
-        });
-    });
-};
-
 const searchSiteContent = withCacheKey(
     withoutConcurrentExecution(
         getCloudflareRequestGlobal,
         async (
-            cacheKey,
+            _,
             input: DataFetcherInput,
             params: Parameters<GitBookDataFetcher['searchSiteContent']>[0]
         ) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return searchSiteContentUseCache(input, params);
-            }
+            'use cache';
+            cacheTag(
+                getCacheTag({
+                    tag: 'site',
+                    site: params.siteId,
+                })
+            );
 
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
+            return trace(
+                `searchSiteContent(${params.organizationId}, ${params.siteId}, ${params.query})`,
                 async () => {
-                    return searchSiteContentUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.hours,
-                    tags: [],
+                    return wrapDataFetcherError(async () => {
+                        const { organizationId, siteId, query, scope } = params;
+                        const api = apiClient(input);
+                        const res = await api.orgs.searchSiteContent(
+                            organizationId,
+                            siteId,
+                            {
+                                query,
+                                ...scope,
+                            },
+                            {},
+                            {
+                                ...noCacheFetchOptions,
+                            }
+                        );
+                        cacheTag(...getCacheTagsFromResponse(res));
+                        cacheLife('hours');
+                        return res.data.items;
+                    });
                 }
             );
-
-            return uncached();
         }
     )
 );
 
-const searchSiteContentUseCache = async (
-    input: DataFetcherInput,
-    params: Parameters<GitBookDataFetcher['searchSiteContent']>[0]
-) => {
-    'use cache';
-    return searchSiteContentUncached(input, params, true);
-};
-
-const searchSiteContentUncached = async (
-    input: DataFetcherInput,
-    params: Parameters<GitBookDataFetcher['searchSiteContent']>[0],
-    withUseCache = false
-) => {
-    if (withUseCache) {
-        cacheTag(
-            getCacheTag({
-                tag: 'site',
-                site: params.siteId,
-            })
-        );
-    }
-
-    return trace(
-        `searchSiteContent.uncached(${params.organizationId}, ${params.siteId}, ${params.query})`,
-        async () => {
-            return wrapDataFetcherError(async () => {
-                const { organizationId, siteId, query, scope } = params;
-                const api = apiClient(input);
-                const res = await api.orgs.searchSiteContent(organizationId, siteId, {
-                    query,
-                    ...scope,
-                });
-                if (withUseCache) {
-                    cacheTag(...getCacheTagsFromResponse(res));
-                    cacheLife('hours');
-                }
-                return res.data.items;
-            });
-        }
-    );
-};
-
 const renderIntegrationUi = withCacheKey(
     withoutConcurrentExecution(
         getCloudflareRequestGlobal,
         async (
-            cacheKey,
+            _,
             input: DataFetcherInput,
             params: { integrationName: string; request: RenderIntegrationUI }
         ) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return renderIntegrationUiUseCache(input, params);
-            }
-
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
-                async () => {
-                    return renderIntegrationUiUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.days,
-                    tags: [
-                        getCacheTag({
-                            tag: 'integration',
-                            integration: params.integrationName,
-                        }),
-                    ],
-                }
+            'use cache';
+            cacheTag(
+                getCacheTag({
+                    tag: 'integration',
+                    integration: params.integrationName,
+                })
             );
 
-            return uncached();
+            return trace(`renderIntegrationUi(${params.integrationName})`, async () => {
+                return wrapDataFetcherError(async () => {
+                    const api = apiClient(input);
+                    const res = await api.integrations.renderIntegrationUiWithPost(
+                        params.integrationName,
+                        params.request,
+                        {
+                            ...noCacheFetchOptions,
+                        }
+                    );
+                    cacheTag(...getCacheTagsFromResponse(res));
+                    cacheLife('days');
+                    return res.data;
+                });
+            });
         }
     )
 );
 
-const renderIntegrationUiUseCache = async (
-    input: DataFetcherInput,
-    params: { integrationName: string; request: RenderIntegrationUI }
-) => {
-    'use cache';
-    return renderIntegrationUiUncached(input, params, true);
-};
-
-const renderIntegrationUiUncached = (
-    input: DataFetcherInput,
-    params: { integrationName: string; request: RenderIntegrationUI },
-    withUseCache = false
-) => {
-    return trace(`renderIntegrationUi.uncached(${params.integrationName})`, async () => {
-        return wrapDataFetcherError(async () => {
-            const api = apiClient(input);
-            const res = await api.integrations.renderIntegrationUiWithPost(
-                params.integrationName,
-                params.request
-            );
-            if (withUseCache) {
-                cacheTag(...getCacheTagsFromResponse(res));
-                cacheLife('days');
-            }
-            return res.data;
-        });
-    });
-};
-
 async function* streamAIResponse(
     input: DataFetcherInput,
     params: Parameters<GitBookDataFetcher['streamAIResponse']>[0]
 ) {
     const api = apiClient(input);
-    const res = await api.orgs.streamAiResponseInSite(params.organizationId, params.siteId, {
-        input: params.input,
-        output: params.output,
-        model: params.model,
-    });
+    const res = await api.orgs.streamAiResponseInSite(
+        params.organizationId,
+        params.siteId,
+        {
+            input: params.input,
+            output: params.output,
+            model: params.model,
+        },
+        {
+            ...noCacheFetchOptions,
+        }
+    );
 
     for await (const event of res) {
         yield event;
diff --git a/packages/gitbook-v2/wrangler.jsonc b/packages/gitbook-v2/wrangler.jsonc
index e316e2a242..7fbecf3bc6 100644
--- a/packages/gitbook-v2/wrangler.jsonc
+++ b/packages/gitbook-v2/wrangler.jsonc
@@ -2,7 +2,11 @@
     "main": ".open-next/worker.js",
     "name": "gitbook-open-v2",
     "compatibility_date": "2025-04-14",
-    "compatibility_flags": ["nodejs_compat", "allow_importable_env"],
+    "compatibility_flags": [
+        "nodejs_compat",
+        "allow_importable_env",
+        "global_fetch_strictly_public"
+    ],
     "assets": {
         "directory": ".open-next/assets",
         "binding": "ASSETS"
diff --git a/packages/gitbook/src/lib/openapi/fetch.ts b/packages/gitbook/src/lib/openapi/fetch.ts
index d1f8c03cef..b496bbe59f 100644
--- a/packages/gitbook/src/lib/openapi/fetch.ts
+++ b/packages/gitbook/src/lib/openapi/fetch.ts
@@ -1,5 +1,4 @@
 import { parseOpenAPI } from '@gitbook/openapi-parser';
-import { unstable_cache } from 'next/cache';
 
 import { type CacheFunctionOptions, cache, noCacheFetchOptions } from '@/lib/cache';
 import type {
@@ -10,7 +9,6 @@ import type {
 } from '@/lib/openapi/types';
 import { getCloudflareRequestGlobal } from '@v2/lib/data/cloudflare';
 import { withCacheKey, withoutConcurrentExecution } from '@v2/lib/data/memoize';
-import { GITBOOK_RUNTIME } from '@v2/lib/env';
 import { assert } from 'ts-essentials';
 import { resolveContentRef } from '../references';
 import { isV2 } from '../v2';
@@ -71,18 +69,8 @@ const fetchFilesystemV1 = cache({
 });
 
 const fetchFilesystemV2 = withCacheKey(
-    withoutConcurrentExecution(getCloudflareRequestGlobal, async (cacheKey, url: string) => {
-        if (GITBOOK_RUNTIME !== 'cloudflare') {
-            return fetchFilesystemUseCache(url);
-        }
-
-        // FIXME: OpenNext doesn't support 'use cache' yet
-        const uncached = unstable_cache(async () => fetchFilesystemUncached(url), [cacheKey], {
-            revalidate: 60 * 60 * 24,
-        });
-
-        const response = await uncached();
-        return response;
+    withoutConcurrentExecution(getCloudflareRequestGlobal, async (_cacheKey, url: string) => {
+        return fetchFilesystemUseCache(url);
     })
 );
 
diff --git a/packages/gitbook/src/lib/tracing.ts b/packages/gitbook/src/lib/tracing.ts
index 38db339240..71b5c072e8 100644
--- a/packages/gitbook/src/lib/tracing.ts
+++ b/packages/gitbook/src/lib/tracing.ts
@@ -28,19 +28,19 @@ export async function trace<T>(
     };
 
     const start = now();
-    let failed = false;
+    let traceError: null | Error = null;
     try {
         return await fn(span);
     } catch (error) {
         span.setAttribute('error', true);
-        failed = true;
+        traceError = error as Error;
         throw error;
     } finally {
         if (process.env.SILENT !== 'true' && process.env.NODE_ENV !== 'development') {
             const end = now();
             // biome-ignore lint/suspicious/noConsole: we want to log performance data
             console.log(
-                `trace ${completeName} ${failed ? 'failed' : 'succeeded'} in ${end - start}ms`,
+                `trace ${completeName} ${traceError ? `failed with ${traceError.message}` : 'succeeded'} in ${end - start}ms`,
                 attributes
             );
         }

From d00dc8ca72e796cc8698df5a04463fc94fe90433 Mon Sep 17 00:00:00 2001
From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com>
Date: Sun, 18 May 2025 13:20:17 +0200
Subject: [PATCH 045/127] Pass spec validation errors through OpenAPIParseError
 (#3240)

---
 .changeset/four-jobs-reply.md        | 5 +++++
 packages/openapi-parser/src/error.ts | 6 +++++-
 packages/openapi-parser/src/v3.ts    | 1 +
 3 files changed, 11 insertions(+), 1 deletion(-)
 create mode 100644 .changeset/four-jobs-reply.md

diff --git a/.changeset/four-jobs-reply.md b/.changeset/four-jobs-reply.md
new file mode 100644
index 0000000000..714b8deef3
--- /dev/null
+++ b/.changeset/four-jobs-reply.md
@@ -0,0 +1,5 @@
+---
+'@gitbook/openapi-parser': patch
+---
+
+Pass scalar's errors through OpenAPIParseError
diff --git a/packages/openapi-parser/src/error.ts b/packages/openapi-parser/src/error.ts
index e0d25d3e29..a7c5398bcb 100644
--- a/packages/openapi-parser/src/error.ts
+++ b/packages/openapi-parser/src/error.ts
@@ -1,3 +1,5 @@
+import type { ErrorObject } from '@scalar/openapi-parser';
+
 type OpenAPIParseErrorCode =
     | 'invalid'
     | 'parse-v2-in-v3'
@@ -12,17 +14,19 @@ export class OpenAPIParseError extends Error {
     public override name = 'OpenAPIParseError';
     public code: OpenAPIParseErrorCode;
     public rootURL: string | null;
-
+    public errors: ErrorObject[] | undefined;
     constructor(
         message: string,
         options: {
             code: OpenAPIParseErrorCode;
             rootURL?: string | null;
             cause?: Error;
+            errors?: ErrorObject[] | undefined;
         }
     ) {
         super(message, { cause: options.cause });
         this.code = options.code;
         this.rootURL = options.rootURL ?? null;
+        this.errors = options.errors;
     }
 }
diff --git a/packages/openapi-parser/src/v3.ts b/packages/openapi-parser/src/v3.ts
index 14a890769a..3b54819cff 100644
--- a/packages/openapi-parser/src/v3.ts
+++ b/packages/openapi-parser/src/v3.ts
@@ -40,6 +40,7 @@ async function untrustedValidate(input: ValidateOpenAPIV3Input) {
         throw new OpenAPIParseError('Invalid OpenAPI document', {
             code: 'invalid',
             rootURL,
+            errors: result.errors,
         });
     }
 

From 12a455dab5b35e4a68e51ca9e002065f3c52e364 Mon Sep 17 00:00:00 2001
From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com>
Date: Sun, 18 May 2025 14:50:05 +0200
Subject: [PATCH 046/127] Fix OpenAPI layout issues (#3242)

---
 .changeset/strong-eggs-glow.md                            | 5 +++++
 .../gitbook/src/components/DocumentView/OpenAPI/style.css | 8 ++++----
 packages/gitbook/src/components/PageAside/PageAside.tsx   | 2 +-
 3 files changed, 10 insertions(+), 5 deletions(-)
 create mode 100644 .changeset/strong-eggs-glow.md

diff --git a/.changeset/strong-eggs-glow.md b/.changeset/strong-eggs-glow.md
new file mode 100644
index 0000000000..7c1e1db039
--- /dev/null
+++ b/.changeset/strong-eggs-glow.md
@@ -0,0 +1,5 @@
+---
+'gitbook': patch
+---
+
+Fix OpenAPI layout issues
diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
index f3c24cb738..213d12dec5 100644
--- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
+++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
@@ -145,7 +145,7 @@
 }
 
 .openapi-column-preview-body {
-    @apply flex flex-col gap-4 sticky top-4 site-header:top-20 site-header-sections:top-32 page-api-block:xl:max-2xl:top-32 print-mode:static;
+    @apply flex flex-col gap-4 sticky top-4 site-header:top-20 site-header:xl:max-2xl:top-32 site-header-sections:top-32 site-header-sections:xl:max-2xl:top-44 print-mode:static;
 }
 
 .openapi-column-preview pre {
@@ -770,7 +770,7 @@ body:has(.openapi-select-popover) {
 }
 
 .openapi-disclosure:has(> .openapi-disclosure-trigger:hover) {
-    @apply bg-tint-subtle;
+    @apply bg-tint-subtle overflow-hidden;
 }
 
 .openapi-disclosure:has(> .openapi-disclosure-trigger:hover),
@@ -778,10 +778,10 @@ body:has(.openapi-select-popover) {
     @apply ring-1 shadow-sm;
 }
 
-.openapi-disclosure[data-expanded="true"]:not(:first-child) {
+.openapi-disclosure[data-expanded="true"]:not(.openapi-schemas-disclosure):not(:first-child) {
     @apply mt-2;
 }
-.openapi-disclosure[data-expanded="true"]:not(:last-child) {
+.openapi-disclosure[data-expanded="true"]:not(.openapi-schemas-disclosure):not(:last-child) {
     @apply mb-2;
 }
 
diff --git a/packages/gitbook/src/components/PageAside/PageAside.tsx b/packages/gitbook/src/components/PageAside/PageAside.tsx
index a51410cbc0..06821d964c 100644
--- a/packages/gitbook/src/components/PageAside/PageAside.tsx
+++ b/packages/gitbook/src/components/PageAside/PageAside.tsx
@@ -87,7 +87,7 @@ export function PageAside(props: {
                 'page-api-block:xl:max-2xl:dark:hover:shadow-tint-1/1',
                 'page-api-block:xl:max-2xl:rounded-md',
                 'page-api-block:xl:max-2xl:h-auto',
-                'page-api-block:xl:max-2xl:my-8',
+                'page-api-block:xl:max-2xl:my-4',
                 'page-api-block:p-2'
             )}
         >

From 223edae17daee2a7a0cecf39b5e0e93cefb12a1b Mon Sep 17 00:00:00 2001
From: conico974 <nicodorseuil@yahoo.fr>
Date: Mon, 19 May 2025 13:47:33 +0200
Subject: [PATCH 047/127] Add env variables for the DOQueue (#3243)

Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
---
 bun.lock                           | 4 ++--
 packages/gitbook-v2/package.json   | 2 +-
 packages/gitbook-v2/wrangler.jsonc | 8 ++++++++
 3 files changed, 11 insertions(+), 3 deletions(-)

diff --git a/bun.lock b/bun.lock
index a7d865c06d..2c13245d05 100644
--- a/bun.lock
+++ b/bun.lock
@@ -145,7 +145,7 @@
       "dependencies": {
         "@gitbook/api": "^0.115.0",
         "@gitbook/cache-tags": "workspace:*",
-        "@opennextjs/cloudflare": "https://pkg.pr.new/@opennextjs/cloudflare@666",
+        "@opennextjs/cloudflare": "1.0.3",
         "@sindresorhus/fnv1a": "^3.1.0",
         "assert-never": "^1.2.1",
         "jwt-decode": "^4.0.0",
@@ -793,7 +793,7 @@
 
     "@opennextjs/aws": ["@opennextjs/aws@3.6.1", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.19.2", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-RYU9K58vEUPXqc3pZO6kr9vBy1MmJZFQZLe0oXBskC005oGju/m4e3DCCP4eZ/Q/HdYQXCoqNXgSGi8VCAYgew=="],
 
-    "@opennextjs/cloudflare": ["@opennextjs/cloudflare@https://pkg.pr.new/@opennextjs/cloudflare@666", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "^3.6.1", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.14.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }],
+    "@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.0.3", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "^3.6.1", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.14.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-+SNhz2JmKkMlWzndTSYZeq0eseAPfEXUUYJA7H9MkgFe36i1no0OdiLuwu9e4uR3yuOzaNWB+skCYHsD6Im9nA=="],
 
     "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
 
diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json
index 646bfb4719..1db128e211 100644
--- a/packages/gitbook-v2/package.json
+++ b/packages/gitbook-v2/package.json
@@ -5,7 +5,7 @@
     "dependencies": {
         "@gitbook/api": "^0.115.0",
         "@gitbook/cache-tags": "workspace:*",
-        "@opennextjs/cloudflare": "https://pkg.pr.new/@opennextjs/cloudflare@666",
+        "@opennextjs/cloudflare": "1.0.3",
         "@sindresorhus/fnv1a": "^3.1.0",
         "assert-never": "^1.2.1",
         "jwt-decode": "^4.0.0",
diff --git a/packages/gitbook-v2/wrangler.jsonc b/packages/gitbook-v2/wrangler.jsonc
index 7fbecf3bc6..6d8ccf9955 100644
--- a/packages/gitbook-v2/wrangler.jsonc
+++ b/packages/gitbook-v2/wrangler.jsonc
@@ -14,6 +14,9 @@
     "observability": {
         "enabled": true
     },
+    "vars": {
+        "NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
+    },
     "env": {
         "preview": {
             "r2_buckets": [
@@ -87,6 +90,11 @@
             ]
         },
         "production": {
+            "vars": {
+                // This is a bit misleading, but it means that we can have 100 concurrent revalidations
+                // This means that we'll have up to 20 durable objects instance running at the same time
+                "MAX_REVALIDATE_CONCURRENCY": "20"
+            },
             "routes": [
                 {
                     "pattern": "open-2c.gitbook.com/*",

From 1c8d9febb3c721f8dd8ae33a0a5142ff0b47c967 Mon Sep 17 00:00:00 2001
From: conico974 <nicodorseuil@yahoo.fr>
Date: Mon, 19 May 2025 17:00:09 +0200
Subject: [PATCH 048/127] Custom Incremental Cache (#3239)

Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
---
 .changeset/clean-crabs-tie.md                 |   5 +
 packages/gitbook-v2/.gitignore                |   1 +
 packages/gitbook-v2/open-next.config.ts       |   4 +-
 .../gitbook-v2/openNext/incrementalCache.ts   | 186 ++++++++++++++++++
 packages/gitbook-v2/package.json              |   2 +-
 5 files changed, 194 insertions(+), 4 deletions(-)
 create mode 100644 .changeset/clean-crabs-tie.md
 create mode 100644 packages/gitbook-v2/openNext/incrementalCache.ts

diff --git a/.changeset/clean-crabs-tie.md b/.changeset/clean-crabs-tie.md
new file mode 100644
index 0000000000..4a618afa39
--- /dev/null
+++ b/.changeset/clean-crabs-tie.md
@@ -0,0 +1,5 @@
+---
+"gitbook-v2": patch
+---
+
+keep data cache in OpenNext between deployment
diff --git a/packages/gitbook-v2/.gitignore b/packages/gitbook-v2/.gitignore
index 84254823d2..a23d5b7565 100644
--- a/packages/gitbook-v2/.gitignore
+++ b/packages/gitbook-v2/.gitignore
@@ -6,6 +6,7 @@
 
 # cloudflare
 .open-next
+.wrangler
 
 # Symbolic links
 public
diff --git a/packages/gitbook-v2/open-next.config.ts b/packages/gitbook-v2/open-next.config.ts
index 4ff4f74e88..060cad541c 100644
--- a/packages/gitbook-v2/open-next.config.ts
+++ b/packages/gitbook-v2/open-next.config.ts
@@ -1,6 +1,4 @@
 import { defineCloudflareConfig } from '@opennextjs/cloudflare';
-import r2IncrementalCache from '@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache';
-import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache';
 import doQueue from '@opennextjs/cloudflare/overrides/queue/do-queue';
 import doShardedTagCache from '@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache';
 import {
@@ -9,7 +7,7 @@ import {
 } from '@opennextjs/cloudflare/overrides/tag-cache/tag-cache-filter';
 
 export default defineCloudflareConfig({
-    incrementalCache: withRegionalCache(r2IncrementalCache, { mode: 'long-lived' }),
+    incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default),
     tagCache: withFilter({
         tagCache: doShardedTagCache({
             baseShardSize: 12,
diff --git a/packages/gitbook-v2/openNext/incrementalCache.ts b/packages/gitbook-v2/openNext/incrementalCache.ts
new file mode 100644
index 0000000000..d4a662af01
--- /dev/null
+++ b/packages/gitbook-v2/openNext/incrementalCache.ts
@@ -0,0 +1,186 @@
+import { createHash } from 'node:crypto';
+
+import { trace } from '@/lib/tracing';
+import type {
+    CacheEntryType,
+    CacheValue,
+    IncrementalCache,
+    WithLastModified,
+} from '@opennextjs/aws/types/overrides.js';
+import { getCloudflareContext } from '@opennextjs/cloudflare';
+
+export const BINDING_NAME = 'NEXT_INC_CACHE_R2_BUCKET';
+export const DEFAULT_PREFIX = 'incremental-cache';
+
+export type KeyOptions = {
+    cacheType?: CacheEntryType;
+};
+
+/**
+ *
+ * It is very similar to the `R2IncrementalCache` in the `@opennextjs/cloudflare` package, but it allow us to trace
+ * the cache operations. It also integrates both R2 and Cache API in a single class.
+ * Having our own, will allow us to customize it in the future if needed.
+ */
+class GitbookIncrementalCache implements IncrementalCache {
+    name = 'GitbookIncrementalCache';
+
+    protected localCache: Cache | undefined;
+
+    async get<CacheType extends CacheEntryType = 'cache'>(
+        key: string,
+        cacheType?: CacheType
+    ): Promise<WithLastModified<CacheValue<CacheType>> | null> {
+        const cacheKey = this.getR2Key(key, cacheType);
+        return trace(
+            {
+                operation: 'openNextIncrementalCacheGet',
+                name: cacheKey,
+            },
+            async (span) => {
+                span.setAttribute('cacheType', cacheType ?? 'cache');
+                const r2 = getCloudflareContext().env[BINDING_NAME];
+                const localCache = await this.getCacheInstance();
+                if (!r2) throw new Error('No R2 bucket');
+                try {
+                    // Check local cache first if available
+                    const localCacheEntry = await localCache.match(this.getCacheUrlKey(cacheKey));
+                    if (localCacheEntry) {
+                        span.setAttribute('cacheHit', 'local');
+                        return localCacheEntry.json();
+                    }
+
+                    const r2Object = await r2.get(cacheKey);
+                    if (!r2Object) return null;
+
+                    span.setAttribute('cacheHit', 'r2');
+                    return {
+                        value: await r2Object.json(),
+                        lastModified: r2Object.uploaded.getTime(),
+                    };
+                } catch (e) {
+                    console.error('Failed to get from cache', e);
+                    return null;
+                }
+            }
+        );
+    }
+
+    async set<CacheType extends CacheEntryType = 'cache'>(
+        key: string,
+        value: CacheValue<CacheType>,
+        cacheType?: CacheType
+    ): Promise<void> {
+        const cacheKey = this.getR2Key(key, cacheType);
+        return trace(
+            {
+                operation: 'openNextIncrementalCacheSet',
+                name: cacheKey,
+            },
+            async (span) => {
+                span.setAttribute('cacheType', cacheType ?? 'cache');
+                const r2 = getCloudflareContext().env[BINDING_NAME];
+                const localCache = await this.getCacheInstance();
+                if (!r2) throw new Error('No R2 bucket');
+
+                try {
+                    await r2.put(cacheKey, JSON.stringify(value));
+
+                    //TODO: Check if there is any places where we don't have tags
+                    // Ideally we should always have tags, but in case we don't, we need to decide how to handle it
+                    // For now we default to a build ID tag, which allow us to invalidate the cache in case something is wrong in this deployment
+                    const tags = this.getTagsFromCacheEntry(value) ?? [
+                        `build_id/${process.env.NEXT_BUILD_ID}`,
+                    ];
+
+                    // We consider R2 as the source of truth, so we update the local cache
+                    // only after a successful R2 write
+                    await localCache.put(
+                        this.getCacheUrlKey(cacheKey),
+                        new Response(
+                            JSON.stringify({
+                                value,
+                                // Note: `Date.now()` returns the time of the last IO rather than the actual time.
+                                //       See https://developers.cloudflare.com/workers/reference/security-model/
+                                lastModified: Date.now(),
+                            }),
+                            {
+                                headers: {
+                                    // Cache-Control default to 30 minutes, will be overridden by `revalidate`
+                                    // In theory we should always get the `revalidate` value
+                                    'cache-control': `max-age=${value.revalidate ?? 60 * 30}`,
+                                    'cache-tag': tags.join(','),
+                                },
+                            }
+                        )
+                    );
+                } catch (e) {
+                    console.error('Failed to set to cache', e);
+                }
+            }
+        );
+    }
+
+    async delete(key: string): Promise<void> {
+        const cacheKey = this.getR2Key(key);
+        return trace(
+            {
+                operation: 'openNextIncrementalCacheDelete',
+                name: cacheKey,
+            },
+            async () => {
+                const r2 = getCloudflareContext().env[BINDING_NAME];
+                const localCache = await this.getCacheInstance();
+                if (!r2) throw new Error('No R2 bucket');
+
+                try {
+                    await r2.delete(cacheKey);
+
+                    // Here again R2 is the source of truth, so we delete from local cache first
+                    await localCache.delete(this.getCacheUrlKey(cacheKey));
+                } catch (e) {
+                    console.error('Failed to delete from cache', e);
+                }
+            }
+        );
+    }
+
+    async getCacheInstance(): Promise<Cache> {
+        if (this.localCache) return this.localCache;
+        this.localCache = await caches.open('incremental-cache');
+        return this.localCache;
+    }
+
+    // Utility function to generate keys for R2/Cache API
+    getR2Key(key: string, cacheType: CacheEntryType = 'cache'): string {
+        const hash = createHash('sha256').update(key).digest('hex');
+        return `${DEFAULT_PREFIX}/${cacheType === 'cache' ? process.env?.NEXT_BUILD_ID : 'dataCache'}/${hash}.${cacheType}`.replace(
+            /\/+/g,
+            '/'
+        );
+    }
+
+    getCacheUrlKey(cacheKey: string): string {
+        return `http://cache.local/${cacheKey}`;
+    }
+
+    getTagsFromCacheEntry<CacheType extends CacheEntryType>(
+        entry: CacheValue<CacheType>
+    ): string[] | undefined {
+        if ('tags' in entry && entry.tags) {
+            return entry.tags;
+        }
+
+        if ('meta' in entry && entry.meta && 'headers' in entry.meta && entry.meta.headers) {
+            const rawTags = entry.meta.headers['x-next-cache-tags'];
+            if (typeof rawTags === 'string') {
+                return rawTags.split(',');
+            }
+        }
+        if ('value' in entry) {
+            return entry.tags;
+        }
+    }
+}
+
+export default new GitbookIncrementalCache();
diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json
index 1db128e211..849aca6b0f 100644
--- a/packages/gitbook-v2/package.json
+++ b/packages/gitbook-v2/package.json
@@ -29,7 +29,7 @@
         "build:v2": "next build",
         "start": "next start",
         "build:v2:cloudflare": "opennextjs-cloudflare build",
-        "dev:v2:cloudflare": "wrangler dev --port 8771",
+        "dev:v2:cloudflare": "wrangler dev --port 8771 --env preview",
         "unit": "bun test",
         "typecheck": "tsc --noEmit"
     }

From 938125e772946438cde884042ea84221420d5a1c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Mon, 19 May 2025 19:10:57 +0200
Subject: [PATCH 049/127] Version Packages (#3180)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
---
 .changeset/big-clocks-rush.md        |  5 ---
 .changeset/breezy-bugs-dream.md      |  5 ---
 .changeset/clean-crabs-tie.md        |  5 ---
 .changeset/eight-vans-judge.md       |  5 ---
 .changeset/empty-badgers-happen.md   |  5 ---
 .changeset/fifty-pots-work.md        |  5 ---
 .changeset/four-jobs-reply.md        |  5 ---
 .changeset/giant-dolphins-approve.md |  6 ----
 .changeset/healthy-parrots-call.md   |  6 ----
 .changeset/lemon-actors-sing.md      |  6 ----
 .changeset/lemon-plants-vanish.md    |  8 -----
 .changeset/modern-windows-type.md    |  5 ---
 .changeset/moody-maps-chew.md        |  5 ---
 .changeset/polite-falcons-agree.md   |  6 ----
 .changeset/rare-radios-thank.md      |  5 ---
 .changeset/real-donkeys-invent.md    |  5 ---
 .changeset/rich-eggs-talk.md         |  5 ---
 .changeset/rich-lamps-compare.md     |  5 ---
 .changeset/sharp-falcons-punch.md    |  6 ----
 .changeset/shy-plums-deny.md         |  5 ---
 .changeset/silly-gorillas-teach.md   |  5 ---
 .changeset/smart-feet-argue.md       |  5 ---
 .changeset/soft-planes-talk.md       |  5 ---
 .changeset/strong-eggs-glow.md       |  5 ---
 .changeset/sweet-schools-flow.md     |  6 ----
 .changeset/ten-apples-march.md       |  5 ---
 .changeset/thirty-donkeys-help.md    |  5 ---
 .changeset/tidy-rings-talk.md        |  6 ----
 .changeset/tough-starfishes-grin.md  |  5 ---
 .changeset/twelve-beers-confess.md   |  5 ---
 .changeset/twelve-news-joke.md       |  5 ---
 .changeset/two-chairs-wait.md        |  5 ---
 .changeset/two-lions-tickle.md       |  6 ----
 .changeset/young-games-talk.md       |  6 ----
 .changeset/young-poets-cry.md        |  5 ---
 packages/colors/CHANGELOG.md         |  8 +++++
 packages/colors/package.json         |  2 +-
 packages/gitbook-v2/CHANGELOG.md     | 19 ++++++++++++
 packages/gitbook-v2/package.json     |  2 +-
 packages/gitbook/CHANGELOG.md        | 46 ++++++++++++++++++++++++++++
 packages/gitbook/package.json        |  2 +-
 packages/openapi-parser/CHANGELOG.md |  6 ++++
 packages/openapi-parser/package.json |  2 +-
 packages/react-openapi/CHANGELOG.md  | 19 ++++++++++++
 packages/react-openapi/package.json  |  2 +-
 45 files changed, 103 insertions(+), 192 deletions(-)
 delete mode 100644 .changeset/big-clocks-rush.md
 delete mode 100644 .changeset/breezy-bugs-dream.md
 delete mode 100644 .changeset/clean-crabs-tie.md
 delete mode 100644 .changeset/eight-vans-judge.md
 delete mode 100644 .changeset/empty-badgers-happen.md
 delete mode 100644 .changeset/fifty-pots-work.md
 delete mode 100644 .changeset/four-jobs-reply.md
 delete mode 100644 .changeset/giant-dolphins-approve.md
 delete mode 100644 .changeset/healthy-parrots-call.md
 delete mode 100644 .changeset/lemon-actors-sing.md
 delete mode 100644 .changeset/lemon-plants-vanish.md
 delete mode 100644 .changeset/modern-windows-type.md
 delete mode 100644 .changeset/moody-maps-chew.md
 delete mode 100644 .changeset/polite-falcons-agree.md
 delete mode 100644 .changeset/rare-radios-thank.md
 delete mode 100644 .changeset/real-donkeys-invent.md
 delete mode 100644 .changeset/rich-eggs-talk.md
 delete mode 100644 .changeset/rich-lamps-compare.md
 delete mode 100644 .changeset/sharp-falcons-punch.md
 delete mode 100644 .changeset/shy-plums-deny.md
 delete mode 100644 .changeset/silly-gorillas-teach.md
 delete mode 100644 .changeset/smart-feet-argue.md
 delete mode 100644 .changeset/soft-planes-talk.md
 delete mode 100644 .changeset/strong-eggs-glow.md
 delete mode 100644 .changeset/sweet-schools-flow.md
 delete mode 100644 .changeset/ten-apples-march.md
 delete mode 100644 .changeset/thirty-donkeys-help.md
 delete mode 100644 .changeset/tidy-rings-talk.md
 delete mode 100644 .changeset/tough-starfishes-grin.md
 delete mode 100644 .changeset/twelve-beers-confess.md
 delete mode 100644 .changeset/twelve-news-joke.md
 delete mode 100644 .changeset/two-chairs-wait.md
 delete mode 100644 .changeset/two-lions-tickle.md
 delete mode 100644 .changeset/young-games-talk.md
 delete mode 100644 .changeset/young-poets-cry.md

diff --git a/.changeset/big-clocks-rush.md b/.changeset/big-clocks-rush.md
deleted file mode 100644
index 665953599f..0000000000
--- a/.changeset/big-clocks-rush.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"gitbook": minor
----
-
-Fix images in reusable content across spaces.
diff --git a/.changeset/breezy-bugs-dream.md b/.changeset/breezy-bugs-dream.md
deleted file mode 100644
index 69d2022873..0000000000
--- a/.changeset/breezy-bugs-dream.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'gitbook': patch
----
-
-Fix openapi-select hover in responses
diff --git a/.changeset/clean-crabs-tie.md b/.changeset/clean-crabs-tie.md
deleted file mode 100644
index 4a618afa39..0000000000
--- a/.changeset/clean-crabs-tie.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"gitbook-v2": patch
----
-
-keep data cache in OpenNext between deployment
diff --git a/.changeset/eight-vans-judge.md b/.changeset/eight-vans-judge.md
deleted file mode 100644
index c432fde1a3..0000000000
--- a/.changeset/eight-vans-judge.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@gitbook/colors": patch
----
-
-Update chroma ratio per step
diff --git a/.changeset/empty-badgers-happen.md b/.changeset/empty-badgers-happen.md
deleted file mode 100644
index 75eaad7165..0000000000
--- a/.changeset/empty-badgers-happen.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"gitbook-v2": patch
----
-
-Only resize images with supported extensions.
diff --git a/.changeset/fifty-pots-work.md b/.changeset/fifty-pots-work.md
deleted file mode 100644
index 0ba6550278..0000000000
--- a/.changeset/fifty-pots-work.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"gitbook": patch
----
-
-Decrease rounding of header logo
diff --git a/.changeset/four-jobs-reply.md b/.changeset/four-jobs-reply.md
deleted file mode 100644
index 714b8deef3..0000000000
--- a/.changeset/four-jobs-reply.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@gitbook/openapi-parser': patch
----
-
-Pass scalar's errors through OpenAPIParseError
diff --git a/.changeset/giant-dolphins-approve.md b/.changeset/giant-dolphins-approve.md
deleted file mode 100644
index 0fa9d77174..0000000000
--- a/.changeset/giant-dolphins-approve.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-'@gitbook/react-openapi': patch
-'gitbook': patch
----
-
-Fix openapi CR preview
diff --git a/.changeset/healthy-parrots-call.md b/.changeset/healthy-parrots-call.md
deleted file mode 100644
index 9503e2afb1..0000000000
--- a/.changeset/healthy-parrots-call.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-"gitbook-v2": patch
-"gitbook": patch
----
-
-Fix URL in sitemap
diff --git a/.changeset/lemon-actors-sing.md b/.changeset/lemon-actors-sing.md
deleted file mode 100644
index d212947861..0000000000
--- a/.changeset/lemon-actors-sing.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-"@gitbook/react-openapi": minor
-"gitbook": minor
----
-
-Design tweaks to code blocks and OpenAPI pages
diff --git a/.changeset/lemon-plants-vanish.md b/.changeset/lemon-plants-vanish.md
deleted file mode 100644
index 0887b10bef..0000000000
--- a/.changeset/lemon-plants-vanish.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-"@gitbook/react-openapi": patch
-"gitbook-v2": patch
-"gitbook": patch
-"@gitbook/colors": patch
----
-
-Fix code highlighting for HTTP
diff --git a/.changeset/modern-windows-type.md b/.changeset/modern-windows-type.md
deleted file mode 100644
index d2135d1c28..0000000000
--- a/.changeset/modern-windows-type.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"gitbook-v2": patch
----
-
-Fix crash on Cloudflare by using latest stable version of Next.js instead of canary
diff --git a/.changeset/moody-maps-chew.md b/.changeset/moody-maps-chew.md
deleted file mode 100644
index 0c06afc2ad..0000000000
--- a/.changeset/moody-maps-chew.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"gitbook": patch
----
-
-Fix resolution of links in reusable contents
diff --git a/.changeset/polite-falcons-agree.md b/.changeset/polite-falcons-agree.md
deleted file mode 100644
index 7743ab7095..0000000000
--- a/.changeset/polite-falcons-agree.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-"gitbook-v2": minor
-"gitbook": minor
----
-
-Add support for reusable content across spaces.
diff --git a/.changeset/rare-radios-thank.md b/.changeset/rare-radios-thank.md
deleted file mode 100644
index 0fb143ce42..0000000000
--- a/.changeset/rare-radios-thank.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"gitbook": patch
----
-
-Better print layouts: wrap code blocks & force table column auto-sizing
diff --git a/.changeset/real-donkeys-invent.md b/.changeset/real-donkeys-invent.md
deleted file mode 100644
index a322af7d72..0000000000
--- a/.changeset/real-donkeys-invent.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"gitbook": patch
----
-
-Fix invalid sitemap.xml generated with relative URLs instead of absolute ones
diff --git a/.changeset/rich-eggs-talk.md b/.changeset/rich-eggs-talk.md
deleted file mode 100644
index e863378b55..0000000000
--- a/.changeset/rich-eggs-talk.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"gitbook": patch
----
-
-Change OpenAPI schema-optional from `info` to `tint` color
diff --git a/.changeset/rich-lamps-compare.md b/.changeset/rich-lamps-compare.md
deleted file mode 100644
index 57b026b7b4..0000000000
--- a/.changeset/rich-lamps-compare.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@gitbook/colors": patch
----
-
-Change lightness check for color step 9 to allow input colors with a higher-than-needed contrast
diff --git a/.changeset/sharp-falcons-punch.md b/.changeset/sharp-falcons-punch.md
deleted file mode 100644
index fbcb613276..0000000000
--- a/.changeset/sharp-falcons-punch.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-'@gitbook/react-openapi': patch
-'gitbook': patch
----
-
-Fix schemas disclosure label causing client error
diff --git a/.changeset/shy-plums-deny.md b/.changeset/shy-plums-deny.md
deleted file mode 100644
index 22193b58e1..0000000000
--- a/.changeset/shy-plums-deny.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@gitbook/react-openapi': patch
----
-
-Missing top-level required OpenAPI alternatives
diff --git a/.changeset/silly-gorillas-teach.md b/.changeset/silly-gorillas-teach.md
deleted file mode 100644
index b5691ce68a..0000000000
--- a/.changeset/silly-gorillas-teach.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@gitbook/react-openapi': patch
----
-
-Handle OpenAPI alternatives from schema.items
diff --git a/.changeset/smart-feet-argue.md b/.changeset/smart-feet-argue.md
deleted file mode 100644
index 079dada9b0..0000000000
--- a/.changeset/smart-feet-argue.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@gitbook/react-openapi': patch
----
-
-Handle invalid OpenAPI Responses
diff --git a/.changeset/soft-planes-talk.md b/.changeset/soft-planes-talk.md
deleted file mode 100644
index d633d90dcd..0000000000
--- a/.changeset/soft-planes-talk.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"gitbook-v2": patch
----
-
-Improve error messages around undefined site sections.
diff --git a/.changeset/strong-eggs-glow.md b/.changeset/strong-eggs-glow.md
deleted file mode 100644
index 7c1e1db039..0000000000
--- a/.changeset/strong-eggs-glow.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'gitbook': patch
----
-
-Fix OpenAPI layout issues
diff --git a/.changeset/sweet-schools-flow.md b/.changeset/sweet-schools-flow.md
deleted file mode 100644
index 6ff44bfa96..0000000000
--- a/.changeset/sweet-schools-flow.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-"gitbook-v2": patch
-"gitbook": patch
----
-
-Increase logging around caching behaviour causing page crashes.
diff --git a/.changeset/ten-apples-march.md b/.changeset/ten-apples-march.md
deleted file mode 100644
index dd13b276c3..0000000000
--- a/.changeset/ten-apples-march.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@gitbook/react-openapi': patch
----
-
-Use default value if string number or boolean in generateSchemaExample
diff --git a/.changeset/thirty-donkeys-help.md b/.changeset/thirty-donkeys-help.md
deleted file mode 100644
index 529b8a9024..0000000000
--- a/.changeset/thirty-donkeys-help.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"gitbook": patch
----
-
-Prevent section group popovers from opening on click
diff --git a/.changeset/tidy-rings-talk.md b/.changeset/tidy-rings-talk.md
deleted file mode 100644
index 75ac0e7f8d..0000000000
--- a/.changeset/tidy-rings-talk.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-"gitbook": patch
-"gitbook-v2": patch
----
-
-Update the regex for validating site redirect
diff --git a/.changeset/tough-starfishes-grin.md b/.changeset/tough-starfishes-grin.md
deleted file mode 100644
index 2f150b59d8..0000000000
--- a/.changeset/tough-starfishes-grin.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"gitbook": patch
----
-
-Always link main logo to the root of the site
diff --git a/.changeset/twelve-beers-confess.md b/.changeset/twelve-beers-confess.md
deleted file mode 100644
index 1c70f6880b..0000000000
--- a/.changeset/twelve-beers-confess.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"gitbook": patch
----
-
-Change `Dropdown`s to use Radix's `DropdownMenu`
diff --git a/.changeset/twelve-news-joke.md b/.changeset/twelve-news-joke.md
deleted file mode 100644
index 3c0d08681f..0000000000
--- a/.changeset/twelve-news-joke.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@gitbook/react-openapi": patch
----
-
-Fix Python code sample "null vs None"
diff --git a/.changeset/two-chairs-wait.md b/.changeset/two-chairs-wait.md
deleted file mode 100644
index 6ca7941574..0000000000
--- a/.changeset/two-chairs-wait.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"gitbook": patch
----
-
-Add border to filled sidebar on gradient theme
diff --git a/.changeset/two-lions-tickle.md b/.changeset/two-lions-tickle.md
deleted file mode 100644
index b9fa676033..0000000000
--- a/.changeset/two-lions-tickle.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-"gitbook-v2": minor
-"gitbook": minor
----
-
-Pass SVG images through image resizing without resizing them to serve them from optimal host.
diff --git a/.changeset/young-games-talk.md b/.changeset/young-games-talk.md
deleted file mode 100644
index 096dc02060..0000000000
--- a/.changeset/young-games-talk.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-"gitbook": patch
-"gitbook-v2": patch
----
-
-Revert investigation work around URL caches.
diff --git a/.changeset/young-poets-cry.md b/.changeset/young-poets-cry.md
deleted file mode 100644
index 8757b729b9..0000000000
--- a/.changeset/young-poets-cry.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"gitbook": patch
----
-
-Fix OpenAPI disclosure label ("Show properties") misalignment on mobile
diff --git a/packages/colors/CHANGELOG.md b/packages/colors/CHANGELOG.md
index 0c1606b101..1695a6c97d 100644
--- a/packages/colors/CHANGELOG.md
+++ b/packages/colors/CHANGELOG.md
@@ -1,5 +1,13 @@
 # @gitbook/colors
 
+## 0.3.3
+
+### Patch Changes
+
+- c3f6b8c: Update chroma ratio per step
+- 5e975ab: Fix code highlighting for HTTP
+- f7a3470: Change lightness check for color step 9 to allow input colors with a higher-than-needed contrast
+
 ## 0.3.2
 
 ### Patch Changes
diff --git a/packages/colors/package.json b/packages/colors/package.json
index 36cd0f47f8..16f54d1ff3 100644
--- a/packages/colors/package.json
+++ b/packages/colors/package.json
@@ -8,7 +8,7 @@
             "default": "./dist/index.js"
         }
     },
-    "version": "0.3.2",
+    "version": "0.3.3",
     "devDependencies": {
         "typescript": "^5.5.3"
     },
diff --git a/packages/gitbook-v2/CHANGELOG.md b/packages/gitbook-v2/CHANGELOG.md
index eb4e718a5b..8a4a4a0288 100644
--- a/packages/gitbook-v2/CHANGELOG.md
+++ b/packages/gitbook-v2/CHANGELOG.md
@@ -1,5 +1,24 @@
 # gitbook-v2
 
+## 0.3.0
+
+### Minor Changes
+
+- 3119066: Add support for reusable content across spaces.
+- 7d7806d: Pass SVG images through image resizing without resizing them to serve them from optimal host.
+
+### Patch Changes
+
+- 1c8d9fe: keep data cache in OpenNext between deployment
+- 778624a: Only resize images with supported extensions.
+- e6ddc0f: Fix URL in sitemap
+- 5e975ab: Fix code highlighting for HTTP
+- e15757d: Fix crash on Cloudflare by using latest stable version of Next.js instead of canary
+- 634e0b4: Improve error messages around undefined site sections.
+- 97b7c79: Increase logging around caching behaviour causing page crashes.
+- 3f29206: Update the regex for validating site redirect
+- dd043df: Revert investigation work around URL caches.
+
 ## 0.2.5
 
 ### Patch Changes
diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json
index 849aca6b0f..355518c4f6 100644
--- a/packages/gitbook-v2/package.json
+++ b/packages/gitbook-v2/package.json
@@ -1,6 +1,6 @@
 {
     "name": "gitbook-v2",
-    "version": "0.2.5",
+    "version": "0.3.0",
     "private": true,
     "dependencies": {
         "@gitbook/api": "^0.115.0",
diff --git a/packages/gitbook/CHANGELOG.md b/packages/gitbook/CHANGELOG.md
index 21003be437..fb855a6305 100644
--- a/packages/gitbook/CHANGELOG.md
+++ b/packages/gitbook/CHANGELOG.md
@@ -1,5 +1,51 @@
 # gitbook
 
+## 0.12.0
+
+### Minor Changes
+
+- 8339e91: Fix images in reusable content across spaces.
+- 326e28e: Design tweaks to code blocks and OpenAPI pages
+- 3119066: Add support for reusable content across spaces.
+- 7d7806d: Pass SVG images through image resizing without resizing them to serve them from optimal host.
+
+### Patch Changes
+
+- c4ebb3f: Fix openapi-select hover in responses
+- aed79fd: Decrease rounding of header logo
+- 42ca7e1: Fix openapi CR preview
+- e6ddc0f: Fix URL in sitemap
+- 5e975ab: Fix code highlighting for HTTP
+- 5d504ff: Fix resolution of links in reusable contents
+- 95a1f65: Better print layouts: wrap code blocks & force table column auto-sizing
+- 0499966: Fix invalid sitemap.xml generated with relative URLs instead of absolute ones
+- 2a805cc: Change OpenAPI schema-optional from `info` to `tint` color
+- 580101d: Fix schemas disclosure label causing client error
+- 12a455d: Fix OpenAPI layout issues
+- 97b7c79: Increase logging around caching behaviour causing page crashes.
+- 373f18f: Prevent section group popovers from opening on click
+- 3f29206: Update the regex for validating site redirect
+- 0c973a3: Always link main logo to the root of the site
+- ae5f1ab: Change `Dropdown`s to use Radix's `DropdownMenu`
+- 0e201d5: Add border to filled sidebar on gradient theme
+- dd043df: Revert investigation work around URL caches.
+- 89a5816: Fix OpenAPI disclosure label ("Show properties") misalignment on mobile
+- Updated dependencies [c3f6b8c]
+- Updated dependencies [d00dc8c]
+- Updated dependencies [42ca7e1]
+- Updated dependencies [326e28e]
+- Updated dependencies [5e975ab]
+- Updated dependencies [f7a3470]
+- Updated dependencies [580101d]
+- Updated dependencies [20ebecb]
+- Updated dependencies [80cb52a]
+- Updated dependencies [cb5598d]
+- Updated dependencies [c6637b0]
+- Updated dependencies [a3ec264]
+  - @gitbook/colors@0.3.3
+  - @gitbook/openapi-parser@2.1.4
+  - @gitbook/react-openapi@1.3.0
+
 ## 0.11.1
 
 ### Patch Changes
diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json
index 4294327546..a3b9da821a 100644
--- a/packages/gitbook/package.json
+++ b/packages/gitbook/package.json
@@ -1,6 +1,6 @@
 {
     "name": "gitbook",
-    "version": "0.11.1",
+    "version": "0.12.0",
     "private": true,
     "scripts": {
         "dev": "env-cmd --silent -f ../../.env.local next dev",
diff --git a/packages/openapi-parser/CHANGELOG.md b/packages/openapi-parser/CHANGELOG.md
index 36f5cbc224..7f6ac7ccd2 100644
--- a/packages/openapi-parser/CHANGELOG.md
+++ b/packages/openapi-parser/CHANGELOG.md
@@ -1,5 +1,11 @@
 # @gitbook/openapi-parser
 
+## 2.1.4
+
+### Patch Changes
+
+- d00dc8c: Pass scalar's errors through OpenAPIParseError
+
 ## 2.1.3
 
 ### Patch Changes
diff --git a/packages/openapi-parser/package.json b/packages/openapi-parser/package.json
index f0829fca1b..e65ebb4dda 100644
--- a/packages/openapi-parser/package.json
+++ b/packages/openapi-parser/package.json
@@ -9,7 +9,7 @@
             "default": "./dist/index.js"
         }
     },
-    "version": "2.1.3",
+    "version": "2.1.4",
     "sideEffects": false,
     "dependencies": {
         "@scalar/openapi-parser": "^0.10.10",
diff --git a/packages/react-openapi/CHANGELOG.md b/packages/react-openapi/CHANGELOG.md
index f74c6f044b..6f36c7dc61 100644
--- a/packages/react-openapi/CHANGELOG.md
+++ b/packages/react-openapi/CHANGELOG.md
@@ -1,5 +1,24 @@
 # @gitbook/react-openapi
 
+## 1.3.0
+
+### Minor Changes
+
+- 326e28e: Design tweaks to code blocks and OpenAPI pages
+
+### Patch Changes
+
+- 42ca7e1: Fix openapi CR preview
+- 5e975ab: Fix code highlighting for HTTP
+- 580101d: Fix schemas disclosure label causing client error
+- 20ebecb: Missing top-level required OpenAPI alternatives
+- 80cb52a: Handle OpenAPI alternatives from schema.items
+- cb5598d: Handle invalid OpenAPI Responses
+- c6637b0: Use default value if string number or boolean in generateSchemaExample
+- a3ec264: Fix Python code sample "null vs None"
+- Updated dependencies [d00dc8c]
+  - @gitbook/openapi-parser@2.1.4
+
 ## 1.2.1
 
 ### Patch Changes
diff --git a/packages/react-openapi/package.json b/packages/react-openapi/package.json
index cd1099c242..d4f170270a 100644
--- a/packages/react-openapi/package.json
+++ b/packages/react-openapi/package.json
@@ -8,7 +8,7 @@
             "default": "./dist/index.js"
         }
     },
-    "version": "1.2.1",
+    "version": "1.3.0",
     "sideEffects": false,
     "dependencies": {
         "@gitbook/openapi-parser": "workspace:*",

From 4b67fe5317962f4fec17fc4f4e6b24f3073015c8 Mon Sep 17 00:00:00 2001
From: Zeno Kapitein <zeno@gitbook.io>
Date: Tue, 20 May 2025 10:25:35 +0200
Subject: [PATCH 050/127] V2: Add `urlObject.hash` to `linker.toLinkForContent`
 (#3246)

---
 .changeset/thin-buckets-grow.md        | 5 +++++
 packages/gitbook-v2/src/lib/context.ts | 2 +-
 2 files changed, 6 insertions(+), 1 deletion(-)
 create mode 100644 .changeset/thin-buckets-grow.md

diff --git a/.changeset/thin-buckets-grow.md b/.changeset/thin-buckets-grow.md
new file mode 100644
index 0000000000..84375c735d
--- /dev/null
+++ b/.changeset/thin-buckets-grow.md
@@ -0,0 +1,5 @@
+---
+"gitbook-v2": patch
+---
+
+Add `urlObject.hash` to `linker.toLinkForContent` to pass through URL fragment identifiers, used in search
diff --git a/packages/gitbook-v2/src/lib/context.ts b/packages/gitbook-v2/src/lib/context.ts
index 22e25a5990..ba0f299574 100644
--- a/packages/gitbook-v2/src/lib/context.ts
+++ b/packages/gitbook-v2/src/lib/context.ts
@@ -165,7 +165,7 @@ export function getBaseContext(input: {
         // 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}${urlObject.search}`;
+            return `/url/${urlObject.host}${urlObject.pathname}${urlObject.search}${urlObject.hash}`;
         };
     }
 

From 57bb146075e768a7dcb4e6edb6a3bd4be0bb2cd0 Mon Sep 17 00:00:00 2001
From: Zeno Kapitein <zeno@gitbook.io>
Date: Tue, 20 May 2025 14:13:57 +0200
Subject: [PATCH 051/127] Dynamic TOC height (#3233)

---
 .changeset/slow-lizards-obey.md               |   5 +
 .../Announcement/AnnouncementBanner.tsx       |   2 +-
 .../gitbook/src/components/Footer/Footer.tsx  |   1 +
 .../RootLayout/CustomizationRootLayout.tsx    |   2 +-
 .../src/components/RootLayout/globals.css     |   2 +-
 .../TableOfContents/TableOfContents.tsx       | 165 ++++++++++--------
 .../TableOfContents/TableOfContentsScript.tsx |  73 ++++++++
 .../src/components/TableOfContents/index.ts   |   5 +-
 packages/gitbook/tailwind.config.ts           |   6 +-
 9 files changed, 179 insertions(+), 82 deletions(-)
 create mode 100644 .changeset/slow-lizards-obey.md
 create mode 100644 packages/gitbook/src/components/TableOfContents/TableOfContentsScript.tsx

diff --git a/.changeset/slow-lizards-obey.md b/.changeset/slow-lizards-obey.md
new file mode 100644
index 0000000000..ce1ef0e04c
--- /dev/null
+++ b/.changeset/slow-lizards-obey.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Make TOC height dynamic based on visible header and footer elements
diff --git a/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx b/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx
index c89bb24804..522bde0ca3 100644
--- a/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx
+++ b/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx
@@ -25,7 +25,7 @@ export function AnnouncementBanner(props: {
     const style = BANNER_STYLES[announcement.style];
 
     return (
-        <div className="announcement-banner theme-bold:bg-header-background pt-4 pb-2">
+        <div id="announcement-banner" className="theme-bold:bg-header-background pt-4 pb-2">
             <div className="scroll-nojump">
                 <div className={tcls('relative', CONTAINER_STYLE)}>
                     <Tag
diff --git a/packages/gitbook/src/components/Footer/Footer.tsx b/packages/gitbook/src/components/Footer/Footer.tsx
index 776c0a4af4..5723d67f3c 100644
--- a/packages/gitbook/src/components/Footer/Footer.tsx
+++ b/packages/gitbook/src/components/Footer/Footer.tsx
@@ -25,6 +25,7 @@ export function Footer(props: { context: GitBookSiteContext }) {
 
     return (
         <footer
+            id="site-footer"
             className={tcls(
                 'border-tint-subtle border-t',
                 // If the footer only contains a mode toggle, we only show it on smaller screens
diff --git a/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx b/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx
index c7d2f35b6e..c6f141425a 100644
--- a/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx
+++ b/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx
@@ -75,7 +75,7 @@ export async function CustomizationRootLayout(props: {
             lang={customization.internationalization.locale}
             className={tcls(
                 customization.header.preset === CustomizationHeaderPreset.None
-                    ? 'site-header-none'
+                    ? null
                     : 'scroll-pt-[76px]', // Take the sticky header in consideration for the scrolling
                 customization.styling.corners === CustomizationCorners.Straight
                     ? ' straight-corners'
diff --git a/packages/gitbook/src/components/RootLayout/globals.css b/packages/gitbook/src/components/RootLayout/globals.css
index 71dae7cc6b..15c09614e3 100644
--- a/packages/gitbook/src/components/RootLayout/globals.css
+++ b/packages/gitbook/src/components/RootLayout/globals.css
@@ -161,6 +161,6 @@ html.dark {
     color-scheme: dark light;
 }
 
-html.announcement-hidden .announcement-banner {
+html.announcement-hidden #announcement-banner {
     @apply hidden;
 }
diff --git a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx
index dcfb287660..7a6993fde1 100644
--- a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx
+++ b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx
@@ -6,6 +6,7 @@ import { tcls } from '@/lib/tailwind';
 
 import { PagesList } from './PagesList';
 import { TOCScrollContainer } from './TOCScroller';
+import { TableOfContentsScript } from './TableOfContentsScript';
 import { Trademark } from './Trademark';
 
 export function TableOfContents(props: {
@@ -17,97 +18,107 @@ export function TableOfContents(props: {
     const { space, customization, pages } = context;
 
     return (
-        <aside // Sidebar container, responsible for setting the right dimensions and position for the sidebar.
-            data-testid="table-of-contents"
-            className={tcls(
-                'group',
-                'text-sm',
+        <>
+            <aside // Sidebar container, responsible for setting the right dimensions and position for the sidebar.
+                data-testid="table-of-contents"
+                id="table-of-contents"
+                className={tcls(
+                    'group',
+                    'text-sm',
 
-                'grow-0',
-                'shrink-0',
-                'basis-full',
-                'lg:basis-72',
-                'page-no-toc:lg:basis-56',
+                    'grow-0',
+                    'shrink-0',
+                    'basis-full',
+                    'lg:basis-72',
+                    'page-no-toc:lg:basis-56',
 
-                'relative',
-                'z-[1]',
-                'lg:sticky',
-                // Without header
-                'lg:top-0',
-                'lg:h-screen',
+                    'relative',
+                    'z-[1]',
+                    'lg:sticky',
 
-                // With header
-                'site-header:lg:top-16',
-                'site-header:lg:h-[calc(100vh_-_4rem)]',
+                    // Server-side static positioning
+                    'lg:top-0',
+                    'lg:h-screen',
+                    'announcement:lg:h-[calc(100vh-4.25rem)]',
 
-                // With header and sections
-                'site-header-sections:lg:top-[6.75rem]',
-                'site-header-sections:lg:h-[calc(100vh_-_6.75rem)]',
+                    'site-header:lg:top-16',
+                    'site-header:lg:h-[calc(100vh-4rem)]',
+                    'announcement:site-header:lg:h-[calc(100vh-4rem-4.25rem)]',
 
-                'pt-6',
-                'pb-4',
-                'sidebar-filled:lg:pr-6',
-                'page-no-toc:lg:pr-0',
+                    'site-header-sections:lg:top-[6.75rem]',
+                    'site-header-sections:lg:h-[calc(100vh-6.75rem)]',
+                    'announcement:site-header-sections:lg:h-[calc(100vh-6.75rem-4.25rem)]',
 
-                'hidden',
-                'navigation-open:!flex',
-                'lg:flex',
-                'page-no-toc:lg:hidden',
-                'page-no-toc:xl:flex',
-                'site-header-none:page-no-toc:lg:flex',
-                'flex-col',
-                'gap-4',
+                    // Client-side dynamic positioning (CSS vars applied by script)
+                    '[html[style*="--toc-top-offset"]_&]:lg:!top-[var(--toc-top-offset)]',
+                    '[html[style*="--toc-height"]_&]:lg:!h-[var(--toc-height)]',
 
-                'navigation-open:border-b',
-                'border-tint-subtle'
-            )}
-        >
-            {header && header}
-            <div // The actual sidebar, either shown with a filled bg or transparent.
-                className={tcls(
-                    'lg:-ms-5',
-                    'relative flex flex-grow flex-col overflow-hidden border-tint-subtle',
+                    'pt-6',
+                    'pb-4',
+                    'sidebar-filled:lg:pr-6',
+                    'page-no-toc:lg:pr-0',
 
-                    'sidebar-filled:bg-tint-subtle',
-                    'theme-muted:bg-tint-subtle',
-                    '[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
-                    '[html.sidebar-filled.theme-muted_&]:bg-tint-base',
-                    '[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base',
-                    '[html.sidebar-filled.theme-gradient_&]:border',
-                    'page-no-toc:!bg-transparent',
+                    'hidden',
+                    'navigation-open:!flex',
+                    'lg:flex',
+                    'page-no-toc:lg:hidden',
+                    'page-no-toc:xl:flex',
+                    'site-header-none:page-no-toc:lg:flex',
+                    'flex-col',
+                    'gap-4',
 
-                    'sidebar-filled:rounded-xl',
-                    'straight-corners:rounded-none'
+                    'navigation-open:border-b',
+                    'border-tint-subtle'
                 )}
             >
-                {innerHeader && <div className="px-5 *:my-4">{innerHeader}</div>}
-                <TOCScrollContainer // The scrollview inside the sidebar
+                {header && header}
+                <div // The actual sidebar, either shown with a filled bg or transparent.
                     className={tcls(
-                        'flex flex-grow flex-col p-2',
-                        customization.trademark.enabled && 'lg:pb-20',
-                        'lg:gutter-stable overflow-y-auto',
-                        '[&::-webkit-scrollbar]:bg-transparent',
-                        '[&::-webkit-scrollbar-thumb]:bg-transparent',
-                        'group-hover:[&::-webkit-scrollbar]:bg-tint-subtle',
-                        'group-hover:[&::-webkit-scrollbar-thumb]:bg-tint-7',
-                        'group-hover:[&::-webkit-scrollbar-thumb:hover]:bg-tint-8'
+                        'lg:-ms-5',
+                        'relative flex flex-grow flex-col overflow-hidden border-tint-subtle',
+
+                        'sidebar-filled:bg-tint-subtle',
+                        'theme-muted:bg-tint-subtle',
+                        '[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
+                        '[html.sidebar-filled.theme-muted_&]:bg-tint-base',
+                        '[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base',
+                        '[html.sidebar-filled.theme-gradient_&]:border',
+                        'page-no-toc:!bg-transparent',
+
+                        'sidebar-filled:rounded-xl',
+                        'straight-corners:rounded-none'
                     )}
                 >
-                    <PagesList
-                        rootPages={pages}
-                        pages={pages}
-                        context={context}
-                        style="page-no-toc:hidden border-tint-subtle sidebar-list-line:border-l"
-                    />
-                    {customization.trademark.enabled ? (
-                        <Trademark
-                            space={space}
-                            customization={customization}
-                            placement={SiteInsightsTrademarkPlacement.Sidebar}
+                    {innerHeader && <div className="px-5 *:my-4">{innerHeader}</div>}
+                    <TOCScrollContainer // The scrollview inside the sidebar
+                        className={tcls(
+                            'flex flex-grow flex-col p-2',
+                            customization.trademark.enabled && 'lg:pb-20',
+                            'lg:gutter-stable overflow-y-auto',
+                            '[&::-webkit-scrollbar]:bg-transparent',
+                            '[&::-webkit-scrollbar-thumb]:bg-transparent',
+                            'group-hover:[&::-webkit-scrollbar]:bg-tint-subtle',
+                            'group-hover:[&::-webkit-scrollbar-thumb]:bg-tint-7',
+                            'group-hover:[&::-webkit-scrollbar-thumb:hover]:bg-tint-8'
+                        )}
+                    >
+                        <PagesList
+                            rootPages={pages}
+                            pages={pages}
+                            context={context}
+                            style="page-no-toc:hidden border-tint-subtle sidebar-list-line:border-l"
                         />
-                    ) : null}
-                </TOCScrollContainer>
-            </div>
-        </aside>
+                        {customization.trademark.enabled ? (
+                            <Trademark
+                                space={space}
+                                customization={customization}
+                                placement={SiteInsightsTrademarkPlacement.Sidebar}
+                            />
+                        ) : null}
+                    </TOCScrollContainer>
+                </div>
+            </aside>
+            <TableOfContentsScript />
+        </>
     );
 }
diff --git a/packages/gitbook/src/components/TableOfContents/TableOfContentsScript.tsx b/packages/gitbook/src/components/TableOfContents/TableOfContentsScript.tsx
new file mode 100644
index 0000000000..a4e6bea18c
--- /dev/null
+++ b/packages/gitbook/src/components/TableOfContents/TableOfContentsScript.tsx
@@ -0,0 +1,73 @@
+'use client';
+
+import { useEffect } from 'react';
+
+/**
+ * Adjusts TableOfContents height based on visible elements
+ */
+export function TableOfContentsScript() {
+    useEffect(() => {
+        const root = document.documentElement;
+
+        // Calculate and set TOC dimensions
+        const updateTocLayout = () => {
+            // Get key elements
+            const header = document.getElementById('site-header');
+            const banner = document.getElementById('announcement-banner');
+            const footer = document.getElementById('site-footer');
+
+            // Set sticky top position based on header
+            const headerHeight = header?.offsetHeight ?? 0;
+            root.style.setProperty('--toc-top-offset', `${headerHeight}px`);
+
+            // Start with full viewport height minus header
+            let height = window.innerHeight - headerHeight;
+
+            // Subtract visible banner (if any)
+            if (banner && window.getComputedStyle(banner).display !== 'none') {
+                const bannerRect = banner.getBoundingClientRect();
+                if (bannerRect.height > 0 && bannerRect.bottom > 0) {
+                    height -= Math.min(bannerRect.height, bannerRect.bottom);
+                }
+            }
+
+            // Subtract visible footer (if any)
+            if (footer) {
+                const footerRect = footer.getBoundingClientRect();
+                if (footerRect.top < window.innerHeight) {
+                    height -= Math.min(footerRect.height, window.innerHeight - footerRect.top);
+                }
+            }
+
+            // Update height
+            root.style.setProperty('--toc-height', `${height}px`);
+        };
+
+        // Initial update
+        updateTocLayout();
+
+        // Let the browser handle scroll throttling naturally
+        window.addEventListener('scroll', updateTocLayout, { passive: true });
+        window.addEventListener('resize', updateTocLayout, { passive: true });
+
+        // Use MutationObserver for DOM changes
+        const observer = new MutationObserver(() => {
+            requestAnimationFrame(updateTocLayout);
+        });
+
+        // Only observe what matters
+        observer.observe(document.documentElement, {
+            subtree: true,
+            attributes: true,
+            attributeFilter: ['style', 'class'],
+        });
+
+        return () => {
+            observer.disconnect();
+            window.removeEventListener('scroll', updateTocLayout);
+            window.removeEventListener('resize', updateTocLayout);
+        };
+    }, []);
+
+    return null;
+}
diff --git a/packages/gitbook/src/components/TableOfContents/index.ts b/packages/gitbook/src/components/TableOfContents/index.ts
index 715a3a239e..6eeff92697 100644
--- a/packages/gitbook/src/components/TableOfContents/index.ts
+++ b/packages/gitbook/src/components/TableOfContents/index.ts
@@ -1 +1,4 @@
-export * from './TableOfContents';
+export { TableOfContents } from './TableOfContents';
+export { PagesList } from './PagesList';
+export { TOCScrollContainer } from './TOCScroller';
+export { Trademark } from './Trademark';
diff --git a/packages/gitbook/tailwind.config.ts b/packages/gitbook/tailwind.config.ts
index 2e5b7c359f..9d248c9a40 100644
--- a/packages/gitbook/tailwind.config.ts
+++ b/packages/gitbook/tailwind.config.ts
@@ -458,12 +458,16 @@ const config: Config = {
             /**
              * Variant when a header is displayed.
              */
-            addVariant('site-header-none', 'html.site-header-none &');
+            addVariant('site-header-none', 'body:not(:has(#site-header:not(.mobile-only))) &');
             addVariant('site-header', 'body:has(#site-header:not(.mobile-only)) &');
             addVariant('site-header-sections', [
                 'body:has(#site-header:not(.mobile-only) #sections) &',
                 'body:has(.page-no-toc):has(#site-header:not(.mobile-only) #variants) &',
             ]);
+            addVariant(
+                'announcement',
+                'html:not(.announcement-hidden):has(#announcement-banner) &'
+            );
 
             const customisationVariants = {
                 // Sidebar styles

From d88dd5ed8af091bae7947ac4ecc8670592bcd2b4 Mon Sep 17 00:00:00 2001
From: conico974 <nicodorseuil@yahoo.fr>
Date: Tue, 20 May 2025 19:04:07 +0200
Subject: [PATCH 052/127] Update OpenNext (#3248)

Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
---
 bun.lock                           | 68 ++++++++++++++++--------------
 packages/gitbook-v2/package.json   |  2 +-
 packages/gitbook-v2/wrangler.jsonc |  6 +--
 3 files changed, 41 insertions(+), 35 deletions(-)

diff --git a/bun.lock b/bun.lock
index 2c13245d05..631adb8ce4 100644
--- a/bun.lock
+++ b/bun.lock
@@ -35,7 +35,7 @@
     },
     "packages/colors": {
       "name": "@gitbook/colors",
-      "version": "0.3.2",
+      "version": "0.3.3",
       "devDependencies": {
         "typescript": "^5.5.3",
       },
@@ -49,7 +49,7 @@
     },
     "packages/gitbook": {
       "name": "gitbook",
-      "version": "0.11.1",
+      "version": "0.12.0",
       "dependencies": {
         "@gitbook/api": "^0.115.0",
         "@gitbook/cache-do": "workspace:*",
@@ -141,11 +141,11 @@
     },
     "packages/gitbook-v2": {
       "name": "gitbook-v2",
-      "version": "0.2.5",
+      "version": "0.3.0",
       "dependencies": {
         "@gitbook/api": "^0.115.0",
         "@gitbook/cache-tags": "workspace:*",
-        "@opennextjs/cloudflare": "1.0.3",
+        "@opennextjs/cloudflare": "1.0.4",
         "@sindresorhus/fnv1a": "^3.1.0",
         "assert-never": "^1.2.1",
         "jwt-decode": "^4.0.0",
@@ -185,7 +185,7 @@
     },
     "packages/openapi-parser": {
       "name": "@gitbook/openapi-parser",
-      "version": "2.1.3",
+      "version": "2.1.4",
       "dependencies": {
         "@scalar/openapi-parser": "^0.10.10",
         "@scalar/openapi-types": "^0.1.9",
@@ -232,7 +232,7 @@
     },
     "packages/react-openapi": {
       "name": "@gitbook/react-openapi",
-      "version": "1.2.1",
+      "version": "1.3.0",
       "dependencies": {
         "@gitbook/openapi-parser": "workspace:*",
         "@scalar/api-client-react": "^1.2.19",
@@ -791,9 +791,9 @@
 
     "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
 
-    "@opennextjs/aws": ["@opennextjs/aws@3.6.1", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.19.2", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-RYU9K58vEUPXqc3pZO6kr9vBy1MmJZFQZLe0oXBskC005oGju/m4e3DCCP4eZ/Q/HdYQXCoqNXgSGi8VCAYgew=="],
+    "@opennextjs/aws": ["@opennextjs/aws@3.6.2", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.25.4", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-26/3GSoj7mKN7XpQFikYM2Lrwal5jlMc4fJO//QAdd5bCSnBaWBAkzr7+VvXAFVIC6eBeDLdtlWiQuUQVEAPZQ=="],
 
-    "@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.0.3", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "^3.6.1", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.14.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-+SNhz2JmKkMlWzndTSYZeq0eseAPfEXUUYJA7H9MkgFe36i1no0OdiLuwu9e4uR3yuOzaNWB+skCYHsD6Im9nA=="],
+    "@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.0.4", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "^3.6.2", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.14.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-DVudGpOSJ91ruPiBJ0kuFmPhEQbXIXLTbvUjAx1OlbwFskG2gvdNIAmF3ZXV6z1VGDO7Q/u2W2ybMZLf7avlrA=="],
 
     "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
 
@@ -3649,7 +3649,7 @@
 
     "@node-minify/core/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
 
-    "@opennextjs/aws/esbuild": ["esbuild@0.19.2", "", { "optionalDependencies": { "@esbuild/android-arm": "0.19.2", "@esbuild/android-arm64": "0.19.2", "@esbuild/android-x64": "0.19.2", "@esbuild/darwin-arm64": "0.19.2", "@esbuild/darwin-x64": "0.19.2", "@esbuild/freebsd-arm64": "0.19.2", "@esbuild/freebsd-x64": "0.19.2", "@esbuild/linux-arm": "0.19.2", "@esbuild/linux-arm64": "0.19.2", "@esbuild/linux-ia32": "0.19.2", "@esbuild/linux-loong64": "0.19.2", "@esbuild/linux-mips64el": "0.19.2", "@esbuild/linux-ppc64": "0.19.2", "@esbuild/linux-riscv64": "0.19.2", "@esbuild/linux-s390x": "0.19.2", "@esbuild/linux-x64": "0.19.2", "@esbuild/netbsd-x64": "0.19.2", "@esbuild/openbsd-x64": "0.19.2", "@esbuild/sunos-x64": "0.19.2", "@esbuild/win32-arm64": "0.19.2", "@esbuild/win32-ia32": "0.19.2", "@esbuild/win32-x64": "0.19.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-G6hPax8UbFakEj3hWO0Vs52LQ8k3lnBhxZWomUJDxfz3rZTLqF5k/FCzuNdLx2RbpBiQQF9H9onlDDH1lZsnjg=="],
+    "@opennextjs/aws/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="],
 
     "@radix-ui/react-collection/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="],
 
@@ -4721,49 +4721,55 @@
 
     "@node-minify/core/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.19.2", "", { "os": "android", "cpu": "arm" }, "sha512-tM8yLeYVe7pRyAu9VMi/Q7aunpLwD139EY1S99xbQkT4/q2qa6eA4ige/WJQYdJ8GBL1K33pPFhPfPdJ/WzT8Q=="],
+    "@opennextjs/aws/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.19.2", "", { "os": "android", "cpu": "arm64" }, "sha512-lsB65vAbe90I/Qe10OjkmrdxSX4UJDjosDgb8sZUKcg3oefEuW2OT2Vozz8ef7wrJbMcmhvCC+hciF8jY/uAkw=="],
+    "@opennextjs/aws/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.19.2", "", { "os": "android", "cpu": "x64" }, "sha512-qK/TpmHt2M/Hg82WXHRc/W/2SGo/l1thtDHZWqFq7oi24AjZ4O/CpPSu6ZuYKFkEgmZlFoa7CooAyYmuvnaG8w=="],
+    "@opennextjs/aws/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.4", "", { "os": "android", "cpu": "arm64" }, "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.19.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Ora8JokrvrzEPEpZO18ZYXkH4asCdc1DLdcVy8TGf5eWtPO1Ie4WroEJzwI52ZGtpODy3+m0a2yEX9l+KUn0tA=="],
+    "@opennextjs/aws/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.4", "", { "os": "android", "cpu": "x64" }, "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.19.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-tP+B5UuIbbFMj2hQaUr6EALlHOIOmlLM2FK7jeFBobPy2ERdohI4Ka6ZFjZ1ZYsrHE/hZimGuU90jusRE0pwDw=="],
+    "@opennextjs/aws/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.19.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-YbPY2kc0acfzL1VPVK6EnAlig4f+l8xmq36OZkU0jzBVHcOTyQDhnKQaLzZudNJQyymd9OqQezeaBgkTGdTGeQ=="],
+    "@opennextjs/aws/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.19.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-nSO5uZT2clM6hosjWHAsS15hLrwCvIWx+b2e3lZ3MwbYSaXwvfO528OF+dLjas1g3bZonciivI8qKR/Hm7IWGw=="],
+    "@opennextjs/aws/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.19.2", "", { "os": "linux", "cpu": "arm" }, "sha512-Odalh8hICg7SOD7XCj0YLpYCEc+6mkoq63UnExDCiRA2wXEmGlK5JVrW50vZR9Qz4qkvqnHcpH+OFEggO3PgTg=="],
+    "@opennextjs/aws/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.19.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-ig2P7GeG//zWlU0AggA3pV1h5gdix0MA3wgB+NsnBXViwiGgY77fuN9Wr5uoCrs2YzaYfogXgsWZbm+HGr09xg=="],
+    "@opennextjs/aws/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.4", "", { "os": "linux", "cpu": "arm" }, "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.19.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-mLfp0ziRPOLSTek0Gd9T5B8AtzKAkoZE70fneiiyPlSnUKKI4lp+mGEnQXcQEHLJAcIYDPSyBvsUbKUG2ri/XQ=="],
+    "@opennextjs/aws/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.19.2", "", { "os": "linux", "cpu": "none" }, "sha512-hn28+JNDTxxCpnYjdDYVMNTR3SKavyLlCHHkufHV91fkewpIyQchS1d8wSbmXhs1fiYDpNww8KTFlJ1dHsxeSw=="],
+    "@opennextjs/aws/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.19.2", "", { "os": "linux", "cpu": "none" }, "sha512-KbXaC0Sejt7vD2fEgPoIKb6nxkfYW9OmFUK9XQE4//PvGIxNIfPk1NmlHmMg6f25x57rpmEFrn1OotASYIAaTg=="],
+    "@opennextjs/aws/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.19.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-dJ0kE8KTqbiHtA3Fc/zn7lCd7pqVr4JcT0JqOnbj4LLzYnp+7h8Qi4yjfq42ZlHfhOCM42rBh0EwHYLL6LEzcw=="],
+    "@opennextjs/aws/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.19.2", "", { "os": "linux", "cpu": "none" }, "sha512-7Z/jKNFufZ/bbu4INqqCN6DDlrmOTmdw6D0gH+6Y7auok2r02Ur661qPuXidPOJ+FSgbEeQnnAGgsVynfLuOEw=="],
+    "@opennextjs/aws/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.19.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-U+RinR6aXXABFCcAY4gSlv4CL1oOVvSSCdseQmGO66H+XyuQGZIUdhG56SZaDJQcLmrSfRmx5XZOWyCJPRqS7g=="],
+    "@opennextjs/aws/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.19.2", "", { "os": "linux", "cpu": "x64" }, "sha512-oxzHTEv6VPm3XXNaHPyUTTte+3wGv7qVQtqaZCrgstI16gCuhNOtBXLEBkBREP57YTd68P0VgDgG73jSD8bwXQ=="],
+    "@opennextjs/aws/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.19.2", "", { "os": "none", "cpu": "x64" }, "sha512-WNa5zZk1XpTTwMDompZmvQLHszDDDN7lYjEHCUmAGB83Bgs20EMs7ICD+oKeT6xt4phV4NDdSi/8OfjPbSbZfQ=="],
+    "@opennextjs/aws/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.4", "", { "os": "linux", "cpu": "x64" }, "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.19.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-S6kI1aT3S++Dedb7vxIuUOb3oAxqxk2Rh5rOXOTYnzN8JzW1VzBd+IqPiSpgitu45042SYD3HCoEyhLKQcDFDw=="],
+    "@opennextjs/aws/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.4", "", { "os": "none", "cpu": "arm64" }, "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.19.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-VXSSMsmb+Z8LbsQGcBMiM+fYObDNRm8p7tkUDMPG/g4fhFX5DEFmjxIEa3N8Zr96SjsJ1woAhF0DUnS3MF3ARw=="],
+    "@opennextjs/aws/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.4", "", { "os": "none", "cpu": "x64" }, "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.19.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-5NayUlSAyb5PQYFAU9x3bHdsqB88RC3aM9lKDAz4X1mo/EchMIT1Q+pSeBXNgkfNmRecLXA0O8xP+x8V+g/LKg=="],
+    "@opennextjs/aws/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.19.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-47gL/ek1v36iN0wL9L4Q2MFdujR0poLZMJwhO2/N3gA89jgHp4MR8DKCmwYtGNksbfJb9JoTtbkoe6sDhg2QTA=="],
+    "@opennextjs/aws/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.2", "", { "os": "win32", "cpu": "x64" }, "sha512-tcuhV7ncXBqbt/Ybf0IyrMcwVOAPDckMK9rXNHtF17UTK18OKLpg08glminN06pt2WCoALhXdLfSPbVvK/6fxw=="],
+    "@opennextjs/aws/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q=="],
+
+    "@opennextjs/aws/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ=="],
+
+    "@opennextjs/aws/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg=="],
+
+    "@opennextjs/aws/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="],
 
     "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="],
 
diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json
index 355518c4f6..5561b7f8d3 100644
--- a/packages/gitbook-v2/package.json
+++ b/packages/gitbook-v2/package.json
@@ -5,7 +5,7 @@
     "dependencies": {
         "@gitbook/api": "^0.115.0",
         "@gitbook/cache-tags": "workspace:*",
-        "@opennextjs/cloudflare": "1.0.3",
+        "@opennextjs/cloudflare": "1.0.4",
         "@sindresorhus/fnv1a": "^3.1.0",
         "assert-never": "^1.2.1",
         "jwt-decode": "^4.0.0",
diff --git a/packages/gitbook-v2/wrangler.jsonc b/packages/gitbook-v2/wrangler.jsonc
index 6d8ccf9955..8d420d3b2d 100644
--- a/packages/gitbook-v2/wrangler.jsonc
+++ b/packages/gitbook-v2/wrangler.jsonc
@@ -91,9 +91,9 @@
         },
         "production": {
             "vars": {
-                // This is a bit misleading, but it means that we can have 100 concurrent revalidations
-                // This means that we'll have up to 20 durable objects instance running at the same time
-                "MAX_REVALIDATE_CONCURRENCY": "20"
+                // This is a bit misleading, but it means that we can have 500 concurrent revalidations
+                // This means that we'll have up to 100 durable objects instance running at the same time
+                "MAX_REVALIDATE_CONCURRENCY": "100"
             },
             "routes": [
                 {

From b259009c9c94e1fb76c11ee40bc99650266a2f87 Mon Sep 17 00:00:00 2001
From: conico974 <nicodorseuil@yahoo.fr>
Date: Tue, 20 May 2025 19:33:33 +0200
Subject: [PATCH 053/127] Add temporary logging for Cloudflare debugging
 (#3249)

Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
---
 packages/gitbook-v2/wrangler.jsonc      |  5 ++++-
 packages/gitbook/src/routes/ogimage.tsx | 15 +++++++++++++++
 2 files changed, 19 insertions(+), 1 deletion(-)

diff --git a/packages/gitbook-v2/wrangler.jsonc b/packages/gitbook-v2/wrangler.jsonc
index 8d420d3b2d..03fbcec65b 100644
--- a/packages/gitbook-v2/wrangler.jsonc
+++ b/packages/gitbook-v2/wrangler.jsonc
@@ -93,7 +93,10 @@
             "vars": {
                 // This is a bit misleading, but it means that we can have 500 concurrent revalidations
                 // This means that we'll have up to 100 durable objects instance running at the same time
-                "MAX_REVALIDATE_CONCURRENCY": "100"
+                "MAX_REVALIDATE_CONCURRENCY": "100",
+                // Temporary variable to find the issue once deployed
+                // TODO: remove this once the issue is fixed
+                "DEBUG_CLOUDFLARE": "true"
             },
             "routes": [
                 {
diff --git a/packages/gitbook/src/routes/ogimage.tsx b/packages/gitbook/src/routes/ogimage.tsx
index 41842ab241..c1cfd97723 100644
--- a/packages/gitbook/src/routes/ogimage.tsx
+++ b/packages/gitbook/src/routes/ogimage.tsx
@@ -311,6 +311,17 @@ async function loadCustomFont(input: { url: string; weight: 400 | 700 }) {
     };
 }
 
+/**
+ * Temporary function to log some data on Cloudflare.
+ * TODO: remove this when we found the issue
+ */
+function logOnCloudflareOnly(message: string) {
+    if (process.env.DEBUG_CLOUDFLARE === 'true') {
+        // biome-ignore lint/suspicious/noConsole: <explanation>
+        console.log(message);
+    }
+}
+
 /**
  * Fetch a resource from the function itself.
  * To avoid error with worker to worker requests in the same zone, we use the `WORKER_SELF_REFERENCE` binding.
@@ -318,6 +329,7 @@ async function loadCustomFont(input: { url: string; weight: 400 | 700 }) {
 async function fetchSelf(url: string) {
     const cloudflare = getCloudflareContext();
     if (cloudflare?.env.WORKER_SELF_REFERENCE) {
+        logOnCloudflareOnly(`Fetching self: ${url}`);
         return await cloudflare.env.WORKER_SELF_REFERENCE.fetch(
             // `getAssetURL` can return a relative URL, so we need to make it absolute
             // the URL doesn't matter, as we're using the worker-self-reference binding
@@ -334,6 +346,9 @@ async function fetchSelf(url: string) {
 async function readImage(response: Response) {
     const contentType = response.headers.get('content-type');
     if (!contentType || !contentType.startsWith('image/')) {
+        logOnCloudflareOnly(`Invalid content type: ${contentType}, 
+            status: ${response.status}
+            rayId: ${response.headers.get('cf-ray')}`);
         throw new Error(`Invalid content type: ${contentType}`);
     }
 

From ba0094a862e28b83728cc05c2503c4b652cf93ed Mon Sep 17 00:00:00 2001
From: conico974 <nicodorseuil@yahoo.fr>
Date: Wed, 21 May 2025 09:52:05 +0200
Subject: [PATCH 054/127] fix ISR in preview in cloudflare (#3250)

Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
---
 .changeset/nasty-moles-visit.md         |  5 +++++
 packages/gitbook-v2/open-next.config.ts |  3 +--
 packages/gitbook-v2/openNext/queue.ts   | 17 +++++++++++++++++
 packages/gitbook-v2/wrangler.jsonc      |  6 +++++-
 4 files changed, 28 insertions(+), 3 deletions(-)
 create mode 100644 .changeset/nasty-moles-visit.md
 create mode 100644 packages/gitbook-v2/openNext/queue.ts

diff --git a/.changeset/nasty-moles-visit.md b/.changeset/nasty-moles-visit.md
new file mode 100644
index 0000000000..031fb81e30
--- /dev/null
+++ b/.changeset/nasty-moles-visit.md
@@ -0,0 +1,5 @@
+---
+"gitbook-v2": patch
+---
+
+fix ISR on preview env
diff --git a/packages/gitbook-v2/open-next.config.ts b/packages/gitbook-v2/open-next.config.ts
index 060cad541c..d35c9ef03e 100644
--- a/packages/gitbook-v2/open-next.config.ts
+++ b/packages/gitbook-v2/open-next.config.ts
@@ -1,5 +1,4 @@
 import { defineCloudflareConfig } from '@opennextjs/cloudflare';
-import doQueue from '@opennextjs/cloudflare/overrides/queue/do-queue';
 import doShardedTagCache from '@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache';
 import {
     softTagFilter,
@@ -20,7 +19,7 @@ export default defineCloudflareConfig({
         // We don't use `revalidatePath`, so we filter out soft tags
         filterFn: softTagFilter,
     }),
-    queue: doQueue,
+    queue: () => import('./openNext/queue').then((m) => m.default),
 
     // Performance improvements as we don't use PPR
     enableCacheInterception: true,
diff --git a/packages/gitbook-v2/openNext/queue.ts b/packages/gitbook-v2/openNext/queue.ts
new file mode 100644
index 0000000000..ab33c479d9
--- /dev/null
+++ b/packages/gitbook-v2/openNext/queue.ts
@@ -0,0 +1,17 @@
+import type { Queue } from '@opennextjs/aws/types/overrides.js';
+import { getCloudflareContext } from '@opennextjs/cloudflare';
+import doQueue from '@opennextjs/cloudflare/overrides/queue/do-queue';
+import memoryQueue from '@opennextjs/cloudflare/overrides/queue/memory-queue';
+
+interface Env {
+    IS_PREVIEW?: string;
+}
+
+export default {
+    name: 'GitbookISRQueue',
+    send: async (msg) => {
+        const { ctx, env } = getCloudflareContext();
+        const isPreview = (env as Env).IS_PREVIEW === 'true';
+        ctx.waitUntil(isPreview ? memoryQueue.send(msg) : doQueue.send(msg));
+    },
+} satisfies Queue;
diff --git a/packages/gitbook-v2/wrangler.jsonc b/packages/gitbook-v2/wrangler.jsonc
index 03fbcec65b..6d9c2b2324 100644
--- a/packages/gitbook-v2/wrangler.jsonc
+++ b/packages/gitbook-v2/wrangler.jsonc
@@ -15,10 +15,14 @@
         "enabled": true
     },
     "vars": {
-        "NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
+        "NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true",
+        "IS_PREVIEW": "false"
     },
     "env": {
         "preview": {
+            "vars": {
+                "IS_PREVIEW": "true"
+            },
             "r2_buckets": [
                 {
                     "binding": "NEXT_INC_CACHE_R2_BUCKET",

From fae34e56ac40f6684beab5031cc1f00e8df35cbc Mon Sep 17 00:00:00 2001
From: Viktor Renkema <49148610+viktorrenkema@users.noreply.github.com>
Date: Wed, 21 May 2025 14:20:39 +0200
Subject: [PATCH 055/127] Calculate class more explicitly & separately to fix
 horizontal table cell alignment issue (#3252)

---
 .../src/components/DocumentView/Table/RecordColumnValue.tsx   | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx b/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx
index aa1bf45b86..50dc0d8270 100644
--- a/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx
+++ b/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx
@@ -115,7 +115,8 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
                 return <Tag className={tcls(['w-full', verticalAlignment])}>{''}</Tag>;
             }
 
-            const horizontalAlignment = `[&_*]:${getColumnAlignment(definition)} ${getColumnAlignment(definition)}`;
+            const horizontalAlignment = getColumnAlignment(definition);
+            const childrenHorizontalAlignment = `[&_*]:${horizontalAlignment}`;
 
             return (
                 <Blocks
@@ -131,6 +132,7 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
                         'leading-normal',
                         verticalAlignment,
                         horizontalAlignment,
+                        childrenHorizontalAlignment,
                     ]}
                     context={context}
                     blockStyle={['w-full', 'max-w-[unset]']}

From 19c20c0be35374d7126db17009a40d313795ced8 Mon Sep 17 00:00:00 2001
From: conico974 <nicodorseuil@yahoo.fr>
Date: Wed, 21 May 2025 15:39:52 +0200
Subject: [PATCH 056/127] Fix OGImage icon fetch on cloudflare (#3253)

Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
---
 packages/gitbook/src/routes/ogimage.tsx | 22 ++--------------------
 1 file changed, 2 insertions(+), 20 deletions(-)

diff --git a/packages/gitbook/src/routes/ogimage.tsx b/packages/gitbook/src/routes/ogimage.tsx
index c1cfd97723..ee586cb1b3 100644
--- a/packages/gitbook/src/routes/ogimage.tsx
+++ b/packages/gitbook/src/routes/ogimage.tsx
@@ -9,7 +9,6 @@ import { getAssetURL } from '@/lib/assets';
 import { filterOutNullable } from '@/lib/typescript';
 import { getCacheTag } from '@gitbook/cache-tags';
 import type { GitBookSiteContext } from '@v2/lib/context';
-import { getCloudflareContext } from '@v2/lib/data/cloudflare';
 import { getResizedImageURL } from '@v2/lib/images';
 
 const googleFontsMap: { [fontName in CustomizationDefaultFont]: string } = {
@@ -322,24 +321,6 @@ function logOnCloudflareOnly(message: string) {
     }
 }
 
-/**
- * Fetch a resource from the function itself.
- * To avoid error with worker to worker requests in the same zone, we use the `WORKER_SELF_REFERENCE` binding.
- */
-async function fetchSelf(url: string) {
-    const cloudflare = getCloudflareContext();
-    if (cloudflare?.env.WORKER_SELF_REFERENCE) {
-        logOnCloudflareOnly(`Fetching self: ${url}`);
-        return await cloudflare.env.WORKER_SELF_REFERENCE.fetch(
-            // `getAssetURL` can return a relative URL, so we need to make it absolute
-            // the URL doesn't matter, as we're using the worker-self-reference binding
-            new URL(url, 'https://worker-self-reference/')
-        );
-    }
-
-    return await fetch(url);
-}
-
 /**
  * Read an image from a response as a base64 encoded string.
  */
@@ -363,6 +344,7 @@ const staticImagesCache = new Map<string, string>();
  * Read a static image and cache it in memory.
  */
 async function readStaticImage(url: string) {
+    logOnCloudflareOnly(`Reading static image: ${url}, cache size: ${staticImagesCache.size}`);
     const cached = staticImagesCache.get(url);
     if (cached) {
         return cached;
@@ -377,7 +359,7 @@ async function readStaticImage(url: string) {
  * Read an image from GitBook itself.
  */
 async function readSelfImage(url: string) {
-    const response = await fetchSelf(url);
+    const response = await fetch(url);
     const image = await readImage(response);
     return image;
 }

From a0c06a72cc8aa255bc8c068e6dca5a5701ff4314 Mon Sep 17 00:00:00 2001
From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com>
Date: Fri, 23 May 2025 09:13:20 +0200
Subject: [PATCH 057/127] Indent JSON python code sample (#3260)

---
 .changeset/rotten-seals-rush.md               |  5 ++++
 .../react-openapi/src/code-samples.test.ts    |  4 +--
 packages/react-openapi/src/code-samples.ts    | 28 +++++++++++--------
 3 files changed, 23 insertions(+), 14 deletions(-)
 create mode 100644 .changeset/rotten-seals-rush.md

diff --git a/.changeset/rotten-seals-rush.md b/.changeset/rotten-seals-rush.md
new file mode 100644
index 0000000000..950e25880c
--- /dev/null
+++ b/.changeset/rotten-seals-rush.md
@@ -0,0 +1,5 @@
+---
+'@gitbook/react-openapi': patch
+---
+
+Indent JSON python code sample
diff --git a/packages/react-openapi/src/code-samples.test.ts b/packages/react-openapi/src/code-samples.test.ts
index 7fd24bb3f8..375cee84be 100644
--- a/packages/react-openapi/src/code-samples.test.ts
+++ b/packages/react-openapi/src/code-samples.test.ts
@@ -400,7 +400,7 @@ describe('python code sample generator', () => {
         const output = generator?.generate(input);
 
         expect(output).toBe(
-            'import requests\n\nresponse = requests.get(\n    "https://example.com/path",\n    headers={"Content-Type":"application/x-www-form-urlencoded"},\n    data={"key":"value"}\n)\n\ndata = response.json()'
+            'import requests\n\nresponse = requests.get(\n    "https://example.com/path",\n    headers={"Content-Type":"application/x-www-form-urlencoded"},\n    data={\n      "key": "value"\n    }\n)\n\ndata = response.json()'
         );
     });
 
@@ -422,7 +422,7 @@ describe('python code sample generator', () => {
         const output = generator?.generate(input);
 
         expect(output).toBe(
-            'import requests\n\nresponse = requests.get(\n    "https://example.com/path",\n    headers={"Content-Type":"application/json"},\n    data=json.dumps({"key":"value","truethy":True,"falsey":False,"nullish":None})\n)\n\ndata = response.json()'
+            'import requests\n\nresponse = requests.get(\n    "https://example.com/path",\n    headers={"Content-Type":"application/json"},\n    data=json.dumps({\n      "key": "value",\n      "truethy": True,\n      "falsey": False,\n      "nullish": None\n    })\n)\n\ndata = response.json()'
         );
     });
 
diff --git a/packages/react-openapi/src/code-samples.ts b/packages/react-openapi/src/code-samples.ts
index 50c6a9204c..8d855bbb67 100644
--- a/packages/react-openapi/src/code-samples.ts
+++ b/packages/react-openapi/src/code-samples.ts
@@ -356,18 +356,22 @@ const BodyGenerators = {
             // Convert JSON to XML if needed
             body = JSON.stringify(convertBodyToXML(body));
         } else {
-            body = stringifyOpenAPI(body, (_key, value) => {
-                switch (value) {
-                    case true:
-                        return '$$__TRUE__$$';
-                    case false:
-                        return '$$__FALSE__$$';
-                    case null:
-                        return '$$__NULL__$$';
-                    default:
-                        return value;
-                }
-            })
+            body = stringifyOpenAPI(
+                body,
+                (_key, value) => {
+                    switch (value) {
+                        case true:
+                            return '$$__TRUE__$$';
+                        case false:
+                            return '$$__FALSE__$$';
+                        case null:
+                            return '$$__NULL__$$';
+                        default:
+                            return value;
+                    }
+                },
+                2
+            )
                 .replaceAll('"$$__TRUE__$$"', 'True')
                 .replaceAll('"$$__FALSE__$$"', 'False')
                 .replaceAll('"$$__NULL__$$"', 'None');

From dc4268db64144c0e144d2541b317969a35fe5c2a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Fri, 23 May 2025 10:01:15 +0200
Subject: [PATCH 058/127] Compute correct urls for variants/sections when
 previewing unpublished site (#3259)

---
 .changeset/cool-seas-approve.md                    |  5 +++++
 .../src/components/Header/SpacesDropdown.tsx       | 14 +++++++++++++-
 .../SiteSections/encodeClientSiteSections.ts       |  4 +++-
 3 files changed, 21 insertions(+), 2 deletions(-)
 create mode 100644 .changeset/cool-seas-approve.md

diff --git a/.changeset/cool-seas-approve.md b/.changeset/cool-seas-approve.md
new file mode 100644
index 0000000000..c97f692e6d
--- /dev/null
+++ b/.changeset/cool-seas-approve.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Fix navigation between sections/variants when previewing a site in v2
diff --git a/packages/gitbook/src/components/Header/SpacesDropdown.tsx b/packages/gitbook/src/components/Header/SpacesDropdown.tsx
index 9728216b74..3fc1335230 100644
--- a/packages/gitbook/src/components/Header/SpacesDropdown.tsx
+++ b/packages/gitbook/src/components/Header/SpacesDropdown.tsx
@@ -2,6 +2,7 @@ import type { SiteSpace } from '@gitbook/api';
 
 import { tcls } from '@/lib/tailwind';
 
+import { joinPath } from '@/lib/paths';
 import type { GitBookSiteContext } from '@v2/lib/context';
 import { DropdownChevron, DropdownMenu } from './DropdownMenu';
 import { SpacesDropdownMenuItem } from './SpacesDropdownMenuItem';
@@ -74,7 +75,7 @@ export function SpacesDropdown(props: {
                         title: otherSiteSpace.title,
                         url: otherSiteSpace.urls.published
                             ? linker.toLinkForContent(otherSiteSpace.urls.published)
-                            : otherSiteSpace.space.urls.app,
+                            : getFallbackSiteSpaceURL(otherSiteSpace, context),
                     }}
                     active={otherSiteSpace.id === siteSpace.id}
                 />
@@ -82,3 +83,14 @@ export function SpacesDropdown(props: {
         </DropdownMenu>
     );
 }
+
+/**
+ * When the site is not published yet, `urls.published` is not available.
+ * To ensure navigation works in preview, we compute a relative URL from the siteSpace path.
+ */
+function getFallbackSiteSpaceURL(siteSpace: SiteSpace, context: GitBookSiteContext) {
+    const { linker, sections } = context;
+    return linker.toPathInSite(
+        sections?.current ? joinPath(sections.current.path, siteSpace.path) : siteSpace.path
+    );
+}
diff --git a/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts b/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts
index 79a007ecc3..49b73a8e1a 100644
--- a/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts
+++ b/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts
@@ -53,6 +53,8 @@ function encodeSection(context: GitBookSiteContext, section: SiteSection) {
         description: section.description,
         icon: section.icon,
         object: section.object,
-        url: section.urls.published ? linker.toLinkForContent(section.urls.published) : '',
+        url: section.urls.published
+            ? linker.toLinkForContent(section.urls.published)
+            : linker.toPathInSite(section.path),
     };
 }

From fa3eb076177571914ab4bd40c83757bba64e0bc7 Mon Sep 17 00:00:00 2001
From: conico974 <nicodorseuil@yahoo.fr>
Date: Fri, 23 May 2025 10:20:07 +0200
Subject: [PATCH 059/127] Cache fonts and static image in memory for OG Image
 (#3258)

Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
---
 .changeset/stupid-plums-perform.md      |  6 +++++
 packages/gitbook/src/routes/ogimage.tsx | 34 ++++++++++++++++---------
 2 files changed, 28 insertions(+), 12 deletions(-)
 create mode 100644 .changeset/stupid-plums-perform.md

diff --git a/.changeset/stupid-plums-perform.md b/.changeset/stupid-plums-perform.md
new file mode 100644
index 0000000000..34707627ea
--- /dev/null
+++ b/.changeset/stupid-plums-perform.md
@@ -0,0 +1,6 @@
+---
+"gitbook": patch
+"gitbook-v2": patch
+---
+
+cache fonts and static image used in OGImage in memory
diff --git a/packages/gitbook/src/routes/ogimage.tsx b/packages/gitbook/src/routes/ogimage.tsx
index ee586cb1b3..b086ffd1cd 100644
--- a/packages/gitbook/src/routes/ogimage.tsx
+++ b/packages/gitbook/src/routes/ogimage.tsx
@@ -72,8 +72,12 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
 
             const fonts = (
                 await Promise.all([
-                    loadGoogleFont({ fontFamily, text: regularText, weight: 400 }),
-                    loadGoogleFont({ fontFamily, text: boldText, weight: 700 }),
+                    getWithCache(`google-font:${fontFamily}:400`, () =>
+                        loadGoogleFont({ fontFamily, text: regularText, weight: 400 })
+                    ),
+                    getWithCache(`google-font:${fontFamily}:700`, () =>
+                        loadGoogleFont({ fontFamily, text: boldText, weight: 700 })
+                    ),
                 ])
             ).filter(filterOutNullable);
 
@@ -338,21 +342,27 @@ async function readImage(response: Response) {
     return `data:${contentType};base64,${base64}`;
 }
 
-const staticImagesCache = new Map<string, string>();
+// biome-ignore lint/suspicious/noExplicitAny: <explanation>
+const staticCache = new Map<string, any>();
+
+// Do we need to limit the in-memory cache size? I think given the usage, we should be fine.
+async function getWithCache<T>(key: string, fn: () => Promise<T>) {
+    const cached = staticCache.get(key) as T;
+    if (cached) {
+        return Promise.resolve(cached);
+    }
+
+    const result = await fn();
+    staticCache.set(key, result);
+    return result;
+}
 
 /**
  * Read a static image and cache it in memory.
  */
 async function readStaticImage(url: string) {
-    logOnCloudflareOnly(`Reading static image: ${url}, cache size: ${staticImagesCache.size}`);
-    const cached = staticImagesCache.get(url);
-    if (cached) {
-        return cached;
-    }
-
-    const image = await readSelfImage(url);
-    staticImagesCache.set(url, image);
-    return image;
+    logOnCloudflareOnly(`Reading static image: ${url}, cache size: ${staticCache.size}`);
+    return getWithCache(`static-image:${url}`, () => readSelfImage(url));
 }
 
 /**

From 6f8ecd611138ce1a148687696a01009aa12452a3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Fri, 23 May 2025 13:16:43 +0200
Subject: [PATCH 060/127] Use more strict context ID for images during preview
 (#3261)

---
 packages/gitbook-v2/src/lib/data/index.ts     |  1 -
 packages/gitbook-v2/src/lib/data/visitor.ts   |  2 +-
 .../images/getImageResizingContextId.test.ts  | 19 +++++++++++++++++
 .../lib/images/getImageResizingContextId.ts   |  6 +++++-
 packages/gitbook-v2/src/lib/preview.test.ts   | 21 +++++++++++++++++++
 packages/gitbook-v2/src/lib/preview.ts        | 13 ++++++++++++
 .../src/lib/{data => }/proxy.test.ts          |  0
 .../gitbook-v2/src/lib/{data => }/proxy.ts    |  0
 8 files changed, 59 insertions(+), 3 deletions(-)
 create mode 100644 packages/gitbook-v2/src/lib/images/getImageResizingContextId.test.ts
 create mode 100644 packages/gitbook-v2/src/lib/preview.test.ts
 create mode 100644 packages/gitbook-v2/src/lib/preview.ts
 rename packages/gitbook-v2/src/lib/{data => }/proxy.test.ts (100%)
 rename packages/gitbook-v2/src/lib/{data => }/proxy.ts (100%)

diff --git a/packages/gitbook-v2/src/lib/data/index.ts b/packages/gitbook-v2/src/lib/data/index.ts
index d79049684a..2e37e2fbb4 100644
--- a/packages/gitbook-v2/src/lib/data/index.ts
+++ b/packages/gitbook-v2/src/lib/data/index.ts
@@ -4,5 +4,4 @@ export * from './pages';
 export * from './urls';
 export * from './errors';
 export * from './lookup';
-export * from './proxy';
 export * from './visitor';
diff --git a/packages/gitbook-v2/src/lib/data/visitor.ts b/packages/gitbook-v2/src/lib/data/visitor.ts
index f93f0afe87..e59e32365c 100644
--- a/packages/gitbook-v2/src/lib/data/visitor.ts
+++ b/packages/gitbook-v2/src/lib/data/visitor.ts
@@ -1,6 +1,6 @@
 import { withLeadingSlash, withTrailingSlash } from '@/lib/paths';
 import type { PublishedSiteContent } from '@gitbook/api';
-import { getProxyRequestIdentifier, isProxyRequest } from './proxy';
+import { getProxyRequestIdentifier, isProxyRequest } from '@v2/lib/proxy';
 
 /**
  * Get the appropriate base path for the visitor authentication cookie.
diff --git a/packages/gitbook-v2/src/lib/images/getImageResizingContextId.test.ts b/packages/gitbook-v2/src/lib/images/getImageResizingContextId.test.ts
new file mode 100644
index 0000000000..eaba01663b
--- /dev/null
+++ b/packages/gitbook-v2/src/lib/images/getImageResizingContextId.test.ts
@@ -0,0 +1,19 @@
+import { describe, expect, it } from 'bun:test';
+import { getImageResizingContextId } from './getImageResizingContextId';
+
+describe('getImageResizingContextId', () => {
+    it('should return proxy identifier for proxy requests', () => {
+        const proxyRequestURL = new URL('https://proxy.gitbook.site/sites/site_foo/hello/world');
+        expect(getImageResizingContextId(proxyRequestURL)).toBe('sites/site_foo');
+    });
+
+    it('should return preview identifier for preview requests', () => {
+        const previewRequestURL = new URL('https://preview/site_foo/hello/world');
+        expect(getImageResizingContextId(previewRequestURL)).toBe('site_foo');
+    });
+
+    it('should return host for regular requests', () => {
+        const regularRequestURL = new URL('https://example.com/docs/foo/hello/world');
+        expect(getImageResizingContextId(regularRequestURL)).toBe('example.com');
+    });
+});
diff --git a/packages/gitbook-v2/src/lib/images/getImageResizingContextId.ts b/packages/gitbook-v2/src/lib/images/getImageResizingContextId.ts
index 82f9225262..40594e2ae2 100644
--- a/packages/gitbook-v2/src/lib/images/getImageResizingContextId.ts
+++ b/packages/gitbook-v2/src/lib/images/getImageResizingContextId.ts
@@ -1,4 +1,5 @@
-import { getProxyRequestIdentifier, isProxyRequest } from '../data';
+import { getPreviewRequestIdentifier, isPreviewRequest } from '@v2/lib/preview';
+import { getProxyRequestIdentifier, isProxyRequest } from '@v2/lib/proxy';
 
 /**
  * Get the site identifier to use for image resizing for an incoming request.
@@ -8,6 +9,9 @@ export function getImageResizingContextId(url: URL): string {
     if (isProxyRequest(url)) {
         return getProxyRequestIdentifier(url);
     }
+    if (isPreviewRequest(url)) {
+        return getPreviewRequestIdentifier(url);
+    }
 
     return url.host;
 }
diff --git a/packages/gitbook-v2/src/lib/preview.test.ts b/packages/gitbook-v2/src/lib/preview.test.ts
new file mode 100644
index 0000000000..bbaf0402bd
--- /dev/null
+++ b/packages/gitbook-v2/src/lib/preview.test.ts
@@ -0,0 +1,21 @@
+import { describe, expect, it } from 'bun:test';
+import { getPreviewRequestIdentifier, isPreviewRequest } from './preview';
+
+describe('isPreviewRequest', () => {
+    it('should return true for preview requests', () => {
+        const previewRequestURL = new URL('https://preview/site_foo/hello/world');
+        expect(isPreviewRequest(previewRequestURL)).toBe(true);
+    });
+
+    it('should return false for non-preview requests', () => {
+        const nonPreviewRequestURL = new URL('https://example.com/docs/foo/hello/world');
+        expect(isPreviewRequest(nonPreviewRequestURL)).toBe(false);
+    });
+});
+
+describe('getPreviewRequestIdentifier', () => {
+    it('should return the correct identifier for preview requests', () => {
+        const previewRequestURL = new URL('https://preview/site_foo/hello/world');
+        expect(getPreviewRequestIdentifier(previewRequestURL)).toBe('site_foo');
+    });
+});
diff --git a/packages/gitbook-v2/src/lib/preview.ts b/packages/gitbook-v2/src/lib/preview.ts
new file mode 100644
index 0000000000..7094d11970
--- /dev/null
+++ b/packages/gitbook-v2/src/lib/preview.ts
@@ -0,0 +1,13 @@
+/**
+ * Check if the request to the site is a preview request.
+ */
+export function isPreviewRequest(requestURL: URL): boolean {
+    return requestURL.host === 'preview';
+}
+
+export function getPreviewRequestIdentifier(requestURL: URL): string {
+    // For preview requests, we extract the site ID from the pathname
+    // e.g. https://preview/site_id/...
+    const pathname = requestURL.pathname.slice(1).split('/');
+    return pathname[0];
+}
diff --git a/packages/gitbook-v2/src/lib/data/proxy.test.ts b/packages/gitbook-v2/src/lib/proxy.test.ts
similarity index 100%
rename from packages/gitbook-v2/src/lib/data/proxy.test.ts
rename to packages/gitbook-v2/src/lib/proxy.test.ts
diff --git a/packages/gitbook-v2/src/lib/data/proxy.ts b/packages/gitbook-v2/src/lib/proxy.ts
similarity index 100%
rename from packages/gitbook-v2/src/lib/data/proxy.ts
rename to packages/gitbook-v2/src/lib/proxy.ts

From af66ff71ed65b0e215377fcfa9424d9837c9e661 Mon Sep 17 00:00:00 2001
From: conico974 <nicodorseuil@yahoo.fr>
Date: Sun, 25 May 2025 16:55:45 +0200
Subject: [PATCH 061/127] Add a force-revalidate api route (#3263)

Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
---
 .changeset/gorgeous-cycles-cheat.md           |  5 ++
 packages/gitbook-v2/next-env.d.ts             |  1 +
 .../pages/api/~gitbook/force-revalidate.ts    | 49 +++++++++++++++++++
 3 files changed, 55 insertions(+)
 create mode 100644 .changeset/gorgeous-cycles-cheat.md
 create mode 100644 packages/gitbook-v2/src/pages/api/~gitbook/force-revalidate.ts

diff --git a/.changeset/gorgeous-cycles-cheat.md b/.changeset/gorgeous-cycles-cheat.md
new file mode 100644
index 0000000000..d13c27765a
--- /dev/null
+++ b/.changeset/gorgeous-cycles-cheat.md
@@ -0,0 +1,5 @@
+---
+"gitbook-v2": patch
+---
+
+add a force-revalidate api route to force bust the cache in case of errors
diff --git a/packages/gitbook-v2/next-env.d.ts b/packages/gitbook-v2/next-env.d.ts
index 1b3be0840f..3cd7048ed9 100644
--- a/packages/gitbook-v2/next-env.d.ts
+++ b/packages/gitbook-v2/next-env.d.ts
@@ -1,5 +1,6 @@
 /// <reference types="next" />
 /// <reference types="next/image-types/global" />
+/// <reference types="next/navigation-types/compat/navigation" />
 
 // NOTE: This file should not be edited
 // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/packages/gitbook-v2/src/pages/api/~gitbook/force-revalidate.ts b/packages/gitbook-v2/src/pages/api/~gitbook/force-revalidate.ts
new file mode 100644
index 0000000000..45e6c7eca1
--- /dev/null
+++ b/packages/gitbook-v2/src/pages/api/~gitbook/force-revalidate.ts
@@ -0,0 +1,49 @@
+import crypto from 'node:crypto';
+import type { NextApiRequest, NextApiResponse } from 'next';
+
+interface JsonBody {
+    // The paths need to be the rewritten one, `res.revalidate` call don't go through the middleware
+    paths: string[];
+}
+
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+    // Only allow POST requests
+    if (req.method !== 'POST') {
+        return res.status(405).json({ error: 'Method not allowed' });
+    }
+    const signatureHeader = req.headers['x-gitbook-signature'] as string | undefined;
+    if (!signatureHeader) {
+        return res.status(400).json({ error: 'Missing signature header' });
+    }
+    // We cannot use env from `@/v2/lib/env` here as it make it crash because of the import "server-only" in the file.
+    if (process.env.GITBOOK_SECRET) {
+        try {
+            const computedSignature = crypto
+                .createHmac('sha256', process.env.GITBOOK_SECRET)
+                .update(JSON.stringify(req.body))
+                .digest('hex');
+
+            if (computedSignature === signatureHeader) {
+                const results = await Promise.allSettled(
+                    (req.body as JsonBody).paths.map((path) => {
+                        // biome-ignore lint/suspicious/noConsole: we want to log here
+                        console.log(`Revalidating path: ${path}`);
+                        return res.revalidate(path);
+                    })
+                );
+                return res.status(200).json({
+                    success: results.every((result) => result.status === 'fulfilled'),
+                    errors: results
+                        .filter((result) => result.status === 'rejected')
+                        .map((result) => (result as PromiseRejectedResult).reason),
+                });
+            }
+            return res.status(401).json({ error: 'Invalid signature' });
+        } catch (error) {
+            console.error('Error during revalidation:', error);
+            return res.status(400).json({ error: 'Invalid request or unable to parse JSON' });
+        }
+    }
+    // If no secret is set, we do not allow revalidation
+    return res.status(403).json({ error: 'Revalidation is disabled' });
+}

From 40df91a3639e8bfd93c589d5fb3c8ff552c5232e Mon Sep 17 00:00:00 2001
From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com>
Date: Mon, 26 May 2025 18:55:11 +0200
Subject: [PATCH 062/127] Deduplicate path parameters from OpenAPI spec (#3264)

---
 .changeset/violet-schools-care.md          |  5 +++++
 packages/react-openapi/src/OpenAPISpec.tsx | 22 +++++++++++++++++++++-
 2 files changed, 26 insertions(+), 1 deletion(-)
 create mode 100644 .changeset/violet-schools-care.md

diff --git a/.changeset/violet-schools-care.md b/.changeset/violet-schools-care.md
new file mode 100644
index 0000000000..e77a98b5cf
--- /dev/null
+++ b/.changeset/violet-schools-care.md
@@ -0,0 +1,5 @@
+---
+'@gitbook/react-openapi': patch
+---
+
+Deduplicate path parameters from OpenAPI spec
diff --git a/packages/react-openapi/src/OpenAPISpec.tsx b/packages/react-openapi/src/OpenAPISpec.tsx
index 49c41cda40..1e6e562690 100644
--- a/packages/react-openapi/src/OpenAPISpec.tsx
+++ b/packages/react-openapi/src/OpenAPISpec.tsx
@@ -18,7 +18,7 @@ export function OpenAPISpec(props: {
 
     const { operation } = data;
 
-    const parameters = operation.parameters ?? [];
+    const parameters = deduplicateParameters(operation.parameters ?? []);
     const parameterGroups = groupParameters(parameters, context);
 
     const securities = 'securities' in data ? data.securities : [];
@@ -113,3 +113,23 @@ function getParameterGroupName(paramIn: string, context: OpenAPIClientContext):
             return paramIn;
     }
 }
+
+/** Deduplicate parameters by name and in.
+ * Some specs have both parameters define at path and operation level.
+ * We only want to display one of them.
+ */
+function deduplicateParameters(parameters: OpenAPI.Parameters): OpenAPI.Parameters {
+    const seen = new Set();
+
+    return parameters.filter((param) => {
+        const key = `${param.name}:${param.in}`;
+
+        if (seen.has(key)) {
+            return false;
+        }
+
+        seen.add(key);
+
+        return true;
+    });
+}

From 4c9a9d0416c91398198027f637b1aa7049f20899 Mon Sep 17 00:00:00 2001
From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com>
Date: Mon, 26 May 2025 20:28:26 +0200
Subject: [PATCH 063/127] Handle nested deprecated properties in
 generateSchemaExample (#3267)

---
 .changeset/thick-chefs-repeat.md              |  5 +++++
 .../src/generateSchemaExample.test.ts         | 20 +++++++++++++++++++
 .../src/generateSchemaExample.ts              |  2 +-
 3 files changed, 26 insertions(+), 1 deletion(-)
 create mode 100644 .changeset/thick-chefs-repeat.md

diff --git a/.changeset/thick-chefs-repeat.md b/.changeset/thick-chefs-repeat.md
new file mode 100644
index 0000000000..8b08d50e3e
--- /dev/null
+++ b/.changeset/thick-chefs-repeat.md
@@ -0,0 +1,5 @@
+---
+'@gitbook/react-openapi': patch
+---
+
+Handle nested deprecated properties in generateSchemaExample
diff --git a/packages/react-openapi/src/generateSchemaExample.test.ts b/packages/react-openapi/src/generateSchemaExample.test.ts
index 3181881b62..682c5f2f38 100644
--- a/packages/react-openapi/src/generateSchemaExample.test.ts
+++ b/packages/react-openapi/src/generateSchemaExample.test.ts
@@ -1017,4 +1017,24 @@ describe('generateSchemaExample', () => {
             },
         });
     });
+
+    it('handles deprecated properties', () => {
+        expect(
+            generateSchemaExample({
+                type: 'object',
+                deprecated: true,
+            })
+        ).toBeUndefined();
+    });
+
+    it('handle nested deprecated properties', () => {
+        expect(
+            generateSchemaExample({
+                type: 'array',
+                items: {
+                    deprecated: true,
+                },
+            })
+        ).toBeUndefined();
+    });
 });
diff --git a/packages/react-openapi/src/generateSchemaExample.ts b/packages/react-openapi/src/generateSchemaExample.ts
index 5a038db06b..595f723d91 100644
--- a/packages/react-openapi/src/generateSchemaExample.ts
+++ b/packages/react-openapi/src/generateSchemaExample.ts
@@ -167,7 +167,7 @@ const getExampleFromSchema = (
     const makeUpRandomData = !!options?.emptyString;
 
     // If the property is deprecated we don't show it in examples.
-    if (schema.deprecated) {
+    if (schema.deprecated || (schema.type === 'array' && schema.items?.deprecated)) {
         return undefined;
     }
 

From 957afd967fc306c404046d27c240bed34bdd2115 Mon Sep 17 00:00:00 2001
From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com>
Date: Mon, 26 May 2025 20:54:07 +0200
Subject: [PATCH 064/127] Add authorization header for OAuth2 (#3266)

---
 .changeset/fast-trees-battle.md                  | 5 +++++
 packages/react-openapi/src/OpenAPICodeSample.tsx | 5 +++++
 2 files changed, 10 insertions(+)
 create mode 100644 .changeset/fast-trees-battle.md

diff --git a/.changeset/fast-trees-battle.md b/.changeset/fast-trees-battle.md
new file mode 100644
index 0000000000..0e75fbbeda
--- /dev/null
+++ b/.changeset/fast-trees-battle.md
@@ -0,0 +1,5 @@
+---
+'@gitbook/react-openapi': patch
+---
+
+Add authorization header for OAuth2
diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx
index 8b67bbedc4..8bffb6d9ad 100644
--- a/packages/react-openapi/src/OpenAPICodeSample.tsx
+++ b/packages/react-openapi/src/OpenAPICodeSample.tsx
@@ -312,6 +312,11 @@ function getSecurityHeaders(securities: OpenAPIOperationData['securities']): {
                 [name]: 'YOUR_API_KEY',
             };
         }
+        case 'oauth2': {
+            return {
+                Authorization: 'Bearer YOUR_OAUTH2_TOKEN',
+            };
+        }
         default: {
             return {};
         }

From 2932077bf90ca92eadbfd8b9c832c5fd6f018e0b Mon Sep 17 00:00:00 2001
From: conico974 <nicodorseuil@yahoo.fr>
Date: Tue, 27 May 2025 21:13:55 +0200
Subject: [PATCH 065/127] Remove trailing slash from linker (#3268)

Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
---
 .changeset/wise-gifts-smash.md            |  5 +++++
 packages/gitbook-v2/src/lib/links.test.ts | 22 +++++++++++++++++++++-
 packages/gitbook-v2/src/lib/links.ts      |  6 +++++-
 3 files changed, 31 insertions(+), 2 deletions(-)
 create mode 100644 .changeset/wise-gifts-smash.md

diff --git a/.changeset/wise-gifts-smash.md b/.changeset/wise-gifts-smash.md
new file mode 100644
index 0000000000..32a85eecb7
--- /dev/null
+++ b/.changeset/wise-gifts-smash.md
@@ -0,0 +1,5 @@
+---
+"gitbook-v2": patch
+---
+
+remove trailing slash from linker
diff --git a/packages/gitbook-v2/src/lib/links.test.ts b/packages/gitbook-v2/src/lib/links.test.ts
index 26a2464f50..f73ec5ee86 100644
--- a/packages/gitbook-v2/src/lib/links.test.ts
+++ b/packages/gitbook-v2/src/lib/links.test.ts
@@ -19,7 +19,7 @@ const siteGitBookIO = createLinker({
     siteBasePath: '/sitename/',
 });
 
-describe('toPathInContent', () => {
+describe('toPathInSpace', () => {
     it('should return the correct path', () => {
         expect(root.toPathInSpace('some/path')).toBe('/some/path');
         expect(variantInSection.toPathInSpace('some/path')).toBe('/section/variant/some/path');
@@ -29,6 +29,16 @@ describe('toPathInContent', () => {
         expect(root.toPathInSpace('/some/path')).toBe('/some/path');
         expect(variantInSection.toPathInSpace('/some/path')).toBe('/section/variant/some/path');
     });
+
+    it('should remove the trailing slash', () => {
+        expect(root.toPathInSpace('some/path/')).toBe('/some/path');
+        expect(variantInSection.toPathInSpace('some/path/')).toBe('/section/variant/some/path');
+    });
+
+    it('should not add a trailing slash', () => {
+        expect(root.toPathInSpace('')).toBe('');
+        expect(variantInSection.toPathInSpace('')).toBe('/section/variant');
+    });
 });
 
 describe('toPathInSite', () => {
@@ -36,6 +46,16 @@ describe('toPathInSite', () => {
         expect(root.toPathInSite('some/path')).toBe('/some/path');
         expect(siteGitBookIO.toPathInSite('some/path')).toBe('/sitename/some/path');
     });
+
+    it('should remove the trailing slash', () => {
+        expect(root.toPathInSite('some/path/')).toBe('/some/path');
+        expect(siteGitBookIO.toPathInSite('some/path/')).toBe('/sitename/some/path');
+    });
+
+    it('should not add a trailing slash', () => {
+        expect(root.toPathInSite('')).toBe('');
+        expect(siteGitBookIO.toPathInSite('')).toBe('/sitename');
+    });
 });
 
 describe('toRelativePathInSite', () => {
diff --git a/packages/gitbook-v2/src/lib/links.ts b/packages/gitbook-v2/src/lib/links.ts
index b84565d1e7..a64bda541c 100644
--- a/packages/gitbook-v2/src/lib/links.ts
+++ b/packages/gitbook-v2/src/lib/links.ts
@@ -128,5 +128,9 @@ export function createLinker(
 function joinPaths(prefix: string, path: string): string {
     const prefixPath = prefix.endsWith('/') ? prefix : `${prefix}/`;
     const suffixPath = path.startsWith('/') ? path.slice(1) : path;
-    return prefixPath + suffixPath;
+    return removeTrailingSlash(prefixPath + suffixPath);
+}
+
+function removeTrailingSlash(path: string): string {
+    return path.endsWith('/') ? path.slice(0, -1) : path;
 }

From b3a7ad6d2cfc1a55fd3c15c0baefcd39bd3228cf Mon Sep 17 00:00:00 2001
From: conico974 <nicodorseuil@yahoo.fr>
Date: Wed, 28 May 2025 17:07:33 +0200
Subject: [PATCH 066/127] Fix href being empty in TOC (#3269)

Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
---
 .changeset/cool-jars-matter.md                              | 5 +++++
 .../src/components/TableOfContents/PageDocumentItem.tsx     | 6 +++++-
 2 files changed, 10 insertions(+), 1 deletion(-)
 create mode 100644 .changeset/cool-jars-matter.md

diff --git a/.changeset/cool-jars-matter.md b/.changeset/cool-jars-matter.md
new file mode 100644
index 0000000000..ff0c934f33
--- /dev/null
+++ b/.changeset/cool-jars-matter.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+fix href being empty in TOC
diff --git a/packages/gitbook/src/components/TableOfContents/PageDocumentItem.tsx b/packages/gitbook/src/components/TableOfContents/PageDocumentItem.tsx
index 2ff3289d7f..4875633ac9 100644
--- a/packages/gitbook/src/components/TableOfContents/PageDocumentItem.tsx
+++ b/packages/gitbook/src/components/TableOfContents/PageDocumentItem.tsx
@@ -17,7 +17,11 @@ export async function PageDocumentItem(props: {
     context: GitBookSiteContext;
 }) {
     const { rootPages, page, context } = props;
-    const href = context.linker.toPathForPage({ pages: rootPages, page });
+    let href = context.linker.toPathForPage({ pages: rootPages, page });
+    // toPathForPage can returns an empty path, this will cause all links to point to the current page.
+    if (href === '') {
+        href = '/';
+    }
 
     return (
         <li className="flex flex-col">

From df2fa42bda09a85a3ca2412ed898e6e26cf77586 Mon Sep 17 00:00:00 2001
From: conico974 <nicodorseuil@yahoo.fr>
Date: Fri, 30 May 2025 14:31:21 +0200
Subject: [PATCH 067/127] Chores: create server worker (#3265)

Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
---
 .../composite/deploy-cloudflare/action.yaml   | 11 ++++++
 .../openNext/customWorkers/default.js         |  9 +++++
 .../customWorkers/defaultWrangler.jsonc       | 38 +++++++++++++++++++
 3 files changed, 58 insertions(+)
 create mode 100644 packages/gitbook-v2/openNext/customWorkers/default.js
 create mode 100644 packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc

diff --git a/.github/composite/deploy-cloudflare/action.yaml b/.github/composite/deploy-cloudflare/action.yaml
index fbc98fc82f..1cd19c4d5d 100644
--- a/.github/composite/deploy-cloudflare/action.yaml
+++ b/.github/composite/deploy-cloudflare/action.yaml
@@ -73,6 +73,17 @@ runs:
               wranglerVersion: '4.10.0'
               environment: ${{ inputs.environment }}
               command: ${{ inputs.deploy == 'true' && 'deploy' || format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook-v2/wrangler.jsonc
+        
+        - name: Temporary deploy server CF worker
+          uses: cloudflare/wrangler-action@v3.14.0
+          with:
+              apiToken: ${{ inputs.apiToken }}
+              accountId: ${{ inputs.accountId }}
+              workingDirectory: ./
+              wranglerVersion: '4.10.0'
+              environment: ${{ inputs.environment }}
+              command: 'deploy --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc'
+        
         - name: Outputs
           shell: bash
           env:
diff --git a/packages/gitbook-v2/openNext/customWorkers/default.js b/packages/gitbook-v2/openNext/customWorkers/default.js
new file mode 100644
index 0000000000..6e2f1af82a
--- /dev/null
+++ b/packages/gitbook-v2/openNext/customWorkers/default.js
@@ -0,0 +1,9 @@
+export default {
+    async fetch() {
+        return new Response('Hello World', {
+            headers: {
+                'Content-Type': 'text/plain',
+            },
+        });
+    },
+};
diff --git a/packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc b/packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
new file mode 100644
index 0000000000..5ade8d025f
--- /dev/null
+++ b/packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
@@ -0,0 +1,38 @@
+{
+    "main": "default.js",
+    "name": "gitbook-open-v2-server",
+    "compatibility_date": "2025-04-14",
+    "compatibility_flags": [
+        "nodejs_compat",
+        "allow_importable_env",
+        "global_fetch_strictly_public"
+    ],
+    "observability": {
+        "enabled": true
+    },
+    "vars": {
+        "NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
+    },
+    "env": {
+        "dev": {
+            "vars": {
+                "STAGE": "dev"
+            }
+        },
+        "preview": {
+            "vars": {
+                "STAGE": "preview"
+            }
+        },
+        "staging": {
+            "vars": {
+                "STAGE": "staging"
+            }
+        },
+        "production": {
+            "vars": {
+                "STAGE": "production"
+            }
+        }
+    }
+}

From 666e8426e1a39fffdb4de586bad633a1a2049365 Mon Sep 17 00:00:00 2001
From: conico974 <nicodorseuil@yahoo.fr>
Date: Fri, 30 May 2025 14:56:00 +0200
Subject: [PATCH 068/127] External middleware in cloudflare (#3254)

Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
---
 .../gradual-deploy-cloudflare/action.yaml     |  83 ++++++++
 .../composite/deploy-cloudflare/action.yaml   |  54 ++++-
 packages/gitbook-v2/open-next.config.ts       |  53 ++---
 .../openNext/customWorkers/default.js         |  16 +-
 .../customWorkers/defaultWrangler.jsonc       |  96 ++++++++-
 .../openNext/customWorkers/middleware.js      |  42 ++++
 .../customWorkers/middlewareWrangler.jsonc    | 185 ++++++++++++++++++
 .../customWorkers/script/updateWrangler.ts    |  26 +++
 packages/gitbook-v2/openNext/queue.ts         |  17 --
 .../gitbook-v2/openNext/queue/middleware.ts   |  21 ++
 packages/gitbook-v2/openNext/queue/server.ts  |   9 +
 .../openNext/tagCache/middleware.ts           |  76 +++++++
 packages/gitbook-v2/package.json              |   2 +
 packages/gitbook-v2/src/lib/data/api.ts       |  23 +--
 14 files changed, 616 insertions(+), 87 deletions(-)
 create mode 100644 .github/actions/gradual-deploy-cloudflare/action.yaml
 create mode 100644 packages/gitbook-v2/openNext/customWorkers/middleware.js
 create mode 100644 packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc
 create mode 100644 packages/gitbook-v2/openNext/customWorkers/script/updateWrangler.ts
 delete mode 100644 packages/gitbook-v2/openNext/queue.ts
 create mode 100644 packages/gitbook-v2/openNext/queue/middleware.ts
 create mode 100644 packages/gitbook-v2/openNext/queue/server.ts
 create mode 100644 packages/gitbook-v2/openNext/tagCache/middleware.ts

diff --git a/.github/actions/gradual-deploy-cloudflare/action.yaml b/.github/actions/gradual-deploy-cloudflare/action.yaml
new file mode 100644
index 0000000000..eff064a754
--- /dev/null
+++ b/.github/actions/gradual-deploy-cloudflare/action.yaml
@@ -0,0 +1,83 @@
+name: Gradual Deploy to Cloudflare
+description: Use gradual deployment to deploy to Cloudflare. This action will upload the middleware and server versions to Cloudflare and kept them bound together
+inputs:
+  apiToken:
+    description: 'Cloudflare API token'
+    required: true
+  accountId:
+    description: 'Cloudflare account ID'
+    required: true
+  environment:
+    description: 'Cloudflare environment to deploy to (staging, production, preview)'
+    required: true
+  middlewareVersionId:
+    description: 'Middleware version ID to deploy'
+    required: true
+  serverVersionId:
+    description: 'Server version ID to deploy'
+    required: true
+outputs:
+  deployment-url: 
+    description: "Deployment URL"
+    value: ${{ steps.deploy_middleware.outputs.deployment-url }}
+runs:
+  using: 'composite'
+  steps:
+      - id: wrangler_status
+        name: Check wrangler deployment status
+        uses: cloudflare/wrangler-action@v3.14.0
+        with:
+            apiToken: ${{ inputs.apiToken }}
+            accountId: ${{ inputs.accountId }}
+            workingDirectory: ./
+            wranglerVersion: '4.10.0'
+            environment: ${{ inputs.environment }}
+            command: deployments status --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
+
+      # This step is used to get the version ID that is currently deployed to Cloudflare.
+      - id: extract_current_version
+        name: Extract current version
+        shell: bash
+        run: |
+          version_id=$(echo "${{ steps.wrangler_status.outputs.command-output }}" | grep -A 3 "(100%)" | grep -oP '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
+          echo "version_id=$version_id" >> $GITHUB_OUTPUT
+
+      - id: deploy_server
+        name: Deploy server to Cloudflare at 0%
+        uses: cloudflare/wrangler-action@v3.14.0
+        with:
+            apiToken: ${{ inputs.apiToken }}
+            accountId: ${{ inputs.accountId }}
+            workingDirectory: ./
+            wranglerVersion: '4.10.0'
+            environment: ${{ inputs.environment }}
+            command: versions deploy ${{ steps.extract_current_version.outputs.version_id }}@100% ${{ inputs.serverVersionId }}@0% -y --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
+
+      # Since we use version overrides headers, we can directly deploy the middleware to 100%.
+      - id: deploy_middleware
+        name: Deploy middleware to Cloudflare at 100%
+        uses: cloudflare/wrangler-action@v3.14.0
+        with:
+            apiToken: ${{ inputs.apiToken }}
+            accountId: ${{ inputs.accountId }}
+            workingDirectory: ./
+            wranglerVersion: '4.10.0'
+            environment: ${{ inputs.environment }}
+            command: versions deploy ${{ inputs.middlewareVersionId }}@100% -y --config ./packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc
+
+      - name: Deploy server to Cloudflare at 100%
+        uses: cloudflare/wrangler-action@v3.14.0
+        with:
+            apiToken: ${{ inputs.apiToken }}
+            accountId: ${{ inputs.accountId }}
+            workingDirectory: ./
+            wranglerVersion: '4.10.0'
+            environment: ${{ inputs.environment }}
+            command: versions deploy ${{ inputs.serverVersionId }}@100% -y --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
+
+      - name: Outputs
+        shell: bash
+        env:
+          DEPLOYMENT_URL: ${{ steps.deploy_middleware.outputs.deployment-url }}
+        run: |
+            echo "URL: ${{ steps.deploy_middleware.outputs.deployment-url }}"
\ No newline at end of file
diff --git a/.github/composite/deploy-cloudflare/action.yaml b/.github/composite/deploy-cloudflare/action.yaml
index 1cd19c4d5d..d1d0d66339 100644
--- a/.github/composite/deploy-cloudflare/action.yaml
+++ b/.github/composite/deploy-cloudflare/action.yaml
@@ -28,7 +28,7 @@ inputs:
 outputs:
     deployment-url: 
       description: "Deployment URL"
-      value: ${{ steps.deploy.outputs.deployment-url }}
+      value: ${{ steps.upload_middleware.outputs.deployment-url }}
 runs:
     using: 'composite'
     steps:
@@ -63,8 +63,9 @@ runs:
           env:
             GITBOOK_RUNTIME: cloudflare
           shell: bash
-        - id: deploy
-          name: Deploy to Cloudflare
+
+        - id: upload_server
+          name: Upload server to Cloudflare
           uses: cloudflare/wrangler-action@v3.14.0
           with:
               apiToken: ${{ inputs.apiToken }}
@@ -72,9 +73,22 @@ runs:
               workingDirectory: ./
               wranglerVersion: '4.10.0'
               environment: ${{ inputs.environment }}
-              command: ${{ inputs.deploy == 'true' && 'deploy' || format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook-v2/wrangler.jsonc
-        
-        - name: Temporary deploy server CF worker
+              command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
+
+        - name: Extract server version worker ID
+          shell: bash
+          id: extract_server_version_id
+          run: |
+              version_id=$(echo '${{ steps.upload_server.outputs.command-output }}' | grep "Worker Version ID" | awk '{print $4}')
+              echo "version_id=$version_id" >> $GITHUB_OUTPUT
+
+        - name: Run updateWrangler scripts
+          shell: bash
+          run: |
+              bun run ./packages/gitbook-v2/openNext/customWorkers/script/updateWrangler.ts ${{ steps.extract_server_version_id.outputs.version_id }}
+
+        - id: upload_middleware
+          name: Upload middleware to Cloudflare
           uses: cloudflare/wrangler-action@v3.14.0
           with:
               apiToken: ${{ inputs.apiToken }}
@@ -82,11 +96,33 @@ runs:
               workingDirectory: ./
               wranglerVersion: '4.10.0'
               environment: ${{ inputs.environment }}
-              command: 'deploy --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc'
+              command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc
         
+        - name: Extract middleware version worker ID
+          shell: bash
+          id: extract_middleware_version_id
+          run: |
+              version_id=$(echo '${{ steps.upload_middleware.outputs.command-output }}' | grep "Worker Version ID" | awk '{print $4}')
+              echo "version_id=$version_id" >> $GITHUB_OUTPUT
+
+        - name: Deploy server and middleware to Cloudflare
+          if: ${{ inputs.deploy == 'true' }}
+          uses: ./.github/actions/gradual-deploy-cloudflare
+          with:
+              apiToken: ${{ inputs.apiToken }}
+              accountId: ${{ inputs.accountId }}
+              opServiceAccount: ${{ inputs.opServiceAccount }}
+              opItem: ${{ inputs.opItem }}
+              environment: ${{ inputs.environment }}
+              serverVersionId: ${{ steps.extract_server_version_id.outputs.version_id }}
+              middlewareVersionId: ${{ steps.extract_middleware_version_id.outputs.version_id }}
+              deploy: ${{ inputs.deploy }}
+
+
         - name: Outputs
           shell: bash
           env:
-            DEPLOYMENT_URL: ${{ steps.deploy.outputs.deployment-url }}
+            DEPLOYMENT_URL: ${{ steps.upload_middleware.outputs.deployment-url }}
           run: |
-              echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
\ No newline at end of file
+              echo "URL: ${{ steps.upload_middleware.outputs.deployment-url }}"
+              echo "Output server: ${{ steps.upload_server.outputs.command-output }}"
\ No newline at end of file
diff --git a/packages/gitbook-v2/open-next.config.ts b/packages/gitbook-v2/open-next.config.ts
index d35c9ef03e..959834739a 100644
--- a/packages/gitbook-v2/open-next.config.ts
+++ b/packages/gitbook-v2/open-next.config.ts
@@ -1,26 +1,29 @@
-import { defineCloudflareConfig } from '@opennextjs/cloudflare';
-import doShardedTagCache from '@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache';
-import {
-    softTagFilter,
-    withFilter,
-} from '@opennextjs/cloudflare/overrides/tag-cache/tag-cache-filter';
+import type { OpenNextConfig } from '@opennextjs/cloudflare';
 
-export default defineCloudflareConfig({
-    incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default),
-    tagCache: withFilter({
-        tagCache: doShardedTagCache({
-            baseShardSize: 12,
-            regionalCache: true,
-            shardReplication: {
-                numberOfSoftReplicas: 2,
-                numberOfHardReplicas: 1,
-            },
-        }),
-        // We don't use `revalidatePath`, so we filter out soft tags
-        filterFn: softTagFilter,
-    }),
-    queue: () => import('./openNext/queue').then((m) => m.default),
-
-    // Performance improvements as we don't use PPR
-    enableCacheInterception: true,
-});
+export default {
+    default: {
+        override: {
+            wrapper: 'cloudflare-node',
+            converter: 'edge',
+            proxyExternalRequest: 'fetch',
+            queue: () => import('./openNext/queue/server').then((m) => m.default),
+            incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default),
+            tagCache: () => import('./openNext/tagCache/middleware').then((m) => m.default),
+        },
+    },
+    middleware: {
+        external: true,
+        override: {
+            wrapper: 'cloudflare-edge',
+            converter: 'edge',
+            proxyExternalRequest: 'fetch',
+            queue: () => import('./openNext/queue/middleware').then((m) => m.default),
+            incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default),
+            tagCache: () => import('./openNext/tagCache/middleware').then((m) => m.default),
+        },
+    },
+    dangerous: {
+        enableCacheInterception: true,
+    },
+    edgeExternals: ['node:crypto'],
+} satisfies OpenNextConfig;
diff --git a/packages/gitbook-v2/openNext/customWorkers/default.js b/packages/gitbook-v2/openNext/customWorkers/default.js
index 6e2f1af82a..bb673e1722 100644
--- a/packages/gitbook-v2/openNext/customWorkers/default.js
+++ b/packages/gitbook-v2/openNext/customWorkers/default.js
@@ -1,9 +1,15 @@
+import { runWithCloudflareRequestContext } from '../../.open-next/cloudflare/init.js';
+
 export default {
-    async fetch() {
-        return new Response('Hello World', {
-            headers: {
-                'Content-Type': 'text/plain',
-            },
+    async fetch(request, env, ctx) {
+        return runWithCloudflareRequestContext(request, env, ctx, async () => {
+            // We can't move the handler import to the top level, otherwise the runtime will not be properly initialized
+            const { handler } = await import(
+                '../../.open-next/server-functions/default/handler.mjs'
+            );
+
+            // - `Request`s are handled by the Next server
+            return handler(request, env, ctx);
         });
     },
 };
diff --git a/packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc b/packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
index 5ade8d025f..3fff07d7c0 100644
--- a/packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
+++ b/packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
@@ -17,22 +17,100 @@
         "dev": {
             "vars": {
                 "STAGE": "dev"
-            }
+            },
+            "r2_buckets": [
+                {
+                    "binding": "NEXT_INC_CACHE_R2_BUCKET",
+                    "bucket_name": "gitbook-open-v2-cache-preview"
+                }
+            ],
+            "services": [
+                {
+                    "binding": "WORKER_SELF_REFERENCE",
+                    "service": "gitbook-open-v2-server-dev"
+                }
+            ]
         },
         "preview": {
             "vars": {
-                "STAGE": "preview"
-            }
+                "STAGE": "preview",
+                // Just as a test for the preview environment to check that everything works
+                "NEXT_PRIVATE_DEBUG_CACHE": "true"
+            },
+            "r2_buckets": [
+                {
+                    "binding": "NEXT_INC_CACHE_R2_BUCKET",
+                    "bucket_name": "gitbook-open-v2-cache-preview"
+                }
+            ],
+            "services": [
+                {
+                    "binding": "WORKER_SELF_REFERENCE",
+                    "service": "gitbook-open-v2-server-preview"
+                }
+            ]
+            // No durable objects on preview, as they block the generation of preview URLs
+            // and we don't need tags invalidation on preview
         },
         "staging": {
-            "vars": {
-                "STAGE": "staging"
-            }
+            "r2_buckets": [
+                {
+                    "binding": "NEXT_INC_CACHE_R2_BUCKET",
+                    "bucket_name": "gitbook-open-v2-cache-staging"
+                }
+            ],
+            "services": [
+                {
+                    "binding": "WORKER_SELF_REFERENCE",
+                    "service": "gitbook-open-v2-server-staging"
+                }
+            ],
+            "durable_objects": {
+                "bindings": [
+                    // We do not need to define migrations for external DOs,
+                    // In fact, defining migrations for external DOs will crash
+                    {
+                        "name": "NEXT_TAG_CACHE_DO_SHARDED",
+                        "class_name": "DOShardedTagCache",
+                        "script_name": "gitbook-open-v2-staging"
+                    }
+                ]
+            },
+            "tail_consumers": [
+                {
+                    "service": "gitbook-x-staging-tail"
+                }
+            ]
         },
         "production": {
-            "vars": {
-                "STAGE": "production"
-            }
+            "r2_buckets": [
+                {
+                    "binding": "NEXT_INC_CACHE_R2_BUCKET",
+                    "bucket_name": "gitbook-open-v2-cache-production"
+                }
+            ],
+            "services": [
+                {
+                    "binding": "WORKER_SELF_REFERENCE",
+                    "service": "gitbook-open-v2-server-production"
+                }
+            ],
+            "durable_objects": {
+                "bindings": [
+                    {
+                        // We do not need to define migrations for external DOs,
+                        // In fact, defining migrations for external DOs will crash
+                        "name": "NEXT_TAG_CACHE_DO_SHARDED",
+                        "class_name": "DOShardedTagCache",
+                        "script_name": "gitbook-open-v2-production"
+                    }
+                ]
+            },
+            "tail_consumers": [
+                {
+                    "service": "gitbook-x-prod-tail"
+                }
+            ]
         }
     }
 }
diff --git a/packages/gitbook-v2/openNext/customWorkers/middleware.js b/packages/gitbook-v2/openNext/customWorkers/middleware.js
new file mode 100644
index 0000000000..78a84a9760
--- /dev/null
+++ b/packages/gitbook-v2/openNext/customWorkers/middleware.js
@@ -0,0 +1,42 @@
+import { WorkerEntrypoint } from 'cloudflare:workers';
+import { runWithCloudflareRequestContext } from '../../.open-next/cloudflare/init.js';
+
+import { handler as middlewareHandler } from '../../.open-next/middleware/handler.mjs';
+
+export { DOQueueHandler } from '../../.open-next/.build/durable-objects/queue.js';
+
+export { DOShardedTagCache } from '../../.open-next/.build/durable-objects/sharded-tag-cache.js';
+
+export default class extends WorkerEntrypoint {
+    async fetch(request) {
+        return runWithCloudflareRequestContext(request, this.env, this.ctx, async () => {
+            // - `Request`s are handled by the Next server
+            const reqOrResp = await middlewareHandler(request, this.env, this.ctx);
+            if (reqOrResp instanceof Response) {
+                return reqOrResp;
+            }
+
+            if (this.env.STAGE !== 'preview') {
+                // https://developers.cloudflare.com/workers/configuration/versions-and-deployments/gradual-deployments/#version-affinity
+                reqOrResp.headers.set(
+                    'Cloudflare-Workers-Version-Overrides',
+                    `gitbook-open-v2-${this.env.STAGE}="${this.env.WORKER_VERSION_ID}"`
+                );
+                return this.env.DEFAULT_WORKER?.fetch(reqOrResp, {
+                    cf: {
+                        cacheEverything: false,
+                    },
+                });
+            }
+            // If we are in preview mode, we need to send the request to the preview URL
+            const modifiedUrl = new URL(reqOrResp.url);
+            modifiedUrl.hostname = this.env.PREVIEW_HOSTNAME;
+            const nextRequest = new Request(modifiedUrl, reqOrResp);
+            return fetch(nextRequest, {
+                cf: {
+                    cacheEverything: false,
+                },
+            });
+        });
+    }
+}
diff --git a/packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc b/packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc
new file mode 100644
index 0000000000..4870c89d54
--- /dev/null
+++ b/packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc
@@ -0,0 +1,185 @@
+{
+    "main": "middleware.js",
+    "name": "gitbook-open-v2",
+    "compatibility_date": "2025-04-14",
+    "compatibility_flags": [
+        "nodejs_compat",
+        "allow_importable_env",
+        "global_fetch_strictly_public"
+    ],
+    "assets": {
+        "directory": "../../.open-next/assets",
+        "binding": "ASSETS"
+    },
+    "observability": {
+        "enabled": true
+    },
+    "vars": {
+        "NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
+    },
+    "env": {
+        "dev": {
+            "vars": {
+                "STAGE": "dev",
+                "NEXT_PRIVATE_DEBUG_CACHE": "true"
+            },
+            "r2_buckets": [
+                {
+                    "binding": "NEXT_INC_CACHE_R2_BUCKET",
+                    "bucket_name": "gitbook-open-v2-cache-preview"
+                }
+            ],
+            "services": [
+                {
+                    "binding": "WORKER_SELF_REFERENCE",
+                    "service": "gitbook-open-v2-dev"
+                },
+                {
+                    "binding": "DEFAULT_WORKER",
+                    "service": "gitbook-open-v2-server-dev"
+                }
+            ]
+        },
+        "preview": {
+            "vars": {
+                "STAGE": "preview",
+                "PREVIEW_HOSTNAME": "TO_REPLACE",
+                "WORKER_VERSION_ID": "TO_REPLACE"
+            },
+            "r2_buckets": [
+                {
+                    "binding": "NEXT_INC_CACHE_R2_BUCKET",
+                    "bucket_name": "gitbook-open-v2-cache-preview"
+                }
+            ],
+            "services": [
+                {
+                    "binding": "WORKER_SELF_REFERENCE",
+                    "service": "gitbook-open-v2-preview"
+                },
+                {
+                    "binding": "DEFAULT_WORKER",
+                    "service": "gitbook-open-v2-server-preview"
+                }
+            ]
+            // No durable objects on preview, as they block the generation of preview URLs
+            // and we don't need tags invalidation on preview
+        },
+        "staging": {
+            "vars": {
+                "STAGE": "staging",
+                "WORKER_VERSION_ID": "TO_REPLACE"
+            },
+            "routes": [
+                {
+                    "pattern": "open-2c.gitbook-staging.com/*",
+                    "zone_name": "gitbook-staging.com"
+                },
+                {
+                    "pattern": "static-2c.gitbook-staging.com/*",
+                    "zone_name": "gitbook-staging.com"
+                }
+            ],
+            "r2_buckets": [
+                {
+                    "binding": "NEXT_INC_CACHE_R2_BUCKET",
+                    "bucket_name": "gitbook-open-v2-cache-staging"
+                }
+            ],
+            "services": [
+                {
+                    "binding": "WORKER_SELF_REFERENCE",
+                    "service": "gitbook-open-v2-staging"
+                },
+                {
+                    "binding": "DEFAULT_WORKER",
+                    "service": "gitbook-open-v2-server-staging"
+                }
+            ],
+            "tail_consumers": [
+                {
+                    "service": "gitbook-x-staging-tail"
+                }
+            ],
+            "durable_objects": {
+                "bindings": [
+                    {
+                        "name": "NEXT_CACHE_DO_QUEUE",
+                        "class_name": "DOQueueHandler"
+                    },
+                    {
+                        "name": "NEXT_TAG_CACHE_DO_SHARDED",
+                        "class_name": "DOShardedTagCache"
+                    }
+                ]
+            },
+            "migrations": [
+                {
+                    "tag": "v1",
+                    "new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache"]
+                }
+            ]
+        },
+        "production": {
+            "vars": {
+                // This is a bit misleading, but it means that we can have 500 concurrent revalidations
+                // This means that we'll have up to 100 durable objects instance running at the same time
+                "MAX_REVALIDATE_CONCURRENCY": "100",
+                // Temporary variable to find the issue once deployed
+                // TODO: remove this once the issue is fixed
+                "DEBUG_CLOUDFLARE": "true",
+                "WORKER_VERSION_ID": "TO_REPLACE",
+                "STAGE": "production"
+            },
+            "routes": [
+                {
+                    "pattern": "open-2c.gitbook.com/*",
+                    "zone_name": "gitbook.com"
+                },
+                {
+                    "pattern": "static-2c.gitbook.com/*",
+                    "zone_name": "gitbook.com"
+                }
+            ],
+            "r2_buckets": [
+                {
+                    "binding": "NEXT_INC_CACHE_R2_BUCKET",
+                    "bucket_name": "gitbook-open-v2-cache-production"
+                }
+            ],
+            "services": [
+                {
+                    "binding": "WORKER_SELF_REFERENCE",
+                    "service": "gitbook-open-v2-production"
+                },
+                {
+                    "binding": "DEFAULT_WORKER",
+                    "service": "gitbook-open-v2-server-production"
+                }
+            ],
+            "tail_consumers": [
+                {
+                    "service": "gitbook-x-prod-tail"
+                }
+            ],
+            "durable_objects": {
+                "bindings": [
+                    {
+                        "name": "NEXT_CACHE_DO_QUEUE",
+                        "class_name": "DOQueueHandler"
+                    },
+                    {
+                        "name": "NEXT_TAG_CACHE_DO_SHARDED",
+                        "class_name": "DOShardedTagCache"
+                    }
+                ]
+            },
+            "migrations": [
+                {
+                    "tag": "v1",
+                    "new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache"]
+                }
+            ]
+        }
+    }
+}
diff --git a/packages/gitbook-v2/openNext/customWorkers/script/updateWrangler.ts b/packages/gitbook-v2/openNext/customWorkers/script/updateWrangler.ts
new file mode 100644
index 0000000000..0fdbf6cc70
--- /dev/null
+++ b/packages/gitbook-v2/openNext/customWorkers/script/updateWrangler.ts
@@ -0,0 +1,26 @@
+// In this script, we use the args from the cli to update the PREVIEW_URL vars in the wrangler config file for the middleware
+import fs from 'node:fs';
+import path from 'node:path';
+
+const wranglerConfigPath = path.join(__dirname, '../middlewareWrangler.jsonc');
+
+const file = fs.readFileSync(wranglerConfigPath, 'utf-8');
+
+const args = process.argv.slice(2);
+// The versionId is in the format xxx-xxx-xxx-xxx, we need the first part to reconstruct the preview URL
+const versionId = args[0];
+
+// The preview URL is in the format https://<versionId>-gitbook-open-v2-server-preview.gitbook.workers.dev
+const previewHostname = `${versionId.split('-')[0]}-gitbook-open-v2-server-preview.gitbook.workers.dev`;
+
+let updatedFile = file.replace(
+    /"PREVIEW_HOSTNAME": "TO_REPLACE"/,
+    `"PREVIEW_HOSTNAME": "${previewHostname}"`
+);
+
+updatedFile = updatedFile.replaceAll(
+    /"WORKER_VERSION_ID": "TO_REPLACE"/g,
+    `"WORKER_VERSION_ID": "${versionId}"`
+);
+
+fs.writeFileSync(wranglerConfigPath, updatedFile);
diff --git a/packages/gitbook-v2/openNext/queue.ts b/packages/gitbook-v2/openNext/queue.ts
deleted file mode 100644
index ab33c479d9..0000000000
--- a/packages/gitbook-v2/openNext/queue.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import type { Queue } from '@opennextjs/aws/types/overrides.js';
-import { getCloudflareContext } from '@opennextjs/cloudflare';
-import doQueue from '@opennextjs/cloudflare/overrides/queue/do-queue';
-import memoryQueue from '@opennextjs/cloudflare/overrides/queue/memory-queue';
-
-interface Env {
-    IS_PREVIEW?: string;
-}
-
-export default {
-    name: 'GitbookISRQueue',
-    send: async (msg) => {
-        const { ctx, env } = getCloudflareContext();
-        const isPreview = (env as Env).IS_PREVIEW === 'true';
-        ctx.waitUntil(isPreview ? memoryQueue.send(msg) : doQueue.send(msg));
-    },
-} satisfies Queue;
diff --git a/packages/gitbook-v2/openNext/queue/middleware.ts b/packages/gitbook-v2/openNext/queue/middleware.ts
new file mode 100644
index 0000000000..2a14dc1d8b
--- /dev/null
+++ b/packages/gitbook-v2/openNext/queue/middleware.ts
@@ -0,0 +1,21 @@
+import { trace } from '@/lib/tracing';
+import type { Queue } from '@opennextjs/aws/types/overrides.js';
+import { getCloudflareContext } from '@opennextjs/cloudflare';
+import doQueue from '@opennextjs/cloudflare/overrides/queue/do-queue';
+import memoryQueue from '@opennextjs/cloudflare/overrides/queue/memory-queue';
+
+interface Env {
+    STAGE?: string;
+}
+
+export default {
+    name: 'GitbookISRQueue',
+    send: async (msg) => {
+        return trace({ operation: 'gitbookISRQueueSend', name: msg.MessageBody.url }, async () => {
+            const { ctx, env } = getCloudflareContext();
+            const hasDurableObject =
+                (env as Env).STAGE !== 'dev' && (env as Env).STAGE !== 'preview';
+            ctx.waitUntil(hasDurableObject ? memoryQueue.send(msg) : doQueue.send(msg));
+        });
+    },
+} satisfies Queue;
diff --git a/packages/gitbook-v2/openNext/queue/server.ts b/packages/gitbook-v2/openNext/queue/server.ts
new file mode 100644
index 0000000000..9a5b3b689b
--- /dev/null
+++ b/packages/gitbook-v2/openNext/queue/server.ts
@@ -0,0 +1,9 @@
+import type { Queue } from '@opennextjs/aws/types/overrides.js';
+
+export default {
+    name: 'GitbookISRQueue',
+    send: async (msg) => {
+        // We should never reach this point in the server. If that's the case, we should log it.
+        console.warn('GitbookISRQueue: send called on server side, this should not happen.', msg);
+    },
+} satisfies Queue;
diff --git a/packages/gitbook-v2/openNext/tagCache/middleware.ts b/packages/gitbook-v2/openNext/tagCache/middleware.ts
new file mode 100644
index 0000000000..173b5dd283
--- /dev/null
+++ b/packages/gitbook-v2/openNext/tagCache/middleware.ts
@@ -0,0 +1,76 @@
+import { trace } from '@/lib/tracing';
+import type { NextModeTagCache } from '@opennextjs/aws/types/overrides.js';
+import doShardedTagCache from '@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache';
+import { softTagFilter } from '@opennextjs/cloudflare/overrides/tag-cache/tag-cache-filter';
+
+const originalTagCache = doShardedTagCache({
+    baseShardSize: 12,
+    regionalCache: true,
+    shardReplication: {
+        numberOfSoftReplicas: 2,
+        numberOfHardReplicas: 1,
+    },
+});
+
+export default {
+    name: 'GitbookTagCache',
+    mode: 'nextMode',
+    getLastRevalidated: async (tags: string[]) => {
+        const tagsToCheck = tags.filter(softTagFilter);
+        if (tagsToCheck.length === 0) {
+            // If we reach here, it probably means that there is an issue that we'll need to address.
+            console.warn(
+                'getLastRevalidated - No valid tags to check for last revalidation, original tags:',
+                tags
+            );
+            return 0; // If no tags to check, return 0
+        }
+        return trace(
+            {
+                operation: 'gitbookTagCacheGetLastRevalidated',
+                name: tagsToCheck.join(', '),
+            },
+            async () => {
+                return await originalTagCache.getLastRevalidated(tagsToCheck);
+            }
+        );
+    },
+    hasBeenRevalidated: async (tags: string[]) => {
+        const tagsToCheck = tags.filter(softTagFilter);
+        if (tagsToCheck.length === 0) {
+            // If we reach here, it probably means that there is an issue that we'll need to address.
+            console.warn(
+                'hasBeenRevalidated - No valid tags to check for revalidation, original tags:',
+                tags
+            );
+            return false; // If no tags to check, return false
+        }
+        return trace(
+            {
+                operation: 'gitbookTagCacheHasBeenRevalidated',
+                name: tagsToCheck.join(', '),
+            },
+            async () => {
+                const result = await originalTagCache.hasBeenRevalidated(tagsToCheck);
+                return result;
+            }
+        );
+    },
+    writeTags: async (tags: string[]) => {
+        return trace(
+            {
+                operation: 'gitbookTagCacheWriteTags',
+                name: tags.join(', '),
+            },
+            async () => {
+                const tagsToWrite = tags.filter(softTagFilter);
+                if (tagsToWrite.length === 0) {
+                    console.warn('writeTags - No valid tags to write');
+                    return; // If no tags to write, exit early
+                }
+                // Write only the filtered tags
+                await originalTagCache.writeTags(tagsToWrite);
+            }
+        );
+    },
+} satisfies NextModeTagCache;
diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json
index 5561b7f8d3..224d55e301 100644
--- a/packages/gitbook-v2/package.json
+++ b/packages/gitbook-v2/package.json
@@ -30,6 +30,8 @@
         "start": "next start",
         "build:v2:cloudflare": "opennextjs-cloudflare build",
         "dev:v2:cloudflare": "wrangler dev --port 8771 --env preview",
+        "dev:v2:cf:middleware": "wrangler dev --port 8771 --inspector-port 9230 --env dev --config ./openNext/customWorkers/middlewareWrangler.jsonc",
+        "dev:v2:cf:server": "wrangler dev --port 8772 --env dev --config ./openNext/customWorkers/defaultWrangler.jsonc",
         "unit": "bun test",
         "typecheck": "tsc --noEmit"
     }
diff --git a/packages/gitbook-v2/src/lib/data/api.ts b/packages/gitbook-v2/src/lib/data/api.ts
index 7a2d03d16d..f8ae6d2711 100644
--- a/packages/gitbook-v2/src/lib/data/api.ts
+++ b/packages/gitbook-v2/src/lib/data/api.ts
@@ -2,14 +2,13 @@ import { trace } from '@/lib/tracing';
 import {
     type ComputedContentSource,
     GitBookAPI,
-    type GitBookAPIServiceBinding,
     type HttpResponse,
     type RenderIntegrationUI,
 } from '@gitbook/api';
 import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-tags';
 import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env';
 import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache';
-import { getCloudflareContext, getCloudflareRequestGlobal } from './cloudflare';
+import { getCloudflareRequestGlobal } from './cloudflare';
 import { DataFetcherError, wrapDataFetcherError } from './errors';
 import { withCacheKey, withoutConcurrentExecution } from './memoize';
 import type { GitBookDataFetcher } from './types';
@@ -828,36 +827,16 @@ async function* streamAIResponse(
     }
 }
 
-let loggedServiceBinding = false;
-
 /**
  * Create a new API client.
  */
 export function apiClient(input: DataFetcherInput = { apiToken: null }) {
     const { apiToken } = input;
-    let serviceBinding: GitBookAPIServiceBinding | undefined;
-
-    const cloudflareContext = getCloudflareContext();
-    if (cloudflareContext) {
-        // @ts-expect-error
-        serviceBinding = cloudflareContext.env.GITBOOK_API as GitBookAPIServiceBinding | undefined;
-        if (!loggedServiceBinding) {
-            loggedServiceBinding = true;
-            if (serviceBinding) {
-                // biome-ignore lint/suspicious/noConsole: we want to log here
-                console.log(`using service binding for the API (${GITBOOK_API_URL})`);
-            } else {
-                // biome-ignore lint/suspicious/noConsole: we want to log here
-                console.warn(`no service binding for the API (${GITBOOK_API_URL})`);
-            }
-        }
-    }
 
     const api = new GitBookAPI({
         authToken: apiToken || GITBOOK_API_TOKEN || undefined,
         endpoint: GITBOOK_API_URL,
         userAgent: GITBOOK_USER_AGENT,
-        serviceBinding,
     });
 
     return api;

From a1da4f023d7d491dde259f526c66d922d5015645 Mon Sep 17 00:00:00 2001
From: conico974 <nicodorseuil@yahoo.fr>
Date: Fri, 30 May 2025 16:19:11 +0200
Subject: [PATCH 069/127] Move DO to a 3rd worker (#3270)

Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
---
 .../composite/deploy-cloudflare/action.yaml   |  10 ++
 .../openNext/customWorkers/default.js         |  21 +++
 .../customWorkers/defaultWrangler.jsonc       |  65 +++++++--
 .../gitbook-v2/openNext/customWorkers/do.js   |  38 ++++++
 .../openNext/customWorkers/doWrangler.jsonc   | 127 ++++++++++++++++++
 .../customWorkers/middlewareWrangler.jsonc    |  49 +++++--
 .../gitbook-v2/openNext/incrementalCache.ts   |  22 ++-
 .../openNext/tagCache/middleware.ts           |   6 +-
 8 files changed, 315 insertions(+), 23 deletions(-)
 create mode 100644 packages/gitbook-v2/openNext/customWorkers/do.js
 create mode 100644 packages/gitbook-v2/openNext/customWorkers/doWrangler.jsonc

diff --git a/.github/composite/deploy-cloudflare/action.yaml b/.github/composite/deploy-cloudflare/action.yaml
index d1d0d66339..7e66a8bf33 100644
--- a/.github/composite/deploy-cloudflare/action.yaml
+++ b/.github/composite/deploy-cloudflare/action.yaml
@@ -64,6 +64,16 @@ runs:
             GITBOOK_RUNTIME: cloudflare
           shell: bash
 
+        - name: Upload the DO worker
+          uses: cloudflare/wrangler-action@v3.14.0
+          with:
+              apiToken: ${{ inputs.apiToken }}
+              accountId: ${{ inputs.accountId }}
+              workingDirectory: ./
+              wranglerVersion: '4.10.0'
+              environment: ${{ inputs.environment }}
+              command: deploy --config ./packages/gitbook-v2/openNext/customWorkers/doWrangler.jsonc
+
         - id: upload_server
           name: Upload server to Cloudflare
           uses: cloudflare/wrangler-action@v3.14.0
diff --git a/packages/gitbook-v2/openNext/customWorkers/default.js b/packages/gitbook-v2/openNext/customWorkers/default.js
index bb673e1722..535c218167 100644
--- a/packages/gitbook-v2/openNext/customWorkers/default.js
+++ b/packages/gitbook-v2/openNext/customWorkers/default.js
@@ -1,5 +1,26 @@
 import { runWithCloudflareRequestContext } from '../../.open-next/cloudflare/init.js';
 
+import { DurableObject } from 'cloudflare:workers';
+
+// Only needed to run locally, in prod we'll use the one from do.js
+export class R2WriteBuffer extends DurableObject {
+    writePromise;
+
+    async write(cacheKey, value) {
+        // We are already writing to this key
+        if (this.writePromise) {
+            return;
+        }
+
+        this.writePromise = this.env.NEXT_INC_CACHE_R2_BUCKET.put(cacheKey, value);
+        this.ctx.waitUntil(
+            this.writePromise.finally(() => {
+                this.writePromise = undefined;
+            })
+        );
+    }
+}
+
 export default {
     async fetch(request, env, ctx) {
         return runWithCloudflareRequestContext(request, env, ctx, async () => {
diff --git a/packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc b/packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
index 3fff07d7c0..e036db1c44 100644
--- a/packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
+++ b/packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
@@ -29,6 +29,20 @@
                     "binding": "WORKER_SELF_REFERENCE",
                     "service": "gitbook-open-v2-server-dev"
                 }
+            ],
+            "durable_objects": {
+                "bindings": [
+                    {
+                        "name": "WRITE_BUFFER",
+                        "class_name": "R2WriteBuffer"
+                    }
+                ]
+            },
+            "migrations": [
+                {
+                    "tag": "v1",
+                    "new_sqlite_classes": ["R2WriteBuffer"]
+                }
             ]
         },
         "preview": {
@@ -48,9 +62,26 @@
                     "binding": "WORKER_SELF_REFERENCE",
                     "service": "gitbook-open-v2-server-preview"
                 }
-            ]
-            // No durable objects on preview, as they block the generation of preview URLs
-            // and we don't need tags invalidation on preview
+            ],
+            "durable_objects": {
+                "bindings": [
+                    {
+                        "name": "WRITE_BUFFER",
+                        "class_name": "R2WriteBuffer",
+                        "script_name": "gitbook-open-v2-do-preview"
+                    },
+                    {
+                        "name": "NEXT_TAG_CACHE_DO_SHARDED",
+                        "class_name": "DOShardedTagCache",
+                        "script_name": "gitbook-open-v2-do-preview"
+                    },
+                    {
+                        "name": "NEXT_CACHE_DO_QUEUE",
+                        "class_name": "DOQueueHandler",
+                        "script_name": "gitbook-open-v2-do-preview"
+                    }
+                ]
+            }
         },
         "staging": {
             "r2_buckets": [
@@ -67,12 +98,20 @@
             ],
             "durable_objects": {
                 "bindings": [
-                    // We do not need to define migrations for external DOs,
-                    // In fact, defining migrations for external DOs will crash
+                    {
+                        "name": "WRITE_BUFFER",
+                        "class_name": "R2WriteBuffer",
+                        "script_name": "gitbook-open-v2-do-staging"
+                    },
                     {
                         "name": "NEXT_TAG_CACHE_DO_SHARDED",
                         "class_name": "DOShardedTagCache",
-                        "script_name": "gitbook-open-v2-staging"
+                        "script_name": "gitbook-open-v2-do-staging"
+                    },
+                    {
+                        "name": "NEXT_CACHE_DO_QUEUE",
+                        "class_name": "DOQueueHandler",
+                        "script_name": "gitbook-open-v2-do-staging"
                     }
                 ]
             },
@@ -98,11 +137,19 @@
             "durable_objects": {
                 "bindings": [
                     {
-                        // We do not need to define migrations for external DOs,
-                        // In fact, defining migrations for external DOs will crash
+                        "name": "WRITE_BUFFER",
+                        "class_name": "R2WriteBuffer",
+                        "script_name": "gitbook-open-v2-do-production"
+                    },
+                    {
                         "name": "NEXT_TAG_CACHE_DO_SHARDED",
                         "class_name": "DOShardedTagCache",
-                        "script_name": "gitbook-open-v2-production"
+                        "script_name": "gitbook-open-v2-do-production"
+                    },
+                    {
+                        "name": "NEXT_CACHE_DO_QUEUE",
+                        "class_name": "DOQueueHandler",
+                        "script_name": "gitbook-open-v2-do-production"
                     }
                 ]
             },
diff --git a/packages/gitbook-v2/openNext/customWorkers/do.js b/packages/gitbook-v2/openNext/customWorkers/do.js
new file mode 100644
index 0000000000..04f3cf3bec
--- /dev/null
+++ b/packages/gitbook-v2/openNext/customWorkers/do.js
@@ -0,0 +1,38 @@
+// This worker only purposes it to host the different DO that we will need in the other workers.
+import { DurableObject } from 'cloudflare:workers';
+
+// `use cache` could cause multiple writes to the same key to happen concurrently, there is a limit of 1 write per key/second
+// so we need to buffer writes to the R2 bucket to avoid hitting this limit.
+export class R2WriteBuffer extends DurableObject {
+    writePromise;
+
+    async write(cacheKey, value) {
+        // We are already writing to this key
+        if (this.writePromise) {
+            return;
+        }
+
+        this.writePromise = this.env.NEXT_INC_CACHE_R2_BUCKET.put(cacheKey, value);
+        this.ctx.waitUntil(
+            this.writePromise.finally(() => {
+                this.writePromise = undefined;
+            })
+        );
+    }
+}
+
+export { DOQueueHandler } from '../../.open-next/.build/durable-objects/queue.js';
+
+export { DOShardedTagCache } from '../../.open-next/.build/durable-objects/sharded-tag-cache.js';
+
+export default {
+    async fetch() {
+        // This worker does not handle any requests, it only provides Durable Objects
+        return new Response('This worker is not meant to handle requests directly', {
+            status: 400,
+            headers: {
+                'Content-Type': 'text/plain',
+            },
+        });
+    },
+};
diff --git a/packages/gitbook-v2/openNext/customWorkers/doWrangler.jsonc b/packages/gitbook-v2/openNext/customWorkers/doWrangler.jsonc
new file mode 100644
index 0000000000..f5d2f726b8
--- /dev/null
+++ b/packages/gitbook-v2/openNext/customWorkers/doWrangler.jsonc
@@ -0,0 +1,127 @@
+{
+    "main": "do.js",
+    "name": "gitbook-open-v2-do",
+    "compatibility_date": "2025-04-14",
+    "compatibility_flags": [
+        "nodejs_compat",
+        "allow_importable_env",
+        "global_fetch_strictly_public"
+    ],
+    "observability": {
+        "enabled": true
+    },
+    "env": {
+        "preview": {
+            "vars": {
+                "STAGE": "preview",
+                "NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
+            },
+            "r2_buckets": [
+                {
+                    "binding": "NEXT_INC_CACHE_R2_BUCKET",
+                    "bucket_name": "gitbook-open-v2-cache-preview"
+                }
+            ],
+            "durable_objects": {
+                "bindings": [
+                    {
+                        "name": "NEXT_CACHE_DO_QUEUE",
+                        "class_name": "DOQueueHandler"
+                    },
+                    {
+                        "name": "NEXT_TAG_CACHE_DO_SHARDED",
+                        "class_name": "DOShardedTagCache"
+                    },
+                    {
+                        "name": "WRITE_BUFFER",
+                        "class_name": "R2WriteBuffer"
+                    }
+                ]
+            },
+            "migrations": [
+                {
+                    "tag": "v1",
+                    "new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache", "R2WriteBuffer"]
+                }
+            ]
+        },
+        "staging": {
+            "vars": {
+                "STAGE": "staging",
+                "NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
+            },
+            "r2_buckets": [
+                {
+                    "binding": "NEXT_INC_CACHE_R2_BUCKET",
+                    "bucket_name": "gitbook-open-v2-cache-staging"
+                }
+            ],
+            "tail_consumers": [
+                {
+                    "service": "gitbook-x-staging-tail"
+                }
+            ],
+            "durable_objects": {
+                "bindings": [
+                    {
+                        "name": "NEXT_CACHE_DO_QUEUE",
+                        "class_name": "DOQueueHandler"
+                    },
+                    {
+                        "name": "NEXT_TAG_CACHE_DO_SHARDED",
+                        "class_name": "DOShardedTagCache"
+                    },
+                    {
+                        "name": "WRITE_BUFFER",
+                        "class_name": "R2WriteBuffer"
+                    }
+                ]
+            },
+            "migrations": [
+                {
+                    "tag": "v1",
+                    "new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache", "R2WriteBuffer"]
+                }
+            ]
+        },
+        "production": {
+            "vars": {
+                "NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true",
+                "STAGE": "production"
+            },
+            "r2_buckets": [
+                {
+                    "binding": "NEXT_INC_CACHE_R2_BUCKET",
+                    "bucket_name": "gitbook-open-v2-cache-production"
+                }
+            ],
+            "tail_consumers": [
+                {
+                    "service": "gitbook-x-prod-tail"
+                }
+            ],
+            "durable_objects": {
+                "bindings": [
+                    {
+                        "name": "NEXT_CACHE_DO_QUEUE",
+                        "class_name": "DOQueueHandler"
+                    },
+                    {
+                        "name": "NEXT_TAG_CACHE_DO_SHARDED",
+                        "class_name": "DOShardedTagCache"
+                    },
+                    {
+                        "name": "WRITE_BUFFER",
+                        "class_name": "R2WriteBuffer"
+                    }
+                ]
+            },
+            "migrations": [
+                {
+                    "tag": "v1",
+                    "new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache", "R2WriteBuffer"]
+                }
+            ]
+        }
+    }
+}
diff --git a/packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc b/packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc
index 4870c89d54..09e48afcc0 100644
--- a/packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc
+++ b/packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc
@@ -61,9 +61,26 @@
                     "binding": "DEFAULT_WORKER",
                     "service": "gitbook-open-v2-server-preview"
                 }
-            ]
-            // No durable objects on preview, as they block the generation of preview URLs
-            // and we don't need tags invalidation on preview
+            ],
+            "durable_objects": {
+                "bindings": [
+                    {
+                        "name": "WRITE_BUFFER",
+                        "class_name": "R2WriteBuffer",
+                        "script_name": "gitbook-open-v2-do-preview"
+                    },
+                    {
+                        "name": "NEXT_TAG_CACHE_DO_SHARDED",
+                        "class_name": "DOShardedTagCache",
+                        "script_name": "gitbook-open-v2-do-preview"
+                    },
+                    {
+                        "name": "NEXT_CACHE_DO_QUEUE",
+                        "class_name": "DOQueueHandler",
+                        "script_name": "gitbook-open-v2-do-preview"
+                    }
+                ]
+            }
         },
         "staging": {
             "vars": {
@@ -104,12 +121,19 @@
             "durable_objects": {
                 "bindings": [
                     {
-                        "name": "NEXT_CACHE_DO_QUEUE",
-                        "class_name": "DOQueueHandler"
+                        "name": "WRITE_BUFFER",
+                        "class_name": "R2WriteBuffer",
+                        "script_name": "gitbook-open-v2-do-staging"
                     },
                     {
                         "name": "NEXT_TAG_CACHE_DO_SHARDED",
-                        "class_name": "DOShardedTagCache"
+                        "class_name": "DOShardedTagCache",
+                        "script_name": "gitbook-open-v2-do-staging"
+                    },
+                    {
+                        "name": "NEXT_CACHE_DO_QUEUE",
+                        "class_name": "DOQueueHandler",
+                        "script_name": "gitbook-open-v2-do-staging"
                     }
                 ]
             },
@@ -165,12 +189,19 @@
             "durable_objects": {
                 "bindings": [
                     {
-                        "name": "NEXT_CACHE_DO_QUEUE",
-                        "class_name": "DOQueueHandler"
+                        "name": "WRITE_BUFFER",
+                        "class_name": "R2WriteBuffer",
+                        "script_name": "gitbook-open-v2-do-production"
                     },
                     {
                         "name": "NEXT_TAG_CACHE_DO_SHARDED",
-                        "class_name": "DOShardedTagCache"
+                        "class_name": "DOShardedTagCache",
+                        "script_name": "gitbook-open-v2-do-production"
+                    },
+                    {
+                        "name": "NEXT_CACHE_DO_QUEUE",
+                        "class_name": "DOQueueHandler",
+                        "script_name": "gitbook-open-v2-do-production"
                     }
                 ]
             },
diff --git a/packages/gitbook-v2/openNext/incrementalCache.ts b/packages/gitbook-v2/openNext/incrementalCache.ts
index d4a662af01..28d1d6b8a1 100644
--- a/packages/gitbook-v2/openNext/incrementalCache.ts
+++ b/packages/gitbook-v2/openNext/incrementalCache.ts
@@ -9,6 +9,8 @@ import type {
 } from '@opennextjs/aws/types/overrides.js';
 import { getCloudflareContext } from '@opennextjs/cloudflare';
 
+import type { DurableObjectNamespace, Rpc } from '@cloudflare/workers-types';
+
 export const BINDING_NAME = 'NEXT_INC_CACHE_R2_BUCKET';
 export const DEFAULT_PREFIX = 'incremental-cache';
 
@@ -79,12 +81,10 @@ class GitbookIncrementalCache implements IncrementalCache {
             },
             async (span) => {
                 span.setAttribute('cacheType', cacheType ?? 'cache');
-                const r2 = getCloudflareContext().env[BINDING_NAME];
                 const localCache = await this.getCacheInstance();
-                if (!r2) throw new Error('No R2 bucket');
 
                 try {
-                    await r2.put(cacheKey, JSON.stringify(value));
+                    await this.writeToR2(cacheKey, JSON.stringify(value));
 
                     //TODO: Check if there is any places where we don't have tags
                     // Ideally we should always have tags, but in case we don't, we need to decide how to handle it
@@ -145,6 +145,22 @@ class GitbookIncrementalCache implements IncrementalCache {
         );
     }
 
+    async writeToR2(key: string, value: string): Promise<void> {
+        const env = getCloudflareContext().env as {
+            WRITE_BUFFER: DurableObjectNamespace<
+                Rpc.DurableObjectBranded & {
+                    write: (key: string, value: string) => Promise<void>;
+                }
+            >;
+        };
+        const id = env.WRITE_BUFFER.idFromName(key);
+
+        // A stub is a client used to invoke methods on the Durable Object
+        const stub = env.WRITE_BUFFER.get(id);
+
+        await stub.write(key, value);
+    }
+
     async getCacheInstance(): Promise<Cache> {
         if (this.localCache) return this.localCache;
         this.localCache = await caches.open('incremental-cache');
diff --git a/packages/gitbook-v2/openNext/tagCache/middleware.ts b/packages/gitbook-v2/openNext/tagCache/middleware.ts
index 173b5dd283..398ceee0d3 100644
--- a/packages/gitbook-v2/openNext/tagCache/middleware.ts
+++ b/packages/gitbook-v2/openNext/tagCache/middleware.ts
@@ -6,6 +6,8 @@ import { softTagFilter } from '@opennextjs/cloudflare/overrides/tag-cache/tag-ca
 const originalTagCache = doShardedTagCache({
     baseShardSize: 12,
     regionalCache: true,
+    // We can probably increase this value even further
+    regionalCacheTtlSec: 60,
     shardReplication: {
         numberOfSoftReplicas: 2,
         numberOfHardReplicas: 1,
@@ -35,7 +37,7 @@ export default {
             }
         );
     },
-    hasBeenRevalidated: async (tags: string[]) => {
+    hasBeenRevalidated: async (tags: string[], lastModified?: number) => {
         const tagsToCheck = tags.filter(softTagFilter);
         if (tagsToCheck.length === 0) {
             // If we reach here, it probably means that there is an issue that we'll need to address.
@@ -51,7 +53,7 @@ export default {
                 name: tagsToCheck.join(', '),
             },
             async () => {
-                const result = await originalTagCache.hasBeenRevalidated(tagsToCheck);
+                const result = await originalTagCache.hasBeenRevalidated(tagsToCheck, lastModified);
                 return result;
             }
         );

From 7d5a6d2255506fc0d4d314a9786a945cb4bf2727 Mon Sep 17 00:00:00 2001
From: conico974 <nicodorseuil@yahoo.fr>
Date: Mon, 2 Jun 2025 15:23:15 +0200
Subject: [PATCH 070/127] Fix hydration error on V2 (#3272)

Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
---
 .changeset/cold-buckets-divide.md             |  5 +++
 .../DocumentView/Table/RecordCard.tsx         | 31 +++++++-------
 .../src/components/RootLayout/globals.css     |  7 ++++
 .../src/components/primitives/Link.tsx        | 41 +++++++++++++++++++
 4 files changed, 69 insertions(+), 15 deletions(-)
 create mode 100644 .changeset/cold-buckets-divide.md

diff --git a/.changeset/cold-buckets-divide.md b/.changeset/cold-buckets-divide.md
new file mode 100644
index 0000000000..1e528bad19
--- /dev/null
+++ b/.changeset/cold-buckets-divide.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+fix nested a tag causing hydration error
diff --git a/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx b/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx
index 2a246d13c6..4218f760e4 100644
--- a/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx
+++ b/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx
@@ -4,7 +4,7 @@ import {
     SiteInsightsLinkPosition,
 } from '@gitbook/api';
 
-import { Link } from '@/components/primitives';
+import { LinkBox, LinkOverlay } from '@/components/primitives';
 import { Image } from '@/components/utils';
 import { resolveContentRef } from '@/lib/references';
 import { type ClassValue, tcls } from '@/lib/tailwind';
@@ -44,7 +44,6 @@ export async function RecordCard(
         <div
             className={tcls(
                 'grid-area-1-1',
-                'z-0',
                 'relative',
                 'grid',
                 'bg-tint-base',
@@ -151,7 +150,6 @@ export async function RecordCard(
         'rounded-md',
         'straight-corners:rounded-none',
         'dark:shadow-transparent',
-        'z-0',
 
         'before:pointer-events-none',
         'before:grid-area-1-1',
@@ -167,19 +165,22 @@ export async function RecordCard(
 
     if (target && targetRef) {
         return (
-            <Link
-                href={target.href}
-                className={tcls(style, 'hover:before:ring-tint-12/5')}
-                insights={{
-                    type: 'link_click',
-                    link: {
-                        target: targetRef,
-                        position: SiteInsightsLinkPosition.Content,
-                    },
-                }}
-            >
+            // We don't use `Link` directly here because we could end up in a situation where
+            // a link is rendered inside a link, which is not allowed in HTML.
+            // It causes an hydration error in React.
+            <LinkBox href={target.href} className={tcls(style, 'hover:before:ring-tint-12/5')}>
+                <LinkOverlay
+                    href={target.href}
+                    insights={{
+                        type: 'link_click',
+                        link: {
+                            target: targetRef,
+                            position: SiteInsightsLinkPosition.Content,
+                        },
+                    }}
+                />
                 {body}
-            </Link>
+            </LinkBox>
         );
     }
 
diff --git a/packages/gitbook/src/components/RootLayout/globals.css b/packages/gitbook/src/components/RootLayout/globals.css
index 15c09614e3..2bb3e1a223 100644
--- a/packages/gitbook/src/components/RootLayout/globals.css
+++ b/packages/gitbook/src/components/RootLayout/globals.css
@@ -148,6 +148,13 @@
             width: 100%;
         }
     }
+
+    .elevate-link {
+        & a[href]:not(.link-overlay) {
+            position: relative;
+            z-index: 20;
+        }
+    }
 }
 
 html {
diff --git a/packages/gitbook/src/components/primitives/Link.tsx b/packages/gitbook/src/components/primitives/Link.tsx
index 5cb026d3a5..cf9d2c24c4 100644
--- a/packages/gitbook/src/components/primitives/Link.tsx
+++ b/packages/gitbook/src/components/primitives/Link.tsx
@@ -3,6 +3,7 @@
 import NextLink, { type LinkProps as NextLinkProps } from 'next/link';
 import React from 'react';
 
+import { tcls } from '@/lib/tailwind';
 import { type TrackEventInput, useTrackEvent } from '../Insights';
 
 // Props from Next, which includes NextLinkProps and all the things anchor elements support.
@@ -75,6 +76,46 @@ export const Link = React.forwardRef(function Link(
     );
 });
 
+/**
+ * A box used to contain a link overlay.
+ * It is used to create a clickable area that can contain other elements.
+ */
+export const LinkBox = React.forwardRef(function LinkBox(
+    props: React.BaseHTMLAttributes<HTMLDivElement>,
+    ref: React.Ref<HTMLDivElement>
+) {
+    const { children, className, ...domProps } = props;
+    return (
+        <div ref={ref} {...domProps} className={tcls('elevate-link relative', className)}>
+            {children}
+        </div>
+    );
+});
+
+/**
+ * A link overlay that can be used to create a clickable area on top of other elements.
+ * It is used to create a link that covers the entire area of the element without encapsulating it in a link tag.
+ * This is useful to avoid nesting links inside links.
+ */
+export const LinkOverlay = React.forwardRef(function LinkOverlay(
+    props: LinkProps,
+    ref: React.Ref<HTMLAnchorElement>
+) {
+    const { children, className, ...domProps } = props;
+    return (
+        <Link
+            ref={ref}
+            {...domProps}
+            className={tcls(
+                'link-overlay static before:absolute before:top-0 before:left-0 before:z-10 before:h-full before:w-full',
+                className
+            )}
+        >
+            {children}
+        </Link>
+    );
+});
+
 /**
  * Check if a link is external, compared to an origin.
  */

From 0f867e533a65532730e014810a161753e81bbf9d Mon Sep 17 00:00:00 2001
From: conico974 <nicodorseuil@yahoo.fr>
Date: Mon, 2 Jun 2025 17:40:04 +0200
Subject: [PATCH 071/127] bump OpenNext to latest (#3276)

Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
---
 bun.lock                                            |  6 +++---
 packages/gitbook-v2/open-next.config.ts             |  2 +-
 packages/gitbook-v2/openNext/queue/middleware.ts    | 11 ++---------
 packages/gitbook-v2/openNext/tagCache/middleware.ts |  3 +++
 packages/gitbook-v2/package.json                    |  2 +-
 5 files changed, 10 insertions(+), 14 deletions(-)

diff --git a/bun.lock b/bun.lock
index 631adb8ce4..96f0f0d203 100644
--- a/bun.lock
+++ b/bun.lock
@@ -145,7 +145,7 @@
       "dependencies": {
         "@gitbook/api": "^0.115.0",
         "@gitbook/cache-tags": "workspace:*",
-        "@opennextjs/cloudflare": "1.0.4",
+        "@opennextjs/cloudflare": "1.1.0",
         "@sindresorhus/fnv1a": "^3.1.0",
         "assert-never": "^1.2.1",
         "jwt-decode": "^4.0.0",
@@ -791,9 +791,9 @@
 
     "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
 
-    "@opennextjs/aws": ["@opennextjs/aws@3.6.2", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.25.4", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-26/3GSoj7mKN7XpQFikYM2Lrwal5jlMc4fJO//QAdd5bCSnBaWBAkzr7+VvXAFVIC6eBeDLdtlWiQuUQVEAPZQ=="],
+    "@opennextjs/aws": ["@opennextjs/aws@3.6.4", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.25.4", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-/bn9N/6dVu9+sC7AptaGJylKUzyDDgqe3yKfvUxXOpy7whIq/+3Bw+q2/bilyQg6FcskbCWUt9nLvNf1hAVmfA=="],
 
-    "@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.0.4", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "^3.6.2", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.14.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-DVudGpOSJ91ruPiBJ0kuFmPhEQbXIXLTbvUjAx1OlbwFskG2gvdNIAmF3ZXV6z1VGDO7Q/u2W2ybMZLf7avlrA=="],
+    "@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.1.0", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.6.4", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.14.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-8DOzpLiewivA1pwRNGVPDXbrF79bzJiHscY+M1tekwvKqEqM26CRYafhrA5IDCeyteNaL9AZD6X4DLuCn/eeYQ=="],
 
     "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
 
diff --git a/packages/gitbook-v2/open-next.config.ts b/packages/gitbook-v2/open-next.config.ts
index 959834739a..1872309119 100644
--- a/packages/gitbook-v2/open-next.config.ts
+++ b/packages/gitbook-v2/open-next.config.ts
@@ -6,7 +6,7 @@ export default {
             wrapper: 'cloudflare-node',
             converter: 'edge',
             proxyExternalRequest: 'fetch',
-            queue: () => import('./openNext/queue/server').then((m) => m.default),
+            queue: () => import('./openNext/queue/middleware').then((m) => m.default),
             incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default),
             tagCache: () => import('./openNext/tagCache/middleware').then((m) => m.default),
         },
diff --git a/packages/gitbook-v2/openNext/queue/middleware.ts b/packages/gitbook-v2/openNext/queue/middleware.ts
index 2a14dc1d8b..5ab486a975 100644
--- a/packages/gitbook-v2/openNext/queue/middleware.ts
+++ b/packages/gitbook-v2/openNext/queue/middleware.ts
@@ -2,20 +2,13 @@ import { trace } from '@/lib/tracing';
 import type { Queue } from '@opennextjs/aws/types/overrides.js';
 import { getCloudflareContext } from '@opennextjs/cloudflare';
 import doQueue from '@opennextjs/cloudflare/overrides/queue/do-queue';
-import memoryQueue from '@opennextjs/cloudflare/overrides/queue/memory-queue';
-
-interface Env {
-    STAGE?: string;
-}
 
 export default {
     name: 'GitbookISRQueue',
     send: async (msg) => {
         return trace({ operation: 'gitbookISRQueueSend', name: msg.MessageBody.url }, async () => {
-            const { ctx, env } = getCloudflareContext();
-            const hasDurableObject =
-                (env as Env).STAGE !== 'dev' && (env as Env).STAGE !== 'preview';
-            ctx.waitUntil(hasDurableObject ? memoryQueue.send(msg) : doQueue.send(msg));
+            const { ctx } = getCloudflareContext();
+            ctx.waitUntil(doQueue.send(msg));
         });
     },
 } satisfies Queue;
diff --git a/packages/gitbook-v2/openNext/tagCache/middleware.ts b/packages/gitbook-v2/openNext/tagCache/middleware.ts
index 398ceee0d3..4f06d7896b 100644
--- a/packages/gitbook-v2/openNext/tagCache/middleware.ts
+++ b/packages/gitbook-v2/openNext/tagCache/middleware.ts
@@ -11,6 +11,9 @@ const originalTagCache = doShardedTagCache({
     shardReplication: {
         numberOfSoftReplicas: 2,
         numberOfHardReplicas: 1,
+        regionalReplication: {
+            defaultRegion: 'enam',
+        },
     },
 });
 
diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json
index 224d55e301..0b790a9bee 100644
--- a/packages/gitbook-v2/package.json
+++ b/packages/gitbook-v2/package.json
@@ -5,7 +5,7 @@
     "dependencies": {
         "@gitbook/api": "^0.115.0",
         "@gitbook/cache-tags": "workspace:*",
-        "@opennextjs/cloudflare": "1.0.4",
+        "@opennextjs/cloudflare": "1.1.0",
         "@sindresorhus/fnv1a": "^3.1.0",
         "assert-never": "^1.2.1",
         "jwt-decode": "^4.0.0",

From 015615de8ab42bec73048aa7480193d8b1ac9444 Mon Sep 17 00:00:00 2001
From: Brett Jephson <brett@gitbook.io>
Date: Tue, 3 Jun 2025 12:18:01 +0100
Subject: [PATCH 072/127] fix: defaultWidth/fullWidth options in images (#3274)

---
 .changeset/breezy-falcons-drop.md                       | 5 +++++
 packages/gitbook/src/components/DocumentView/Images.tsx | 2 +-
 2 files changed, 6 insertions(+), 1 deletion(-)
 create mode 100644 .changeset/breezy-falcons-drop.md

diff --git a/.changeset/breezy-falcons-drop.md b/.changeset/breezy-falcons-drop.md
new file mode 100644
index 0000000000..834e46bfca
--- /dev/null
+++ b/.changeset/breezy-falcons-drop.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Respect fullWidth and defaultWidth for images
diff --git a/packages/gitbook/src/components/DocumentView/Images.tsx b/packages/gitbook/src/components/DocumentView/Images.tsx
index baaab50bdc..20018b6f49 100644
--- a/packages/gitbook/src/components/DocumentView/Images.tsx
+++ b/packages/gitbook/src/components/DocumentView/Images.tsx
@@ -29,7 +29,7 @@ export function Images(props: BlockProps<DocumentBlockImages>) {
                 align === 'center' && 'justify-center',
                 align === 'right' && 'justify-end',
                 align === 'left' && 'justify-start',
-                isMultipleImages && ['grid', 'grid-flow-col', 'max-w-none']
+                isMultipleImages && ['grid', 'grid-flow-col']
             )}
         >
             {block.nodes.map((node: any, _i: number) => (

From c9373efdb514800809ccabf40fd8da2dad13a5ca Mon Sep 17 00:00:00 2001
From: Zeno Kapitein <zeno@gitbook.io>
Date: Tue, 3 Jun 2025 15:18:04 +0200
Subject: [PATCH 073/127] Fix bold header links hover color (#3255)

---
 .changeset/strong-poets-move.md                       | 5 +++++
 packages/gitbook/src/components/Header/HeaderLink.tsx | 2 +-
 2 files changed, 6 insertions(+), 1 deletion(-)
 create mode 100644 .changeset/strong-poets-move.md

diff --git a/.changeset/strong-poets-move.md b/.changeset/strong-poets-move.md
new file mode 100644
index 0000000000..9914aa44c9
--- /dev/null
+++ b/.changeset/strong-poets-move.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Fix bold header links hover color
diff --git a/packages/gitbook/src/components/Header/HeaderLink.tsx b/packages/gitbook/src/components/Header/HeaderLink.tsx
index fcfb28c723..6b13fef714 100644
--- a/packages/gitbook/src/components/Header/HeaderLink.tsx
+++ b/packages/gitbook/src/components/Header/HeaderLink.tsx
@@ -159,7 +159,7 @@ function getHeaderLinkClassName(_props: { headerPreset: CustomizationHeaderPrese
         'links-accent:py-0.5', // Prevent underline from being cut off at the bottom
 
         'theme-bold:text-header-link',
-        'theme-bold:hover:text-header-link'
+        'theme-bold:hover:!text-header-link/7'
     );
 }
 

From 7b5a3616c48291577b15b805ecb490bf2310bbd7 Mon Sep 17 00:00:00 2001
From: Addison <42930383+addisonschultz@users.noreply.github.com>
Date: Tue, 3 Jun 2025 16:07:55 +0100
Subject: [PATCH 074/127] Add UTM tags to the preview site badge (#3277)

---
 README.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index f9702f88e3..8a9f5c2fdc 100644
--- a/README.md
+++ b/README.md
@@ -150,11 +150,11 @@ See `LICENSE` for more information.
 </p>
 
 ```md
-[![GitBook](https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=3F89A1)](https://gitbook.com/)
+[![GitBook](https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=3F89A1)](https://www.gitbook.com/preview?utm_source=gitbook_readme_badge&utm_medium=organic&utm_campaign=preview_documentation&utm_content=link)
 ```
 
 ```html
-<a href="https://gitbook.com">
+<a href="https://www.gitbook.com/preview?utm_source=gitbook_readme_badge&utm_medium=organic&utm_campaign=preview_documentation&utm_content=link">
     <img
         src="https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=3F89A1"
     />

From c730845e17c0858132c176439cf4a5b707cb0047 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Wed, 4 Jun 2025 16:33:01 +0100
Subject: [PATCH 075/127] Fix missing title for button to close announcement
 banner (#3279)

---
 .changeset/sharp-hats-applaud.md                             | 5 +++++
 .../src/components/Announcement/AnnouncementBanner.tsx       | 4 ++++
 packages/gitbook/src/components/Cookies/CookiesToast.tsx     | 4 ++--
 packages/gitbook/src/intl/translations/de.ts                 | 2 +-
 packages/gitbook/src/intl/translations/en.ts                 | 2 +-
 packages/gitbook/src/intl/translations/es.ts                 | 2 +-
 packages/gitbook/src/intl/translations/fr.ts                 | 2 +-
 packages/gitbook/src/intl/translations/ja.ts                 | 2 +-
 packages/gitbook/src/intl/translations/nl.ts                 | 2 +-
 packages/gitbook/src/intl/translations/no.ts                 | 2 +-
 packages/gitbook/src/intl/translations/pt-br.ts              | 2 +-
 packages/gitbook/src/intl/translations/zh.ts                 | 2 +-
 12 files changed, 20 insertions(+), 11 deletions(-)
 create mode 100644 .changeset/sharp-hats-applaud.md

diff --git a/.changeset/sharp-hats-applaud.md b/.changeset/sharp-hats-applaud.md
new file mode 100644
index 0000000000..a3c5e8da9c
--- /dev/null
+++ b/.changeset/sharp-hats-applaud.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Fix missing title on button to close the announcement banner.
diff --git a/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx b/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx
index 522bde0ca3..2bdd61caa7 100644
--- a/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx
+++ b/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx
@@ -1,5 +1,6 @@
 'use client';
 
+import { tString, useLanguage } from '@/intl/client';
 import * as storage from '@/lib/local-storage';
 import type { ResolvedContentRef } from '@/lib/references';
 import { tcls } from '@/lib/tailwind';
@@ -18,6 +19,8 @@ export function AnnouncementBanner(props: {
 }) {
     const { announcement, contentRef } = props;
 
+    const language = useLanguage();
+
     const hasLink = announcement.link && contentRef?.href;
     const closeable = announcement.style !== 'danger';
 
@@ -81,6 +84,7 @@ export function AnnouncementBanner(props: {
                             className={`absolute top-0 right-4 mt-2 mr-2 rounded straight-corners:rounded-none p-1.5 transition-all hover:ring-1 sm:right-6 md:right-8 ${style.close}`}
                             type="button"
                             onClick={dismissAnnouncement}
+                            title={tString(language, 'close')}
                         >
                             <Icon icon="close" className="size-4" />
                         </button>
diff --git a/packages/gitbook/src/components/Cookies/CookiesToast.tsx b/packages/gitbook/src/components/Cookies/CookiesToast.tsx
index 13dc7605d0..8e1afc4c32 100644
--- a/packages/gitbook/src/components/Cookies/CookiesToast.tsx
+++ b/packages/gitbook/src/components/Cookies/CookiesToast.tsx
@@ -72,7 +72,7 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
             <button
                 type="button"
                 onClick={() => setShow(false)}
-                aria-label={tString(language, 'cookies_close')}
+                aria-label={tString(language, 'close')}
                 className={tcls(
                     'absolute',
                     'top-3',
@@ -85,7 +85,7 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
                     'rounded-sm',
                     'hover:bg-tint-hover'
                 )}
-                title={tString(language, 'cookies_close')}
+                title={tString(language, 'close')}
             >
                 <Icon icon="xmark" className={tcls('size-4')} />
             </button>
diff --git a/packages/gitbook/src/intl/translations/de.ts b/packages/gitbook/src/intl/translations/de.ts
index b42da6c55f..9ed91a152c 100644
--- a/packages/gitbook/src/intl/translations/de.ts
+++ b/packages/gitbook/src/intl/translations/de.ts
@@ -40,7 +40,7 @@ export const de = {
     cookies_prompt_privacy: 'Datenschutzrichtlinie',
     cookies_accept: 'Akzeptieren',
     cookies_reject: 'Ablehnen',
-    cookies_close: 'Schließen',
+    close: 'Schließen',
     edit_on_git: 'Bearbeiten auf ${1}',
     notfound_title: 'Seite nicht gefunden',
     notfound: 'Die gesuchte Seite existiert nicht.',
diff --git a/packages/gitbook/src/intl/translations/en.ts b/packages/gitbook/src/intl/translations/en.ts
index df9cc50993..6fa837ebb8 100644
--- a/packages/gitbook/src/intl/translations/en.ts
+++ b/packages/gitbook/src/intl/translations/en.ts
@@ -40,7 +40,7 @@ export const en = {
     cookies_prompt_privacy: 'privacy policy',
     cookies_accept: 'Accept',
     cookies_reject: 'Reject',
-    cookies_close: 'Close',
+    close: 'Close',
     edit_on_git: 'Edit on ${1}',
     notfound_title: 'Page not found',
     notfound: "The page you are looking for doesn't exist.",
diff --git a/packages/gitbook/src/intl/translations/es.ts b/packages/gitbook/src/intl/translations/es.ts
index 2d0d465652..d2ca1bb9d5 100644
--- a/packages/gitbook/src/intl/translations/es.ts
+++ b/packages/gitbook/src/intl/translations/es.ts
@@ -42,7 +42,7 @@ export const es: TranslationLanguage = {
     cookies_prompt_privacy: 'política de privacidad',
     cookies_accept: 'Aceptar',
     cookies_reject: 'Rechazar',
-    cookies_close: 'Cerrar',
+    close: 'Cerrar',
     edit_on_git: 'Editar en ${1}',
     notfound_title: 'Página no encontrada',
     notfound: 'La página que buscas no existe.',
diff --git a/packages/gitbook/src/intl/translations/fr.ts b/packages/gitbook/src/intl/translations/fr.ts
index 71d340332b..922f7554d0 100644
--- a/packages/gitbook/src/intl/translations/fr.ts
+++ b/packages/gitbook/src/intl/translations/fr.ts
@@ -42,7 +42,7 @@ export const fr: TranslationLanguage = {
     cookies_prompt_privacy: 'politique de confidentialité',
     cookies_accept: 'Accepter',
     cookies_reject: 'Rejeter',
-    cookies_close: 'Fermer',
+    close: 'Fermer',
     edit_on_git: 'Modifier sur ${1}',
     notfound_title: 'Page non trouvée',
     notfound: "La page que vous cherchez n'existe pas.",
diff --git a/packages/gitbook/src/intl/translations/ja.ts b/packages/gitbook/src/intl/translations/ja.ts
index f3f5480689..6eaa789ed8 100644
--- a/packages/gitbook/src/intl/translations/ja.ts
+++ b/packages/gitbook/src/intl/translations/ja.ts
@@ -42,7 +42,7 @@ export const ja: TranslationLanguage = {
     cookies_prompt_privacy: 'プライバシーポリシー',
     cookies_accept: '同意する',
     cookies_reject: '拒否する',
-    cookies_close: '閉じる',
+    close: '閉じる',
     edit_on_git: '${1}で編集',
     notfound_title: 'ページが見つかりません',
     notfound: 'お探しのページは存在しません。',
diff --git a/packages/gitbook/src/intl/translations/nl.ts b/packages/gitbook/src/intl/translations/nl.ts
index 6bfb45e9a6..d61bfb71d9 100644
--- a/packages/gitbook/src/intl/translations/nl.ts
+++ b/packages/gitbook/src/intl/translations/nl.ts
@@ -42,7 +42,7 @@ export const nl: TranslationLanguage = {
     cookies_prompt_privacy: 'privacyverklaring',
     cookies_accept: 'Accepteren',
     cookies_reject: 'Weigeren',
-    cookies_close: 'Sluiten',
+    close: 'Sluiten',
     edit_on_git: 'Bewerk op ${1}',
     notfound_title: 'Pagina niet gevonden',
     notfound: 'De pagina die je zoekt, bestaat niet.',
diff --git a/packages/gitbook/src/intl/translations/no.ts b/packages/gitbook/src/intl/translations/no.ts
index 6be6b413e3..ddd94e5466 100644
--- a/packages/gitbook/src/intl/translations/no.ts
+++ b/packages/gitbook/src/intl/translations/no.ts
@@ -42,7 +42,7 @@ export const no: TranslationLanguage = {
     cookies_prompt_privacy: 'personvernerklæringen',
     cookies_accept: 'Godta',
     cookies_reject: 'Avslå',
-    cookies_close: 'Lukk',
+    close: 'Lukk',
     edit_on_git: 'Rediger på ${1}',
     notfound_title: 'Siden ble ikke funnet',
     notfound: 'Siden du leter etter eksisterer ikke.',
diff --git a/packages/gitbook/src/intl/translations/pt-br.ts b/packages/gitbook/src/intl/translations/pt-br.ts
index 35a40cd45f..c3e2244785 100644
--- a/packages/gitbook/src/intl/translations/pt-br.ts
+++ b/packages/gitbook/src/intl/translations/pt-br.ts
@@ -40,7 +40,7 @@ export const pt_br = {
     cookies_prompt_privacy: 'política de privacidade',
     cookies_accept: 'Aceitar',
     cookies_reject: 'Rejeitar',
-    cookies_close: 'Fechar',
+    close: 'Fechar',
     edit_on_git: 'Editar no ${1}',
     notfound_title: 'Página não encontrada',
     notfound: 'A página que você está procurando não existe.',
diff --git a/packages/gitbook/src/intl/translations/zh.ts b/packages/gitbook/src/intl/translations/zh.ts
index 08efdddde6..096b0d8cb3 100644
--- a/packages/gitbook/src/intl/translations/zh.ts
+++ b/packages/gitbook/src/intl/translations/zh.ts
@@ -40,7 +40,7 @@ export const zh: TranslationLanguage = {
     cookies_prompt_privacy: '隐私政策',
     cookies_accept: '接受',
     cookies_reject: '拒绝',
-    cookies_close: '关闭',
+    close: '关闭',
     edit_on_git: '在${1}上编辑',
     notfound_title: '页面未找到',
     notfound: '您要找的页面不存在。',

From 521052d84f0091091bdba97ab7853160e14d2fcb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Thu, 5 Jun 2025 20:43:15 +0100
Subject: [PATCH 076/127] Remove withoutConcurrentExecution and withCacheKey
 (#3281)

---
 .changeset/nervous-students-judge.md          |    6 +
 packages/gitbook-v2/src/lib/data/api.ts       | 1049 ++++++++---------
 .../gitbook-v2/src/lib/data/cloudflare.ts     |   12 -
 .../gitbook-v2/src/lib/data/memoize.test.ts   |   52 -
 packages/gitbook-v2/src/lib/data/memoize.ts   |   87 --
 packages/gitbook/src/lib/openapi/fetch.ts     |   10 +-
 6 files changed, 483 insertions(+), 733 deletions(-)
 create mode 100644 .changeset/nervous-students-judge.md
 delete mode 100644 packages/gitbook-v2/src/lib/data/memoize.test.ts
 delete mode 100644 packages/gitbook-v2/src/lib/data/memoize.ts

diff --git a/.changeset/nervous-students-judge.md b/.changeset/nervous-students-judge.md
new file mode 100644
index 0000000000..a24d325b74
--- /dev/null
+++ b/.changeset/nervous-students-judge.md
@@ -0,0 +1,6 @@
+---
+"gitbook-v2": patch
+"gitbook": patch
+---
+
+Fix concurrent execution in Vercel causing pages to not be attached to the proper tags.
diff --git a/packages/gitbook-v2/src/lib/data/api.ts b/packages/gitbook-v2/src/lib/data/api.ts
index f8ae6d2711..44313bfc57 100644
--- a/packages/gitbook-v2/src/lib/data/api.ts
+++ b/packages/gitbook-v2/src/lib/data/api.ts
@@ -8,9 +8,7 @@ import {
 import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-tags';
 import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env';
 import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache';
-import { getCloudflareRequestGlobal } from './cloudflare';
 import { DataFetcherError, wrapDataFetcherError } from './errors';
-import { withCacheKey, withoutConcurrentExecution } from './memoize';
 import type { GitBookDataFetcher } from './types';
 
 interface DataFetcherInput {
@@ -196,613 +194,518 @@ export function createDataFetcher(
     };
 }
 
-const getUserById = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (_, input: DataFetcherInput, params: { userId: string }) => {
-            'use cache';
-            return trace(`getUserById(${params.userId})`, async () => {
-                return wrapDataFetcherError(async () => {
-                    const api = apiClient(input);
-                    const res = await api.users.getUserById(params.userId, {
-                        ...noCacheFetchOptions,
-                    });
-                    cacheTag(...getCacheTagsFromResponse(res));
-                    cacheLife('days');
-                    return res.data;
-                });
+const getUserById = async (input: DataFetcherInput, params: { userId: string }) => {
+    'use cache';
+    return trace(`getUserById(${params.userId})`, async () => {
+        return wrapDataFetcherError(async () => {
+            const api = apiClient(input);
+            const res = await api.users.getUserById(params.userId, {
+                ...noCacheFetchOptions,
             });
-        }
-    )
-);
-
-const getSpace = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (
-            _,
-            input: DataFetcherInput,
-            params: { spaceId: string; shareKey: string | undefined }
-        ) => {
-            'use cache';
-            cacheTag(
-                getCacheTag({
-                    tag: 'space',
-                    space: params.spaceId,
-                })
-            );
+            cacheTag(...getCacheTagsFromResponse(res));
+            cacheLife('days');
+            return res.data;
+        });
+    });
+};
 
-            return trace(`getSpace(${params.spaceId}, ${params.shareKey})`, async () => {
-                return wrapDataFetcherError(async () => {
-                    const api = apiClient(input);
-                    const res = await api.spaces.getSpaceById(
-                        params.spaceId,
-                        {
-                            shareKey: params.shareKey,
-                        },
-                        {
-                            ...noCacheFetchOptions,
-                        }
-                    );
-                    cacheTag(...getCacheTagsFromResponse(res));
-                    cacheLife('days');
-                    return res.data;
-                });
-            });
-        }
-    )
-);
-
-const getChangeRequest = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (
-            _,
-            input: DataFetcherInput,
-            params: { spaceId: string; changeRequestId: string }
-        ) => {
-            'use cache';
-            cacheTag(
-                getCacheTag({
-                    tag: 'change-request',
-                    space: params.spaceId,
-                    changeRequest: params.changeRequestId,
-                })
-            );
+const getSpace = async (
+    input: DataFetcherInput,
+    params: { spaceId: string; shareKey: string | undefined }
+) => {
+    'use cache';
+    cacheTag(
+        getCacheTag({
+            tag: 'space',
+            space: params.spaceId,
+        })
+    );
 
-            return trace(
-                `getChangeRequest(${params.spaceId}, ${params.changeRequestId})`,
-                async () => {
-                    return wrapDataFetcherError(async () => {
-                        const api = apiClient(input);
-                        const res = await api.spaces.getChangeRequestById(
-                            params.spaceId,
-                            params.changeRequestId,
-                            {
-                                ...noCacheFetchOptions,
-                            }
-                        );
-                        cacheTag(...getCacheTagsFromResponse(res));
-                        cacheLife('minutes');
-                        return res.data;
-                    });
+    return trace(`getSpace(${params.spaceId}, ${params.shareKey})`, async () => {
+        return wrapDataFetcherError(async () => {
+            const api = apiClient(input);
+            const res = await api.spaces.getSpaceById(
+                params.spaceId,
+                {
+                    shareKey: params.shareKey,
+                },
+                {
+                    ...noCacheFetchOptions,
                 }
             );
-        }
-    )
-);
-
-const getRevision = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (
-            _,
-            input: DataFetcherInput,
-            params: { spaceId: string; revisionId: string; metadata: boolean }
-        ) => {
-            'use cache';
-            return trace(`getRevision(${params.spaceId}, ${params.revisionId})`, async () => {
-                return wrapDataFetcherError(async () => {
-                    const api = apiClient(input);
-                    const res = await api.spaces.getRevisionById(
-                        params.spaceId,
-                        params.revisionId,
-                        {
-                            metadata: params.metadata,
-                        },
-                        {
-                            ...noCacheFetchOptions,
-                        }
-                    );
-                    cacheTag(...getCacheTagsFromResponse(res));
-                    cacheLife('max');
-                    return res.data;
-                });
-            });
-        }
-    )
-);
-
-const getRevisionPages = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (
-            _,
-            input: DataFetcherInput,
-            params: { spaceId: string; revisionId: string; metadata: boolean }
-        ) => {
-            'use cache';
-            return trace(`getRevisionPages(${params.spaceId}, ${params.revisionId})`, async () => {
-                return wrapDataFetcherError(async () => {
-                    const api = apiClient(input);
-                    const res = await api.spaces.listPagesInRevisionById(
-                        params.spaceId,
-                        params.revisionId,
-                        {
-                            metadata: params.metadata,
-                        },
-                        {
-                            ...noCacheFetchOptions,
-                        }
-                    );
-                    cacheTag(...getCacheTagsFromResponse(res));
-                    cacheLife('max');
-                    return res.data.pages;
-                });
-            });
-        }
-    )
-);
-
-const getRevisionFile = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (
-            _,
-            input: DataFetcherInput,
-            params: { spaceId: string; revisionId: string; fileId: string }
-        ) => {
-            'use cache';
-            return trace(
-                `getRevisionFile(${params.spaceId}, ${params.revisionId}, ${params.fileId})`,
-                async () => {
-                    return wrapDataFetcherError(async () => {
-                        const api = apiClient(input);
-                        const res = await api.spaces.getFileInRevisionById(
-                            params.spaceId,
-                            params.revisionId,
-                            params.fileId,
-                            {},
-                            {
-                                ...noCacheFetchOptions,
-                            }
-                        );
-                        cacheTag(...getCacheTagsFromResponse(res));
-                        cacheLife('max');
-                        return res.data;
-                    });
+            cacheTag(...getCacheTagsFromResponse(res));
+            cacheLife('days');
+            return res.data;
+        });
+    });
+};
+
+const getChangeRequest = async (
+    input: DataFetcherInput,
+    params: { spaceId: string; changeRequestId: string }
+) => {
+    'use cache';
+    cacheTag(
+        getCacheTag({
+            tag: 'change-request',
+            space: params.spaceId,
+            changeRequest: params.changeRequestId,
+        })
+    );
+
+    return trace(`getChangeRequest(${params.spaceId}, ${params.changeRequestId})`, async () => {
+        return wrapDataFetcherError(async () => {
+            const api = apiClient(input);
+            const res = await api.spaces.getChangeRequestById(
+                params.spaceId,
+                params.changeRequestId,
+                {
+                    ...noCacheFetchOptions,
                 }
             );
-        }
-    )
-);
-
-const getRevisionPageMarkdown = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (
-            _,
-            input: DataFetcherInput,
-            params: { spaceId: string; revisionId: string; pageId: string }
-        ) => {
-            'use cache';
-            return trace(
-                `getRevisionPageMarkdown(${params.spaceId}, ${params.revisionId}, ${params.pageId})`,
-                async () => {
-                    return wrapDataFetcherError(async () => {
-                        const api = apiClient(input);
-                        const res = await api.spaces.getPageInRevisionById(
-                            params.spaceId,
-                            params.revisionId,
-                            params.pageId,
-                            {
-                                format: 'markdown',
-                            },
-                            {
-                                ...noCacheFetchOptions,
-                            }
-                        );
-
-                        cacheTag(...getCacheTagsFromResponse(res));
-                        cacheLife('max');
-
-                        if (!('markdown' in res.data)) {
-                            throw new DataFetcherError('Page is not a document', 404);
-                        }
-                        return res.data.markdown;
-                    });
+            cacheTag(...getCacheTagsFromResponse(res));
+            cacheLife('minutes');
+            return res.data;
+        });
+    });
+};
+
+const getRevision = async (
+    input: DataFetcherInput,
+    params: { spaceId: string; revisionId: string; metadata: boolean }
+) => {
+    'use cache';
+    return trace(`getRevision(${params.spaceId}, ${params.revisionId})`, async () => {
+        return wrapDataFetcherError(async () => {
+            const api = apiClient(input);
+            const res = await api.spaces.getRevisionById(
+                params.spaceId,
+                params.revisionId,
+                {
+                    metadata: params.metadata,
+                },
+                {
+                    ...noCacheFetchOptions,
                 }
             );
-        }
-    )
-);
-
-const getRevisionPageByPath = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (
-            _,
-            input: DataFetcherInput,
-            params: { spaceId: string; revisionId: string; path: string }
-        ) => {
-            'use cache';
-            return trace(
-                `getRevisionPageByPath(${params.spaceId}, ${params.revisionId}, ${params.path})`,
-                async () => {
-                    const encodedPath = encodeURIComponent(params.path);
-                    return wrapDataFetcherError(async () => {
-                        const api = apiClient(input);
-                        const res = await api.spaces.getPageInRevisionByPath(
-                            params.spaceId,
-                            params.revisionId,
-                            encodedPath,
-                            {},
-                            {
-                                ...noCacheFetchOptions,
-                            }
-                        );
-                        cacheTag(...getCacheTagsFromResponse(res));
-                        cacheLife('max');
-                        return res.data;
-                    });
+            cacheTag(...getCacheTagsFromResponse(res));
+            cacheLife('max');
+            return res.data;
+        });
+    });
+};
+
+const getRevisionPages = async (
+    input: DataFetcherInput,
+    params: { spaceId: string; revisionId: string; metadata: boolean }
+) => {
+    'use cache';
+    return trace(`getRevisionPages(${params.spaceId}, ${params.revisionId})`, async () => {
+        return wrapDataFetcherError(async () => {
+            const api = apiClient(input);
+            const res = await api.spaces.listPagesInRevisionById(
+                params.spaceId,
+                params.revisionId,
+                {
+                    metadata: params.metadata,
+                },
+                {
+                    ...noCacheFetchOptions,
                 }
             );
-        }
-    )
-);
-
-const getDocument = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (_, input: DataFetcherInput, params: { spaceId: string; documentId: string }) => {
-            'use cache';
-            return trace(`getDocument(${params.spaceId}, ${params.documentId})`, async () => {
-                return wrapDataFetcherError(async () => {
-                    const api = apiClient(input);
-                    const res = await api.spaces.getDocumentById(
-                        params.spaceId,
-                        params.documentId,
-                        {},
-                        {
-                            ...noCacheFetchOptions,
-                        }
-                    );
-                    cacheTag(...getCacheTagsFromResponse(res));
-                    cacheLife('max');
-                    return res.data;
-                });
+            cacheTag(...getCacheTagsFromResponse(res));
+            cacheLife('max');
+            return res.data.pages;
+        });
+    });
+};
+
+const getRevisionFile = async (
+    input: DataFetcherInput,
+    params: { spaceId: string; revisionId: string; fileId: string }
+) => {
+    'use cache';
+    return trace(
+        `getRevisionFile(${params.spaceId}, ${params.revisionId}, ${params.fileId})`,
+        async () => {
+            return wrapDataFetcherError(async () => {
+                const api = apiClient(input);
+                const res = await api.spaces.getFileInRevisionById(
+                    params.spaceId,
+                    params.revisionId,
+                    params.fileId,
+                    {},
+                    {
+                        ...noCacheFetchOptions,
+                    }
+                );
+                cacheTag(...getCacheTagsFromResponse(res));
+                cacheLife('max');
+                return res.data;
             });
         }
-    )
-);
-
-const getComputedDocument = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (
-            _,
-            input: DataFetcherInput,
-            params: {
-                spaceId: string;
-                organizationId: string;
-                source: ComputedContentSource;
-                seed: string;
-            }
-        ) => {
-            'use cache';
-            cacheTag(
-                ...getComputedContentSourceCacheTags(
+    );
+};
+
+const getRevisionPageMarkdown = async (
+    input: DataFetcherInput,
+    params: { spaceId: string; revisionId: string; pageId: string }
+) => {
+    'use cache';
+    return trace(
+        `getRevisionPageMarkdown(${params.spaceId}, ${params.revisionId}, ${params.pageId})`,
+        async () => {
+            return wrapDataFetcherError(async () => {
+                const api = apiClient(input);
+                const res = await api.spaces.getPageInRevisionById(
+                    params.spaceId,
+                    params.revisionId,
+                    params.pageId,
                     {
-                        spaceId: params.spaceId,
-                        organizationId: params.organizationId,
+                        format: 'markdown',
                     },
-                    params.source
-                )
-            );
+                    {
+                        ...noCacheFetchOptions,
+                    }
+                );
+
+                cacheTag(...getCacheTagsFromResponse(res));
+                cacheLife('max');
 
-            return trace(
-                `getComputedDocument(${params.spaceId}, ${params.organizationId}, ${params.source.type}, ${params.seed})`,
-                async () => {
-                    return wrapDataFetcherError(async () => {
-                        const api = apiClient(input);
-                        const res = await api.spaces.getComputedDocument(
-                            params.spaceId,
-                            {
-                                source: params.source,
-                                seed: params.seed,
-                            },
-                            {},
-                            {
-                                ...noCacheFetchOptions,
-                            }
-                        );
-                        cacheTag(...getCacheTagsFromResponse(res));
-                        cacheLife('max');
-                        return res.data;
-                    });
+                if (!('markdown' in res.data)) {
+                    throw new DataFetcherError('Page is not a document', 404);
                 }
-            );
+                return res.data.markdown;
+            });
         }
-    )
-);
-
-const getReusableContent = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (
-            _,
-            input: DataFetcherInput,
-            params: { spaceId: string; revisionId: string; reusableContentId: string }
-        ) => {
-            'use cache';
-            return trace(
-                `getReusableContent(${params.spaceId}, ${params.revisionId}, ${params.reusableContentId})`,
-                async () => {
-                    return wrapDataFetcherError(async () => {
-                        const api = apiClient(input);
-                        const res = await api.spaces.getReusableContentInRevisionById(
-                            params.spaceId,
-                            params.revisionId,
-                            params.reusableContentId,
-                            {},
-                            {
-                                ...noCacheFetchOptions,
-                            }
-                        );
-                        cacheTag(...getCacheTagsFromResponse(res));
-                        cacheLife('max');
-                        return res.data;
-                    });
-                }
-            );
+    );
+};
+
+const getRevisionPageByPath = async (
+    input: DataFetcherInput,
+    params: { spaceId: string; revisionId: string; path: string }
+) => {
+    'use cache';
+    return trace(
+        `getRevisionPageByPath(${params.spaceId}, ${params.revisionId}, ${params.path})`,
+        async () => {
+            const encodedPath = encodeURIComponent(params.path);
+            return wrapDataFetcherError(async () => {
+                const api = apiClient(input);
+                const res = await api.spaces.getPageInRevisionByPath(
+                    params.spaceId,
+                    params.revisionId,
+                    encodedPath,
+                    {},
+                    {
+                        ...noCacheFetchOptions,
+                    }
+                );
+                cacheTag(...getCacheTagsFromResponse(res));
+                cacheLife('max');
+                return res.data;
+            });
         }
-    )
-);
-
-const getLatestOpenAPISpecVersionContent = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (_, input: DataFetcherInput, params: { organizationId: string; slug: string }) => {
-            'use cache';
-            cacheTag(
-                getCacheTag({
-                    tag: 'openapi',
-                    organization: params.organizationId,
-                    openAPISpec: params.slug,
-                })
-            );
+    );
+};
 
-            return trace(
-                `getLatestOpenAPISpecVersionContent(${params.organizationId}, ${params.slug})`,
-                async () => {
-                    return wrapDataFetcherError(async () => {
-                        const api = apiClient(input);
-                        const res = await api.orgs.getLatestOpenApiSpecVersionContent(
-                            params.organizationId,
-                            params.slug,
-                            {
-                                ...noCacheFetchOptions,
-                            }
-                        );
-                        cacheTag(...getCacheTagsFromResponse(res));
-                        cacheLife('max');
-                        return res.data;
-                    });
+const getDocument = async (
+    input: DataFetcherInput,
+    params: { spaceId: string; documentId: string }
+) => {
+    'use cache';
+    return trace(`getDocument(${params.spaceId}, ${params.documentId})`, async () => {
+        return wrapDataFetcherError(async () => {
+            const api = apiClient(input);
+            const res = await api.spaces.getDocumentById(
+                params.spaceId,
+                params.documentId,
+                {},
+                {
+                    ...noCacheFetchOptions,
                 }
             );
+            cacheTag(...getCacheTagsFromResponse(res));
+            cacheLife('max');
+            return res.data;
+        });
+    });
+};
+
+const getComputedDocument = async (
+    input: DataFetcherInput,
+    params: {
+        spaceId: string;
+        organizationId: string;
+        source: ComputedContentSource;
+        seed: string;
+    }
+) => {
+    'use cache';
+    cacheTag(
+        ...getComputedContentSourceCacheTags(
+            {
+                spaceId: params.spaceId,
+                organizationId: params.organizationId,
+            },
+            params.source
+        )
+    );
+
+    return trace(
+        `getComputedDocument(${params.spaceId}, ${params.organizationId}, ${params.source.type}, ${params.seed})`,
+        async () => {
+            return wrapDataFetcherError(async () => {
+                const api = apiClient(input);
+                const res = await api.spaces.getComputedDocument(
+                    params.spaceId,
+                    {
+                        source: params.source,
+                        seed: params.seed,
+                    },
+                    {},
+                    {
+                        ...noCacheFetchOptions,
+                    }
+                );
+                cacheTag(...getCacheTagsFromResponse(res));
+                cacheLife('max');
+                return res.data;
+            });
         }
-    )
-);
-
-const getPublishedContentSite = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (
-            _,
-            input: DataFetcherInput,
-            params: { organizationId: string; siteId: string; siteShareKey: string | undefined }
-        ) => {
-            'use cache';
-            cacheTag(
-                getCacheTag({
-                    tag: 'site',
-                    site: params.siteId,
-                })
-            );
+    );
+};
 
-            return trace(
-                `getPublishedContentSite(${params.organizationId}, ${params.siteId}, ${params.siteShareKey})`,
-                async () => {
-                    return wrapDataFetcherError(async () => {
-                        const api = apiClient(input);
-                        const res = await api.orgs.getPublishedContentSite(
-                            params.organizationId,
-                            params.siteId,
-                            {
-                                shareKey: params.siteShareKey,
-                            },
-                            {
-                                ...noCacheFetchOptions,
-                            }
-                        );
-                        cacheTag(...getCacheTagsFromResponse(res));
-                        cacheLife('days');
-                        return res.data;
-                    });
-                }
-            );
+const getReusableContent = async (
+    input: DataFetcherInput,
+    params: { spaceId: string; revisionId: string; reusableContentId: string }
+) => {
+    'use cache';
+    return trace(
+        `getReusableContent(${params.spaceId}, ${params.revisionId}, ${params.reusableContentId})`,
+        async () => {
+            return wrapDataFetcherError(async () => {
+                const api = apiClient(input);
+                const res = await api.spaces.getReusableContentInRevisionById(
+                    params.spaceId,
+                    params.revisionId,
+                    params.reusableContentId,
+                    {},
+                    {
+                        ...noCacheFetchOptions,
+                    }
+                );
+                cacheTag(...getCacheTagsFromResponse(res));
+                cacheLife('max');
+                return res.data;
+            });
         }
-    )
-);
-
-const getSiteRedirectBySource = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (
-            _,
-            input: DataFetcherInput,
-            params: {
-                organizationId: string;
-                siteId: string;
-                siteShareKey: string | undefined;
-                source: string;
-            }
-        ) => {
-            'use cache';
-            cacheTag(
-                getCacheTag({
-                    tag: 'site',
-                    site: params.siteId,
-                })
-            );
+    );
+};
 
-            return trace(
-                `getSiteRedirectBySource(${params.organizationId}, ${params.siteId}, ${params.siteShareKey}, ${params.source})`,
-                async () => {
-                    return wrapDataFetcherError(async () => {
-                        const api = apiClient(input);
-                        const res = await api.orgs.getSiteRedirectBySource(
-                            params.organizationId,
-                            params.siteId,
-                            {
-                                shareKey: params.siteShareKey,
-                                source: params.source,
-                            },
-                            {
-                                ...noCacheFetchOptions,
-                            }
-                        );
-                        cacheTag(...getCacheTagsFromResponse(res));
-                        cacheLife('days');
-                        return res.data;
-                    });
-                }
-            );
+const getLatestOpenAPISpecVersionContent = async (
+    input: DataFetcherInput,
+    params: { organizationId: string; slug: string }
+) => {
+    'use cache';
+    cacheTag(
+        getCacheTag({
+            tag: 'openapi',
+            organization: params.organizationId,
+            openAPISpec: params.slug,
+        })
+    );
+
+    return trace(
+        `getLatestOpenAPISpecVersionContent(${params.organizationId}, ${params.slug})`,
+        async () => {
+            return wrapDataFetcherError(async () => {
+                const api = apiClient(input);
+                const res = await api.orgs.getLatestOpenApiSpecVersionContent(
+                    params.organizationId,
+                    params.slug,
+                    {
+                        ...noCacheFetchOptions,
+                    }
+                );
+                cacheTag(...getCacheTagsFromResponse(res));
+                cacheLife('max');
+                return res.data;
+            });
         }
-    )
-);
-
-const getEmbedByUrl = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (_, input: DataFetcherInput, params: { spaceId: string; url: string }) => {
-            'use cache';
-            cacheTag(
-                getCacheTag({
-                    tag: 'space',
-                    space: params.spaceId,
-                })
-            );
+    );
+};
 
-            return trace(`getEmbedByUrl(${params.spaceId}, ${params.url})`, async () => {
-                return wrapDataFetcherError(async () => {
-                    const api = apiClient(input);
-                    const res = await api.spaces.getEmbedByUrlInSpace(
-                        params.spaceId,
-                        {
-                            url: params.url,
-                        },
-                        {
-                            ...noCacheFetchOptions,
-                        }
-                    );
-                    cacheTag(...getCacheTagsFromResponse(res));
-                    cacheLife('weeks');
-                    return res.data;
-                });
+const getPublishedContentSite = async (
+    input: DataFetcherInput,
+    params: { organizationId: string; siteId: string; siteShareKey: string | undefined }
+) => {
+    'use cache';
+    cacheTag(
+        getCacheTag({
+            tag: 'site',
+            site: params.siteId,
+        })
+    );
+
+    return trace(
+        `getPublishedContentSite(${params.organizationId}, ${params.siteId}, ${params.siteShareKey})`,
+        async () => {
+            return wrapDataFetcherError(async () => {
+                const api = apiClient(input);
+                const res = await api.orgs.getPublishedContentSite(
+                    params.organizationId,
+                    params.siteId,
+                    {
+                        shareKey: params.siteShareKey,
+                    },
+                    {
+                        ...noCacheFetchOptions,
+                    }
+                );
+                cacheTag(...getCacheTagsFromResponse(res));
+                cacheLife('days');
+                return res.data;
             });
         }
-    )
-);
-
-const searchSiteContent = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (
-            _,
-            input: DataFetcherInput,
-            params: Parameters<GitBookDataFetcher['searchSiteContent']>[0]
-        ) => {
-            'use cache';
-            cacheTag(
-                getCacheTag({
-                    tag: 'site',
-                    site: params.siteId,
-                })
-            );
+    );
+};
 
-            return trace(
-                `searchSiteContent(${params.organizationId}, ${params.siteId}, ${params.query})`,
-                async () => {
-                    return wrapDataFetcherError(async () => {
-                        const { organizationId, siteId, query, scope } = params;
-                        const api = apiClient(input);
-                        const res = await api.orgs.searchSiteContent(
-                            organizationId,
-                            siteId,
-                            {
-                                query,
-                                ...scope,
-                            },
-                            {},
-                            {
-                                ...noCacheFetchOptions,
-                            }
-                        );
-                        cacheTag(...getCacheTagsFromResponse(res));
-                        cacheLife('hours');
-                        return res.data.items;
-                    });
-                }
-            );
+const getSiteRedirectBySource = async (
+    input: DataFetcherInput,
+    params: {
+        organizationId: string;
+        siteId: string;
+        siteShareKey: string | undefined;
+        source: string;
+    }
+) => {
+    'use cache';
+    cacheTag(
+        getCacheTag({
+            tag: 'site',
+            site: params.siteId,
+        })
+    );
+
+    return trace(
+        `getSiteRedirectBySource(${params.organizationId}, ${params.siteId}, ${params.siteShareKey}, ${params.source})`,
+        async () => {
+            return wrapDataFetcherError(async () => {
+                const api = apiClient(input);
+                const res = await api.orgs.getSiteRedirectBySource(
+                    params.organizationId,
+                    params.siteId,
+                    {
+                        shareKey: params.siteShareKey,
+                        source: params.source,
+                    },
+                    {
+                        ...noCacheFetchOptions,
+                    }
+                );
+                cacheTag(...getCacheTagsFromResponse(res));
+                cacheLife('days');
+                return res.data;
+            });
         }
-    )
-);
-
-const renderIntegrationUi = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (
-            _,
-            input: DataFetcherInput,
-            params: { integrationName: string; request: RenderIntegrationUI }
-        ) => {
-            'use cache';
-            cacheTag(
-                getCacheTag({
-                    tag: 'integration',
-                    integration: params.integrationName,
-                })
+    );
+};
+
+const getEmbedByUrl = async (input: DataFetcherInput, params: { spaceId: string; url: string }) => {
+    'use cache';
+    cacheTag(
+        getCacheTag({
+            tag: 'space',
+            space: params.spaceId,
+        })
+    );
+
+    return trace(`getEmbedByUrl(${params.spaceId}, ${params.url})`, async () => {
+        return wrapDataFetcherError(async () => {
+            const api = apiClient(input);
+            const res = await api.spaces.getEmbedByUrlInSpace(
+                params.spaceId,
+                {
+                    url: params.url,
+                },
+                {
+                    ...noCacheFetchOptions,
+                }
             );
+            cacheTag(...getCacheTagsFromResponse(res));
+            cacheLife('weeks');
+            return res.data;
+        });
+    });
+};
+
+const searchSiteContent = async (
+    input: DataFetcherInput,
+    params: Parameters<GitBookDataFetcher['searchSiteContent']>[0]
+) => {
+    'use cache';
+    cacheTag(
+        getCacheTag({
+            tag: 'site',
+            site: params.siteId,
+        })
+    );
 
-            return trace(`renderIntegrationUi(${params.integrationName})`, async () => {
-                return wrapDataFetcherError(async () => {
-                    const api = apiClient(input);
-                    const res = await api.integrations.renderIntegrationUiWithPost(
-                        params.integrationName,
-                        params.request,
-                        {
-                            ...noCacheFetchOptions,
-                        }
-                    );
-                    cacheTag(...getCacheTagsFromResponse(res));
-                    cacheLife('days');
-                    return res.data;
-                });
+    return trace(
+        `searchSiteContent(${params.organizationId}, ${params.siteId}, ${params.query})`,
+        async () => {
+            return wrapDataFetcherError(async () => {
+                const { organizationId, siteId, query, scope } = params;
+                const api = apiClient(input);
+                const res = await api.orgs.searchSiteContent(
+                    organizationId,
+                    siteId,
+                    {
+                        query,
+                        ...scope,
+                    },
+                    {},
+                    {
+                        ...noCacheFetchOptions,
+                    }
+                );
+                cacheTag(...getCacheTagsFromResponse(res));
+                cacheLife('hours');
+                return res.data.items;
             });
         }
-    )
-);
+    );
+};
+
+const renderIntegrationUi = async (
+    input: DataFetcherInput,
+    params: { integrationName: string; request: RenderIntegrationUI }
+) => {
+    'use cache';
+    cacheTag(
+        getCacheTag({
+            tag: 'integration',
+            integration: params.integrationName,
+        })
+    );
+
+    return trace(`renderIntegrationUi(${params.integrationName})`, async () => {
+        return wrapDataFetcherError(async () => {
+            const api = apiClient(input);
+            const res = await api.integrations.renderIntegrationUiWithPost(
+                params.integrationName,
+                params.request,
+                {
+                    ...noCacheFetchOptions,
+                }
+            );
+            cacheTag(...getCacheTagsFromResponse(res));
+            cacheLife('days');
+            return res.data;
+        });
+    });
+};
 
 async function* streamAIResponse(
     input: DataFetcherInput,
diff --git a/packages/gitbook-v2/src/lib/data/cloudflare.ts b/packages/gitbook-v2/src/lib/data/cloudflare.ts
index ac3125603d..ca996690b6 100644
--- a/packages/gitbook-v2/src/lib/data/cloudflare.ts
+++ b/packages/gitbook-v2/src/lib/data/cloudflare.ts
@@ -11,15 +11,3 @@ export function getCloudflareContext() {
 
     return getCloudflareContextOpenNext();
 }
-
-/**
- * Return an object representing the current request.
- */
-export function getCloudflareRequestGlobal() {
-    const context = getCloudflareContext();
-    if (!context) {
-        return null;
-    }
-
-    return context.cf;
-}
diff --git a/packages/gitbook-v2/src/lib/data/memoize.test.ts b/packages/gitbook-v2/src/lib/data/memoize.test.ts
deleted file mode 100644
index 24c1014087..0000000000
--- a/packages/gitbook-v2/src/lib/data/memoize.test.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { describe, expect, it, mock } from 'bun:test';
-import { AsyncLocalStorage } from 'node:async_hooks';
-import { withCacheKey, withoutConcurrentExecution } from './memoize';
-
-describe('withoutConcurrentExecution', () => {
-    it('should memoize the function based on the cache key', async () => {
-        const fn = mock(async (_cacheKey: string, a: number, b: number) => a + b);
-        const memoized = withoutConcurrentExecution(() => null, fn);
-
-        const p1 = memoized('c1', 1, 2);
-        const p2 = memoized('c1', 1, 2);
-        const p3 = memoized('c3', 2, 3);
-
-        expect(await p1).toBe(await p2);
-        expect(await p1).not.toBe(await p3);
-        expect(fn.mock.calls.length).toBe(2);
-    });
-
-    it('should support caching per request', async () => {
-        const fn = mock(async () => Math.random());
-
-        const request1 = { id: 'request1' };
-        const request2 = { id: 'request2' };
-
-        const requestContext = new AsyncLocalStorage<{ id: string }>();
-
-        const memoized = withoutConcurrentExecution(() => requestContext.getStore(), fn);
-
-        // Both in the same request
-        const promise1 = requestContext.run(request1, () => memoized('c1'));
-        const promise2 = requestContext.run(request1, () => memoized('c1'));
-
-        // In a different request
-        const promise3 = requestContext.run(request2, () => memoized('c1'));
-
-        expect(await promise1).toBe(await promise2);
-        expect(await promise1).not.toBe(await promise3);
-        expect(fn.mock.calls.length).toBe(2);
-    });
-});
-
-describe('withCacheKey', () => {
-    it('should wrap the function by passing the cache key', async () => {
-        const fn = mock(
-            async (cacheKey: string, arg: { a: number; b: number }, c: number) =>
-                `${cacheKey}, result=${arg.a + arg.b + c}`
-        );
-        const memoized = withCacheKey(fn);
-        expect(await memoized({ a: 1, b: 2 }, 4)).toBe('[[["a",1],["b",2]],4], result=7');
-        expect(fn.mock.calls.length).toBe(1);
-    });
-});
diff --git a/packages/gitbook-v2/src/lib/data/memoize.ts b/packages/gitbook-v2/src/lib/data/memoize.ts
deleted file mode 100644
index ff1ff5e841..0000000000
--- a/packages/gitbook-v2/src/lib/data/memoize.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-/**
- * Wrap a function by preventing concurrent executions of the same function.
- * With a logic to work per-request in Cloudflare Workers.
- */
-export function withoutConcurrentExecution<ArgsType extends any[], ReturnType>(
-    getGlobalContext: () => object | null | undefined,
-    wrapped: (key: string, ...args: ArgsType) => Promise<ReturnType>
-): (cacheKey: string, ...args: ArgsType) => Promise<ReturnType> {
-    const globalPromiseCache = new WeakMap<object, Map<string, Promise<ReturnType>>>();
-
-    return (key: string, ...args: ArgsType) => {
-        const globalContext = getGlobalContext() ?? globalThis;
-
-        /**
-         * Cache storage that is scoped to the current request when executed in Cloudflare Workers,
-         * to avoid "Cannot perform I/O on behalf of a different request" errors.
-         */
-        const promiseCache =
-            globalPromiseCache.get(globalContext) ?? new Map<string, Promise<ReturnType>>();
-        globalPromiseCache.set(globalContext, promiseCache);
-
-        const concurrent = promiseCache.get(key);
-        if (concurrent) {
-            return concurrent;
-        }
-
-        const promise = (async () => {
-            try {
-                const result = await wrapped(key, ...args);
-                return result;
-            } finally {
-                promiseCache.delete(key);
-            }
-        })();
-
-        promiseCache.set(key, promise);
-
-        return promise;
-    };
-}
-
-/**
- * Wrap a function by passing it a cache key that is computed from the function arguments.
- */
-export function withCacheKey<ArgsType extends any[], ReturnType>(
-    wrapped: (cacheKey: string, ...args: ArgsType) => Promise<ReturnType>
-): (...args: ArgsType) => Promise<ReturnType> {
-    return (...args: ArgsType) => {
-        const cacheKey = getCacheKey(args);
-        return wrapped(cacheKey, ...args);
-    };
-}
-
-/**
- * Compute a cache key from the function arguments.
- */
-function getCacheKey(args: any[]) {
-    return JSON.stringify(deepSortValue(args));
-}
-
-function deepSortValue(value: unknown): unknown {
-    if (
-        typeof value === 'string' ||
-        typeof value === 'number' ||
-        typeof value === 'boolean' ||
-        value === null ||
-        value === undefined
-    ) {
-        return value;
-    }
-
-    if (Array.isArray(value)) {
-        return value.map(deepSortValue);
-    }
-
-    if (value && typeof value === 'object') {
-        return Object.entries(value)
-            .map(([key, subValue]) => {
-                return [key, deepSortValue(subValue)] as const;
-            })
-            .sort((a, b) => {
-                return a[0].localeCompare(b[0]);
-            });
-    }
-
-    return value;
-}
diff --git a/packages/gitbook/src/lib/openapi/fetch.ts b/packages/gitbook/src/lib/openapi/fetch.ts
index b496bbe59f..889ea742ae 100644
--- a/packages/gitbook/src/lib/openapi/fetch.ts
+++ b/packages/gitbook/src/lib/openapi/fetch.ts
@@ -7,8 +7,6 @@ import type {
     OpenAPIWebhookBlock,
     ResolveOpenAPIBlockArgs,
 } from '@/lib/openapi/types';
-import { getCloudflareRequestGlobal } from '@v2/lib/data/cloudflare';
-import { withCacheKey, withoutConcurrentExecution } from '@v2/lib/data/memoize';
 import { assert } from 'ts-essentials';
 import { resolveContentRef } from '../references';
 import { isV2 } from '../v2';
@@ -48,7 +46,7 @@ export async function fetchOpenAPIFilesystem(
 
 function fetchFilesystem(url: string) {
     if (isV2()) {
-        return fetchFilesystemV2(url);
+        return fetchFilesystemUseCache(url);
     }
 
     return fetchFilesystemV1(url);
@@ -68,12 +66,6 @@ const fetchFilesystemV1 = cache({
     },
 });
 
-const fetchFilesystemV2 = withCacheKey(
-    withoutConcurrentExecution(getCloudflareRequestGlobal, async (_cacheKey, url: string) => {
-        return fetchFilesystemUseCache(url);
-    })
-);
-
 const fetchFilesystemUseCache = async (url: string) => {
     'use cache';
     return fetchFilesystemUncached(url);

From 7d3fe231959f17a3c4a08174d870b46a324eed97 Mon Sep 17 00:00:00 2001
From: Zeno Kapitein <zeno@gitbook.io>
Date: Sat, 7 Jun 2025 12:48:16 +0100
Subject: [PATCH 077/127] Add circular corners and depth styles (#3280)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: Samy Pessé <samypesse@gmail.com>
---
 .changeset/fair-crews-wink.md                    |  5 +++++
 bun.lock                                         | 12 ++++++------
 package.json                                     |  2 +-
 packages/cache-tags/package.json                 |  2 +-
 packages/gitbook-v2/package.json                 |  2 +-
 packages/gitbook/e2e/internal.spec.ts            | 11 +++++++++++
 packages/gitbook/e2e/util.ts                     |  2 ++
 packages/gitbook/package.json                    |  2 +-
 .../Announcement/AnnouncementBanner.tsx          |  4 ++--
 .../src/components/Cookies/CookiesToast.tsx      |  3 +++
 .../src/components/DocumentView/Block.tsx        |  6 ++++++
 .../gitbook/src/components/DocumentView/Hint.tsx |  1 +
 .../src/components/DocumentView/Images.tsx       |  9 ++-------
 .../components/DocumentView/OpenAPI/scalar.css   |  2 +-
 .../components/DocumentView/OpenAPI/style.css    | 10 +++++-----
 .../components/DocumentView/Table/RecordCard.tsx |  7 +++++--
 .../components/DocumentView/Table/ViewGrid.tsx   |  3 ++-
 .../PageAside/AsideSectionHighlight.tsx          |  1 +
 .../components/PageAside/ScrollSectionsList.tsx  |  1 +
 .../RootLayout/CustomizationRootLayout.tsx       |  6 ++----
 .../src/components/Search/SearchAskAnswer.tsx    |  1 +
 .../src/components/Search/SearchButton.tsx       |  6 ++++++
 .../src/components/Search/SearchModal.tsx        |  4 +++-
 .../components/Search/SearchPageResultItem.tsx   |  1 +
 .../Search/SearchQuestionResultItem.tsx          |  1 +
 .../Search/SearchSectionResultItem.tsx           |  1 +
 .../components/SiteSections/SiteSectionList.tsx  | 16 +++++++++++++++-
 .../components/SiteSections/SiteSectionTabs.tsx  |  6 +++---
 .../components/TableOfContents/PageLinkItem.tsx  |  1 +
 .../TableOfContents/TableOfContents.tsx          |  3 ++-
 .../TableOfContents/ToggleableLinkItem.tsx       |  2 +-
 .../src/components/TableOfContents/Trademark.tsx |  1 +
 .../src/components/ThemeToggler/ThemeToggler.tsx |  1 +
 .../gitbook/src/components/primitives/Button.tsx |  9 +++++++--
 .../gitbook/src/components/primitives/Card.tsx   |  1 +
 packages/gitbook/src/lib/utils.ts                |  1 +
 packages/gitbook/tailwind.config.ts              |  5 ++++-
 packages/react-contentkit/package.json           |  2 +-
 38 files changed, 110 insertions(+), 43 deletions(-)
 create mode 100644 .changeset/fair-crews-wink.md

diff --git a/.changeset/fair-crews-wink.md b/.changeset/fair-crews-wink.md
new file mode 100644
index 0000000000..817854d1d7
--- /dev/null
+++ b/.changeset/fair-crews-wink.md
@@ -0,0 +1,5 @@
+---
+"gitbook": minor
+---
+
+Add circular corners and depth styling
diff --git a/bun.lock b/bun.lock
index 96f0f0d203..cf33dddcca 100644
--- a/bun.lock
+++ b/bun.lock
@@ -26,7 +26,7 @@
       "name": "@gitbook/cache-tags",
       "version": "0.3.1",
       "dependencies": {
-        "@gitbook/api": "^0.115.0",
+        "@gitbook/api": "^0.118.0",
         "assert-never": "^1.2.1",
       },
       "devDependencies": {
@@ -51,7 +51,7 @@
       "name": "gitbook",
       "version": "0.12.0",
       "dependencies": {
-        "@gitbook/api": "^0.115.0",
+        "@gitbook/api": "^0.118.0",
         "@gitbook/cache-do": "workspace:*",
         "@gitbook/cache-tags": "workspace:*",
         "@gitbook/colors": "workspace:*",
@@ -143,7 +143,7 @@
       "name": "gitbook-v2",
       "version": "0.3.0",
       "dependencies": {
-        "@gitbook/api": "^0.115.0",
+        "@gitbook/api": "^0.118.0",
         "@gitbook/cache-tags": "workspace:*",
         "@opennextjs/cloudflare": "1.1.0",
         "@sindresorhus/fnv1a": "^3.1.0",
@@ -202,7 +202,7 @@
       "name": "@gitbook/react-contentkit",
       "version": "0.7.0",
       "dependencies": {
-        "@gitbook/api": "^0.115.0",
+        "@gitbook/api": "^0.118.0",
         "@gitbook/icons": "workspace:*",
         "classnames": "^2.5.1",
       },
@@ -260,7 +260,7 @@
   },
   "overrides": {
     "@codemirror/state": "6.4.1",
-    "@gitbook/api": "^0.115.0",
+    "@gitbook/api": "^0.118.0",
     "react": "^19.0.0",
     "react-dom": "^19.0.0",
   },
@@ -625,7 +625,7 @@
 
     "@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="],
 
-    "@gitbook/api": ["@gitbook/api@0.115.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-Lyj+1WVNnE/Zuuqa/1ZdnUQfUiNE6es89RFK6CJ+Tb36TFwls6mbHKXCZsBwSYyoMYTVK39WQ3Nob6Nw6+TWCA=="],
+    "@gitbook/api": ["@gitbook/api@0.118.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-5bpvXyGNsMOn1Ee7uzohDz/yOgxpVyLZMLu6THYwbG9UeY6BFsWISeqTw03COCX42rVLU5zFReDxRTb7lfZtCw=="],
 
     "@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"],
 
diff --git a/package.json b/package.json
index 5398e52f9f..8ee2b33518 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
     "packageManager": "bun@1.2.11",
     "overrides": {
         "@codemirror/state": "6.4.1",
-        "@gitbook/api": "^0.115.0",
+        "@gitbook/api": "^0.118.0",
         "react": "^19.0.0",
         "react-dom": "^19.0.0"
     },
diff --git a/packages/cache-tags/package.json b/packages/cache-tags/package.json
index 8850dafbd6..37e243bdb6 100644
--- a/packages/cache-tags/package.json
+++ b/packages/cache-tags/package.json
@@ -10,7 +10,7 @@
     },
     "version": "0.3.1",
     "dependencies": {
-        "@gitbook/api": "^0.115.0",
+        "@gitbook/api": "^0.118.0",
         "assert-never": "^1.2.1"
     },
     "devDependencies": {
diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json
index 0b790a9bee..6eaa3a37c3 100644
--- a/packages/gitbook-v2/package.json
+++ b/packages/gitbook-v2/package.json
@@ -3,7 +3,7 @@
     "version": "0.3.0",
     "private": true,
     "dependencies": {
-        "@gitbook/api": "^0.115.0",
+        "@gitbook/api": "^0.118.0",
         "@gitbook/cache-tags": "workspace:*",
         "@opennextjs/cloudflare": "1.1.0",
         "@sindresorhus/fnv1a": "^3.1.0",
diff --git a/packages/gitbook/e2e/internal.spec.ts b/packages/gitbook/e2e/internal.spec.ts
index a70a7e5d8d..17f03b9318 100644
--- a/packages/gitbook/e2e/internal.spec.ts
+++ b/packages/gitbook/e2e/internal.spec.ts
@@ -1,6 +1,7 @@
 import {
     CustomizationBackground,
     CustomizationCorners,
+    CustomizationDepth,
     CustomizationHeaderPreset,
     CustomizationIconsStyle,
     CustomizationSidebarListStyle,
@@ -817,6 +818,16 @@ const testCases: TestsCase[] = [
                 }),
                 run: waitForCookiesDialog,
             },
+            {
+                name: `With flat and circular corners - Theme mode ${themeMode}`,
+                url: getCustomizationURL({
+                    styling: {
+                        depth: CustomizationDepth.Flat,
+                        corners: CustomizationCorners.Circular,
+                    },
+                }),
+                run: waitForCookiesDialog,
+            },
         ]),
     },
     {
diff --git a/packages/gitbook/e2e/util.ts b/packages/gitbook/e2e/util.ts
index 61113f8264..f54b70fbe2 100644
--- a/packages/gitbook/e2e/util.ts
+++ b/packages/gitbook/e2e/util.ts
@@ -3,6 +3,7 @@ import {
     CustomizationBackground,
     CustomizationCorners,
     CustomizationDefaultFont,
+    CustomizationDepth,
     type CustomizationHeaderItem,
     CustomizationHeaderPreset,
     CustomizationIconsStyle,
@@ -278,6 +279,7 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
             dangerColor: { light: '#FB2C36', dark: '#FB2C36' },
             successColor: { light: '#00C950', dark: '#00C950' },
             corners: CustomizationCorners.Rounded,
+            depth: CustomizationDepth.Subtle,
             font: CustomizationDefaultFont.Inter,
             background: CustomizationBackground.Plain,
             icons: CustomizationIconsStyle.Regular,
diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json
index a3b9da821a..8443bb615a 100644
--- a/packages/gitbook/package.json
+++ b/packages/gitbook/package.json
@@ -16,7 +16,7 @@
         "clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math"
     },
     "dependencies": {
-        "@gitbook/api": "^0.115.0",
+        "@gitbook/api": "^0.118.0",
         "@gitbook/cache-do": "workspace:*",
         "@gitbook/cache-tags": "workspace:*",
         "@gitbook/colors": "workspace:*",
diff --git a/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx b/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx
index 2bdd61caa7..2b07504f3f 100644
--- a/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx
+++ b/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx
@@ -34,7 +34,7 @@ export function AnnouncementBanner(props: {
                     <Tag
                         href={contentRef?.href ?? ''}
                         className={tcls(
-                            'flex w-full items-start justify-center overflow-hidden rounded-md straight-corners:rounded-none px-4 py-3 text-neutral-strong text-sm theme-bold:ring-1 theme-gradient:ring-1 ring-inset transition-colors',
+                            'flex w-full items-start justify-center overflow-hidden circular-corners:rounded-xl rounded-md straight-corners:rounded-none px-4 py-3 text-neutral-strong text-sm theme-bold:ring-1 theme-gradient:ring-1 ring-inset transition-colors',
                             style.container,
                             closeable && 'pr-12',
                             hasLink && style.hover
@@ -81,7 +81,7 @@ export function AnnouncementBanner(props: {
                     </Tag>
                     {closeable ? (
                         <button
-                            className={`absolute top-0 right-4 mt-2 mr-2 rounded straight-corners:rounded-none p-1.5 transition-all hover:ring-1 sm:right-6 md:right-8 ${style.close}`}
+                            className={`absolute top-0 right-4 mt-2 mr-2 rounded circular-corners:rounded-lg straight-corners:rounded-none p-1.5 transition-all hover:ring-1 sm:right-6 md:right-8 ${style.close}`}
                             type="button"
                             onClick={dismissAnnouncement}
                             title={tString(language, 'close')}
diff --git a/packages/gitbook/src/components/Cookies/CookiesToast.tsx b/packages/gitbook/src/components/Cookies/CookiesToast.tsx
index 8e1afc4c32..281e6ad911 100644
--- a/packages/gitbook/src/components/Cookies/CookiesToast.tsx
+++ b/packages/gitbook/src/components/Cookies/CookiesToast.tsx
@@ -47,9 +47,11 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
                 'bg-tint-base',
                 'rounded',
                 'straight-corners:rounded-none',
+                'circular-corners:rounded-2xl',
                 'ring-1',
                 'ring-tint-subtle',
                 'shadow-1xs',
+                'depth-flat:shadow-none',
                 'p-4',
                 'pr-8',
                 'bottom-4',
@@ -83,6 +85,7 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
                     'justify-center',
                     'items-center',
                     'rounded-sm',
+                    'circular-corners:rounded-full',
                     'hover:bg-tint-hover'
                 )}
                 title={tString(language, 'close')}
diff --git a/packages/gitbook/src/components/DocumentView/Block.tsx b/packages/gitbook/src/components/DocumentView/Block.tsx
index 60998b50b1..3dba09bf56 100644
--- a/packages/gitbook/src/components/DocumentView/Block.tsx
+++ b/packages/gitbook/src/components/DocumentView/Block.tsx
@@ -113,6 +113,9 @@ export function Block<T extends DocumentBlock>(props: BlockProps<T>) {
             case 'code-line':
             case 'tabs-item':
                 throw new Error(`Blocks (${block.type}) should be directly rendered by parent`);
+            case 'columns':
+            case 'column':
+                return null;
             default:
                 return nullIfNever(block);
         }
@@ -177,6 +180,9 @@ export function BlockSkeleton(props: { block: DocumentBlock; style: ClassValue }
         case 'code-line':
         case 'tabs-item':
             throw new Error(`Blocks (${block.type}) should be directly rendered by parent`);
+        case 'columns':
+        case 'column':
+            return null;
         default:
             return nullIfNever(block);
     }
diff --git a/packages/gitbook/src/components/DocumentView/Hint.tsx b/packages/gitbook/src/components/DocumentView/Hint.tsx
index 59ff1e0ed7..79961ee172 100644
--- a/packages/gitbook/src/components/DocumentView/Hint.tsx
+++ b/packages/gitbook/src/components/DocumentView/Hint.tsx
@@ -23,6 +23,7 @@ export function Hint(props: BlockProps<DocumentBlockHint>) {
                 'rounded-md',
                 hasHeading ? 'rounded-l' : null,
                 'straight-corners:rounded-none',
+                'circular-corners:rounded-xl',
                 'overflow-hidden',
                 hasHeading ? ['border-l-2', hintStyle.containerWithHeader] : hintStyle.container,
 
diff --git a/packages/gitbook/src/components/DocumentView/Images.tsx b/packages/gitbook/src/components/DocumentView/Images.tsx
index 20018b6f49..91d869002e 100644
--- a/packages/gitbook/src/components/DocumentView/Images.tsx
+++ b/packages/gitbook/src/components/DocumentView/Images.tsx
@@ -1,9 +1,4 @@
-import type {
-    DocumentBlockImage,
-    DocumentBlockImageDimension,
-    DocumentBlockImages,
-    JSONDocument,
-} from '@gitbook/api';
+import type { DocumentBlockImage, DocumentBlockImages, JSONDocument, Length } from '@gitbook/api';
 
 import { Image, type ImageResponsiveSize } from '@/components/utils';
 import { resolveContentRef } from '@/lib/references';
@@ -119,7 +114,7 @@ async function ImageBlock(props: {
  * When using relative values, the converted dimension will be relative to the parent element's size.
  */
 function getImageDimension<DefaultValue>(
-    dimension: DocumentBlockImageDimension | undefined,
+    dimension: Length | undefined,
     defaultValue: DefaultValue
 ): string | DefaultValue {
     if (typeof dimension === 'number') {
diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/scalar.css b/packages/gitbook/src/components/DocumentView/OpenAPI/scalar.css
index 8096a9fbef..b7eec226f9 100644
--- a/packages/gitbook/src/components/DocumentView/OpenAPI/scalar.css
+++ b/packages/gitbook/src/components/DocumentView/OpenAPI/scalar.css
@@ -271,7 +271,7 @@ body {
 }
 .scalar-activate-button {
     @apply flex gap-2 items-center;
-    @apply bg-primary-solid text-contrast-primary-solid hover:bg-primary-solid-hover hover:text-contrast-primary-solid-hover contrast-more:ring-1 rounded-md straight-corners:rounded-none place-self-start;
+    @apply bg-primary-solid text-contrast-primary-solid hover:bg-primary-solid-hover hover:text-contrast-primary-solid-hover contrast-more:ring-1 rounded-md straight-corners:rounded-none circular-corners:rounded-full circular-corners:px-3 place-self-start;
     @apply ring-1 ring-tint hover:ring-tint-hover;
     @apply shadow-sm shadow-tint dark:shadow-tint-1 hover:shadow-md active:shadow-none;
     @apply contrast-more:ring-tint-12 contrast-more:hover:ring-2 contrast-more:hover:ring-tint-12;
diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
index 213d12dec5..1a88b4faa6 100644
--- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
+++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
@@ -481,7 +481,7 @@
 .openapi-panel,
 .openapi-codesample,
 .openapi-response-examples {
-    @apply border rounded-md straight-corners:rounded-none bg-tint-subtle border-tint-subtle shadow-sm;
+    @apply border rounded-md straight-corners:rounded-none circular-corners:rounded-xl bg-tint-subtle border-tint-subtle shadow-sm;
 }
 
 .openapi-panel pre,
@@ -571,7 +571,7 @@ body:has(.openapi-select-popover) {
 }
 
 .openapi-select-popover {
-    @apply min-w-32 z-10 max-w-[max(20rem,var(--trigger-width))] overflow-x-hidden max-h-52 overflow-y-auto p-1.5 border border-tint-subtle bg-tint-base backdrop-blur-xl rounded-md straight-corners:rounded-none;
+    @apply min-w-32 z-10 max-w-[max(20rem,var(--trigger-width))] overflow-x-hidden max-h-52 overflow-y-auto p-1.5 border border-tint-subtle bg-tint-base backdrop-blur-xl rounded-md circular-corners:rounded-xl straight-corners:rounded-none;
     @apply shadow-md shadow-tint-12/1 dark:shadow-tint-1/1;
 }
 
@@ -680,7 +680,7 @@ body:has(.openapi-select-popover) {
 
 /* Disclosure group */
 .openapi-disclosure-group {
-    @apply border-tint-subtle transition-all border-b border-x overflow-auto last:rounded-b-md straight-corners:last:rounded-none first:rounded-t-md straight-corners:first:rounded-none first:border-t relative;
+    @apply border-tint-subtle transition-all border-b border-x overflow-auto last:rounded-b-md straight-corners:last:rounded-none circular-corners:last:rounded-b-xl first:rounded-t-md straight-corners:first:rounded-none circular-corners:first:rounded-t-xl first:border-t relative;
 }
 
 .openapi-disclosure-group:has(.openapi-disclosure-group-trigger:hover) {
@@ -786,7 +786,7 @@ body:has(.openapi-select-popover) {
 }
 
 .openapi-disclosure-trigger-label {
-    @apply absolute right-3 px-2 h-5 justify-end shrink-0 ring-tint-subtle truncate text-tint duration-300 transition-all rounded straight-corners:rounded-none flex flex-row gap-1 items-center text-xs;
+    @apply absolute right-3 px-2 h-5 justify-end shrink-0 ring-tint-subtle truncate text-tint duration-300 transition-all rounded straight-corners:rounded-none circular-corners:rounded-xl flex flex-row gap-1 items-center text-xs;
 }
 
 .openapi-disclosure-trigger-label span {
@@ -848,7 +848,7 @@ body:has(.openapi-select-popover) {
 }
 
 .openapi-tooltip {
-    @apply flex items-center gap-1 bg-tint-base border border-tint-subtle text-tint-strong rounded-md straight-corners:rounded-none font-medium px-1.5 py-0.5 shadow-sm text-[13px];
+    @apply flex items-center gap-1 bg-tint-base border border-tint-subtle text-tint-strong rounded-md straight-corners:rounded-none circular-corners:rounded-lg font-medium px-1.5 py-0.5 shadow-sm text-[13px];
 }
 
 .openapi-tooltip svg {
diff --git a/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx b/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx
index 4218f760e4..4bc1e0ce85 100644
--- a/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx
+++ b/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx
@@ -50,8 +50,9 @@ export async function RecordCard(
                 'w-[calc(100%+2px)]',
                 'h-[calc(100%+2px)]',
                 'inset-[-1px]',
-                'rounded-[7px]',
+                'rounded',
                 'straight-corners:rounded-none',
+                'circular-corners:rounded-xl',
                 'overflow-hidden',
                 '[&_.heading>div:first-child]:hidden',
                 '[&_.heading>div]:text-[.8em]',
@@ -147,8 +148,10 @@ export async function RecordCard(
         'grid',
         'shadow-1xs',
         'shadow-tint-9/1',
-        'rounded-md',
+        'depth-flat:shadow-none',
+        'rounded',
         'straight-corners:rounded-none',
+        'circular-corners:rounded-xl',
         'dark:shadow-transparent',
 
         'before:pointer-events-none',
diff --git a/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx b/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx
index 9f2b98ba44..606b2305b5 100644
--- a/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx
+++ b/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx
@@ -38,7 +38,8 @@ export function ViewGrid(props: TableViewProps<DocumentTableViewGrid>) {
                         className={tcls(
                             tableWidth,
                             styles.rowGroup,
-                            'straight-corners:rounded-none'
+                            'straight-corners:rounded-none',
+                            'circular-corners:rounded-xl'
                         )}
                     >
                         <div role="row" className={tcls('flex', 'w-full')}>
diff --git a/packages/gitbook/src/components/PageAside/AsideSectionHighlight.tsx b/packages/gitbook/src/components/PageAside/AsideSectionHighlight.tsx
index 41775ca782..4b18d3d909 100644
--- a/packages/gitbook/src/components/PageAside/AsideSectionHighlight.tsx
+++ b/packages/gitbook/src/components/PageAside/AsideSectionHighlight.tsx
@@ -30,6 +30,7 @@ export function AsideSectionHighlight({
 
                 'rounded-md',
                 'straight-corners:rounded-none',
+                'circular-corners:rounded-2xl',
                 'sidebar-list-line:rounded-l-none',
 
                 'sidebar-list-pill:bg-primary',
diff --git a/packages/gitbook/src/components/PageAside/ScrollSectionsList.tsx b/packages/gitbook/src/components/PageAside/ScrollSectionsList.tsx
index d9b02ebdf8..519c1ea15b 100644
--- a/packages/gitbook/src/components/PageAside/ScrollSectionsList.tsx
+++ b/packages/gitbook/src/components/PageAside/ScrollSectionsList.tsx
@@ -82,6 +82,7 @@ export function ScrollSectionsList(props: { sections: DocumentSection[] }) {
 
                             'rounded-md',
                             'straight-corners:rounded-none',
+                            'circular-corners:rounded-2xl',
                             'sidebar-list-line:rounded-l-none',
 
                             'hover:bg-tint-hover',
diff --git a/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx b/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx
index c6f141425a..ffaad1fff8 100644
--- a/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx
+++ b/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx
@@ -1,5 +1,4 @@
 import {
-    CustomizationCorners,
     CustomizationHeaderPreset,
     CustomizationIconsStyle,
     CustomizationSidebarBackgroundStyle,
@@ -77,14 +76,13 @@ export async function CustomizationRootLayout(props: {
                 customization.header.preset === CustomizationHeaderPreset.None
                     ? null
                     : 'scroll-pt-[76px]', // Take the sticky header in consideration for the scrolling
-                customization.styling.corners === CustomizationCorners.Straight
-                    ? ' straight-corners'
-                    : '',
+                customization.styling.corners && `${customization.styling.corners}-corners`,
                 'theme' in customization.styling && `theme-${customization.styling.theme}`,
                 tintColor ? ' tint' : 'no-tint',
                 sidebarStyles.background && `sidebar-${sidebarStyles.background}`,
                 sidebarStyles.list && `sidebar-list-${sidebarStyles.list}`,
                 'links' in customization.styling && `links-${customization.styling.links}`,
+                'depth' in customization.styling && `depth-${customization.styling.depth}`,
                 fontNotoColorEmoji.variable,
                 ibmPlexMono.variable,
                 fontData.type === 'default' ? fontData.variable : 'font-custom',
diff --git a/packages/gitbook/src/components/Search/SearchAskAnswer.tsx b/packages/gitbook/src/components/Search/SearchAskAnswer.tsx
index 8a7dce493d..f7c4b1fc8e 100644
--- a/packages/gitbook/src/components/Search/SearchAskAnswer.tsx
+++ b/packages/gitbook/src/components/Search/SearchAskAnswer.tsx
@@ -176,6 +176,7 @@ function AnswerFollowupQuestions(props: { followupQuestions: string[] }) {
                         'py-2',
                         'rounded',
                         'straight-corners:rounded-none',
+                        'circular-corners:rounded-full',
                         'text-tint',
                         'hover:bg-tint-hover',
                         'focus-within:bg-tint-hover'
diff --git a/packages/gitbook/src/components/Search/SearchButton.tsx b/packages/gitbook/src/components/Search/SearchButton.tsx
index ab8516d059..7bcc4f7481 100644
--- a/packages/gitbook/src/components/Search/SearchButton.tsx
+++ b/packages/gitbook/src/components/Search/SearchButton.tsx
@@ -45,21 +45,25 @@ export function SearchButton(props: { children?: React.ReactNode; style?: ClassV
                 'w-full',
                 'py-2',
                 'px-3',
+                'circular-corners:px-4',
                 'gap-2',
 
                 'bg-tint-base',
 
                 'ring-1',
                 'ring-tint-12/2',
+                'depth-flat:ring-tint-subtle',
 
                 'shadow-sm',
                 'shadow-tint-12/3',
                 'dark:shadow-none',
+                'depth-flat:shadow-none',
 
                 'text-tint',
 
                 'rounded-lg',
                 'straight-corners:rounded-sm',
+                'circular-corners:rounded-full',
 
                 'contrast-more:ring-tint-12',
                 'contrast-more:text-tint-strong',
@@ -68,10 +72,12 @@ export function SearchButton(props: { children?: React.ReactNode; style?: ClassV
                 'hover:bg-tint-subtle',
                 'hover:shadow-md',
                 'hover:scale-102',
+                'depth-flat:hover:scale-100',
                 'hover:ring-tint-hover',
                 'hover:text-tint-strong',
                 'focus:shadow-md',
                 'focus:scale-102',
+                'depth-flat:focus:scale-100',
                 'focus:ring-tint-hover',
                 'focus:text-tint-strong',
 
diff --git a/packages/gitbook/src/components/Search/SearchModal.tsx b/packages/gitbook/src/components/Search/SearchModal.tsx
index bdc747241a..c8046ca333 100644
--- a/packages/gitbook/src/components/Search/SearchModal.tsx
+++ b/packages/gitbook/src/components/Search/SearchModal.tsx
@@ -225,9 +225,11 @@ function SearchModalBody(
                 'w-full',
                 'rounded-lg',
                 'straight-corners:rounded-sm',
+                'circular-corners:rounded-2xl',
                 'ring-1',
-                'ring-tint-hover',
+                'ring-tint',
                 'shadow-2xl',
+                'depth-flat:shadow-none',
                 'overflow-hidden',
                 'dark:ring-inset',
                 'dark:ring-tint'
diff --git a/packages/gitbook/src/components/Search/SearchPageResultItem.tsx b/packages/gitbook/src/components/Search/SearchPageResultItem.tsx
index 6b4d0e1c0e..aa97e621a4 100644
--- a/packages/gitbook/src/components/Search/SearchPageResultItem.tsx
+++ b/packages/gitbook/src/components/Search/SearchPageResultItem.tsx
@@ -108,6 +108,7 @@ export const SearchPageResultItem = React.forwardRef(function SearchPageResultIt
                     'p-2',
                     'rounded',
                     'straight-corners:rounded-none',
+                    'circular-corners:rounded-full',
                     active ? ['bg-primary-solid', 'text-contrast-primary-solid'] : ['opacity-6']
                 )}
             >
diff --git a/packages/gitbook/src/components/Search/SearchQuestionResultItem.tsx b/packages/gitbook/src/components/Search/SearchQuestionResultItem.tsx
index 83f585c82b..787632a805 100644
--- a/packages/gitbook/src/components/Search/SearchQuestionResultItem.tsx
+++ b/packages/gitbook/src/components/Search/SearchQuestionResultItem.tsx
@@ -72,6 +72,7 @@ export const SearchQuestionResultItem = React.forwardRef(function SearchQuestion
                     'rounded',
                     'self-center',
                     'straight-corners:rounded-none',
+                    'circular-corners:rounded-full',
                     active ? ['bg-primary-solid', 'text-contrast-primary-solid'] : ['opacity-6']
                 )}
             >
diff --git a/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx b/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx
index ee2daa1ffb..960b1eb38a 100644
--- a/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx
+++ b/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx
@@ -77,6 +77,7 @@ export const SearchSectionResultItem = React.forwardRef(function SearchSectionRe
                     'p-2',
                     'rounded',
                     'straight-corners:rounded-none',
+                    'circular-corners:rounded-full',
                     'bg-primary-solid',
                     'text-contrast-primary-solid',
                     'hidden',
diff --git a/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx b/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx
index 6026296839..dc78a3c3c2 100644
--- a/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx
+++ b/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx
@@ -84,7 +84,21 @@ export function SiteSectionListItem(props: {
             href={section.url}
             aria-current={isActive && 'page'}
             className={tcls(
-                'group/section-link flex flex-row items-center gap-3 rounded-md straight-corners:rounded-none px-3 py-2 transition-all hover:bg-tint-hover hover:text-tint-strong contrast-more:hover:ring-1 contrast-more:hover:ring-tint',
+                'group/section-link',
+                'flex',
+                'flex-row',
+                'items-center',
+                'gap-3',
+                'rounded-md',
+                'straight-corners:rounded-none',
+                'circular-corners:rounded-xl',
+                'px-3',
+                'py-2',
+                'transition-all',
+                'hover:bg-tint-hover',
+                'hover:text-tint-strong',
+                'contrast-more:hover:ring-1',
+                'contrast-more:hover:ring-tint',
                 isActive
                     ? 'font-semibold text-primary-subtle hover:bg-primary-hover hover:text-primary contrast-more:text-primary contrast-more:hover:text-primary-strong contrast-more:hover:ring-1 contrast-more:hover:ring-primary-hover'
                     : null,
diff --git a/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx b/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx
index 8496845507..8d6df8eafa 100644
--- a/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx
+++ b/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx
@@ -133,7 +133,7 @@ export function SiteSectionTabs(props: { sections: ClientSiteSections }) {
                 }}
             >
                 <NavigationMenu.Viewport
-                    className="relative mt-3 h-[var(--radix-navigation-menu-viewport-height)] w-[calc(100vw_-_2rem)] origin-[top_center] overflow-hidden rounded-lg straight-corners:rounded-sm bg-tint-base shadow-lg shadow-tint-10/6 ring-1 ring-tint-subtle duration-250 data-[state=closed]:duration-150 motion-safe:transition-[width,_height,_transform] data-[state=closed]:motion-safe:animate-scaleOut data-[state=open]:motion-safe:animate-scaleIn md:mx-0 md:w-[var(--radix-navigation-menu-viewport-width)] dark:shadow-tint-1/6"
+                    className="relative mt-3 h-[var(--radix-navigation-menu-viewport-height)] w-[calc(100vw_-_2rem)] origin-[top_center] overflow-hidden rounded-lg straight-corners:rounded-sm bg-tint-base depth-flat:shadow-none shadow-lg shadow-tint-10/6 ring-1 ring-tint-subtle duration-250 data-[state=closed]:duration-150 motion-safe:transition-[width,_height,_transform] data-[state=closed]:motion-safe:animate-scaleOut data-[state=open]:motion-safe:animate-scaleIn md:mx-0 md:w-[var(--radix-navigation-menu-viewport-width)] dark:shadow-tint-1/6"
                     style={{
                         translate:
                             undefined /* don't move this to a Tailwind class as Radix renders viewport incorrectly for a few frames */,
@@ -157,7 +157,7 @@ const SectionTab = React.forwardRef(function SectionTab(
             ref={ref}
             {...rest}
             className={tcls(
-                'group relative my-2 flex select-none items-center justify-between rounded straight-corners:rounded-none px-3 py-1',
+                'group relative my-2 flex select-none items-center justify-between rounded circular-corners:rounded-full straight-corners:rounded-none px-3 py-1',
                 isActive
                     ? 'text-primary-subtle'
                     : 'text-tint hover:bg-tint-hover hover:text-tint-strong'
@@ -186,7 +186,7 @@ const SectionGroupTab = React.forwardRef(function SectionGroupTab(
             ref={ref}
             {...rest}
             className={tcls(
-                'group relative my-2 flex select-none items-center justify-between rounded straight-corners:rounded-none px-3 py-1 transition-colors hover:cursor-default',
+                'group relative my-2 flex select-none items-center justify-between rounded circular-corners:rounded-full straight-corners:rounded-none px-3 py-1 transition-colors hover:cursor-default',
                 isActive
                     ? 'text-primary-subtle'
                     : 'text-tint hover:bg-tint-hover hover:text-tint-strong'
diff --git a/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx b/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx
index 636cc514d9..35f350a0c3 100644
--- a/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx
+++ b/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx
@@ -30,6 +30,7 @@ export async function PageLinkItem(props: { page: RevisionPageLink; context: Git
                     'text-tint-strong/7',
                     'rounded-md',
                     'straight-corners:rounded-none',
+                    'circular-corners:rounded-xl',
                     'before:content-none',
                     'font-normal',
                     'hover:bg-tint',
diff --git a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx
index 7a6993fde1..73e9769a92 100644
--- a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx
+++ b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx
@@ -86,7 +86,8 @@ export function TableOfContents(props: {
                         'page-no-toc:!bg-transparent',
 
                         'sidebar-filled:rounded-xl',
-                        'straight-corners:rounded-none'
+                        'straight-corners:rounded-none',
+                        '[html.sidebar-filled.circular-corners_&]:page-has-toc:rounded-3xl'
                     )}
                 >
                     {innerHeader && <div className="px-5 *:my-4">{innerHeader}</div>}
diff --git a/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx b/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx
index 38d6674514..92502433ba 100644
--- a/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx
+++ b/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx
@@ -69,7 +69,7 @@ function LinkItem(
             className={tcls(
                 'group/toclink relative transition-colors',
                 'flex flex-row justify-between',
-                'rounded-md straight-corners:rounded-none p-1.5 pl-3',
+                'circular-corners:rounded-2xl rounded-md straight-corners:rounded-none p-1.5 pl-3',
                 'text-balance font-normal text-sm text-tint-strong/7 hover:bg-tint-hover hover:text-tint-strong contrast-more:text-tint-strong',
                 'hover:contrast-more:text-tint-strong hover:contrast-more:ring-1 hover:contrast-more:ring-tint-12',
                 'before:contents[] before:-left-px before:absolute before:inset-y-0',
diff --git a/packages/gitbook/src/components/TableOfContents/Trademark.tsx b/packages/gitbook/src/components/TableOfContents/Trademark.tsx
index 19f5aed660..aa2d37c0ac 100644
--- a/packages/gitbook/src/components/TableOfContents/Trademark.tsx
+++ b/packages/gitbook/src/components/TableOfContents/Trademark.tsx
@@ -102,6 +102,7 @@ export function TrademarkLink(props: {
 
                 'rounded-lg',
                 'straight-corners:rounded-none',
+                'circular-corners:rounded-2xl',
 
                 'hover:bg-tint',
                 'hover:text-tint-strong',
diff --git a/packages/gitbook/src/components/ThemeToggler/ThemeToggler.tsx b/packages/gitbook/src/components/ThemeToggler/ThemeToggler.tsx
index af943dcbce..93f15e47c4 100644
--- a/packages/gitbook/src/components/ThemeToggler/ThemeToggler.tsx
+++ b/packages/gitbook/src/components/ThemeToggler/ThemeToggler.tsx
@@ -69,6 +69,7 @@ function ThemeButton(props: {
                 'p-2',
                 'rounded',
                 'straight-corners:rounded-none',
+                'circular-corners:rounded-full',
                 'transition-all',
                 'text-tint',
                 'contrast-more:text-tint-strong',
diff --git a/packages/gitbook/src/components/primitives/Button.tsx b/packages/gitbook/src/components/primitives/Button.tsx
index b7edac7ab0..58697d5b7f 100644
--- a/packages/gitbook/src/components/primitives/Button.tsx
+++ b/packages/gitbook/src/components/primitives/Button.tsx
@@ -40,8 +40,10 @@ const variantClasses = {
     ],
     secondary: [
         'bg-tint',
+        'depth-flat:bg-transparent',
         'text-tint',
         'hover:bg-tint-hover',
+        'depth-flat:hover:bg-tint-hover',
         'hover:text-primary',
         'contrast-more:bg-tint-subtle',
     ],
@@ -60,8 +62,8 @@ export function Button({
     ...rest
 }: ButtonProps & { target?: HTMLAttributeAnchorTarget }) {
     const sizes = {
-        default: ['text-base', 'px-4', 'py-2'],
-        medium: ['text-sm', 'px-3', 'py-1.5'],
+        default: ['text-base', 'font-semibold', 'px-5', 'py-2', 'circular-corners:px-6'],
+        medium: ['text-sm', 'px-3.5', 'py-1.5', 'circular-corners:px-4'],
         small: ['text-xs', 'py-2', iconOnly ? 'px-2' : 'px-3'],
     };
 
@@ -74,6 +76,7 @@ export function Button({
         'gap-2',
         'rounded-md',
         'straight-corners:rounded-none',
+        'circular-corners:rounded-full',
         // 'place-self-start',
 
         'ring-1',
@@ -85,12 +88,14 @@ export function Button({
         'dark:shadow-tint-1',
         'hover:shadow-md',
         'active:shadow-none',
+        'depth-flat:shadow-none',
 
         'contrast-more:ring-tint-12',
         'contrast-more:hover:ring-2',
         'contrast-more:hover:ring-tint-12',
 
         'hover:scale-104',
+        'depth-flat:hover:scale-100',
         'active:scale-100',
         'transition-all',
 
diff --git a/packages/gitbook/src/components/primitives/Card.tsx b/packages/gitbook/src/components/primitives/Card.tsx
index bb40142f31..a796ba9eff 100644
--- a/packages/gitbook/src/components/primitives/Card.tsx
+++ b/packages/gitbook/src/components/primitives/Card.tsx
@@ -30,6 +30,7 @@ export async function Card(
                 'ring-tint-subtle',
                 'rounded',
                 'straight-corners:rounded-none',
+                'circular-corners:rounded-2xl',
                 'px-5',
                 'py-3',
                 'transition-shadow',
diff --git a/packages/gitbook/src/lib/utils.ts b/packages/gitbook/src/lib/utils.ts
index 0db784cf2d..d7a98610d0 100644
--- a/packages/gitbook/src/lib/utils.ts
+++ b/packages/gitbook/src/lib/utils.ts
@@ -17,6 +17,7 @@ export function defaultCustomization(): api.SiteCustomizationSettings {
             background: api.CustomizationBackground.Plain,
             icons: api.CustomizationIconsStyle.Regular,
             links: api.CustomizationLinksStyle.Default,
+            depth: api.CustomizationDepth.Subtle,
             sidebar: {
                 background: api.CustomizationSidebarBackgroundStyle.Default,
                 list: api.CustomizationSidebarListStyle.Default,
diff --git a/packages/gitbook/tailwind.config.ts b/packages/gitbook/tailwind.config.ts
index 9d248c9a40..7981c337b2 100644
--- a/packages/gitbook/tailwind.config.ts
+++ b/packages/gitbook/tailwind.config.ts
@@ -483,7 +483,10 @@ const config: Config = {
                 theme: ['theme-clean', 'theme-muted', 'theme-bold', 'theme-gradient'],
 
                 // Corner styles
-                corner: ['straight-corners'],
+                corner: ['straight-corners', 'rounded-corners', 'circular-corners'],
+
+                // Depth styles
+                depth: ['depth-flat', 'depth-subtle'],
 
                 // Link styles
                 links: ['links-default', 'links-accent'],
diff --git a/packages/react-contentkit/package.json b/packages/react-contentkit/package.json
index 09e6cc8e7c..16eab1f683 100644
--- a/packages/react-contentkit/package.json
+++ b/packages/react-contentkit/package.json
@@ -10,7 +10,7 @@
     },
     "dependencies": {
         "classnames": "^2.5.1",
-        "@gitbook/api": "^0.115.0",
+        "@gitbook/api": "^0.118.0",
         "@gitbook/icons": "workspace:*"
     },
     "peerDependencies": {

From 6aa3ff9c5efa2ebd6dd1fb96683c4d5b4f8d2cd4 Mon Sep 17 00:00:00 2001
From: Zeno Kapitein <zeno@gitbook.io>
Date: Sun, 8 Jun 2025 00:38:41 +0100
Subject: [PATCH 078/127] Fix various styling issues (#3283)

---
 .changeset/big-gorillas-perform.md                       | 9 +++++++++
 packages/gitbook/src/components/DocumentView/Caption.tsx | 2 +-
 .../gitbook/src/components/DocumentView/StepperStep.tsx  | 6 +++---
 .../gitbook/src/components/Header/SpacesDropdown.tsx     | 2 +-
 packages/gitbook/src/components/PageBody/PageCover.tsx   | 2 +-
 .../src/components/PageBody/PageFooterNavigation.tsx     | 1 +
 .../src/components/TableOfContents/TableOfContents.tsx   | 1 +
 7 files changed, 17 insertions(+), 6 deletions(-)
 create mode 100644 .changeset/big-gorillas-perform.md

diff --git a/.changeset/big-gorillas-perform.md b/.changeset/big-gorillas-perform.md
new file mode 100644
index 0000000000..88ec555a00
--- /dev/null
+++ b/.changeset/big-gorillas-perform.md
@@ -0,0 +1,9 @@
+---
+"gitbook": patch
+---
+
+Fix three small visual issues
+
+- Fix sidebar showing on `no-toc` pages in the gradient theme
+- Fix variant selector truncating incorrectly in header when sections are present
+- Fix page cover alignment on `lg` screens without TOC
diff --git a/packages/gitbook/src/components/DocumentView/Caption.tsx b/packages/gitbook/src/components/DocumentView/Caption.tsx
index f7649e68a8..c7075e6bad 100644
--- a/packages/gitbook/src/components/DocumentView/Caption.tsx
+++ b/packages/gitbook/src/components/DocumentView/Caption.tsx
@@ -42,7 +42,7 @@ export function Caption(
             'after:pointer-events-none',
             fit ? 'w-fit' : null,
             withBorder
-                ? 'rounded straight-corners:rounded-none after:border-tint-subtle after:border after:rounded straight-corners:after:rounded-none dark:after:mix-blend-plus-lighter after:pointer-events-none'
+                ? 'rounded circular-corners:rounded-2xl straight-corners:rounded-none after:border-tint-subtle after:border after:rounded circular-corners:after:rounded-2xl straight-corners:after:rounded-none dark:after:mix-blend-plus-lighter after:pointer-events-none'
                 : null,
         ],
         style,
diff --git a/packages/gitbook/src/components/DocumentView/StepperStep.tsx b/packages/gitbook/src/components/DocumentView/StepperStep.tsx
index 46e70ed603..5c7456ad60 100644
--- a/packages/gitbook/src/components/DocumentView/StepperStep.tsx
+++ b/packages/gitbook/src/components/DocumentView/StepperStep.tsx
@@ -34,13 +34,13 @@ export function StepperStep(props: BlockProps<DocumentBlockStepperStep>) {
             <div className="relative select-none">
                 <div
                     className={tcls(
-                        'can-override-bg can-override-text flex size-[calc(1.75rem+1px)] items-center justify-center rounded-full bg-primary-subtle tabular-nums',
-                        'font-medium text-primary'
+                        'can-override-bg can-override-text flex size-[calc(1.75rem+1px)] items-center justify-center rounded-full bg-primary-solid theme-muted:bg-primary-subtle tabular-nums',
+                        'font-medium text-contrast-primary-solid theme-muted:text-primary'
                     )}
                 >
                     {index + 1}
                 </div>
-                <div className="can-override-bg absolute top-9 bottom-2 left-[0.875rem] w-px bg-primary-subtle" />
+                <div className="can-override-bg absolute top-9 bottom-2 left-[0.875rem] w-px bg-primary-7 theme-muted:bg-primary-subtle" />
             </div>
             <Blocks
                 {...contextProps}
diff --git a/packages/gitbook/src/components/Header/SpacesDropdown.tsx b/packages/gitbook/src/components/Header/SpacesDropdown.tsx
index 3fc1335230..1523774493 100644
--- a/packages/gitbook/src/components/Header/SpacesDropdown.tsx
+++ b/packages/gitbook/src/components/Header/SpacesDropdown.tsx
@@ -62,7 +62,7 @@ export function SpacesDropdown(props: {
                         className
                     )}
                 >
-                    <span className={tcls('line-clamp-1', 'grow')}>{siteSpace.title}</span>
+                    <span className={tcls('truncate', 'grow')}>{siteSpace.title}</span>
                     <DropdownChevron />
                 </div>
             }
diff --git a/packages/gitbook/src/components/PageBody/PageCover.tsx b/packages/gitbook/src/components/PageBody/PageCover.tsx
index d434d5eba9..29fb1a4923 100644
--- a/packages/gitbook/src/components/PageBody/PageCover.tsx
+++ b/packages/gitbook/src/components/PageBody/PageCover.tsx
@@ -37,7 +37,7 @@ export async function PageCover(props: {
                           'lg:ml-0',
                           !page.layout.tableOfContents &&
                           context.customization.header.preset !== 'none'
-                              ? 'lg:-ml-64'
+                              ? 'xl:-ml-64'
                               : null,
                       ]
                     : ['sm:mx-auto', 'max-w-3xl', 'sm:rounded-md', 'mb-8']
diff --git a/packages/gitbook/src/components/PageBody/PageFooterNavigation.tsx b/packages/gitbook/src/components/PageBody/PageFooterNavigation.tsx
index 620cb5970d..b0190b6b18 100644
--- a/packages/gitbook/src/components/PageBody/PageFooterNavigation.tsx
+++ b/packages/gitbook/src/components/PageBody/PageFooterNavigation.tsx
@@ -107,6 +107,7 @@ function NavigationCard(
                 'border',
                 'border-tint-subtle',
                 'rounded',
+                'circular-corners:rounded-2xl',
                 'straight-corners:rounded-none',
                 'hover:border-primary',
                 'text-pretty',
diff --git a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx
index 73e9769a92..1fb8b59442 100644
--- a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx
+++ b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx
@@ -84,6 +84,7 @@ export function TableOfContents(props: {
                         '[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base',
                         '[html.sidebar-filled.theme-gradient_&]:border',
                         'page-no-toc:!bg-transparent',
+                        'page-no-toc:!border-none',
 
                         'sidebar-filled:rounded-xl',
                         'straight-corners:rounded-none',

From 33726c88a04f7c833204eddb6816934b47923769 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Sun, 8 Jun 2025 12:13:14 +0200
Subject: [PATCH 079/127] Generate a llms-full version of the docs (#3285)

---
 .changeset/orange-hounds-sparkle.md           |   5 +
 bun.lock                                      |  41 +++-
 .../[siteData]/llms-full.txt/route.ts         |  14 ++
 packages/gitbook/e2e/internal.spec.ts         |  32 +++
 packages/gitbook/package.json                 |   9 +-
 packages/gitbook/src/lib/urls.ts              |  14 ++
 packages/gitbook/src/routes/llms-full.ts      | 209 ++++++++++++++++++
 packages/gitbook/src/routes/llms.ts           |   2 +-
 8 files changed, 319 insertions(+), 7 deletions(-)
 create mode 100644 .changeset/orange-hounds-sparkle.md
 create mode 100644 packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/llms-full.txt/route.ts
 create mode 100644 packages/gitbook/src/routes/llms-full.ts

diff --git a/.changeset/orange-hounds-sparkle.md b/.changeset/orange-hounds-sparkle.md
new file mode 100644
index 0000000000..28f2b6aa6e
--- /dev/null
+++ b/.changeset/orange-hounds-sparkle.md
@@ -0,0 +1,5 @@
+---
+"gitbook-v2": patch
+---
+
+Generate a llms-full.txt version of the docs site
diff --git a/bun.lock b/bun.lock
index cf33dddcca..5a2653c915 100644
--- a/bun.lock
+++ b/bun.lock
@@ -80,14 +80,19 @@
         "jwt-decode": "^4.0.0",
         "katex": "^0.16.9",
         "mathjax": "^3.2.2",
+        "mdast-util-from-markdown": "^2.0.2",
+        "mdast-util-frontmatter": "^2.0.1",
+        "mdast-util-gfm": "^3.1.0",
         "mdast-util-to-markdown": "^2.1.2",
         "memoizee": "^0.4.17",
+        "micromark-extension-frontmatter": "^2.0.0",
+        "micromark-extension-gfm": "^3.0.0",
         "next": "14.2.26",
         "next-themes": "^0.2.1",
         "nuqs": "^2.2.3",
         "object-hash": "^3.0.0",
         "openapi-types": "^12.1.3",
-        "p-map": "^7.0.0",
+        "p-map": "^7.0.3",
         "parse-cache-control": "^1.0.1",
         "partial-json": "^0.1.7",
         "react": "^19.0.0",
@@ -104,6 +109,8 @@
         "tailwind-merge": "^2.2.0",
         "tailwind-shades": "^1.1.2",
         "unified": "^11.0.5",
+        "unist-util-remove": "^4.0.0",
+        "unist-util-visit": "^5.0.0",
         "url-join": "^5.0.0",
         "usehooks-ts": "^3.1.0",
         "zod": "^3.24.2",
@@ -1809,6 +1816,8 @@
 
     "fastq": ["fastq@1.17.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w=="],
 
+    "fault": ["fault@2.0.1", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ=="],
+
     "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
 
     "fdir": ["fdir@6.4.3", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw=="],
@@ -1835,6 +1844,8 @@
 
     "form-data": ["form-data@4.0.1", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw=="],
 
+    "format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="],
+
     "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
 
     "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="],
@@ -2181,9 +2192,11 @@
 
     "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA=="],
 
-    "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA=="],
+    "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="],
+
+    "mdast-util-frontmatter": ["mdast-util-frontmatter@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "escape-string-regexp": "^5.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0" } }, "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA=="],
 
-    "mdast-util-gfm": ["mdast-util-gfm@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw=="],
+    "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
 
     "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
 
@@ -2227,6 +2240,8 @@
 
     "micromark-core-commonmark": ["micromark-core-commonmark@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA=="],
 
+    "micromark-extension-frontmatter": ["micromark-extension-frontmatter@2.0.0", "", { "dependencies": { "fault": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg=="],
+
     "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
 
     "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
@@ -2395,7 +2410,7 @@
 
     "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
 
-    "p-map": ["p-map@7.0.2", "", {}, "sha512-z4cYYMMdKHzw4O5UkWJImbZynVIo0lSGTXc7bzB1e/rrDqkgGUNysK/o4bTr+0+xKvvLoTyGqYC4Fgljy9qe1Q=="],
+    "p-map": ["p-map@7.0.3", "", {}, "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA=="],
 
     "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
 
@@ -2829,6 +2844,8 @@
 
     "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
 
+    "unist-util-remove": ["unist-util-remove@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg=="],
+
     "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
 
     "unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
@@ -4115,14 +4132,20 @@
 
     "make-dir/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
 
-    "mdast-util-gfm/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="],
+    "mdast-util-gfm-footnote/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA=="],
 
     "mdast-util-gfm-footnote/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="],
 
+    "mdast-util-gfm-strikethrough/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA=="],
+
     "mdast-util-gfm-strikethrough/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="],
 
+    "mdast-util-gfm-table/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA=="],
+
     "mdast-util-gfm-table/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="],
 
+    "mdast-util-gfm-task-list-item/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA=="],
+
     "mdast-util-gfm-task-list-item/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="],
 
     "meow/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
@@ -4191,6 +4214,10 @@
 
     "read-yaml-file/pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="],
 
+    "remark-gfm/mdast-util-gfm": ["mdast-util-gfm@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw=="],
+
+    "remark-parse/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA=="],
+
     "remark-stringify/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="],
 
     "rimraf/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
@@ -5019,6 +5046,10 @@
 
     "read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
 
+    "remark-gfm/mdast-util-gfm/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA=="],
+
+    "remark-gfm/mdast-util-gfm/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="],
+
     "rimraf/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
 
     "rimraf/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
diff --git a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/llms-full.txt/route.ts b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/llms-full.txt/route.ts
new file mode 100644
index 0000000000..7702d6101c
--- /dev/null
+++ b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/llms-full.txt/route.ts
@@ -0,0 +1,14 @@
+import type { NextRequest } from 'next/server';
+
+import { serveLLMsFullTxt } from '@/routes/llms-full';
+import { type RouteLayoutParams, getStaticSiteContext } from '@v2/app/utils';
+
+export const dynamic = 'force-static';
+
+export async function GET(
+    _request: NextRequest,
+    { params }: { params: Promise<RouteLayoutParams> }
+) {
+    const { context } = await getStaticSiteContext(await params);
+    return serveLLMsFullTxt(context);
+}
diff --git a/packages/gitbook/e2e/internal.spec.ts b/packages/gitbook/e2e/internal.spec.ts
index 17f03b9318..925521e02b 100644
--- a/packages/gitbook/e2e/internal.spec.ts
+++ b/packages/gitbook/e2e/internal.spec.ts
@@ -431,6 +431,38 @@ const testCases: TestsCase[] = [
             },
         ],
     },
+    {
+        name: 'llms.txt',
+        skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel',
+        contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/',
+        tests: [
+            {
+                name: 'llms.txt',
+                url: 'llms.txt',
+                screenshot: false,
+                run: async (_page, response) => {
+                    expect(response?.status()).toBe(200);
+                    expect(response?.headers()['content-type']).toContain('text/markdown');
+                },
+            },
+        ],
+    },
+    {
+        name: 'llms-full.txt',
+        skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel',
+        contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/',
+        tests: [
+            {
+                name: 'llms-full.txt',
+                url: 'llms-full.txt',
+                screenshot: false,
+                run: async (_page, response) => {
+                    expect(response?.status()).toBe(200);
+                    expect(response?.headers()['content-type']).toContain('text/markdown');
+                },
+            },
+        ],
+    },
     {
         name: 'Site subdirectory (proxy)',
         skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel',
diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json
index 8443bb615a..62952f844f 100644
--- a/packages/gitbook/package.json
+++ b/packages/gitbook/package.json
@@ -46,13 +46,20 @@
         "katex": "^0.16.9",
         "mathjax": "^3.2.2",
         "mdast-util-to-markdown": "^2.1.2",
+        "mdast-util-from-markdown": "^2.0.2",
+        "mdast-util-frontmatter": "^2.0.1",
+        "mdast-util-gfm": "^3.1.0",
+        "micromark-extension-gfm": "^3.0.0",
+        "micromark-extension-frontmatter": "^2.0.0",
+        "unist-util-remove": "^4.0.0",
+        "unist-util-visit": "^5.0.0",
         "memoizee": "^0.4.17",
         "next": "14.2.26",
         "next-themes": "^0.2.1",
         "nuqs": "^2.2.3",
         "object-hash": "^3.0.0",
         "openapi-types": "^12.1.3",
-        "p-map": "^7.0.0",
+        "p-map": "^7.0.3",
         "parse-cache-control": "^1.0.1",
         "partial-json": "^0.1.7",
         "react": "^19.0.0",
diff --git a/packages/gitbook/src/lib/urls.ts b/packages/gitbook/src/lib/urls.ts
index 0bfa89e769..05dba4f3d0 100644
--- a/packages/gitbook/src/lib/urls.ts
+++ b/packages/gitbook/src/lib/urls.ts
@@ -8,3 +8,17 @@ export function checkIsHttpURL(input: string | URL): boolean {
     const parsed = new URL(input);
     return parsed.protocol === 'http:' || parsed.protocol === 'https:';
 }
+
+/**
+ * True for absolute URLs (`scheme:*`) or hash-only anchors.
+ */
+export function checkIsExternalURL(input: string): boolean {
+    return URL.canParse(input);
+}
+
+/**
+ * True for a hash-only anchor.
+ */
+export function checkIsAnchor(input: string): boolean {
+    return input.startsWith('#');
+}
diff --git a/packages/gitbook/src/routes/llms-full.ts b/packages/gitbook/src/routes/llms-full.ts
new file mode 100644
index 0000000000..90efb73f63
--- /dev/null
+++ b/packages/gitbook/src/routes/llms-full.ts
@@ -0,0 +1,209 @@
+import path from 'node:path';
+import { joinPath } from '@/lib/paths';
+import { getIndexablePages } from '@/lib/sitemap';
+import { getSiteStructureSections } from '@/lib/sites';
+import { checkIsAnchor, checkIsExternalURL } from '@/lib/urls';
+import type { RevisionPageDocument, SiteSection, SiteSpace } from '@gitbook/api';
+import { type GitBookSiteContext, checkIsRootSiteContext } from '@v2/lib/context';
+import { throwIfDataError } from '@v2/lib/data';
+import assertNever from 'assert-never';
+import type { Link, Paragraph, Root } from 'mdast';
+import { fromMarkdown } from 'mdast-util-from-markdown';
+import { frontmatterFromMarkdown } from 'mdast-util-frontmatter';
+import { gfmFromMarkdown, gfmToMarkdown } from 'mdast-util-gfm';
+import { toMarkdown } from 'mdast-util-to-markdown';
+import { frontmatter } from 'micromark-extension-frontmatter';
+import { gfm } from 'micromark-extension-gfm';
+import { pMapIterable } from 'p-map';
+import { remove } from 'unist-util-remove';
+import { visit } from 'unist-util-visit';
+
+// We limit the concurrency to 100 to avoid reaching limit with concurrent requests
+// or file descriptor limits.
+const MAX_CONCURRENCY = 100;
+
+/**
+ * Generate a llms-full.txt file for the site.
+ * As the result can be large, we stream it as we generate it.
+ */
+export async function serveLLMsFullTxt(context: GitBookSiteContext) {
+    if (!checkIsRootSiteContext(context)) {
+        return new Response('llms.txt is only served from the root of the site', { status: 404 });
+    }
+
+    return new Response(
+        new ReadableStream<Uint8Array>({
+            async pull(controller) {
+                await streamMarkdownFromSiteStructure(context, controller);
+                controller.close();
+            },
+        }),
+        {
+            headers: {
+                'Content-Type': 'text/markdown; charset=utf-8',
+            },
+        }
+    );
+}
+
+/**
+ * Stream markdown from site structure.
+ */
+async function streamMarkdownFromSiteStructure(
+    context: GitBookSiteContext,
+    stream: ReadableStreamDefaultController<Uint8Array>
+): Promise<void> {
+    switch (context.structure.type) {
+        case 'sections':
+            return streamMarkdownFromSections(
+                context,
+                stream,
+                getSiteStructureSections(context.structure, { ignoreGroups: true })
+            );
+        case 'siteSpaces':
+            return streamMarkdownFromSiteSpaces(context, stream, context.structure.structure, '');
+        default:
+            assertNever(context.structure);
+    }
+}
+
+/**
+ * Stream markdown from site sections.
+ */
+async function streamMarkdownFromSections(
+    context: GitBookSiteContext,
+    stream: ReadableStreamDefaultController<Uint8Array>,
+    siteSections: SiteSection[]
+): Promise<void> {
+    for (const siteSection of siteSections) {
+        await streamMarkdownFromSiteSpaces(
+            context,
+            stream,
+            siteSection.siteSpaces,
+            siteSection.path
+        );
+    }
+}
+
+/**
+ * Stream markdown from site spaces.
+ */
+async function streamMarkdownFromSiteSpaces(
+    context: GitBookSiteContext,
+    stream: ReadableStreamDefaultController<Uint8Array>,
+    siteSpaces: SiteSpace[],
+    basePath: string
+): Promise<void> {
+    const { dataFetcher } = context;
+
+    for (const siteSpace of siteSpaces) {
+        const siteSpaceUrl = siteSpace.urls.published;
+        if (!siteSpaceUrl) {
+            continue;
+        }
+        const rootPages = await throwIfDataError(
+            dataFetcher.getRevisionPages({
+                spaceId: siteSpace.space.id,
+                revisionId: siteSpace.space.revision,
+                metadata: false,
+            })
+        );
+        const pages = getIndexablePages(rootPages);
+
+        for await (const markdown of pMapIterable(
+            pages,
+            async ({ page }) => {
+                if (page.type !== 'document') {
+                    return '';
+                }
+
+                return getMarkdownForPage(
+                    context,
+                    siteSpace,
+                    page,
+                    joinPath(basePath, siteSpace.path)
+                );
+            },
+            {
+                concurrency: MAX_CONCURRENCY,
+            }
+        )) {
+            stream.enqueue(new TextEncoder().encode(markdown));
+        }
+    }
+}
+
+/**
+ * Get markdown from a page.
+ */
+async function getMarkdownForPage(
+    context: GitBookSiteContext,
+    siteSpace: SiteSpace,
+    page: RevisionPageDocument,
+    basePath: string
+): Promise<string> {
+    const { dataFetcher } = context;
+
+    const pageMarkdown = await throwIfDataError(
+        dataFetcher.getRevisionPageMarkdown({
+            spaceId: siteSpace.space.id,
+            revisionId: siteSpace.space.revision,
+            pageId: page.id,
+        })
+    );
+
+    const tree = fromMarkdown(pageMarkdown, {
+        extensions: [frontmatter(['yaml']), gfm()],
+        mdastExtensions: [frontmatterFromMarkdown(['yaml']), gfmFromMarkdown()],
+    });
+
+    // Remove frontmatter
+    remove(tree, 'yaml');
+
+    if (page.description) {
+        // The first node is the page title as a H1, we insert the description as a paragraph
+        // after it.
+        const descriptionNode: Paragraph = {
+            type: 'paragraph',
+            children: [{ type: 'text', value: page.description }],
+        };
+        tree.children.splice(1, 0, descriptionNode);
+    }
+
+    // Rewrite relative links to absolute links
+    transformLinks(context, tree, { currentPagePath: page.path, basePath });
+
+    const markdown = toMarkdown(tree, { extensions: [gfmToMarkdown()] });
+    return `${markdown}\n\n`;
+}
+
+/**
+ * Re-writes the URL of every relative <a> link so it is expressed from the site-root.
+ */
+export function transformLinks(
+    context: GitBookSiteContext,
+    tree: Root,
+    options: { currentPagePath: string; basePath: string }
+): Root {
+    const { linker } = context;
+    const { currentPagePath, basePath } = options;
+    const currentDir = path.posix.dirname(currentPagePath);
+
+    visit(tree, 'link', (node: Link) => {
+        const original = node.url;
+
+        // Skip anchors, mailto:, http(s):, protocol-like, or already-rooted paths
+        if (checkIsExternalURL(original) || checkIsAnchor(original) || original.startsWith('/')) {
+            return;
+        }
+
+        // Resolve against the current page’s directory and strip any leading “/”
+        const pathInSite = path.posix
+            .normalize(path.posix.join(basePath, currentDir, original))
+            .replace(/^\/+/, '');
+
+        node.url = linker.toPathInSite(pathInSite);
+    });
+
+    return tree;
+}
diff --git a/packages/gitbook/src/routes/llms.ts b/packages/gitbook/src/routes/llms.ts
index 24f9119505..a7e953cbad 100644
--- a/packages/gitbook/src/routes/llms.ts
+++ b/packages/gitbook/src/routes/llms.ts
@@ -46,7 +46,7 @@ export async function serveLLMsTxt(
         }),
         {
             headers: {
-                'Content-Type': 'text/plain; charset=utf-8',
+                'Content-Type': 'text/markdown; charset=utf-8',
             },
         }
     );

From fa12f9eeb6dda45df2efc864aee804e5c8d8acf4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Sun, 8 Jun 2025 14:57:50 +0200
Subject: [PATCH 080/127] Page cover dark-mode specific (#3287)

---
 .changeset/forty-readers-mix.md                      |  5 +++++
 bun.lock                                             | 12 ++++++------
 package.json                                         |  2 +-
 packages/cache-tags/package.json                     |  2 +-
 packages/gitbook-v2/package.json                     |  2 +-
 packages/gitbook/e2e/internal.spec.ts                | 11 +++++++++++
 packages/gitbook/package.json                        |  2 +-
 .../gitbook/src/components/PageBody/PageCover.tsx    |  7 +++++++
 packages/react-contentkit/package.json               |  2 +-
 9 files changed, 34 insertions(+), 11 deletions(-)
 create mode 100644 .changeset/forty-readers-mix.md

diff --git a/.changeset/forty-readers-mix.md b/.changeset/forty-readers-mix.md
new file mode 100644
index 0000000000..98bede85d1
--- /dev/null
+++ b/.changeset/forty-readers-mix.md
@@ -0,0 +1,5 @@
+---
+"gitbook": minor
+---
+
+Support dark-mode specific page cover image
diff --git a/bun.lock b/bun.lock
index 5a2653c915..e36213e8a1 100644
--- a/bun.lock
+++ b/bun.lock
@@ -26,7 +26,7 @@
       "name": "@gitbook/cache-tags",
       "version": "0.3.1",
       "dependencies": {
-        "@gitbook/api": "^0.118.0",
+        "@gitbook/api": "^0.119.0",
         "assert-never": "^1.2.1",
       },
       "devDependencies": {
@@ -51,7 +51,7 @@
       "name": "gitbook",
       "version": "0.12.0",
       "dependencies": {
-        "@gitbook/api": "^0.118.0",
+        "@gitbook/api": "^0.119.0",
         "@gitbook/cache-do": "workspace:*",
         "@gitbook/cache-tags": "workspace:*",
         "@gitbook/colors": "workspace:*",
@@ -150,7 +150,7 @@
       "name": "gitbook-v2",
       "version": "0.3.0",
       "dependencies": {
-        "@gitbook/api": "^0.118.0",
+        "@gitbook/api": "^0.119.0",
         "@gitbook/cache-tags": "workspace:*",
         "@opennextjs/cloudflare": "1.1.0",
         "@sindresorhus/fnv1a": "^3.1.0",
@@ -209,7 +209,7 @@
       "name": "@gitbook/react-contentkit",
       "version": "0.7.0",
       "dependencies": {
-        "@gitbook/api": "^0.118.0",
+        "@gitbook/api": "^0.119.0",
         "@gitbook/icons": "workspace:*",
         "classnames": "^2.5.1",
       },
@@ -267,7 +267,7 @@
   },
   "overrides": {
     "@codemirror/state": "6.4.1",
-    "@gitbook/api": "^0.118.0",
+    "@gitbook/api": "^0.119.0",
     "react": "^19.0.0",
     "react-dom": "^19.0.0",
   },
@@ -632,7 +632,7 @@
 
     "@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="],
 
-    "@gitbook/api": ["@gitbook/api@0.118.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-5bpvXyGNsMOn1Ee7uzohDz/yOgxpVyLZMLu6THYwbG9UeY6BFsWISeqTw03COCX42rVLU5zFReDxRTb7lfZtCw=="],
+    "@gitbook/api": ["@gitbook/api@0.119.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-l2gfJ5+1HT3Sj1OQCh72lUOGU9LV9WbDjgKPjxIFz+NgOhSest1bJ3JhmHP66Xd4ETpmm0YQqnq4y18wKRMjBQ=="],
 
     "@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"],
 
diff --git a/package.json b/package.json
index 8ee2b33518..b4a3acc96b 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
     "packageManager": "bun@1.2.11",
     "overrides": {
         "@codemirror/state": "6.4.1",
-        "@gitbook/api": "^0.118.0",
+        "@gitbook/api": "^0.119.0",
         "react": "^19.0.0",
         "react-dom": "^19.0.0"
     },
diff --git a/packages/cache-tags/package.json b/packages/cache-tags/package.json
index 37e243bdb6..7109f569d1 100644
--- a/packages/cache-tags/package.json
+++ b/packages/cache-tags/package.json
@@ -10,7 +10,7 @@
     },
     "version": "0.3.1",
     "dependencies": {
-        "@gitbook/api": "^0.118.0",
+        "@gitbook/api": "^0.119.0",
         "assert-never": "^1.2.1"
     },
     "devDependencies": {
diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json
index 6eaa3a37c3..a0ead700f2 100644
--- a/packages/gitbook-v2/package.json
+++ b/packages/gitbook-v2/package.json
@@ -3,7 +3,7 @@
     "version": "0.3.0",
     "private": true,
     "dependencies": {
-        "@gitbook/api": "^0.118.0",
+        "@gitbook/api": "^0.119.0",
         "@gitbook/cache-tags": "workspace:*",
         "@opennextjs/cloudflare": "1.1.0",
         "@sindresorhus/fnv1a": "^3.1.0",
diff --git a/packages/gitbook/e2e/internal.spec.ts b/packages/gitbook/e2e/internal.spec.ts
index 925521e02b..a6d87fbf82 100644
--- a/packages/gitbook/e2e/internal.spec.ts
+++ b/packages/gitbook/e2e/internal.spec.ts
@@ -5,6 +5,7 @@ import {
     CustomizationHeaderPreset,
     CustomizationIconsStyle,
     CustomizationSidebarListStyle,
+    CustomizationThemeMode,
 } from '@gitbook/api';
 import { expect } from '@playwright/test';
 import jwt from 'jsonwebtoken';
@@ -681,6 +682,16 @@ const testCases: TestsCase[] = [
                 url: 'page-options/page-with-cover',
                 run: waitForCookiesDialog,
             },
+            {
+                name: 'With cover for dark mode',
+                url: `page-options/page-with-dark-cover${getCustomizationURL({
+                    themes: {
+                        default: CustomizationThemeMode.Dark,
+                        toggeable: false,
+                    },
+                })}`,
+                run: waitForCookiesDialog,
+            },
             {
                 name: 'With hero cover',
                 url: 'page-options/page-with-hero-cover',
diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json
index 62952f844f..fba4221a85 100644
--- a/packages/gitbook/package.json
+++ b/packages/gitbook/package.json
@@ -16,7 +16,7 @@
         "clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math"
     },
     "dependencies": {
-        "@gitbook/api": "^0.118.0",
+        "@gitbook/api": "^0.119.0",
         "@gitbook/cache-do": "workspace:*",
         "@gitbook/cache-tags": "workspace:*",
         "@gitbook/colors": "workspace:*",
diff --git a/packages/gitbook/src/components/PageBody/PageCover.tsx b/packages/gitbook/src/components/PageBody/PageCover.tsx
index 29fb1a4923..67aabd2124 100644
--- a/packages/gitbook/src/components/PageBody/PageCover.tsx
+++ b/packages/gitbook/src/components/PageBody/PageCover.tsx
@@ -22,6 +22,7 @@ export async function PageCover(props: {
 }) {
     const { as, page, cover, context } = props;
     const resolved = cover.ref ? await resolveContentRef(cover.ref, context) : null;
+    const resolvedDark = cover.refDark ? await resolveContentRef(cover.refDark, context) : null;
 
     return (
         <div
@@ -58,6 +59,12 @@ export async function PageCover(props: {
                                   height: defaultPageCover.height,
                               },
                           },
+                    dark: resolvedDark
+                        ? {
+                              src: resolvedDark.href,
+                              size: resolvedDark.file?.dimensions,
+                          }
+                        : null,
                 }}
                 resize={
                     // When using the default cover, we don't want to resize as it's a SVG
diff --git a/packages/react-contentkit/package.json b/packages/react-contentkit/package.json
index 16eab1f683..aa2c162ee6 100644
--- a/packages/react-contentkit/package.json
+++ b/packages/react-contentkit/package.json
@@ -10,7 +10,7 @@
     },
     "dependencies": {
         "classnames": "^2.5.1",
-        "@gitbook/api": "^0.118.0",
+        "@gitbook/api": "^0.119.0",
         "@gitbook/icons": "workspace:*"
     },
     "peerDependencies": {

From d5992f1a5c3402fa496742afa49624348a6a5f3c Mon Sep 17 00:00:00 2001
From: conico974 <nicodorseuil@yahoo.fr>
Date: Mon, 9 Jun 2025 10:40:44 +0200
Subject: [PATCH 081/127] Bump OpenNext to latest (#3288)

Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
---
 bun.lock                                       |  8 +++++---
 .../openNext/customWorkers/doWrangler.jsonc    | 18 ++++++++++++++++++
 packages/gitbook-v2/package.json               |  2 +-
 3 files changed, 24 insertions(+), 4 deletions(-)

diff --git a/bun.lock b/bun.lock
index e36213e8a1..dfd8d2ce4f 100644
--- a/bun.lock
+++ b/bun.lock
@@ -152,7 +152,7 @@
       "dependencies": {
         "@gitbook/api": "^0.119.0",
         "@gitbook/cache-tags": "workspace:*",
-        "@opennextjs/cloudflare": "1.1.0",
+        "@opennextjs/cloudflare": "1.2.1",
         "@sindresorhus/fnv1a": "^3.1.0",
         "assert-never": "^1.2.1",
         "jwt-decode": "^4.0.0",
@@ -798,9 +798,9 @@
 
     "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
 
-    "@opennextjs/aws": ["@opennextjs/aws@3.6.4", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.25.4", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-/bn9N/6dVu9+sC7AptaGJylKUzyDDgqe3yKfvUxXOpy7whIq/+3Bw+q2/bilyQg6FcskbCWUt9nLvNf1hAVmfA=="],
+    "@opennextjs/aws": ["@opennextjs/aws@3.6.5", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "cookie": "^1.0.2", "esbuild": "0.25.4", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-wni+CWlRCyWfhNfekQBBPPkrDDnaGdZLN9hMybKI0wKOKTO+zhPOqR65Eh3V0pzWAi84Sureb5mdMuLwCxAAcw=="],
 
-    "@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.1.0", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.6.4", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.14.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-8DOzpLiewivA1pwRNGVPDXbrF79bzJiHscY+M1tekwvKqEqM26CRYafhrA5IDCeyteNaL9AZD6X4DLuCn/eeYQ=="],
+    "@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.2.1", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.6.5", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.19.1" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-cOco+nHwlo/PLB1bThF8IIvaS8PgAT9MEI5ZttFO/qt6spgvr2lUaPkpjgSIQmI3sBIEG2cLUykvQ2nbbZEcVw=="],
 
     "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
 
@@ -3666,6 +3666,8 @@
 
     "@node-minify/core/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
 
+    "@opennextjs/aws/cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
+
     "@opennextjs/aws/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="],
 
     "@radix-ui/react-collection/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="],
diff --git a/packages/gitbook-v2/openNext/customWorkers/doWrangler.jsonc b/packages/gitbook-v2/openNext/customWorkers/doWrangler.jsonc
index f5d2f726b8..e239202bef 100644
--- a/packages/gitbook-v2/openNext/customWorkers/doWrangler.jsonc
+++ b/packages/gitbook-v2/openNext/customWorkers/doWrangler.jsonc
@@ -22,6 +22,12 @@
                     "bucket_name": "gitbook-open-v2-cache-preview"
                 }
             ],
+            "services": [
+                {
+                    "binding": "WORKER_SELF_REFERENCE",
+                    "service": "gitbook-open-v2-preview"
+                }
+            ],
             "durable_objects": {
                 "bindings": [
                     {
@@ -61,6 +67,12 @@
                     "service": "gitbook-x-staging-tail"
                 }
             ],
+            "services": [
+                {
+                    "binding": "WORKER_SELF_REFERENCE",
+                    "service": "gitbook-open-v2-staging"
+                }
+            ],
             "durable_objects": {
                 "bindings": [
                     {
@@ -100,6 +112,12 @@
                     "service": "gitbook-x-prod-tail"
                 }
             ],
+            "services": [
+                {
+                    "binding": "WORKER_SELF_REFERENCE",
+                    "service": "gitbook-open-v2-production"
+                }
+            ],
             "durable_objects": {
                 "bindings": [
                     {
diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json
index a0ead700f2..48aabef556 100644
--- a/packages/gitbook-v2/package.json
+++ b/packages/gitbook-v2/package.json
@@ -5,7 +5,7 @@
     "dependencies": {
         "@gitbook/api": "^0.119.0",
         "@gitbook/cache-tags": "workspace:*",
-        "@opennextjs/cloudflare": "1.1.0",
+        "@opennextjs/cloudflare": "1.2.1",
         "@sindresorhus/fnv1a": "^3.1.0",
         "assert-never": "^1.2.1",
         "jwt-decode": "^4.0.0",

From f58b904ba6585d44f42f70d93ab72c70b7de6447 Mon Sep 17 00:00:00 2001
From: conico974 <nicodorseuil@yahoo.fr>
Date: Mon, 9 Jun 2025 11:37:29 +0200
Subject: [PATCH 082/127] Encode customization header (#3289)

Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
---
 .changeset/lazy-pants-matter.md           | 6 ++++++
 packages/gitbook-v2/src/middleware.ts     | 3 ++-
 packages/gitbook/src/lib/customization.ts | 5 ++++-
 packages/gitbook/src/middleware.ts        | 4 +++-
 4 files changed, 15 insertions(+), 3 deletions(-)
 create mode 100644 .changeset/lazy-pants-matter.md

diff --git a/.changeset/lazy-pants-matter.md b/.changeset/lazy-pants-matter.md
new file mode 100644
index 0000000000..6852b473b9
--- /dev/null
+++ b/.changeset/lazy-pants-matter.md
@@ -0,0 +1,6 @@
+---
+"gitbook-v2": patch
+"gitbook": patch
+---
+
+encode customization header
diff --git a/packages/gitbook-v2/src/middleware.ts b/packages/gitbook-v2/src/middleware.ts
index 916d53d61d..49ffb9dba8 100644
--- a/packages/gitbook-v2/src/middleware.ts
+++ b/packages/gitbook-v2/src/middleware.ts
@@ -226,7 +226,8 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
         const customization = siteRequestURL.searchParams.get('customization');
         if (customization && validateSerializedCustomization(customization)) {
             routeType = 'dynamic';
-            requestHeaders.set(MiddlewareHeaders.Customization, customization);
+            // We need to encode the customization headers, otherwise it will fail for some customization values containing non ASCII chars on vercel.
+            requestHeaders.set(MiddlewareHeaders.Customization, encodeURIComponent(customization));
         }
         const theme = siteRequestURL.searchParams.get('theme');
         if (theme === CustomizationThemeMode.Dark || theme === CustomizationThemeMode.Light) {
diff --git a/packages/gitbook/src/lib/customization.ts b/packages/gitbook/src/lib/customization.ts
index 38440234b1..d23a0a88cb 100644
--- a/packages/gitbook/src/lib/customization.ts
+++ b/packages/gitbook/src/lib/customization.ts
@@ -12,9 +12,12 @@ export async function getDynamicCustomizationSettings(
 ): Promise<SiteCustomizationSettings> {
     const headersList = await headers();
     const extend = headersList.get(MiddlewareHeaders.Customization);
+
     if (extend) {
         try {
-            const parsedSettings = rison.decode_object<SiteCustomizationSettings>(extend);
+            // We need to decode it first as it is URL encoded, then decode the Rison object
+            const unencoded = decodeURIComponent(extend);
+            const parsedSettings = rison.decode_object<SiteCustomizationSettings>(unencoded);
 
             return parsedSettings;
         } catch (_error) {}
diff --git a/packages/gitbook/src/middleware.ts b/packages/gitbook/src/middleware.ts
index 3e5f682ccc..9edb751243 100644
--- a/packages/gitbook/src/middleware.ts
+++ b/packages/gitbook/src/middleware.ts
@@ -201,7 +201,9 @@ export async function middleware(request: NextRequest) {
 
     const customization = url.searchParams.get('customization');
     if (customization && validateSerializedCustomization(customization)) {
-        headers.set(MiddlewareHeaders.Customization, customization);
+        // We need to encode the customization to ensure it is properly encoded in vercel.
+        // We do it here as well so that we have a single method to decode it later.
+        headers.set(MiddlewareHeaders.Customization, encodeURIComponent(customization));
     }
 
     const theme = url.searchParams.get('theme');

From c0ee60ec13c09efe7e96bff4f41d8001906e1317 Mon Sep 17 00:00:00 2001
From: Brett Jephson <brett@gitbook.io>
Date: Mon, 9 Jun 2025 11:37:56 +0100
Subject: [PATCH 083/127] RND-6843: columns block (#3257)

Co-authored-by: Steven Hall <steven@gitbook.io>
---
 .changeset/flat-wolves-poke.md                |  6 ++
 packages/gitbook/e2e/internal.spec.ts         |  1 +
 .../src/components/DocumentView/Block.tsx     | 12 +--
 .../DocumentView/Columns/Columns.tsx          | 80 +++++++++++++++++++
 .../components/DocumentView/Columns/index.ts  |  1 +
 5 files changed, 94 insertions(+), 6 deletions(-)
 create mode 100644 .changeset/flat-wolves-poke.md
 create mode 100644 packages/gitbook/src/components/DocumentView/Columns/Columns.tsx
 create mode 100644 packages/gitbook/src/components/DocumentView/Columns/index.ts

diff --git a/.changeset/flat-wolves-poke.md b/.changeset/flat-wolves-poke.md
new file mode 100644
index 0000000000..2b5c201cd1
--- /dev/null
+++ b/.changeset/flat-wolves-poke.md
@@ -0,0 +1,6 @@
+---
+"gitbook-v2": patch
+"gitbook": patch
+---
+
+Adds Columns layout block to GBO
diff --git a/packages/gitbook/e2e/internal.spec.ts b/packages/gitbook/e2e/internal.spec.ts
index a6d87fbf82..3efe00bd27 100644
--- a/packages/gitbook/e2e/internal.spec.ts
+++ b/packages/gitbook/e2e/internal.spec.ts
@@ -666,6 +666,7 @@ const testCases: TestsCase[] = [
                 name: 'Stepper',
                 url: 'blocks/stepper',
             },
+            { name: 'Columns', url: 'blocks/columns' },
         ],
     },
     {
diff --git a/packages/gitbook/src/components/DocumentView/Block.tsx b/packages/gitbook/src/components/DocumentView/Block.tsx
index 3dba09bf56..ee4f9c7c8e 100644
--- a/packages/gitbook/src/components/DocumentView/Block.tsx
+++ b/packages/gitbook/src/components/DocumentView/Block.tsx
@@ -12,6 +12,7 @@ import type { ClassValue } from '@/lib/tailwind';
 
 import { BlockContentRef } from './BlockContentRef';
 import { CodeBlock } from './CodeBlock';
+import { Columns } from './Columns';
 import { Divider } from './Divider';
 import type { DocumentContextProps } from './DocumentView';
 import { Drawing } from './Drawing';
@@ -68,6 +69,8 @@ export function Block<T extends DocumentBlock>(props: BlockProps<T>) {
                 return <List {...props} block={block} />;
             case 'list-item':
                 return <ListItem {...props} block={block} />;
+            case 'columns':
+                return <Columns {...props} block={block} />;
             case 'code':
                 return <CodeBlock {...props} block={block} />;
             case 'hint':
@@ -112,10 +115,8 @@ export function Block<T extends DocumentBlock>(props: BlockProps<T>) {
             case 'image':
             case 'code-line':
             case 'tabs-item':
-                throw new Error(`Blocks (${block.type}) should be directly rendered by parent`);
-            case 'columns':
             case 'column':
-                return null;
+                throw new Error(`Blocks (${block.type}) should be directly rendered by parent`);
             default:
                 return nullIfNever(block);
         }
@@ -171,6 +172,7 @@ export function BlockSkeleton(props: { block: DocumentBlock; style: ClassValue }
         case 'integration':
         case 'stepper':
         case 'reusable-content':
+        case 'columns':
             return <SkeletonCard id={id} style={style} />;
         case 'embed':
         case 'images':
@@ -179,10 +181,8 @@ export function BlockSkeleton(props: { block: DocumentBlock; style: ClassValue }
         case 'image':
         case 'code-line':
         case 'tabs-item':
-            throw new Error(`Blocks (${block.type}) should be directly rendered by parent`);
-        case 'columns':
         case 'column':
-            return null;
+            throw new Error(`Blocks (${block.type}) should be directly rendered by parent`);
         default:
             return nullIfNever(block);
     }
diff --git a/packages/gitbook/src/components/DocumentView/Columns/Columns.tsx b/packages/gitbook/src/components/DocumentView/Columns/Columns.tsx
new file mode 100644
index 0000000000..4d74a0490d
--- /dev/null
+++ b/packages/gitbook/src/components/DocumentView/Columns/Columns.tsx
@@ -0,0 +1,80 @@
+import { type ClassValue, tcls } from '@/lib/tailwind';
+import type { DocumentBlockColumns, Length } from '@gitbook/api';
+import type { BlockProps } from '../Block';
+import { Blocks } from '../Blocks';
+
+export function Columns(props: BlockProps<DocumentBlockColumns>) {
+    const { block, style, ancestorBlocks, document, context } = props;
+    return (
+        <div className={tcls('flex flex-col gap-x-8 md:flex-row', style)}>
+            {block.nodes.map((columnBlock) => {
+                const width = columnBlock.data.width;
+                const { className, style } = transformLengthToCSS(width);
+                return (
+                    <Column key={columnBlock.key} className={className} style={style}>
+                        <Blocks
+                            key={columnBlock.key}
+                            nodes={columnBlock.nodes}
+                            document={document}
+                            ancestorBlocks={[...ancestorBlocks, block, columnBlock]}
+                            context={context}
+                            blockStyle="flip-heading-hash"
+                            style="w-full space-y-4"
+                        />
+                    </Column>
+                );
+            })}
+        </div>
+    );
+}
+
+export function Column(props: {
+    children?: React.ReactNode;
+    className?: ClassValue;
+    style?: React.CSSProperties;
+}) {
+    return (
+        <div className={tcls('flex-col', props.className)} style={props.style}>
+            {props.children}
+        </div>
+    );
+}
+
+function transformLengthToCSS(length: Length | undefined) {
+    if (!length) {
+        return { className: ['md:w-full'] }; // default to full width if no length is specified
+    }
+
+    if (typeof length === 'number') {
+        return { style: undefined }; // not implemented yet with non-percentage lengths
+    }
+
+    if (length.unit === '%') {
+        return {
+            className: [
+                'md:flex-shrink-0',
+                COLUMN_WIDTHS[Math.round(length.value * 0.01 * (COLUMN_WIDTHS.length - 1))],
+            ],
+        };
+    }
+
+    return { style: undefined }; // not implemented yet with non-percentage lengths
+}
+
+// Tailwind CSS classes for column widths.
+// The index of the array corresponds to the percentage width of the column.
+const COLUMN_WIDTHS = [
+    'md:w-0',
+    'md:w-1/12',
+    'md:w-2/12',
+    'md:w-3/12',
+    'md:w-4/12',
+    'md:w-5/12',
+    'md:w-6/12',
+    'md:w-7/12',
+    'md:w-8/12',
+    'md:w-9/12',
+    'md:w-10/12',
+    'md:w-11/12',
+    'md:w-full',
+];
diff --git a/packages/gitbook/src/components/DocumentView/Columns/index.ts b/packages/gitbook/src/components/DocumentView/Columns/index.ts
new file mode 100644
index 0000000000..a8b4f25b41
--- /dev/null
+++ b/packages/gitbook/src/components/DocumentView/Columns/index.ts
@@ -0,0 +1 @@
+export * from './Columns';

From 231167d3e1ec3da9863d634051247045919f6cb8 Mon Sep 17 00:00:00 2001
From: Zeno Kapitein <zeno@gitbook.io>
Date: Tue, 10 Jun 2025 17:48:06 +0200
Subject: [PATCH 084/127] Make icons for page groups more contrasting (#3286)

---
 .changeset/sharp-jeans-burn.md                               | 5 +++++
 .../gitbook/src/components/TableOfContents/TOCPageIcon.tsx   | 2 +-
 .../src/components/TableOfContents/ToggleableLinkItem.tsx    | 2 +-
 3 files changed, 7 insertions(+), 2 deletions(-)
 create mode 100644 .changeset/sharp-jeans-burn.md

diff --git a/.changeset/sharp-jeans-burn.md b/.changeset/sharp-jeans-burn.md
new file mode 100644
index 0000000000..c990f8b7bf
--- /dev/null
+++ b/.changeset/sharp-jeans-burn.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Make icons for page groups more contrasting
diff --git a/packages/gitbook/src/components/TableOfContents/TOCPageIcon.tsx b/packages/gitbook/src/components/TableOfContents/TOCPageIcon.tsx
index aef502c446..266f3c1e2b 100644
--- a/packages/gitbook/src/components/TableOfContents/TOCPageIcon.tsx
+++ b/packages/gitbook/src/components/TableOfContents/TOCPageIcon.tsx
@@ -13,7 +13,7 @@ export function TOCPageIcon({ page }: { page: RevisionPage }) {
             page={page}
             style={tcls(
                 'text-base',
-                'text-tint-strong/6',
+                '[.toclink_&]:text-tint-strong/6',
                 'group-aria-current-page/toclink:text-primary-subtle',
                 'contrast-more:group-aria-current-page/toclink:text-primary',
 
diff --git a/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx b/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx
index 92502433ba..b118501f4c 100644
--- a/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx
+++ b/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx
@@ -67,7 +67,7 @@ function LinkItem(
             insights={insights}
             aria-current={isActive ? 'page' : undefined}
             className={tcls(
-                'group/toclink relative transition-colors',
+                'group/toclink toclink relative transition-colors',
                 'flex flex-row justify-between',
                 'circular-corners:rounded-2xl rounded-md straight-corners:rounded-none p-1.5 pl-3',
                 'text-balance font-normal text-sm text-tint-strong/7 hover:bg-tint-hover hover:text-tint-strong contrast-more:text-tint-strong',

From 2860beacdc2562d7a359524b80fd9bb21f9edade Mon Sep 17 00:00:00 2001
From: conico974 <nicodorseuil@yahoo.fr>
Date: Tue, 10 Jun 2025 19:05:07 +0200
Subject: [PATCH 085/127] revert to use withoutConcurrentExecution (#3292)

Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
---
 packages/gitbook-v2/src/lib/data/api.ts       | 1027 +++++++++--------
 packages/gitbook-v2/src/lib/data/memoize.ts   |   92 ++
 packages/gitbook/e2e/customers.spec.ts        |   10 +-
 .../components/DocumentView/InlineLink.tsx    |    3 +-
 packages/gitbook/src/lib/references.tsx       |    2 +-
 5 files changed, 652 insertions(+), 482 deletions(-)
 create mode 100644 packages/gitbook-v2/src/lib/data/memoize.ts

diff --git a/packages/gitbook-v2/src/lib/data/api.ts b/packages/gitbook-v2/src/lib/data/api.ts
index 44313bfc57..52ba17c1a3 100644
--- a/packages/gitbook-v2/src/lib/data/api.ts
+++ b/packages/gitbook-v2/src/lib/data/api.ts
@@ -9,6 +9,7 @@ import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-t
 import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env';
 import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache';
 import { DataFetcherError, wrapDataFetcherError } from './errors';
+import { withCacheKey, withoutConcurrentExecution } from './memoize';
 import type { GitBookDataFetcher } from './types';
 
 interface DataFetcherInput {
@@ -194,518 +195,594 @@ export function createDataFetcher(
     };
 }
 
-const getUserById = async (input: DataFetcherInput, params: { userId: string }) => {
-    'use cache';
-    return trace(`getUserById(${params.userId})`, async () => {
-        return wrapDataFetcherError(async () => {
-            const api = apiClient(input);
-            const res = await api.users.getUserById(params.userId, {
-                ...noCacheFetchOptions,
+const getUserById = withCacheKey(
+    withoutConcurrentExecution(async (_, input: DataFetcherInput, params: { userId: string }) => {
+        'use cache';
+        return trace(`getUserById(${params.userId})`, async () => {
+            return wrapDataFetcherError(async () => {
+                const api = apiClient(input);
+                const res = await api.users.getUserById(params.userId, {
+                    ...noCacheFetchOptions,
+                });
+                cacheTag(...getCacheTagsFromResponse(res));
+                cacheLife('days');
+                return res.data;
             });
-            cacheTag(...getCacheTagsFromResponse(res));
-            cacheLife('days');
-            return res.data;
         });
-    });
-};
+    })
+);
+
+const getSpace = withCacheKey(
+    withoutConcurrentExecution(
+        async (
+            _,
+            input: DataFetcherInput,
+            params: { spaceId: string; shareKey: string | undefined }
+        ) => {
+            'use cache';
+            cacheTag(
+                getCacheTag({
+                    tag: 'space',
+                    space: params.spaceId,
+                })
+            );
 
-const getSpace = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; shareKey: string | undefined }
-) => {
-    'use cache';
-    cacheTag(
-        getCacheTag({
-            tag: 'space',
-            space: params.spaceId,
-        })
-    );
+            return trace(`getSpace(${params.spaceId}, ${params.shareKey})`, async () => {
+                return wrapDataFetcherError(async () => {
+                    const api = apiClient(input);
+                    const res = await api.spaces.getSpaceById(
+                        params.spaceId,
+                        {
+                            shareKey: params.shareKey,
+                        },
+                        {
+                            ...noCacheFetchOptions,
+                        }
+                    );
+                    cacheTag(...getCacheTagsFromResponse(res));
+                    cacheLife('days');
+                    return res.data;
+                });
+            });
+        }
+    )
+);
+
+const getChangeRequest = withCacheKey(
+    withoutConcurrentExecution(
+        async (
+            _,
+            input: DataFetcherInput,
+            params: { spaceId: string; changeRequestId: string }
+        ) => {
+            'use cache';
+            cacheTag(
+                getCacheTag({
+                    tag: 'change-request',
+                    space: params.spaceId,
+                    changeRequest: params.changeRequestId,
+                })
+            );
 
-    return trace(`getSpace(${params.spaceId}, ${params.shareKey})`, async () => {
-        return wrapDataFetcherError(async () => {
-            const api = apiClient(input);
-            const res = await api.spaces.getSpaceById(
-                params.spaceId,
-                {
-                    shareKey: params.shareKey,
-                },
-                {
-                    ...noCacheFetchOptions,
+            return trace(
+                `getChangeRequest(${params.spaceId}, ${params.changeRequestId})`,
+                async () => {
+                    return wrapDataFetcherError(async () => {
+                        const api = apiClient(input);
+                        const res = await api.spaces.getChangeRequestById(
+                            params.spaceId,
+                            params.changeRequestId,
+                            {
+                                ...noCacheFetchOptions,
+                            }
+                        );
+                        cacheTag(...getCacheTagsFromResponse(res));
+                        cacheLife('minutes');
+                        return res.data;
+                    });
                 }
             );
-            cacheTag(...getCacheTagsFromResponse(res));
-            cacheLife('days');
-            return res.data;
-        });
-    });
-};
-
-const getChangeRequest = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; changeRequestId: string }
-) => {
-    'use cache';
-    cacheTag(
-        getCacheTag({
-            tag: 'change-request',
-            space: params.spaceId,
-            changeRequest: params.changeRequestId,
-        })
-    );
-
-    return trace(`getChangeRequest(${params.spaceId}, ${params.changeRequestId})`, async () => {
-        return wrapDataFetcherError(async () => {
-            const api = apiClient(input);
-            const res = await api.spaces.getChangeRequestById(
-                params.spaceId,
-                params.changeRequestId,
-                {
-                    ...noCacheFetchOptions,
+        }
+    )
+);
+
+const getRevision = withCacheKey(
+    withoutConcurrentExecution(
+        async (
+            _,
+            input: DataFetcherInput,
+            params: { spaceId: string; revisionId: string; metadata: boolean }
+        ) => {
+            'use cache';
+            return trace(`getRevision(${params.spaceId}, ${params.revisionId})`, async () => {
+                return wrapDataFetcherError(async () => {
+                    const api = apiClient(input);
+                    const res = await api.spaces.getRevisionById(
+                        params.spaceId,
+                        params.revisionId,
+                        {
+                            metadata: params.metadata,
+                        },
+                        {
+                            ...noCacheFetchOptions,
+                        }
+                    );
+                    cacheTag(...getCacheTagsFromResponse(res));
+                    cacheLife('max');
+                    return res.data;
+                });
+            });
+        }
+    )
+);
+
+const getRevisionPages = withCacheKey(
+    withoutConcurrentExecution(
+        async (
+            _,
+            input: DataFetcherInput,
+            params: { spaceId: string; revisionId: string; metadata: boolean }
+        ) => {
+            'use cache';
+            return trace(`getRevisionPages(${params.spaceId}, ${params.revisionId})`, async () => {
+                return wrapDataFetcherError(async () => {
+                    const api = apiClient(input);
+                    const res = await api.spaces.listPagesInRevisionById(
+                        params.spaceId,
+                        params.revisionId,
+                        {
+                            metadata: params.metadata,
+                        },
+                        {
+                            ...noCacheFetchOptions,
+                        }
+                    );
+                    cacheTag(...getCacheTagsFromResponse(res));
+                    cacheLife('max');
+                    return res.data.pages;
+                });
+            });
+        }
+    )
+);
+
+const getRevisionFile = withCacheKey(
+    withoutConcurrentExecution(
+        async (
+            _,
+            input: DataFetcherInput,
+            params: { spaceId: string; revisionId: string; fileId: string }
+        ) => {
+            'use cache';
+            return trace(
+                `getRevisionFile(${params.spaceId}, ${params.revisionId}, ${params.fileId})`,
+                async () => {
+                    return wrapDataFetcherError(async () => {
+                        const api = apiClient(input);
+                        const res = await api.spaces.getFileInRevisionById(
+                            params.spaceId,
+                            params.revisionId,
+                            params.fileId,
+                            {},
+                            {
+                                ...noCacheFetchOptions,
+                            }
+                        );
+                        cacheTag(...getCacheTagsFromResponse(res));
+                        cacheLife('max');
+                        return res.data;
+                    });
                 }
             );
-            cacheTag(...getCacheTagsFromResponse(res));
-            cacheLife('minutes');
-            return res.data;
-        });
-    });
-};
-
-const getRevision = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; revisionId: string; metadata: boolean }
-) => {
-    'use cache';
-    return trace(`getRevision(${params.spaceId}, ${params.revisionId})`, async () => {
-        return wrapDataFetcherError(async () => {
-            const api = apiClient(input);
-            const res = await api.spaces.getRevisionById(
-                params.spaceId,
-                params.revisionId,
-                {
-                    metadata: params.metadata,
-                },
-                {
-                    ...noCacheFetchOptions,
+        }
+    )
+);
+
+const getRevisionPageMarkdown = withCacheKey(
+    withoutConcurrentExecution(
+        async (
+            _,
+            input: DataFetcherInput,
+            params: { spaceId: string; revisionId: string; pageId: string }
+        ) => {
+            'use cache';
+            return trace(
+                `getRevisionPageMarkdown(${params.spaceId}, ${params.revisionId}, ${params.pageId})`,
+                async () => {
+                    return wrapDataFetcherError(async () => {
+                        const api = apiClient(input);
+                        const res = await api.spaces.getPageInRevisionById(
+                            params.spaceId,
+                            params.revisionId,
+                            params.pageId,
+                            {
+                                format: 'markdown',
+                            },
+                            {
+                                ...noCacheFetchOptions,
+                            }
+                        );
+
+                        cacheTag(...getCacheTagsFromResponse(res));
+                        cacheLife('max');
+
+                        if (!('markdown' in res.data)) {
+                            throw new DataFetcherError('Page is not a document', 404);
+                        }
+                        return res.data.markdown;
+                    });
                 }
             );
-            cacheTag(...getCacheTagsFromResponse(res));
-            cacheLife('max');
-            return res.data;
-        });
-    });
-};
-
-const getRevisionPages = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; revisionId: string; metadata: boolean }
-) => {
-    'use cache';
-    return trace(`getRevisionPages(${params.spaceId}, ${params.revisionId})`, async () => {
-        return wrapDataFetcherError(async () => {
-            const api = apiClient(input);
-            const res = await api.spaces.listPagesInRevisionById(
-                params.spaceId,
-                params.revisionId,
-                {
-                    metadata: params.metadata,
-                },
-                {
-                    ...noCacheFetchOptions,
+        }
+    )
+);
+
+const getRevisionPageByPath = withCacheKey(
+    withoutConcurrentExecution(
+        async (
+            _,
+            input: DataFetcherInput,
+            params: { spaceId: string; revisionId: string; path: string }
+        ) => {
+            'use cache';
+            return trace(
+                `getRevisionPageByPath(${params.spaceId}, ${params.revisionId}, ${params.path})`,
+                async () => {
+                    const encodedPath = encodeURIComponent(params.path);
+                    return wrapDataFetcherError(async () => {
+                        const api = apiClient(input);
+                        const res = await api.spaces.getPageInRevisionByPath(
+                            params.spaceId,
+                            params.revisionId,
+                            encodedPath,
+                            {},
+                            {
+                                ...noCacheFetchOptions,
+                            }
+                        );
+                        cacheTag(...getCacheTagsFromResponse(res));
+                        cacheLife('max');
+                        return res.data;
+                    });
                 }
             );
-            cacheTag(...getCacheTagsFromResponse(res));
-            cacheLife('max');
-            return res.data.pages;
-        });
-    });
-};
-
-const getRevisionFile = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; revisionId: string; fileId: string }
-) => {
-    'use cache';
-    return trace(
-        `getRevisionFile(${params.spaceId}, ${params.revisionId}, ${params.fileId})`,
-        async () => {
-            return wrapDataFetcherError(async () => {
-                const api = apiClient(input);
-                const res = await api.spaces.getFileInRevisionById(
-                    params.spaceId,
-                    params.revisionId,
-                    params.fileId,
-                    {},
-                    {
-                        ...noCacheFetchOptions,
-                    }
-                );
-                cacheTag(...getCacheTagsFromResponse(res));
-                cacheLife('max');
-                return res.data;
+        }
+    )
+);
+
+const getDocument = withCacheKey(
+    withoutConcurrentExecution(
+        async (_, input: DataFetcherInput, params: { spaceId: string; documentId: string }) => {
+            'use cache';
+            return trace(`getDocument(${params.spaceId}, ${params.documentId})`, async () => {
+                return wrapDataFetcherError(async () => {
+                    const api = apiClient(input);
+                    const res = await api.spaces.getDocumentById(
+                        params.spaceId,
+                        params.documentId,
+                        {},
+                        {
+                            ...noCacheFetchOptions,
+                        }
+                    );
+                    cacheTag(...getCacheTagsFromResponse(res));
+                    cacheLife('max');
+                    return res.data;
+                });
             });
         }
-    );
-};
-
-const getRevisionPageMarkdown = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; revisionId: string; pageId: string }
-) => {
-    'use cache';
-    return trace(
-        `getRevisionPageMarkdown(${params.spaceId}, ${params.revisionId}, ${params.pageId})`,
-        async () => {
-            return wrapDataFetcherError(async () => {
-                const api = apiClient(input);
-                const res = await api.spaces.getPageInRevisionById(
-                    params.spaceId,
-                    params.revisionId,
-                    params.pageId,
+    )
+);
+
+const getComputedDocument = withCacheKey(
+    withoutConcurrentExecution(
+        async (
+            _,
+            input: DataFetcherInput,
+            params: {
+                spaceId: string;
+                organizationId: string;
+                source: ComputedContentSource;
+                seed: string;
+            }
+        ) => {
+            'use cache';
+            cacheTag(
+                ...getComputedContentSourceCacheTags(
                     {
-                        format: 'markdown',
+                        spaceId: params.spaceId,
+                        organizationId: params.organizationId,
                     },
-                    {
-                        ...noCacheFetchOptions,
-                    }
-                );
-
-                cacheTag(...getCacheTagsFromResponse(res));
-                cacheLife('max');
+                    params.source
+                )
+            );
 
-                if (!('markdown' in res.data)) {
-                    throw new DataFetcherError('Page is not a document', 404);
+            return trace(
+                `getComputedDocument(${params.spaceId}, ${params.organizationId}, ${params.source.type}, ${params.seed})`,
+                async () => {
+                    return wrapDataFetcherError(async () => {
+                        const api = apiClient(input);
+                        const res = await api.spaces.getComputedDocument(
+                            params.spaceId,
+                            {
+                                source: params.source,
+                                seed: params.seed,
+                            },
+                            {},
+                            {
+                                ...noCacheFetchOptions,
+                            }
+                        );
+                        cacheTag(...getCacheTagsFromResponse(res));
+                        cacheLife('max');
+                        return res.data;
+                    });
                 }
-                return res.data.markdown;
-            });
-        }
-    );
-};
-
-const getRevisionPageByPath = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; revisionId: string; path: string }
-) => {
-    'use cache';
-    return trace(
-        `getRevisionPageByPath(${params.spaceId}, ${params.revisionId}, ${params.path})`,
-        async () => {
-            const encodedPath = encodeURIComponent(params.path);
-            return wrapDataFetcherError(async () => {
-                const api = apiClient(input);
-                const res = await api.spaces.getPageInRevisionByPath(
-                    params.spaceId,
-                    params.revisionId,
-                    encodedPath,
-                    {},
-                    {
-                        ...noCacheFetchOptions,
-                    }
-                );
-                cacheTag(...getCacheTagsFromResponse(res));
-                cacheLife('max');
-                return res.data;
-            });
+            );
         }
-    );
-};
-
-const getDocument = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; documentId: string }
-) => {
-    'use cache';
-    return trace(`getDocument(${params.spaceId}, ${params.documentId})`, async () => {
-        return wrapDataFetcherError(async () => {
-            const api = apiClient(input);
-            const res = await api.spaces.getDocumentById(
-                params.spaceId,
-                params.documentId,
-                {},
-                {
-                    ...noCacheFetchOptions,
+    )
+);
+
+const getReusableContent = withCacheKey(
+    withoutConcurrentExecution(
+        async (
+            _,
+            input: DataFetcherInput,
+            params: { spaceId: string; revisionId: string; reusableContentId: string }
+        ) => {
+            'use cache';
+            return trace(
+                `getReusableContent(${params.spaceId}, ${params.revisionId}, ${params.reusableContentId})`,
+                async () => {
+                    return wrapDataFetcherError(async () => {
+                        const api = apiClient(input);
+                        const res = await api.spaces.getReusableContentInRevisionById(
+                            params.spaceId,
+                            params.revisionId,
+                            params.reusableContentId,
+                            {},
+                            {
+                                ...noCacheFetchOptions,
+                            }
+                        );
+                        cacheTag(...getCacheTagsFromResponse(res));
+                        cacheLife('max');
+                        return res.data;
+                    });
                 }
             );
-            cacheTag(...getCacheTagsFromResponse(res));
-            cacheLife('max');
-            return res.data;
-        });
-    });
-};
-
-const getComputedDocument = async (
-    input: DataFetcherInput,
-    params: {
-        spaceId: string;
-        organizationId: string;
-        source: ComputedContentSource;
-        seed: string;
-    }
-) => {
-    'use cache';
-    cacheTag(
-        ...getComputedContentSourceCacheTags(
-            {
-                spaceId: params.spaceId,
-                organizationId: params.organizationId,
-            },
-            params.source
-        )
-    );
-
-    return trace(
-        `getComputedDocument(${params.spaceId}, ${params.organizationId}, ${params.source.type}, ${params.seed})`,
-        async () => {
-            return wrapDataFetcherError(async () => {
-                const api = apiClient(input);
-                const res = await api.spaces.getComputedDocument(
-                    params.spaceId,
-                    {
-                        source: params.source,
-                        seed: params.seed,
-                    },
-                    {},
-                    {
-                        ...noCacheFetchOptions,
-                    }
-                );
-                cacheTag(...getCacheTagsFromResponse(res));
-                cacheLife('max');
-                return res.data;
-            });
         }
-    );
-};
+    )
+);
+
+const getLatestOpenAPISpecVersionContent = withCacheKey(
+    withoutConcurrentExecution(
+        async (_, input: DataFetcherInput, params: { organizationId: string; slug: string }) => {
+            'use cache';
+            cacheTag(
+                getCacheTag({
+                    tag: 'openapi',
+                    organization: params.organizationId,
+                    openAPISpec: params.slug,
+                })
+            );
 
-const getReusableContent = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; revisionId: string; reusableContentId: string }
-) => {
-    'use cache';
-    return trace(
-        `getReusableContent(${params.spaceId}, ${params.revisionId}, ${params.reusableContentId})`,
-        async () => {
-            return wrapDataFetcherError(async () => {
-                const api = apiClient(input);
-                const res = await api.spaces.getReusableContentInRevisionById(
-                    params.spaceId,
-                    params.revisionId,
-                    params.reusableContentId,
-                    {},
-                    {
-                        ...noCacheFetchOptions,
-                    }
-                );
-                cacheTag(...getCacheTagsFromResponse(res));
-                cacheLife('max');
-                return res.data;
-            });
+            return trace(
+                `getLatestOpenAPISpecVersionContent(${params.organizationId}, ${params.slug})`,
+                async () => {
+                    return wrapDataFetcherError(async () => {
+                        const api = apiClient(input);
+                        const res = await api.orgs.getLatestOpenApiSpecVersionContent(
+                            params.organizationId,
+                            params.slug,
+                            {
+                                ...noCacheFetchOptions,
+                            }
+                        );
+                        cacheTag(...getCacheTagsFromResponse(res));
+                        cacheLife('max');
+                        return res.data;
+                    });
+                }
+            );
         }
-    );
-};
-
-const getLatestOpenAPISpecVersionContent = async (
-    input: DataFetcherInput,
-    params: { organizationId: string; slug: string }
-) => {
-    'use cache';
-    cacheTag(
-        getCacheTag({
-            tag: 'openapi',
-            organization: params.organizationId,
-            openAPISpec: params.slug,
-        })
-    );
+    )
+);
+
+const getPublishedContentSite = withCacheKey(
+    withoutConcurrentExecution(
+        async (
+            _,
+            input: DataFetcherInput,
+            params: { organizationId: string; siteId: string; siteShareKey: string | undefined }
+        ) => {
+            'use cache';
+            cacheTag(
+                getCacheTag({
+                    tag: 'site',
+                    site: params.siteId,
+                })
+            );
 
-    return trace(
-        `getLatestOpenAPISpecVersionContent(${params.organizationId}, ${params.slug})`,
-        async () => {
-            return wrapDataFetcherError(async () => {
-                const api = apiClient(input);
-                const res = await api.orgs.getLatestOpenApiSpecVersionContent(
-                    params.organizationId,
-                    params.slug,
-                    {
-                        ...noCacheFetchOptions,
-                    }
-                );
-                cacheTag(...getCacheTagsFromResponse(res));
-                cacheLife('max');
-                return res.data;
-            });
+            return trace(
+                `getPublishedContentSite(${params.organizationId}, ${params.siteId}, ${params.siteShareKey})`,
+                async () => {
+                    return wrapDataFetcherError(async () => {
+                        const api = apiClient(input);
+                        const res = await api.orgs.getPublishedContentSite(
+                            params.organizationId,
+                            params.siteId,
+                            {
+                                shareKey: params.siteShareKey,
+                            },
+                            {
+                                ...noCacheFetchOptions,
+                            }
+                        );
+                        cacheTag(...getCacheTagsFromResponse(res));
+                        cacheLife('days');
+                        return res.data;
+                    });
+                }
+            );
         }
-    );
-};
-
-const getPublishedContentSite = async (
-    input: DataFetcherInput,
-    params: { organizationId: string; siteId: string; siteShareKey: string | undefined }
-) => {
-    'use cache';
-    cacheTag(
-        getCacheTag({
-            tag: 'site',
-            site: params.siteId,
-        })
-    );
+    )
+);
+
+const getSiteRedirectBySource = withCacheKey(
+    withoutConcurrentExecution(
+        async (
+            _,
+            input: DataFetcherInput,
+            params: {
+                organizationId: string;
+                siteId: string;
+                siteShareKey: string | undefined;
+                source: string;
+            }
+        ) => {
+            'use cache';
+            cacheTag(
+                getCacheTag({
+                    tag: 'site',
+                    site: params.siteId,
+                })
+            );
 
-    return trace(
-        `getPublishedContentSite(${params.organizationId}, ${params.siteId}, ${params.siteShareKey})`,
-        async () => {
-            return wrapDataFetcherError(async () => {
-                const api = apiClient(input);
-                const res = await api.orgs.getPublishedContentSite(
-                    params.organizationId,
-                    params.siteId,
-                    {
-                        shareKey: params.siteShareKey,
-                    },
-                    {
-                        ...noCacheFetchOptions,
-                    }
-                );
-                cacheTag(...getCacheTagsFromResponse(res));
-                cacheLife('days');
-                return res.data;
-            });
+            return trace(
+                `getSiteRedirectBySource(${params.organizationId}, ${params.siteId}, ${params.siteShareKey}, ${params.source})`,
+                async () => {
+                    return wrapDataFetcherError(async () => {
+                        const api = apiClient(input);
+                        const res = await api.orgs.getSiteRedirectBySource(
+                            params.organizationId,
+                            params.siteId,
+                            {
+                                shareKey: params.siteShareKey,
+                                source: params.source,
+                            },
+                            {
+                                ...noCacheFetchOptions,
+                            }
+                        );
+                        cacheTag(...getCacheTagsFromResponse(res));
+                        cacheLife('days');
+                        return res.data;
+                    });
+                }
+            );
         }
-    );
-};
-
-const getSiteRedirectBySource = async (
-    input: DataFetcherInput,
-    params: {
-        organizationId: string;
-        siteId: string;
-        siteShareKey: string | undefined;
-        source: string;
-    }
-) => {
-    'use cache';
-    cacheTag(
-        getCacheTag({
-            tag: 'site',
-            site: params.siteId,
-        })
-    );
+    )
+);
+
+const getEmbedByUrl = withCacheKey(
+    withoutConcurrentExecution(
+        async (_, input: DataFetcherInput, params: { spaceId: string; url: string }) => {
+            'use cache';
+            cacheTag(
+                getCacheTag({
+                    tag: 'space',
+                    space: params.spaceId,
+                })
+            );
 
-    return trace(
-        `getSiteRedirectBySource(${params.organizationId}, ${params.siteId}, ${params.siteShareKey}, ${params.source})`,
-        async () => {
-            return wrapDataFetcherError(async () => {
-                const api = apiClient(input);
-                const res = await api.orgs.getSiteRedirectBySource(
-                    params.organizationId,
-                    params.siteId,
-                    {
-                        shareKey: params.siteShareKey,
-                        source: params.source,
-                    },
-                    {
-                        ...noCacheFetchOptions,
-                    }
-                );
-                cacheTag(...getCacheTagsFromResponse(res));
-                cacheLife('days');
-                return res.data;
+            return trace(`getEmbedByUrl(${params.spaceId}, ${params.url})`, async () => {
+                return wrapDataFetcherError(async () => {
+                    const api = apiClient(input);
+                    const res = await api.spaces.getEmbedByUrlInSpace(
+                        params.spaceId,
+                        {
+                            url: params.url,
+                        },
+                        {
+                            ...noCacheFetchOptions,
+                        }
+                    );
+                    cacheTag(...getCacheTagsFromResponse(res));
+                    cacheLife('weeks');
+                    return res.data;
+                });
             });
         }
-    );
-};
-
-const getEmbedByUrl = async (input: DataFetcherInput, params: { spaceId: string; url: string }) => {
-    'use cache';
-    cacheTag(
-        getCacheTag({
-            tag: 'space',
-            space: params.spaceId,
-        })
-    );
+    )
+);
+
+const searchSiteContent = withCacheKey(
+    withoutConcurrentExecution(
+        async (
+            _,
+            input: DataFetcherInput,
+            params: Parameters<GitBookDataFetcher['searchSiteContent']>[0]
+        ) => {
+            'use cache';
+            cacheTag(
+                getCacheTag({
+                    tag: 'site',
+                    site: params.siteId,
+                })
+            );
 
-    return trace(`getEmbedByUrl(${params.spaceId}, ${params.url})`, async () => {
-        return wrapDataFetcherError(async () => {
-            const api = apiClient(input);
-            const res = await api.spaces.getEmbedByUrlInSpace(
-                params.spaceId,
-                {
-                    url: params.url,
-                },
-                {
-                    ...noCacheFetchOptions,
+            return trace(
+                `searchSiteContent(${params.organizationId}, ${params.siteId}, ${params.query})`,
+                async () => {
+                    return wrapDataFetcherError(async () => {
+                        const { organizationId, siteId, query, scope } = params;
+                        const api = apiClient(input);
+                        const res = await api.orgs.searchSiteContent(
+                            organizationId,
+                            siteId,
+                            {
+                                query,
+                                ...scope,
+                            },
+                            {},
+                            {
+                                ...noCacheFetchOptions,
+                            }
+                        );
+                        cacheTag(...getCacheTagsFromResponse(res));
+                        cacheLife('hours');
+                        return res.data.items;
+                    });
                 }
             );
-            cacheTag(...getCacheTagsFromResponse(res));
-            cacheLife('weeks');
-            return res.data;
-        });
-    });
-};
-
-const searchSiteContent = async (
-    input: DataFetcherInput,
-    params: Parameters<GitBookDataFetcher['searchSiteContent']>[0]
-) => {
-    'use cache';
-    cacheTag(
-        getCacheTag({
-            tag: 'site',
-            site: params.siteId,
-        })
-    );
+        }
+    )
+);
+
+const renderIntegrationUi = withCacheKey(
+    withoutConcurrentExecution(
+        async (
+            _,
+            input: DataFetcherInput,
+            params: { integrationName: string; request: RenderIntegrationUI }
+        ) => {
+            'use cache';
+            cacheTag(
+                getCacheTag({
+                    tag: 'integration',
+                    integration: params.integrationName,
+                })
+            );
 
-    return trace(
-        `searchSiteContent(${params.organizationId}, ${params.siteId}, ${params.query})`,
-        async () => {
-            return wrapDataFetcherError(async () => {
-                const { organizationId, siteId, query, scope } = params;
-                const api = apiClient(input);
-                const res = await api.orgs.searchSiteContent(
-                    organizationId,
-                    siteId,
-                    {
-                        query,
-                        ...scope,
-                    },
-                    {},
-                    {
-                        ...noCacheFetchOptions,
-                    }
-                );
-                cacheTag(...getCacheTagsFromResponse(res));
-                cacheLife('hours');
-                return res.data.items;
+            return trace(`renderIntegrationUi(${params.integrationName})`, async () => {
+                return wrapDataFetcherError(async () => {
+                    const api = apiClient(input);
+                    const res = await api.integrations.renderIntegrationUiWithPost(
+                        params.integrationName,
+                        params.request,
+                        {
+                            ...noCacheFetchOptions,
+                        }
+                    );
+                    cacheTag(...getCacheTagsFromResponse(res));
+                    cacheLife('days');
+                    return res.data;
+                });
             });
         }
-    );
-};
-
-const renderIntegrationUi = async (
-    input: DataFetcherInput,
-    params: { integrationName: string; request: RenderIntegrationUI }
-) => {
-    'use cache';
-    cacheTag(
-        getCacheTag({
-            tag: 'integration',
-            integration: params.integrationName,
-        })
-    );
-
-    return trace(`renderIntegrationUi(${params.integrationName})`, async () => {
-        return wrapDataFetcherError(async () => {
-            const api = apiClient(input);
-            const res = await api.integrations.renderIntegrationUiWithPost(
-                params.integrationName,
-                params.request,
-                {
-                    ...noCacheFetchOptions,
-                }
-            );
-            cacheTag(...getCacheTagsFromResponse(res));
-            cacheLife('days');
-            return res.data;
-        });
-    });
-};
+    )
+);
 
 async function* streamAIResponse(
     input: DataFetcherInput,
diff --git a/packages/gitbook-v2/src/lib/data/memoize.ts b/packages/gitbook-v2/src/lib/data/memoize.ts
new file mode 100644
index 0000000000..5110d015fa
--- /dev/null
+++ b/packages/gitbook-v2/src/lib/data/memoize.ts
@@ -0,0 +1,92 @@
+import { cache } from 'react';
+
+// This is used to create a context specific to the current request.
+// This version works both in cloudflare and in vercel.
+const getRequestContext = cache(() => ({}));
+
+/**
+ * Wrap a function by preventing concurrent executions of the same function.
+ * With a logic to work per-request in Cloudflare Workers.
+ */
+export function withoutConcurrentExecution<ArgsType extends any[], ReturnType>(
+    wrapped: (key: string, ...args: ArgsType) => Promise<ReturnType>
+): (cacheKey: string, ...args: ArgsType) => Promise<ReturnType> {
+    const globalPromiseCache = new WeakMap<object, Map<string, Promise<ReturnType>>>();
+
+    return (key: string, ...args: ArgsType) => {
+        const globalContext = getRequestContext();
+
+        /**
+         * Cache storage that is scoped to the current request when executed in Cloudflare Workers,
+         * to avoid "Cannot perform I/O on behalf of a different request" errors.
+         */
+        const promiseCache =
+            globalPromiseCache.get(globalContext) ?? new Map<string, Promise<ReturnType>>();
+        globalPromiseCache.set(globalContext, promiseCache);
+
+        const concurrent = promiseCache.get(key);
+        if (concurrent) {
+            return concurrent;
+        }
+
+        const promise = (async () => {
+            try {
+                const result = await wrapped(key, ...args);
+                return result;
+            } finally {
+                promiseCache.delete(key);
+            }
+        })();
+
+        promiseCache.set(key, promise);
+
+        return promise;
+    };
+}
+
+/**
+ * Wrap a function by passing it a cache key that is computed from the function arguments.
+ */
+export function withCacheKey<ArgsType extends any[], ReturnType>(
+    wrapped: (cacheKey: string, ...args: ArgsType) => Promise<ReturnType>
+): (...args: ArgsType) => Promise<ReturnType> {
+    return (...args: ArgsType) => {
+        const cacheKey = getCacheKey(args);
+        return wrapped(cacheKey, ...args);
+    };
+}
+
+/**
+ * Compute a cache key from the function arguments.
+ */
+function getCacheKey(args: any[]) {
+    return JSON.stringify(deepSortValue(args));
+}
+
+function deepSortValue(value: unknown): unknown {
+    if (
+        typeof value === 'string' ||
+        typeof value === 'number' ||
+        typeof value === 'boolean' ||
+        value === null ||
+        value === undefined
+    ) {
+        return value;
+    }
+
+    if (Array.isArray(value)) {
+        return value.map(deepSortValue);
+    }
+
+    if (value && typeof value === 'object') {
+        return Object.entries(value)
+            .map(([key, subValue]) => {
+                return [key, deepSortValue(subValue)] as const;
+            })
+            .sort((a, b) => {
+                return a[0].localeCompare(b[0]);
+            });
+    }
+
+    return value;
+}
diff --git a/packages/gitbook/e2e/customers.spec.ts b/packages/gitbook/e2e/customers.spec.ts
index f0612ba8e0..aa8e6e4a7c 100644
--- a/packages/gitbook/e2e/customers.spec.ts
+++ b/packages/gitbook/e2e/customers.spec.ts
@@ -204,11 +204,11 @@ const testCases: TestsCase[] = [
         contentBaseURL: 'https://sosovalue-white-paper.gitbook.io',
         tests: [{ name: 'Home', url: '/' }],
     },
-    {
-        name: 'docs.revrobotics.com',
-        contentBaseURL: 'https://docs.revrobotics.com',
-        tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
-    },
+    // {
+    //     name: 'docs.revrobotics.com',
+    //     contentBaseURL: 'https://docs.revrobotics.com',
+    //     tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
+    // },
     {
         name: 'chartschool.stockcharts.com',
         contentBaseURL: 'https://chartschool.stockcharts.com',
diff --git a/packages/gitbook/src/components/DocumentView/InlineLink.tsx b/packages/gitbook/src/components/DocumentView/InlineLink.tsx
index 37a812bb3c..f5bd7f798b 100644
--- a/packages/gitbook/src/components/DocumentView/InlineLink.tsx
+++ b/packages/gitbook/src/components/DocumentView/InlineLink.tsx
@@ -12,7 +12,8 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
 
     const resolved = context.contentContext
         ? await resolveContentRef(inline.data.ref, context.contentContext, {
-              resolveAnchorText: true,
+              // We don't want to resolve the anchor text here, as it can be very expensive and will block rendering if there is a lot of anchors link.
+              resolveAnchorText: false,
           })
         : null;
 
diff --git a/packages/gitbook/src/lib/references.tsx b/packages/gitbook/src/lib/references.tsx
index 7248ab2ded..8cfef861d0 100644
--- a/packages/gitbook/src/lib/references.tsx
+++ b/packages/gitbook/src/lib/references.tsx
@@ -147,7 +147,7 @@ export async function resolveContentRef(
 
             // Compute the text to display for the link
             if (anchor) {
-                text = `#${anchor}`;
+                text = page.title;
                 ancestors.push({
                     label: page.title,
                     icon: <PageIcon page={page} style={iconStyle} />,

From e7a591d9f6bae7b157d06e96ae8b891937c2fb71 Mon Sep 17 00:00:00 2001
From: Zeno Kapitein <zeno@gitbook.io>
Date: Tue, 10 Jun 2025 19:25:29 +0200
Subject: [PATCH 086/127] Fix border being added to cards (#3294)

---
 .changeset/tame-mangos-battle.md                    | 5 +++++
 packages/gitbook/src/components/primitives/Link.tsx | 5 +----
 2 files changed, 6 insertions(+), 4 deletions(-)
 create mode 100644 .changeset/tame-mangos-battle.md

diff --git a/.changeset/tame-mangos-battle.md b/.changeset/tame-mangos-battle.md
new file mode 100644
index 0000000000..844bf321a5
--- /dev/null
+++ b/.changeset/tame-mangos-battle.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Fix border being added to cards
diff --git a/packages/gitbook/src/components/primitives/Link.tsx b/packages/gitbook/src/components/primitives/Link.tsx
index cf9d2c24c4..ff9f5ebde1 100644
--- a/packages/gitbook/src/components/primitives/Link.tsx
+++ b/packages/gitbook/src/components/primitives/Link.tsx
@@ -106,10 +106,7 @@ export const LinkOverlay = React.forwardRef(function LinkOverlay(
         <Link
             ref={ref}
             {...domProps}
-            className={tcls(
-                'link-overlay static before:absolute before:top-0 before:left-0 before:z-10 before:h-full before:w-full',
-                className
-            )}
+            className={tcls('link-overlay absolute inset-0 z-10', className)}
         >
             {children}
         </Link>

From fa3d2aae08ab1fd4073601358440c7fa253f54a4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Wed, 11 Jun 2025 12:27:24 +0200
Subject: [PATCH 087/127] Fix inline expression crashing pages (#3297)

---
 .github/workflows/deploy-preview.yaml         |  8 ++---
 bun.lock                                      | 12 +++----
 package.json                                  |  2 +-
 packages/cache-tags/package.json              |  2 +-
 packages/gitbook-v2/package.json              |  2 +-
 packages/gitbook/package.json                 |  2 +-
 .../src/components/DocumentView/Block.tsx     |  8 +----
 .../src/components/DocumentView/Inline.tsx    | 31 +++++--------------
 packages/gitbook/src/lib/typescript.ts        |  7 +++++
 packages/react-contentkit/package.json        |  2 +-
 10 files changed, 31 insertions(+), 45 deletions(-)

diff --git a/.github/workflows/deploy-preview.yaml b/.github/workflows/deploy-preview.yaml
index 7d8eec6109..c445004c0a 100644
--- a/.github/workflows/deploy-preview.yaml
+++ b/.github/workflows/deploy-preview.yaml
@@ -159,7 +159,7 @@ jobs:
         runs-on: ubuntu-latest
         name: Visual Testing v1
         needs: deploy-v1-cloudflare
-        timeout-minutes: 8
+        timeout-minutes: 10
         steps:
             - name: Checkout
               uses: actions/checkout@v4
@@ -220,7 +220,7 @@ jobs:
         runs-on: ubuntu-latest
         name: Visual Testing Customers v1
         needs: deploy-v1-cloudflare
-        timeout-minutes: 8
+        timeout-minutes: 10
         steps:
             - name: Checkout
               uses: actions/checkout@v4
@@ -240,7 +240,7 @@ jobs:
         runs-on: ubuntu-latest
         name: Visual Testing Customers v2
         needs: deploy-v2-vercel
-        timeout-minutes: 8
+        timeout-minutes: 10
         steps:
             - name: Checkout
               uses: actions/checkout@v4
@@ -261,7 +261,7 @@ jobs:
         runs-on: ubuntu-latest
         name: Visual Testing Customers v2 (Cloudflare)
         needs: deploy-v2-cloudflare
-        timeout-minutes: 8
+        timeout-minutes: 10
         steps:
             - name: Checkout
               uses: actions/checkout@v4
diff --git a/bun.lock b/bun.lock
index dfd8d2ce4f..e3d406e11d 100644
--- a/bun.lock
+++ b/bun.lock
@@ -26,7 +26,7 @@
       "name": "@gitbook/cache-tags",
       "version": "0.3.1",
       "dependencies": {
-        "@gitbook/api": "^0.119.0",
+        "@gitbook/api": "^0.120.0",
         "assert-never": "^1.2.1",
       },
       "devDependencies": {
@@ -51,7 +51,7 @@
       "name": "gitbook",
       "version": "0.12.0",
       "dependencies": {
-        "@gitbook/api": "^0.119.0",
+        "@gitbook/api": "^0.120.0",
         "@gitbook/cache-do": "workspace:*",
         "@gitbook/cache-tags": "workspace:*",
         "@gitbook/colors": "workspace:*",
@@ -150,7 +150,7 @@
       "name": "gitbook-v2",
       "version": "0.3.0",
       "dependencies": {
-        "@gitbook/api": "^0.119.0",
+        "@gitbook/api": "^0.120.0",
         "@gitbook/cache-tags": "workspace:*",
         "@opennextjs/cloudflare": "1.2.1",
         "@sindresorhus/fnv1a": "^3.1.0",
@@ -209,7 +209,7 @@
       "name": "@gitbook/react-contentkit",
       "version": "0.7.0",
       "dependencies": {
-        "@gitbook/api": "^0.119.0",
+        "@gitbook/api": "^0.120.0",
         "@gitbook/icons": "workspace:*",
         "classnames": "^2.5.1",
       },
@@ -267,7 +267,7 @@
   },
   "overrides": {
     "@codemirror/state": "6.4.1",
-    "@gitbook/api": "^0.119.0",
+    "@gitbook/api": "^0.120.0",
     "react": "^19.0.0",
     "react-dom": "^19.0.0",
   },
@@ -632,7 +632,7 @@
 
     "@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="],
 
-    "@gitbook/api": ["@gitbook/api@0.119.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-l2gfJ5+1HT3Sj1OQCh72lUOGU9LV9WbDjgKPjxIFz+NgOhSest1bJ3JhmHP66Xd4ETpmm0YQqnq4y18wKRMjBQ=="],
+    "@gitbook/api": ["@gitbook/api@0.120.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-FiRmPiSBwobMxmNjd14QkkOdM95BAPLDDRShgpS9Vsd8lHjNMyZfrJKVJTsJUuFcgYoi4cqNw9yu/TiUBUgv3g=="],
 
     "@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"],
 
diff --git a/package.json b/package.json
index b4a3acc96b..dbb94b88ed 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
     "packageManager": "bun@1.2.11",
     "overrides": {
         "@codemirror/state": "6.4.1",
-        "@gitbook/api": "^0.119.0",
+        "@gitbook/api": "^0.120.0",
         "react": "^19.0.0",
         "react-dom": "^19.0.0"
     },
diff --git a/packages/cache-tags/package.json b/packages/cache-tags/package.json
index 7109f569d1..eafeabc44c 100644
--- a/packages/cache-tags/package.json
+++ b/packages/cache-tags/package.json
@@ -10,7 +10,7 @@
     },
     "version": "0.3.1",
     "dependencies": {
-        "@gitbook/api": "^0.119.0",
+        "@gitbook/api": "^0.120.0",
         "assert-never": "^1.2.1"
     },
     "devDependencies": {
diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json
index 48aabef556..3993e746d3 100644
--- a/packages/gitbook-v2/package.json
+++ b/packages/gitbook-v2/package.json
@@ -3,7 +3,7 @@
     "version": "0.3.0",
     "private": true,
     "dependencies": {
-        "@gitbook/api": "^0.119.0",
+        "@gitbook/api": "^0.120.0",
         "@gitbook/cache-tags": "workspace:*",
         "@opennextjs/cloudflare": "1.2.1",
         "@sindresorhus/fnv1a": "^3.1.0",
diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json
index fba4221a85..74ef66a34e 100644
--- a/packages/gitbook/package.json
+++ b/packages/gitbook/package.json
@@ -16,7 +16,7 @@
         "clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math"
     },
     "dependencies": {
-        "@gitbook/api": "^0.119.0",
+        "@gitbook/api": "^0.120.0",
         "@gitbook/cache-do": "workspace:*",
         "@gitbook/cache-tags": "workspace:*",
         "@gitbook/colors": "workspace:*",
diff --git a/packages/gitbook/src/components/DocumentView/Block.tsx b/packages/gitbook/src/components/DocumentView/Block.tsx
index ee4f9c7c8e..c9f4ef493a 100644
--- a/packages/gitbook/src/components/DocumentView/Block.tsx
+++ b/packages/gitbook/src/components/DocumentView/Block.tsx
@@ -10,6 +10,7 @@ import {
 } from '@/components/primitives';
 import type { ClassValue } from '@/lib/tailwind';
 
+import { nullIfNever } from '@/lib/typescript';
 import { BlockContentRef } from './BlockContentRef';
 import { CodeBlock } from './CodeBlock';
 import { Columns } from './Columns';
@@ -45,13 +46,6 @@ export interface BlockProps<Block extends DocumentBlock> extends DocumentContext
     style?: ClassValue;
 }
 
-/**
- * Alternative to `assertNever` that returns `null` instead of throwing an error.
- */
-function nullIfNever(_value: never): null {
-    return null;
-}
-
 export function Block<T extends DocumentBlock>(props: BlockProps<T>) {
     const { block, style, isEstimatedOffscreen, context } = props;
 
diff --git a/packages/gitbook/src/components/DocumentView/Inline.tsx b/packages/gitbook/src/components/DocumentView/Inline.tsx
index 9101ffef20..1e1b091cef 100644
--- a/packages/gitbook/src/components/DocumentView/Inline.tsx
+++ b/packages/gitbook/src/components/DocumentView/Inline.tsx
@@ -1,16 +1,6 @@
-import type {
-    DocumentInline,
-    DocumentInlineAnnotation,
-    DocumentInlineButton,
-    DocumentInlineEmoji,
-    DocumentInlineImage,
-    DocumentInlineLink,
-    DocumentInlineMath,
-    DocumentInlineMention,
-    JSONDocument,
-} from '@gitbook/api';
-import assertNever from 'assert-never';
+import type { DocumentInline, JSONDocument } from '@gitbook/api';
 
+import { nullIfNever } from '@/lib/typescript';
 import { Annotation } from './Annotation/Annotation';
 import type { DocumentContextProps } from './DocumentView';
 import { Emoji } from './Emoji';
@@ -39,16 +29,7 @@ export interface InlineProps<T extends DocumentInline> extends DocumentContextPr
     children?: React.ReactNode;
 }
 
-export function Inline<
-    T extends
-        | DocumentInlineImage
-        | DocumentInlineAnnotation
-        | DocumentInlineEmoji
-        | DocumentInlineLink
-        | DocumentInlineMath
-        | DocumentInlineMention
-        | DocumentInlineButton,
->(props: InlineProps<T>) {
+export function Inline<T extends DocumentInline>(props: InlineProps<T>) {
     const { inline, ...contextProps } = props;
 
     switch (inline.type) {
@@ -66,7 +47,11 @@ export function Inline<
             return <InlineImage {...contextProps} inline={inline} />;
         case 'button':
             return <InlineButton {...contextProps} inline={inline} />;
+        case 'expression':
+            // The GitBook API should take care of evaluating expressions.
+            // We should never need to render them.
+            return null;
         default:
-            assertNever(inline);
+            return nullIfNever(inline);
     }
 }
diff --git a/packages/gitbook/src/lib/typescript.ts b/packages/gitbook/src/lib/typescript.ts
index 43940d4a2a..f13574a5a4 100644
--- a/packages/gitbook/src/lib/typescript.ts
+++ b/packages/gitbook/src/lib/typescript.ts
@@ -4,3 +4,10 @@
 export function filterOutNullable<T>(value: T): value is NonNullable<T> {
     return !!value;
 }
+
+/**
+ * Alternative to `assertNever` that returns `null` instead of throwing an error.
+ */
+export function nullIfNever(_value: never): null {
+    return null;
+}
diff --git a/packages/react-contentkit/package.json b/packages/react-contentkit/package.json
index aa2c162ee6..eee7aa85b6 100644
--- a/packages/react-contentkit/package.json
+++ b/packages/react-contentkit/package.json
@@ -10,7 +10,7 @@
     },
     "dependencies": {
         "classnames": "^2.5.1",
-        "@gitbook/api": "^0.119.0",
+        "@gitbook/api": "^0.120.0",
         "@gitbook/icons": "workspace:*"
     },
     "peerDependencies": {

From 664ae8bff6ea1c2c391a370f6d934289fae26b8b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Greg=20Berg=C3=A9?= <berge.greg@gmail.com>
Date: Wed, 11 Jun 2025 13:40:59 +0200
Subject: [PATCH 088/127] Make heading from stepper visible in outline (#3301)

---
 packages/gitbook/src/lib/document-sections.ts | 128 ++++++++++++------
 1 file changed, 86 insertions(+), 42 deletions(-)

diff --git a/packages/gitbook/src/lib/document-sections.ts b/packages/gitbook/src/lib/document-sections.ts
index 38619a5881..9e59122365 100644
--- a/packages/gitbook/src/lib/document-sections.ts
+++ b/packages/gitbook/src/lib/document-sections.ts
@@ -1,4 +1,4 @@
-import type { JSONDocument } from '@gitbook/api';
+import type { DocumentBlock, JSONDocument } from '@gitbook/api';
 import type { GitBookAnyContext } from '@v2/lib/context';
 
 import { getNodeText } from './document';
@@ -19,58 +19,102 @@ export interface DocumentSection {
 export async function getDocumentSections(
     context: GitBookAnyContext,
     document: JSONDocument
+): Promise<DocumentSection[]> {
+    return getSectionsFromNodes(document.nodes, context);
+}
+
+/**
+ * Extract a list of sections from a list of nodes.
+ */
+async function getSectionsFromNodes(
+    nodes: DocumentBlock[],
+    context: GitBookAnyContext
 ): Promise<DocumentSection[]> {
     const sections: DocumentSection[] = [];
     let depth = 0;
 
-    for (const block of document.nodes) {
-        if ((block.type === 'heading-1' || block.type === 'heading-2') && block.meta?.id) {
-            if (block.type === 'heading-1') {
+    for (const block of nodes) {
+        switch (block.type) {
+            case 'heading-1': {
+                const id = block.meta?.id;
+                if (!id) {
+                    continue;
+                }
                 depth = 1;
-            }
-            const title = getNodeText(block);
-            const id = block.meta.id;
-
-            sections.push({
-                id,
-                title,
-                depth: block.type === 'heading-1' ? 1 : depth > 0 ? 2 : 1,
-            });
-        }
-
-        if ((block.type === 'swagger' || block.type === 'openapi-operation') && block.meta?.id) {
-            const { data: operation } = await resolveOpenAPIOperationBlock({
-                block,
-                context,
-            });
-            if (operation) {
+                const title = getNodeText(block);
                 sections.push({
-                    id: block.meta.id,
-                    tag: operation.method.toUpperCase(),
-                    title: operation.operation.summary || operation.path,
+                    id,
+                    title,
                     depth: 1,
-                    deprecated: operation.operation.deprecated,
                 });
+                continue;
             }
-        }
-
-        if (
-            block.type === 'openapi-schemas' &&
-            !block.data.grouped &&
-            block.meta?.id &&
-            block.data.schemas.length === 1
-        ) {
-            const { data } = await resolveOpenAPISchemasBlock({
-                block,
-                context,
-            });
-            const schema = data?.schemas[0];
-            if (schema) {
+            case 'heading-2': {
+                const id = block.meta?.id;
+                if (!id) {
+                    continue;
+                }
+                const title = getNodeText(block);
                 sections.push({
-                    id: block.meta.id,
-                    title: `The ${schema.name} object`,
-                    depth: 1,
+                    id,
+                    title,
+                    depth: depth > 0 ? 2 : 1,
+                });
+                continue;
+            }
+            case 'stepper': {
+                const stepNodes = await Promise.all(
+                    block.nodes.map(async (step) => getSectionsFromNodes(step.nodes, context))
+                );
+                for (const stepSections of stepNodes) {
+                    sections.push(...stepSections);
+                }
+                continue;
+            }
+            case 'swagger':
+            case 'openapi-operation': {
+                const id = block.meta?.id;
+                if (!id) {
+                    continue;
+                }
+                const { data: operation } = await resolveOpenAPIOperationBlock({
+                    block,
+                    context,
+                });
+                if (operation) {
+                    sections.push({
+                        id,
+                        tag: operation.method.toUpperCase(),
+                        title: operation.operation.summary || operation.path,
+                        depth: 1,
+                        deprecated: operation.operation.deprecated,
+                    });
+                }
+                continue;
+            }
+            case 'openapi-schemas': {
+                const id = block.meta?.id;
+                if (!id) {
+                    continue;
+                }
+                if (block.data.grouped || block.data.schemas.length !== 1) {
+                    // Skip grouped schemas, they are not sections
+                    continue;
+                }
+
+                const { data } = await resolveOpenAPISchemasBlock({
+                    block,
+                    context,
                 });
+                const schema = data?.schemas[0];
+                if (schema) {
+                    sections.push({
+                        id,
+                        title: `The ${schema.name} object`,
+                        depth: 1,
+                    });
+                }
+                continue;
             }
         }
     }

From fbfcca5dae76be67ac885b83d30cf358a012f148 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Wed, 11 Jun 2025 13:41:26 +0200
Subject: [PATCH 089/127] Fix rendering of font in ogimage (#3299)

---
 .changeset/poor-dodos-lick.md                 |   5 +
 .changeset/warm-roses-sleep.md                |   5 +
 bun.lock                                      |  98 +++++++++++++--
 package.json                                  |  10 +-
 packages/cache-tags/package.json              |   2 +-
 packages/fonts/.gitignore                     |   2 +
 packages/fonts/README.md                      |   3 +
 packages/fonts/bin/generate.ts                |  91 ++++++++++++++
 packages/fonts/package.json                   |  31 +++++
 .../__snapshots__/getDefaultFont.test.ts.snap |  57 +++++++++
 packages/fonts/src/fonts.ts                   |   5 +
 packages/fonts/src/getDefaultFont.test.ts     | 119 ++++++++++++++++++
 packages/fonts/src/getDefaultFont.ts          | 112 +++++++++++++++++
 packages/fonts/src/index.ts                   |   2 +
 packages/fonts/src/types.ts                   |  17 +++
 packages/fonts/tsconfig.build.json            |   4 +
 packages/fonts/tsconfig.json                  |  24 ++++
 packages/fonts/turbo.json                     |   9 ++
 packages/gitbook-v2/package.json              |   2 +-
 packages/gitbook/package.json                 |   3 +-
 packages/gitbook/src/routes/ogimage.tsx       |  85 +++++--------
 packages/icons/package.json                   |   4 +-
 packages/icons/tsconfig.build.json            |   4 +
 packages/react-contentkit/package.json        |   2 +-
 24 files changed, 624 insertions(+), 72 deletions(-)
 create mode 100644 .changeset/poor-dodos-lick.md
 create mode 100644 .changeset/warm-roses-sleep.md
 create mode 100644 packages/fonts/.gitignore
 create mode 100644 packages/fonts/README.md
 create mode 100644 packages/fonts/bin/generate.ts
 create mode 100644 packages/fonts/package.json
 create mode 100644 packages/fonts/src/__snapshots__/getDefaultFont.test.ts.snap
 create mode 100644 packages/fonts/src/fonts.ts
 create mode 100644 packages/fonts/src/getDefaultFont.test.ts
 create mode 100644 packages/fonts/src/getDefaultFont.ts
 create mode 100644 packages/fonts/src/index.ts
 create mode 100644 packages/fonts/src/types.ts
 create mode 100644 packages/fonts/tsconfig.build.json
 create mode 100644 packages/fonts/tsconfig.json
 create mode 100644 packages/fonts/turbo.json
 create mode 100644 packages/icons/tsconfig.build.json

diff --git a/.changeset/poor-dodos-lick.md b/.changeset/poor-dodos-lick.md
new file mode 100644
index 0000000000..ca02d7382b
--- /dev/null
+++ b/.changeset/poor-dodos-lick.md
@@ -0,0 +1,5 @@
+---
+"@gitbook/fonts": minor
+---
+
+Initial version of the package
diff --git a/.changeset/warm-roses-sleep.md b/.changeset/warm-roses-sleep.md
new file mode 100644
index 0000000000..e14b06cdc2
--- /dev/null
+++ b/.changeset/warm-roses-sleep.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Fix ogimage using incorrect Google Font depending on language.
diff --git a/bun.lock b/bun.lock
index e3d406e11d..3ff6d61c4c 100644
--- a/bun.lock
+++ b/bun.lock
@@ -26,7 +26,7 @@
       "name": "@gitbook/cache-tags",
       "version": "0.3.1",
       "dependencies": {
-        "@gitbook/api": "^0.120.0",
+        "@gitbook/api": "catalog:",
         "assert-never": "^1.2.1",
       },
       "devDependencies": {
@@ -47,15 +47,27 @@
         "emoji-assets": "^8.0.0",
       },
     },
+    "packages/fonts": {
+      "name": "@gitbook/fonts",
+      "version": "0.0.0",
+      "dependencies": {
+        "@gitbook/api": "catalog:",
+      },
+      "devDependencies": {
+        "google-font-metadata": "^6.0.3",
+        "typescript": "^5.5.3",
+      },
+    },
     "packages/gitbook": {
       "name": "gitbook",
       "version": "0.12.0",
       "dependencies": {
-        "@gitbook/api": "^0.120.0",
+        "@gitbook/api": "catalog:",
         "@gitbook/cache-do": "workspace:*",
         "@gitbook/cache-tags": "workspace:*",
         "@gitbook/colors": "workspace:*",
         "@gitbook/emoji-codepoints": "workspace:*",
+        "@gitbook/fonts": "workspace:*",
         "@gitbook/icons": "workspace:*",
         "@gitbook/openapi-parser": "workspace:*",
         "@gitbook/react-contentkit": "workspace:*",
@@ -150,7 +162,7 @@
       "name": "gitbook-v2",
       "version": "0.3.0",
       "dependencies": {
-        "@gitbook/api": "^0.120.0",
+        "@gitbook/api": "catalog:",
         "@gitbook/cache-tags": "workspace:*",
         "@opennextjs/cloudflare": "1.2.1",
         "@sindresorhus/fnv1a": "^3.1.0",
@@ -209,7 +221,7 @@
       "name": "@gitbook/react-contentkit",
       "version": "0.7.0",
       "dependencies": {
-        "@gitbook/api": "^0.120.0",
+        "@gitbook/api": "catalog:",
         "@gitbook/icons": "workspace:*",
         "classnames": "^2.5.1",
       },
@@ -267,10 +279,12 @@
   },
   "overrides": {
     "@codemirror/state": "6.4.1",
-    "@gitbook/api": "^0.120.0",
     "react": "^19.0.0",
     "react-dom": "^19.0.0",
   },
+  "catalog": {
+    "@gitbook/api": "^0.120.0",
+  },
   "packages": {
     "@ai-sdk/provider": ["@ai-sdk/provider@1.1.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew=="],
 
@@ -604,6 +618,8 @@
 
     "@esbuild/win32-x64": ["@esbuild/win32-x64@0.24.2", "", { "os": "win32", "cpu": "x64" }, "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg=="],
 
+    "@evan/concurrency": ["@evan/concurrency@0.0.3", "", {}, "sha512-vjhkm2nrXoM39G4aP/U4CC5vFv/ZlMRSjbTII0N65J9R0EpgjdGswnHKS1KSjfGAp/9zKSNaojBgi0SxKnGapw=="],
+
     "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
 
     "@floating-ui/core": ["@floating-ui/core@1.6.8", "", { "dependencies": { "@floating-ui/utils": "^0.2.8" } }, "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA=="],
@@ -644,6 +660,8 @@
 
     "@gitbook/fontawesome-pro": ["@gitbook/fontawesome-pro@1.0.8", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "^6.6.0" } }, "sha512-i4PgiuGyUb52Muhc52kK3aMJIMfMkA2RbPW30tre8a6M8T6mWTfYo6gafSgjNvF1vH29zcuB8oBYnF0gO4XcHA=="],
 
+    "@gitbook/fonts": ["@gitbook/fonts@workspace:packages/fonts"],
+
     "@gitbook/icons": ["@gitbook/icons@workspace:packages/icons"],
 
     "@gitbook/openapi-parser": ["@gitbook/openapi-parser@workspace:packages/openapi-parser"],
@@ -798,6 +816,22 @@
 
     "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
 
+    "@octokit/auth-token": ["@octokit/auth-token@5.1.2", "", {}, "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw=="],
+
+    "@octokit/core": ["@octokit/core@6.1.5", "", { "dependencies": { "@octokit/auth-token": "^5.0.0", "@octokit/graphql": "^8.2.2", "@octokit/request": "^9.2.3", "@octokit/request-error": "^6.1.8", "@octokit/types": "^14.0.0", "before-after-hook": "^3.0.2", "universal-user-agent": "^7.0.0" } }, "sha512-vvmsN0r7rguA+FySiCsbaTTobSftpIDIpPW81trAmsv9TGxg3YCujAxRYp/Uy8xmDgYCzzgulG62H7KYUFmeIg=="],
+
+    "@octokit/endpoint": ["@octokit/endpoint@10.1.4", "", { "dependencies": { "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA=="],
+
+    "@octokit/graphql": ["@octokit/graphql@8.2.2", "", { "dependencies": { "@octokit/request": "^9.2.3", "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA=="],
+
+    "@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
+
+    "@octokit/request": ["@octokit/request@9.2.3", "", { "dependencies": { "@octokit/endpoint": "^10.1.4", "@octokit/request-error": "^6.1.8", "@octokit/types": "^14.0.0", "fast-content-type-parse": "^2.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-Ma+pZU8PXLOEYzsWf0cn/gY+ME57Wq8f49WTXA8FMHp2Ps9djKw//xYJ1je8Hm0pR2lU9FUGeJRWOtxq6olt4w=="],
+
+    "@octokit/request-error": ["@octokit/request-error@6.1.8", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ=="],
+
+    "@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
+
     "@opennextjs/aws": ["@opennextjs/aws@3.6.5", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "cookie": "^1.0.2", "esbuild": "0.25.4", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-wni+CWlRCyWfhNfekQBBPPkrDDnaGdZLN9hMybKI0wKOKTO+zhPOqR65Eh3V0pzWAi84Sureb5mdMuLwCxAAcw=="],
 
     "@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.2.1", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.6.5", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.19.1" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-cOco+nHwlo/PLB1bThF8IIvaS8PgAT9MEI5ZttFO/qt6spgvr2lUaPkpjgSIQmI3sBIEG2cLUykvQ2nbbZEcVw=="],
@@ -1470,6 +1504,8 @@
 
     "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
 
+    "before-after-hook": ["before-after-hook@3.0.2", "", {}, "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A=="],
+
     "better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "^1.0.0" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="],
 
     "bignumber.js": ["bignumber.js@9.1.2", "", {}, "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug=="],
@@ -1482,6 +1518,8 @@
 
     "body-parser": ["body-parser@2.0.2", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "3.1.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.5.2", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "^3.0.0", "type-is": "~1.6.18" } }, "sha512-SNMk0OONlQ01uk8EPeiBvTW7W4ovpL5b1O3t1sjpPgfxOQ6BqQJ6XjxinDPR79Z6HdcD5zBBwr5ssiTlgdNztQ=="],
 
+    "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
+
     "bowser": ["bowser@2.11.0", "", {}, "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="],
 
     "boxen": ["boxen@4.2.0", "", { "dependencies": { "ansi-align": "^3.0.0", "camelcase": "^5.3.1", "chalk": "^3.0.0", "cli-boxes": "^2.2.0", "string-width": "^4.1.0", "term-size": "^2.1.0", "type-fest": "^0.8.1", "widest-line": "^3.1.0" } }, "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ=="],
@@ -1506,6 +1544,8 @@
 
     "bytes": ["bytes@3.1.0", "", {}, "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="],
 
+    "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
+
     "cacheable": ["cacheable@1.8.9", "", { "dependencies": { "hookified": "^1.7.1", "keyv": "^5.3.1" } }, "sha512-FicwAUyWnrtnd4QqYAoRlNs44/a1jTL7XDKqm5gJ90wz1DQPlC7U2Rd1Tydpv+E7WAr4sQHuw8Q8M3nZMAyecQ=="],
 
     "cacheable-request": ["cacheable-request@6.1.0", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^3.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^4.1.0", "responselike": "^1.0.2" } }, "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg=="],
@@ -1602,10 +1642,16 @@
 
     "css-functions-list": ["css-functions-list@3.2.3", "", {}, "sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA=="],
 
+    "css-select": ["css-select@5.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg=="],
+
     "css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="],
 
+    "css-what": ["css-what@6.1.0", "", {}, "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="],
+
     "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
 
+    "cssom": ["cssom@0.5.0", "", {}, "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw=="],
+
     "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
 
     "cva": ["cva@1.0.0-beta.2", "", { "dependencies": { "clsx": "^2.1.1" }, "peerDependencies": { "typescript": ">= 4.5.5 < 6" }, "optionalPeers": ["typescript"] }, "sha512-dqcOFe247I5pKxfuzqfq3seLL5iMYsTgo40Uw7+pKZAntPgFtR7Tmy59P5IVIq/XgB0NQWoIvYDt9TwHkuK8Cg=="],
@@ -1662,6 +1708,14 @@
 
     "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
 
+    "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
+
+    "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
+
+    "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
+
+    "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
+
     "dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="],
 
     "dotenv": ["dotenv@16.4.7", "", {}, "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ=="],
@@ -1694,7 +1748,7 @@
 
     "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="],
 
-    "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
+    "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
 
     "env-cmd": ["env-cmd@10.1.0", "", { "dependencies": { "commander": "^4.0.0", "cross-spawn": "^7.0.0" }, "bin": { "env-cmd": "bin/env-cmd.js" } }, "sha512-mMdWTT9XKN7yNth/6N6g2GuKuJTsKMDHlQFUDacb/heQRRWOTIZ42t1rMHnQu4jYxU1ajdTeJM+9eEETlqToMA=="],
 
@@ -1800,6 +1854,8 @@
 
     "external-editor": ["external-editor@3.1.0", "", { "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" } }, "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew=="],
 
+    "fast-content-type-parse": ["fast-content-type-parse@2.0.1", "", {}, "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q=="],
+
     "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
 
     "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
@@ -1904,6 +1960,8 @@
 
     "google-auth-library": ["google-auth-library@5.10.1", "", { "dependencies": { "arrify": "^2.0.0", "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "fast-text-encoding": "^1.0.0", "gaxios": "^2.1.0", "gcp-metadata": "^3.4.0", "gtoken": "^4.1.0", "jws": "^4.0.0", "lru-cache": "^5.0.0" } }, "sha512-rOlaok5vlpV9rSiUu5EpR0vVpc+PhN62oF4RyX/6++DG1VsaulAFEMlDYBLjJDDPI6OcNOCGAKy9UVB/3NIDXg=="],
 
+    "google-font-metadata": ["google-font-metadata@6.0.3", "", { "dependencies": { "@evan/concurrency": "^0.0.3", "@octokit/core": "^6.1.2", "cac": "^6.7.14", "consola": "^3.3.3", "deepmerge": "^4.3.1", "json-stringify-pretty-compact": "^4.0.0", "linkedom": "^0.18.6", "pathe": "^1.1.2", "picocolors": "^1.1.1", "playwright": "^1.49.1", "stylis": "^4.3.4", "zod": "^3.24.1" }, "bin": { "gfm": "dist/cli.mjs" } }, "sha512-62+QB+nmBKppHrxwvILZkV/EvKctEAdzkKBIJY26TtwZkUjeEqmAQnE5uHvtMTdB8odqGpQu5LAKPqqUYHI9wA=="],
+
     "google-p12-pem": ["google-p12-pem@2.0.5", "", { "dependencies": { "node-forge": "^0.10.0" }, "bin": { "gp12-pem": "build/src/bin/gp12-pem.js" } }, "sha512-7RLkxwSsMsYh9wQ5Vb2zRtkAHvqPvfoMGag+nugl1noYO7gf0844Yr9TIFA5NEBMAeVt2Z+Imu7CQMp3oNatzQ=="],
 
     "googleapis": ["googleapis@47.0.0", "", { "dependencies": { "google-auth-library": "^5.6.1", "googleapis-common": "^3.2.0" } }, "sha512-+Fnjgcc3Na/rk57dwxqW1V0HJXJFjnt3aqFlckULqAqsPkmex/AyJJe6MSlXHC37ZmlXEb9ZtPGUp5ZzRDXpHg=="],
@@ -1980,12 +2038,16 @@
 
     "hosted-git-info": ["hosted-git-info@2.8.9", "", {}, "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="],
 
+    "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="],
+
     "html-tags": ["html-tags@3.3.1", "", {}, "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ=="],
 
     "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
 
     "html-whitespace-sensitive-tag-names": ["html-whitespace-sensitive-tag-names@3.0.1", "", {}, "sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA=="],
 
+    "htmlparser2": ["htmlparser2@10.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.1", "entities": "^6.0.0" } }, "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g=="],
+
     "http-cache-semantics": ["http-cache-semantics@4.1.1", "", {}, "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="],
 
     "http-errors": ["http-errors@1.4.0", "", { "dependencies": { "inherits": "2.0.1", "statuses": ">= 1.2.1 < 2" } }, "sha512-oLjPqve1tuOl5aRhv8GK5eHpqP1C9fb+Ol+XTLjKfLltE44zdDbEdjPSbU7Ch5rSNsVFqZn97SrMmZLdu1/YMw=="],
@@ -2096,6 +2158,8 @@
 
     "json-stringify-deterministic": ["json-stringify-deterministic@1.0.12", "", {}, "sha512-q3PN0lbUdv0pmurkBNdJH3pfFvOTL/Zp0lquqpvcjfKzt6Y0j49EPHAmVHCAS4Ceq/Y+PejWTzyiVpoY71+D6g=="],
 
+    "json-stringify-pretty-compact": ["json-stringify-pretty-compact@4.0.0", "", {}, "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q=="],
+
     "json-xml-parse": ["json-xml-parse@1.3.0", "", {}, "sha512-MVosauc/3W2wL4dd4yaJzH5oXw+HOUfptn0+d4+bFghMiJFop7MaqIwFXJNLiRnNYJNQ6L4o7B+53n5wcvoLFw=="],
 
     "jsondiffpatch": ["jsondiffpatch@0.6.0", "", { "dependencies": { "@types/diff-match-patch": "^1.0.36", "chalk": "^5.3.0", "diff-match-patch": "^1.0.5" }, "bin": { "jsondiffpatch": "bin/jsondiffpatch.js" } }, "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ=="],
@@ -2134,6 +2198,8 @@
 
     "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
 
+    "linkedom": ["linkedom@0.18.11", "", { "dependencies": { "css-select": "^5.1.0", "cssom": "^0.5.0", "html-escaper": "^3.0.3", "htmlparser2": "^10.0.0", "uhyphen": "^0.2.0" } }, "sha512-K03GU3FUlnhBAP0jPb7tN7YJl7LbjZx30Z8h6wgLXusnKF7+BEZvfEbdkN/lO9LfFzxN3S0ZAriDuJ/13dIsLA=="],
+
     "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
 
     "lodash.castarray": ["lodash.castarray@4.4.0", "", {}, "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q=="],
@@ -2364,6 +2430,8 @@
 
     "npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
 
+    "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
+
     "nuqs": ["nuqs@2.2.3", "", { "dependencies": { "mitt": "^3.0.1" }, "peerDependencies": { "@remix-run/react": ">=2", "next": ">=14.2.0", "react": ">=18.2.0 || ^19.0.0-0", "react-router-dom": ">=6" }, "optionalPeers": ["@remix-run/react", "next", "react-router-dom"] }, "sha512-nMCcUW06KSqEXA0xp+LiRqDpIE59BVYbjZLe0HUisJAlswfihHYSsAjYTzV0lcE1thfh8uh+LqUHGdQ8qq8rfA=="],
 
     "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
@@ -2454,7 +2522,7 @@
 
     "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="],
 
-    "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
+    "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
 
     "pcre-to-regexp": ["pcre-to-regexp@1.1.0", "", {}, "sha512-KF9XxmUQJ2DIlMj3TqNqY1AWvyvTuIuq11CuuekxyaYMiFuMKGgQrePYMX5bXKLhLG3sDI4CsGAYHPaT7VV7+g=="],
 
@@ -2722,6 +2790,8 @@
 
     "stylelint": ["stylelint@16.16.0", "", { "dependencies": { "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "@csstools/media-query-list-parser": "^4.0.2", "@csstools/selector-specificity": "^5.0.0", "@dual-bundle/import-meta-resolve": "^4.1.0", "balanced-match": "^2.0.0", "colord": "^2.9.3", "cosmiconfig": "^9.0.0", "css-functions-list": "^3.2.3", "css-tree": "^3.1.0", "debug": "^4.3.7", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", "file-entry-cache": "^10.0.7", "global-modules": "^2.0.0", "globby": "^11.1.0", "globjoin": "^0.1.4", "html-tags": "^3.3.1", "ignore": "^7.0.3", "imurmurhash": "^0.1.4", "is-plain-object": "^5.0.0", "known-css-properties": "^0.35.0", "mathml-tag-names": "^2.1.3", "meow": "^13.2.0", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.5.3", "postcss-resolve-nested-selector": "^0.1.6", "postcss-safe-parser": "^7.0.1", "postcss-selector-parser": "^7.1.0", "postcss-value-parser": "^4.2.0", "resolve-from": "^5.0.0", "string-width": "^4.2.3", "supports-hyperlinks": "^3.2.0", "svg-tags": "^1.0.0", "table": "^6.9.0", "write-file-atomic": "^5.0.1" }, "bin": { "stylelint": "bin/stylelint.mjs" } }, "sha512-40X5UOb/0CEFnZVEHyN260HlSSUxPES+arrUphOumGWgXERHfwCD0kNBVILgQSij8iliYVwlc0V7M5bcLP9vPg=="],
 
+    "stylis": ["stylis@4.3.6", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="],
+
     "sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="],
 
     "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
@@ -2826,6 +2896,8 @@
 
     "ufo": ["ufo@1.5.4", "", {}, "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ=="],
 
+    "uhyphen": ["uhyphen@0.2.0", "", {}, "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA=="],
+
     "uid-promise": ["uid-promise@1.0.0", "", {}, "sha512-R8375j0qwXyIu/7R0tjdF06/sElHqbmdmWC9M2qQHpEVbvE4I5+38KJI7LUUmQMp7NVq4tKHiBMkT0NFM453Ig=="],
 
     "undici": ["undici@5.28.4", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g=="],
@@ -2852,6 +2924,8 @@
 
     "unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="],
 
+    "universal-user-agent": ["universal-user-agent@7.0.3", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="],
+
     "universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
 
     "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
@@ -4014,6 +4088,8 @@
 
     "@vercel/static-config/ajv": ["ajv@8.6.3", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw=="],
 
+    "@vue/compiler-core/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
+
     "@vue/compiler-sfc/postcss": ["postcss@8.4.47", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.0", "source-map-js": "^1.2.1" } }, "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ=="],
 
     "@vueuse/integrations/@vueuse/core": ["@vueuse/core@11.2.0", "", { "dependencies": { "@types/web-bluetooth": "^0.0.20", "@vueuse/metadata": "11.2.0", "@vueuse/shared": "11.2.0", "vue-demi": ">=0.14.10" } }, "sha512-JIUwRcOqOWzcdu1dGlfW04kaJhW3EXnnjJJfLTtddJanymTL7lF1C0+dVVZ/siLfc73mWn+cGP1PE1PKPruRSA=="],
@@ -4062,6 +4138,8 @@
 
     "decamelize-keys/map-obj": ["map-obj@1.0.1", "", {}, "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg=="],
 
+    "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
+
     "edge-runtime/async-listen": ["async-listen@3.0.1", "", {}, "sha512-cWMaNwUJnf37C/S5TfCkk/15MwbPRwVYALA2jtjkbHjCmAPiDXyNJy2q3p1KAZzDLHAWyarUWSujUoHR4pEgrA=="],
 
     "edge-runtime/picocolors": ["picocolors@1.0.0", "", {}, "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="],
@@ -4110,6 +4188,8 @@
 
     "google-auth-library/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
 
+    "google-font-metadata/picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
     "googleapis-common/uuid": ["uuid@7.0.3", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg=="],
 
     "got/get-stream": ["get-stream@4.1.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w=="],
@@ -4178,6 +4258,8 @@
 
     "package-json/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
 
+    "parse5/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
+
     "path-match/path-to-regexp": ["path-to-regexp@1.9.0", "", { "dependencies": { "isarray": "0.0.1" } }, "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g=="],
 
     "path-scurry/lru-cache": ["lru-cache@11.0.2", "", {}, "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA=="],
@@ -4270,6 +4352,8 @@
 
     "ts-node/arg": ["arg@4.1.0", "", {}, "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg=="],
 
+    "unenv/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
+
     "update-notifier/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="],
 
     "url-parse-lax/prepend-http": ["prepend-http@2.0.0", "", {}, "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA=="],
diff --git a/package.json b/package.json
index dbb94b88ed..1faa750ba8 100644
--- a/package.json
+++ b/package.json
@@ -7,10 +7,9 @@
         "turbo": "^2.5.0",
         "vercel": "^39.3.0"
     },
-    "packageManager": "bun@1.2.11",
+    "packageManager": "bun@1.2.15",
     "overrides": {
         "@codemirror/state": "6.4.1",
-        "@gitbook/api": "^0.120.0",
         "react": "^19.0.0",
         "react-dom": "^19.0.0"
     },
@@ -34,7 +33,12 @@
         "download:env": "op read op://gitbook-x-dev/gitbook-open/.env.local >> .env.local",
         "clean": "turbo run clean"
     },
-    "workspaces": ["packages/*"],
+    "workspaces": {
+        "packages": ["packages/*"],
+        "catalog": {
+            "@gitbook/api": "^0.120.0"
+        }
+    },
     "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"
diff --git a/packages/cache-tags/package.json b/packages/cache-tags/package.json
index eafeabc44c..a57048f2f6 100644
--- a/packages/cache-tags/package.json
+++ b/packages/cache-tags/package.json
@@ -10,7 +10,7 @@
     },
     "version": "0.3.1",
     "dependencies": {
-        "@gitbook/api": "^0.120.0",
+        "@gitbook/api": "catalog:",
         "assert-never": "^1.2.1"
     },
     "devDependencies": {
diff --git a/packages/fonts/.gitignore b/packages/fonts/.gitignore
new file mode 100644
index 0000000000..253839862f
--- /dev/null
+++ b/packages/fonts/.gitignore
@@ -0,0 +1,2 @@
+dist/
+src/data/*.json
diff --git a/packages/fonts/README.md b/packages/fonts/README.md
new file mode 100644
index 0000000000..fef253f27f
--- /dev/null
+++ b/packages/fonts/README.md
@@ -0,0 +1,3 @@
+# `@gitbook/fonts`
+
+Utilities to lookup default fonts supported by GitBook.
diff --git a/packages/fonts/bin/generate.ts b/packages/fonts/bin/generate.ts
new file mode 100644
index 0000000000..d4326d615c
--- /dev/null
+++ b/packages/fonts/bin/generate.ts
@@ -0,0 +1,91 @@
+import fs from 'node:fs/promises';
+import path from 'node:path';
+
+import { APIv2 } from 'google-font-metadata';
+
+import { CustomizationDefaultFont } from '@gitbook/api';
+
+import type { FontDefinitions } from '../src/types';
+
+const googleFontsMap: { [fontName in CustomizationDefaultFont]: string } = {
+    [CustomizationDefaultFont.Inter]: 'inter',
+    [CustomizationDefaultFont.FiraSans]: 'fira-sans-extra-condensed',
+    [CustomizationDefaultFont.IBMPlexSerif]: 'ibm-plex-serif',
+    [CustomizationDefaultFont.Lato]: 'lato',
+    [CustomizationDefaultFont.Merriweather]: 'merriweather',
+    [CustomizationDefaultFont.NotoSans]: 'noto-sans',
+    [CustomizationDefaultFont.OpenSans]: 'open-sans',
+    [CustomizationDefaultFont.Overpass]: 'overpass',
+    [CustomizationDefaultFont.Poppins]: 'poppins',
+    [CustomizationDefaultFont.Raleway]: 'raleway',
+    [CustomizationDefaultFont.Roboto]: 'roboto',
+    [CustomizationDefaultFont.RobotoSlab]: 'roboto-slab',
+    [CustomizationDefaultFont.SourceSansPro]: 'source-sans-3',
+    [CustomizationDefaultFont.Ubuntu]: 'ubuntu',
+    [CustomizationDefaultFont.ABCFavorit]: 'inter',
+};
+
+/**
+ * Scripts to generate the list of all icons.
+ */
+async function main() {
+    // @ts-expect-error - we build the object
+    const output: FontDefinitions = {};
+
+    for (const font of Object.values(CustomizationDefaultFont)) {
+        const googleFontName = googleFontsMap[font];
+        const fontMetadata = APIv2[googleFontName.toLowerCase()];
+        if (!fontMetadata) {
+            throw new Error(`Font ${googleFontName} not found`);
+        }
+
+        output[font] = {
+            font: googleFontName,
+            unicodeRange: fontMetadata.unicodeRange,
+            variants: {
+                '400': {},
+                '700': {},
+            },
+        };
+
+        Object.keys(output[font].variants).forEach((weight) => {
+            const variants = fontMetadata.variants[weight];
+            const normalVariant = variants.normal;
+            if (!normalVariant) {
+                throw new Error(`Font ${googleFontName} has no normal variant`);
+            }
+
+            output[font].variants[weight] = {};
+            Object.entries(normalVariant).forEach(([script, url]) => {
+                output[font].variants[weight][script] = url.url.woff;
+            });
+        });
+    }
+
+    await writeDataFile('fonts', JSON.stringify(output, null, 2));
+}
+
+/**
+ * We write both in dist and src as the build process might have happen already
+ * and tsc doesn't copy the files.
+ */
+async function writeDataFile(name, content) {
+    const srcData = path.resolve(__dirname, '../src/data');
+    const distData = path.resolve(__dirname, '../dist/data');
+
+    // Ensure the directories exists
+    await Promise.all([
+        fs.mkdir(srcData, { recursive: true }),
+        fs.mkdir(distData, { recursive: true }),
+    ]);
+
+    await Promise.all([
+        fs.writeFile(path.resolve(srcData, `${name}.json`), content),
+        fs.writeFile(path.resolve(distData, `${name}.json`), content),
+    ]);
+}
+
+main().catch((error) => {
+    console.error(`Error generating icons list: ${error}`);
+    process.exit(1);
+});
diff --git a/packages/fonts/package.json b/packages/fonts/package.json
new file mode 100644
index 0000000000..dbfb2d7ca0
--- /dev/null
+++ b/packages/fonts/package.json
@@ -0,0 +1,31 @@
+{
+    "name": "@gitbook/fonts",
+    "type": "module",
+    "exports": {
+        ".": {
+            "types": "./dist/index.d.ts",
+            "development": "./src/index.ts",
+            "default": "./dist/index.js"
+        }
+    },
+    "version": "0.0.0",
+    "dependencies": {
+        "@gitbook/api": "catalog:"
+    },
+    "devDependencies": {
+        "google-font-metadata": "^6.0.3",
+        "typescript": "^5.5.3"
+    },
+    "scripts": {
+        "generate": "bun ./bin/generate.js",
+        "build": "tsc --project tsconfig.build.json",
+        "typecheck": "tsc --noEmit",
+        "dev": "tsc -w",
+        "clean": "rm -rf ./dist && rm -rf ./src/data",
+        "unit": "bun test"
+    },
+    "files": ["dist", "src", "bin", "README.md", "CHANGELOG.md"],
+    "engines": {
+        "node": ">=20.0.0"
+    }
+}
diff --git a/packages/fonts/src/__snapshots__/getDefaultFont.test.ts.snap b/packages/fonts/src/__snapshots__/getDefaultFont.test.ts.snap
new file mode 100644
index 0000000000..89184be22c
--- /dev/null
+++ b/packages/fonts/src/__snapshots__/getDefaultFont.test.ts.snap
@@ -0,0 +1,57 @@
+// Bun Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`getDefaultFont should return correct object for Latin text 1`] = `
+{
+  "font": "Inter",
+  "url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
+}
+`;
+
+exports[`getDefaultFont should return correct object for Cyrillic text 1`] = `
+{
+  "font": "Inter",
+  "url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZthjp-Ek-_0ewmM.woff",
+}
+`;
+
+exports[`getDefaultFont should return correct object for Greek text 1`] = `
+{
+  "font": "Inter",
+  "url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZxhjp-Ek-_0ewmM.woff",
+}
+`;
+
+exports[`getDefaultFont should handle mixed script text 1`] = `
+{
+  "font": "Inter",
+  "url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZthjp-Ek-_0ewmM.woff",
+}
+`;
+
+exports[`getDefaultFont should handle different font weights: regular 1`] = `
+{
+  "font": "Inter",
+  "url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
+}
+`;
+
+exports[`getDefaultFont should handle different font weights: bold 1`] = `
+{
+  "font": "Inter",
+  "url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuFuYAZ9hjp-Ek-_0ew.woff",
+}
+`;
+
+exports[`getDefaultFont should handle different fonts: inter 1`] = `
+{
+  "font": "Inter",
+  "url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
+}
+`;
+
+exports[`getDefaultFont should handle different fonts: roboto 1`] = `
+{
+  "font": "Roboto",
+  "url": "https://fonts.gstatic.com/s/roboto/v32/KFOmCnqEu92Fr1Mu4mxMKTU1Kg.woff",
+}
+`;
diff --git a/packages/fonts/src/fonts.ts b/packages/fonts/src/fonts.ts
new file mode 100644
index 0000000000..c9544efb19
--- /dev/null
+++ b/packages/fonts/src/fonts.ts
@@ -0,0 +1,5 @@
+import type { FontDefinitions } from './types';
+
+import rawFonts from './data/fonts.json' with { type: 'json' };
+
+export const fonts: FontDefinitions = rawFonts;
diff --git a/packages/fonts/src/getDefaultFont.test.ts b/packages/fonts/src/getDefaultFont.test.ts
new file mode 100644
index 0000000000..58afbeac93
--- /dev/null
+++ b/packages/fonts/src/getDefaultFont.test.ts
@@ -0,0 +1,119 @@
+import { describe, expect, it } from 'bun:test';
+import { CustomizationDefaultFont } from '@gitbook/api';
+import { getDefaultFont } from './getDefaultFont';
+
+describe('getDefaultFont', () => {
+    it('should return null for invalid font', () => {
+        const result = getDefaultFont({
+            font: 'invalid-font' as CustomizationDefaultFont,
+            text: 'Hello',
+            weight: 400,
+        });
+        expect(result).toBeNull();
+    });
+
+    it('should return null for invalid weight', () => {
+        const result = getDefaultFont({
+            font: CustomizationDefaultFont.Inter,
+            text: 'Hello',
+            weight: 999 as any,
+        });
+        expect(result).toBeNull();
+    });
+
+    it('should return null for text not supported by any script', () => {
+        const result = getDefaultFont({
+            font: CustomizationDefaultFont.Inter,
+            text: '😀', // Emoji not supported by Inter
+            weight: 400,
+        });
+        expect(result).toBeNull();
+    });
+
+    it('should return correct object for Latin text', () => {
+        const result = getDefaultFont({
+            font: CustomizationDefaultFont.Inter,
+            text: 'Hello World',
+            weight: 400,
+        });
+        expect(result).not.toBeNull();
+        expect(result?.font).toBe(CustomizationDefaultFont.Inter);
+        expect(result).toMatchSnapshot();
+    });
+
+    it('should return correct object for Cyrillic text', () => {
+        const result = getDefaultFont({
+            font: CustomizationDefaultFont.Inter,
+            text: 'Привет мир',
+            weight: 400,
+        });
+        expect(result).not.toBeNull();
+        expect(result?.font).toBe(CustomizationDefaultFont.Inter);
+        expect(result).toMatchSnapshot();
+    });
+
+    it('should return correct object for Greek text', () => {
+        const result = getDefaultFont({
+            font: CustomizationDefaultFont.Inter,
+            text: 'Γεια σας',
+            weight: 400,
+        });
+        expect(result).not.toBeNull();
+        expect(result?.font).toBe(CustomizationDefaultFont.Inter);
+        expect(result).toMatchSnapshot();
+    });
+
+    it('should handle mixed script text', () => {
+        const result = getDefaultFont({
+            font: CustomizationDefaultFont.Inter,
+            text: 'Hello Привет',
+            weight: 400,
+        });
+        expect(result).not.toBeNull();
+        expect(result?.font).toBe(CustomizationDefaultFont.Inter);
+        expect(result).toMatchSnapshot();
+    });
+
+    it('should handle different font weights', () => {
+        const regular = getDefaultFont({
+            font: CustomizationDefaultFont.Inter,
+            text: 'Hello',
+            weight: 400,
+        });
+        const bold = getDefaultFont({
+            font: CustomizationDefaultFont.Inter,
+            text: 'Hello',
+            weight: 700,
+        });
+        expect(regular).not.toBeNull();
+        expect(bold).not.toBeNull();
+        expect(regular).toMatchSnapshot('regular');
+        expect(bold).toMatchSnapshot('bold');
+    });
+
+    it('should handle empty string', () => {
+        const result = getDefaultFont({
+            font: CustomizationDefaultFont.Inter,
+            text: '',
+            weight: 400,
+        });
+        expect(result).toBeNull();
+    });
+
+    it('should handle different fonts', () => {
+        const inter = getDefaultFont({
+            font: CustomizationDefaultFont.Inter,
+            text: 'Hello',
+            weight: 400,
+        });
+        const roboto = getDefaultFont({
+            font: CustomizationDefaultFont.Roboto,
+            text: 'Hello',
+            weight: 400,
+        });
+        expect(inter).not.toBeNull();
+        expect(roboto).not.toBeNull();
+        expect(inter).toMatchSnapshot('inter');
+        expect(roboto).toMatchSnapshot('roboto');
+    });
+});
diff --git a/packages/fonts/src/getDefaultFont.ts b/packages/fonts/src/getDefaultFont.ts
new file mode 100644
index 0000000000..ea18e8bba2
--- /dev/null
+++ b/packages/fonts/src/getDefaultFont.ts
@@ -0,0 +1,112 @@
+import type { CustomizationDefaultFont } from '@gitbook/api';
+import { fonts } from './fonts';
+import type { FontWeight } from './types';
+
+/**
+ * Get the URL to load a font for a text.
+ */
+export function getDefaultFont(input: {
+    /**
+     * GitBook font to use.
+     */
+    font: CustomizationDefaultFont;
+
+    /**
+     * Text to display with the font.
+     */
+    text: string;
+
+    /**
+     * Font weight to use.
+     */
+    weight: FontWeight;
+}): { font: string; url: string } | null {
+    if (!input.text.trim()) {
+        return null;
+    }
+
+    const fontDefinition = fonts[input.font];
+    if (!fontDefinition) {
+        return null;
+    }
+
+    const variant = fontDefinition.variants[`${input.weight}`];
+    if (!variant) {
+        return null;
+    }
+
+    const script = getBestUnicodeRange(input.text, fontDefinition.unicodeRange);
+    if (!script) {
+        return null;
+    }
+
+    return variant[script]
+        ? {
+              font: input.font,
+              url: variant[script],
+          }
+        : null;
+}
+
+/**
+ * Determine which named @font-face unicode-range covers
+ * the greatest share of the characters in `text`.
+ *
+ * @param text   The text you want to inspect.
+ * @param ranges An object whose keys are range names and whose
+ *               values are CSS-style comma-separated unicode-range
+ *               declarations (e.g. "U+0370-03FF,U+1F00-1FFF").
+ * @returns      The key of the best-matching range, or `null`
+ *               when nothing matches at all.
+ */
+function getBestUnicodeRange(text: string, ranges: Record<string, string>): string | null {
+    // ---------- helper: parse "U+XXXX" or "U+XXXX-YYYY" ----------
+    const parseOne = (token: string): [number, number] | null => {
+        token = token.trim().toUpperCase();
+        if (!token.startsWith('U+')) return null;
+
+        const body = token.slice(2); // drop "U+"
+        const [startHex, endHex] = body.split('-');
+        const start = Number.parseInt(startHex, 16);
+        const end = endHex ? Number.parseInt(endHex, 16) : start;
+
+        if (Number.isNaN(start) || Number.isNaN(end) || end < start) return null;
+        return [start, end];
+    };
+
+    // ---------- helper: build lookup table ----------
+    const parsed: Record<string, [number, number][]> = {};
+    for (const [label, list] of Object.entries(ranges)) {
+        parsed[label] = list
+            .split(',')
+            .map(parseOne)
+            .filter((x): x is [number, number] => x !== null);
+    }
+
+    // ---------- tally code-point hits ----------
+    const hits: Record<string, number> = Object.fromEntries(Object.keys(parsed).map((k) => [k, 0]));
+
+    for (let i = 0; i < text.length; ) {
+        const cp = text.codePointAt(i)!;
+        i += cp > 0xffff ? 2 : 1; // advance by 1 UTF-16 code-unit (or 2 for surrogates)
+
+        for (const [label, rangesArr] of Object.entries(parsed)) {
+            if (rangesArr.some(([lo, hi]) => cp >= lo && cp <= hi)) {
+                hits[label]++;
+            }
+        }
+    }
+
+    // ---------- choose the "best" ----------
+    let winner: string | null = null;
+    let maxCount = 0;
+
+    for (const [label, count] of Object.entries(hits)) {
+        if (count > maxCount) {
+            maxCount = count;
+            winner = label;
+        }
+    }
+
+    return maxCount > 0 ? winner : null;
+}
diff --git a/packages/fonts/src/index.ts b/packages/fonts/src/index.ts
new file mode 100644
index 0000000000..fbb3584899
--- /dev/null
+++ b/packages/fonts/src/index.ts
@@ -0,0 +1,2 @@
+export * from './getDefaultFont';
+export * from './types';
diff --git a/packages/fonts/src/types.ts b/packages/fonts/src/types.ts
new file mode 100644
index 0000000000..899b32c49f
--- /dev/null
+++ b/packages/fonts/src/types.ts
@@ -0,0 +1,17 @@
+import type { CustomizationDefaultFont } from '@gitbook/api';
+
+export type FontWeight = 400 | 700;
+
+export type FontDefinition = {
+    font: string;
+    unicodeRange: {
+        [script: string]: string;
+    };
+    variants: {
+        [weight in string]: {
+            [script: string]: string;
+        };
+    };
+};
+
+export type FontDefinitions = { [fontName in CustomizationDefaultFont]: FontDefinition };
diff --git a/packages/fonts/tsconfig.build.json b/packages/fonts/tsconfig.build.json
new file mode 100644
index 0000000000..e4828bc1f6
--- /dev/null
+++ b/packages/fonts/tsconfig.build.json
@@ -0,0 +1,4 @@
+{
+    "extends": "./tsconfig.json",
+    "exclude": ["node_modules", "src/**/*.test.ts"]
+}
diff --git a/packages/fonts/tsconfig.json b/packages/fonts/tsconfig.json
new file mode 100644
index 0000000000..2b3fe87c5f
--- /dev/null
+++ b/packages/fonts/tsconfig.json
@@ -0,0 +1,24 @@
+{
+    "compilerOptions": {
+        "lib": ["es2023"],
+        "module": "ESNext",
+        "target": "es2022",
+        "strict": true,
+        "esModuleInterop": true,
+        "skipLibCheck": true,
+        "moduleResolution": "bundler",
+        "allowJs": true,
+        "noEmit": false,
+        "declaration": true,
+        "outDir": "dist",
+        "resolveJsonModule": true,
+        "isolatedModules": true,
+        "jsx": "react-jsx",
+        "incremental": true,
+        "types": [
+            "bun-types" // add Bun global
+        ]
+    },
+    "include": ["src/**/*.ts"],
+    "exclude": ["node_modules"]
+}
diff --git a/packages/fonts/turbo.json b/packages/fonts/turbo.json
new file mode 100644
index 0000000000..9097cda33c
--- /dev/null
+++ b/packages/fonts/turbo.json
@@ -0,0 +1,9 @@
+{
+    "extends": ["//"],
+    "tasks": {
+        "generate": {
+            "inputs": ["bin/**/*", "package.json"],
+            "outputs": ["src/data/*.json", "dist/data/*.json"]
+        }
+    }
+}
diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json
index 3993e746d3..5723cdbc79 100644
--- a/packages/gitbook-v2/package.json
+++ b/packages/gitbook-v2/package.json
@@ -3,7 +3,7 @@
     "version": "0.3.0",
     "private": true,
     "dependencies": {
-        "@gitbook/api": "^0.120.0",
+        "@gitbook/api": "catalog:",
         "@gitbook/cache-tags": "workspace:*",
         "@opennextjs/cloudflare": "1.2.1",
         "@sindresorhus/fnv1a": "^3.1.0",
diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json
index 74ef66a34e..666cd114d0 100644
--- a/packages/gitbook/package.json
+++ b/packages/gitbook/package.json
@@ -16,12 +16,13 @@
         "clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math"
     },
     "dependencies": {
-        "@gitbook/api": "^0.120.0",
+        "@gitbook/api": "catalog:",
         "@gitbook/cache-do": "workspace:*",
         "@gitbook/cache-tags": "workspace:*",
         "@gitbook/colors": "workspace:*",
         "@gitbook/emoji-codepoints": "workspace:*",
         "@gitbook/icons": "workspace:*",
+        "@gitbook/fonts": "workspace:*",
         "@gitbook/openapi-parser": "workspace:*",
         "@gitbook/react-contentkit": "workspace:*",
         "@gitbook/react-math": "workspace:*",
diff --git a/packages/gitbook/src/routes/ogimage.tsx b/packages/gitbook/src/routes/ogimage.tsx
index b086ffd1cd..e55dcfc2dd 100644
--- a/packages/gitbook/src/routes/ogimage.tsx
+++ b/packages/gitbook/src/routes/ogimage.tsx
@@ -1,5 +1,6 @@
 import { CustomizationDefaultFont, CustomizationHeaderPreset } from '@gitbook/api';
 import { colorContrast } from '@gitbook/colors';
+import { type FontWeight, getDefaultFont } from '@gitbook/fonts';
 import { redirect } from 'next/navigation';
 import { ImageResponse } from 'next/og';
 
@@ -11,24 +12,6 @@ import { getCacheTag } from '@gitbook/cache-tags';
 import type { GitBookSiteContext } from '@v2/lib/context';
 import { getResizedImageURL } from '@v2/lib/images';
 
-const googleFontsMap: { [fontName in CustomizationDefaultFont]: string } = {
-    [CustomizationDefaultFont.Inter]: 'Inter',
-    [CustomizationDefaultFont.FiraSans]: 'Fira Sans Extra Condensed',
-    [CustomizationDefaultFont.IBMPlexSerif]: 'IBM Plex Serif',
-    [CustomizationDefaultFont.Lato]: 'Lato',
-    [CustomizationDefaultFont.Merriweather]: 'Merriweather',
-    [CustomizationDefaultFont.NotoSans]: 'Noto Sans',
-    [CustomizationDefaultFont.OpenSans]: 'Open Sans',
-    [CustomizationDefaultFont.Overpass]: 'Overpass',
-    [CustomizationDefaultFont.Poppins]: 'Poppins',
-    [CustomizationDefaultFont.Raleway]: 'Raleway',
-    [CustomizationDefaultFont.Roboto]: 'Roboto',
-    [CustomizationDefaultFont.RobotoSlab]: 'Roboto Slab',
-    [CustomizationDefaultFont.SourceSansPro]: 'Source Sans 3',
-    [CustomizationDefaultFont.Ubuntu]: 'Ubuntu',
-    [CustomizationDefaultFont.ABCFavorit]: 'Inter',
-};
-
 /**
  * Render the OpenGraph image for a site content.
  */
@@ -65,19 +48,15 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
     const { fontFamily, fonts } = await (async () => {
         // google fonts
         if (typeof customization.styling.font === 'string') {
-            const fontFamily = googleFontsMap[customization.styling.font] ?? 'Inter';
+            const fontFamily = customization.styling.font ?? CustomizationDefaultFont.Inter;
 
             const regularText = pageDescription;
             const boldText = `${contentTitle}${pageTitle}`;
 
             const fonts = (
                 await Promise.all([
-                    getWithCache(`google-font:${fontFamily}:400`, () =>
-                        loadGoogleFont({ fontFamily, text: regularText, weight: 400 })
-                    ),
-                    getWithCache(`google-font:${fontFamily}:700`, () =>
-                        loadGoogleFont({ fontFamily, text: boldText, weight: 700 })
-                    ),
+                    loadGoogleFont({ font: fontFamily, text: regularText, weight: 400 }),
+                    loadGoogleFont({ font: fontFamily, text: boldText, weight: 700 }),
                 ])
             ).filter(filterOutNullable);
 
@@ -260,37 +239,31 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
     );
 }
 
-async function loadGoogleFont(input: { fontFamily: string; text: string; weight: 400 | 700 }) {
-    const { fontFamily, text, weight } = input;
-
-    if (!text.trim()) {
-        return null;
-    }
-
-    const url = new URL('https://fonts.googleapis.com/css2');
-    url.searchParams.set('family', `${fontFamily}:wght@${weight}`);
-    url.searchParams.set('text', text);
-
-    const result = await fetch(url.href);
-    if (!result.ok) {
-        return null;
-    }
-
-    const css = await result.text();
-    const resource = css.match(/src: url\((.+)\) format\('(opentype|truetype)'\)/);
-    const resourceUrl = resource ? resource[1] : null;
-
-    if (resourceUrl) {
-        const response = await fetch(resourceUrl);
-        if (response.ok) {
-            const data = await response.arrayBuffer();
-            return {
-                name: fontFamily,
-                data,
-                style: 'normal' as const,
-                weight,
-            };
-        }
+async function loadGoogleFont(input: {
+    font: CustomizationDefaultFont;
+    text: string;
+    weight: FontWeight;
+}) {
+    const lookup = getDefaultFont({
+        font: input.font,
+        text: input.text,
+        weight: input.weight,
+    });
+
+    // If we found a font file, load it
+    if (lookup) {
+        return getWithCache(`google-font-files:${lookup.url}`, async () => {
+            const response = await fetch(lookup.url);
+            if (response.ok) {
+                const data = await response.arrayBuffer();
+                return {
+                    name: lookup.font,
+                    data,
+                    style: 'normal' as const,
+                    weight: input.weight,
+                };
+            }
+        });
     }
 
     // If for some reason we can't load the font, we'll just use the default one
diff --git a/packages/icons/package.json b/packages/icons/package.json
index 472b7a7b3b..0e0b802123 100644
--- a/packages/icons/package.json
+++ b/packages/icons/package.json
@@ -32,7 +32,7 @@
     },
     "scripts": {
         "generate": "bun ./bin/gen-list.js",
-        "build": "tsc",
+        "build": "tsc --project tsconfig.build.json",
         "typecheck": "tsc --noEmit",
         "dev": "tsc -w",
         "clean": "rm -rf ./dist && rm -rf ./src/data",
@@ -41,7 +41,7 @@
     "bin": {
         "gitbook-icons": "./bin/gitbook-icons.js"
     },
-    "files": ["dist", "src", "bin", "data", "README.md", "CHANGELOG.md"],
+    "files": ["dist", "src", "bin", "README.md", "CHANGELOG.md"],
     "engines": {
         "node": ">=20.0.0"
     }
diff --git a/packages/icons/tsconfig.build.json b/packages/icons/tsconfig.build.json
new file mode 100644
index 0000000000..e4828bc1f6
--- /dev/null
+++ b/packages/icons/tsconfig.build.json
@@ -0,0 +1,4 @@
+{
+    "extends": "./tsconfig.json",
+    "exclude": ["node_modules", "src/**/*.test.ts"]
+}
diff --git a/packages/react-contentkit/package.json b/packages/react-contentkit/package.json
index eee7aa85b6..af2f568544 100644
--- a/packages/react-contentkit/package.json
+++ b/packages/react-contentkit/package.json
@@ -10,7 +10,7 @@
     },
     "dependencies": {
         "classnames": "^2.5.1",
-        "@gitbook/api": "^0.120.0",
+        "@gitbook/api": "catalog:",
         "@gitbook/icons": "workspace:*"
     },
     "peerDependencies": {

From 2d64c78787f4f94f8d67275eb8db8748ce7801a7 Mon Sep 17 00:00:00 2001
From: conico974 <nicodorseuil@yahoo.fr>
Date: Wed, 11 Jun 2025 13:47:34 +0200
Subject: [PATCH 090/127] return null for 404 cached page in the incremental
 cache (#3296)

Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
---
 .../gitbook-v2/openNext/incrementalCache.ts   | 21 ++++++++++++++++---
 1 file changed, 18 insertions(+), 3 deletions(-)

diff --git a/packages/gitbook-v2/openNext/incrementalCache.ts b/packages/gitbook-v2/openNext/incrementalCache.ts
index 28d1d6b8a1..8e4ca98635 100644
--- a/packages/gitbook-v2/openNext/incrementalCache.ts
+++ b/packages/gitbook-v2/openNext/incrementalCache.ts
@@ -49,17 +49,20 @@ class GitbookIncrementalCache implements IncrementalCache {
                     const localCacheEntry = await localCache.match(this.getCacheUrlKey(cacheKey));
                     if (localCacheEntry) {
                         span.setAttribute('cacheHit', 'local');
-                        return localCacheEntry.json();
+                        const result = (await localCacheEntry.json()) as WithLastModified<
+                            CacheValue<CacheType>
+                        >;
+                        return this.returnNullOn404(result);
                     }
 
                     const r2Object = await r2.get(cacheKey);
                     if (!r2Object) return null;
 
                     span.setAttribute('cacheHit', 'r2');
-                    return {
+                    return this.returnNullOn404({
                         value: await r2Object.json(),
                         lastModified: r2Object.uploaded.getTime(),
-                    };
+                    });
                 } catch (e) {
                     console.error('Failed to get from cache', e);
                     return null;
@@ -68,6 +71,18 @@ class GitbookIncrementalCache implements IncrementalCache {
         );
     }
 
+    //TODO: This is a workaround to handle 404 responses in the cache.
+    // It should be handled by OpenNext cache interception directly. This should be removed once OpenNext cache interception is fixed.
+    returnNullOn404<CacheType extends CacheEntryType = 'cache'>(
+        cacheEntry: WithLastModified<CacheValue<CacheType>> | null
+    ): WithLastModified<CacheValue<CacheType>> | null {
+        if (!cacheEntry?.value) return null;
+        if ('meta' in cacheEntry.value && cacheEntry.value.meta?.status === 404) {
+            return null;
+        }
+        return cacheEntry;
+    }
+
     async set<CacheType extends CacheEntryType = 'cache'>(
         key: string,
         value: CacheValue<CacheType>,

From 4fb2a4a6d7c4da9583bf7d83a2b03c718d128dc5 Mon Sep 17 00:00:00 2001
From: Zeno Kapitein <zeno@gitbook.io>
Date: Wed, 11 Jun 2025 14:03:45 +0200
Subject: [PATCH 091/127] Rework full-width layout, add support for full-width
 page option (#3293)

Co-authored-by: conico974 <nicodorseuil@yahoo.fr>
---
 .changeset/long-cameras-protect.md               |  5 +++++
 .../src/components/DocumentView/Blocks.tsx       |  4 ++--
 .../components/DocumentView/Columns/Columns.tsx  |  2 +-
 .../src/components/DocumentView/Divider.tsx      |  2 +-
 .../gitbook/src/components/Footer/Footer.tsx     | 10 +++++++---
 .../gitbook/src/components/Header/Header.tsx     |  5 +----
 .../src/components/PageAside/PageAside.tsx       |  4 +---
 .../gitbook/src/components/PageBody/PageBody.tsx | 16 +++++++---------
 .../src/components/PageBody/PageCover.tsx        | 14 ++++++++++----
 .../components/PageBody/PageFooterNavigation.tsx |  2 +-
 .../src/components/PageBody/PageHeader.tsx       |  9 ++++++++-
 .../components/PageBody/PreservePageLayout.tsx   | 12 ++++++------
 .../src/components/RootLayout/globals.css        |  4 ----
 .../src/components/SitePage/SitePageSkeleton.tsx |  2 +-
 .../src/components/SpaceLayout/SpaceLayout.tsx   |  1 +
 .../TableOfContents/TableOfContents.tsx          |  1 +
 packages/gitbook/src/components/layout.ts        |  2 +-
 packages/gitbook/tailwind.config.ts              | 10 +++++++++-
 18 files changed, 63 insertions(+), 42 deletions(-)
 create mode 100644 .changeset/long-cameras-protect.md

diff --git a/.changeset/long-cameras-protect.md b/.changeset/long-cameras-protect.md
new file mode 100644
index 0000000000..358b9be33d
--- /dev/null
+++ b/.changeset/long-cameras-protect.md
@@ -0,0 +1,5 @@
+---
+"gitbook": minor
+---
+
+Rework full-width layout, add support for full-width page option
diff --git a/packages/gitbook/src/components/DocumentView/Blocks.tsx b/packages/gitbook/src/components/DocumentView/Blocks.tsx
index 903acd2955..4bbe605d87 100644
--- a/packages/gitbook/src/components/DocumentView/Blocks.tsx
+++ b/packages/gitbook/src/components/DocumentView/Blocks.tsx
@@ -70,8 +70,8 @@ export function UnwrappedBlocks<TBlock extends DocumentBlock>(props: UnwrappedBl
                 style={[
                     'mx-auto w-full decoration-primary/6',
                     node.data && 'fullWidth' in node.data && node.data.fullWidth
-                        ? 'max-w-screen-xl'
-                        : 'max-w-3xl',
+                        ? 'max-w-screen-2xl'
+                        : 'page-full-width:ml-0 max-w-3xl',
                     blockStyle,
                 ]}
                 isEstimatedOffscreen={isOffscreen}
diff --git a/packages/gitbook/src/components/DocumentView/Columns/Columns.tsx b/packages/gitbook/src/components/DocumentView/Columns/Columns.tsx
index 4d74a0490d..181095434e 100644
--- a/packages/gitbook/src/components/DocumentView/Columns/Columns.tsx
+++ b/packages/gitbook/src/components/DocumentView/Columns/Columns.tsx
@@ -19,7 +19,7 @@ export function Columns(props: BlockProps<DocumentBlockColumns>) {
                             ancestorBlocks={[...ancestorBlocks, block, columnBlock]}
                             context={context}
                             blockStyle="flip-heading-hash"
-                            style="w-full space-y-4"
+                            style="w-full space-y-4 *:max-w-full"
                         />
                     </Column>
                 );
diff --git a/packages/gitbook/src/components/DocumentView/Divider.tsx b/packages/gitbook/src/components/DocumentView/Divider.tsx
index 8f18593e2f..f787d044b5 100644
--- a/packages/gitbook/src/components/DocumentView/Divider.tsx
+++ b/packages/gitbook/src/components/DocumentView/Divider.tsx
@@ -7,5 +7,5 @@ import type { BlockProps } from './Block';
 export function Divider(props: BlockProps<DocumentBlockDivider>) {
     const { style } = props;
 
-    return <hr className={tcls(style, 'border-tint-subtle')} />;
+    return <hr className={tcls(style, 'page-full-width:max-w-full border-tint-subtle')} />;
 }
diff --git a/packages/gitbook/src/components/Footer/Footer.tsx b/packages/gitbook/src/components/Footer/Footer.tsx
index 5723d67f3c..454ac0f421 100644
--- a/packages/gitbook/src/components/Footer/Footer.tsx
+++ b/packages/gitbook/src/components/Footer/Footer.tsx
@@ -36,12 +36,16 @@ export function Footer(props: { context: GitBookSiteContext }) {
                 <div className={tcls(CONTAINER_STYLE, 'px-4', 'py-8', 'lg:py-12', 'mx-auto')}>
                     <div
                         className={tcls(
-                            'mx-auto grid max-w-3xl justify-between gap-12 lg:max-w-none',
+                            'lg:!max-w-none mx-auto grid max-w-3xl site-full-width:max-w-screen-2xl justify-between gap-12',
                             'grid-cols-[auto_auto]',
                             'lg:grid-cols-[18rem_minmax(auto,_48rem)_auto]',
                             'xl:grid-cols-[18rem_minmax(auto,_48rem)_14rem]',
+                            'site-full-width:lg:grid-cols-[18rem_minmax(auto,_80rem)_auto]',
+                            'site-full-width:xl:grid-cols-[18rem_minmax(auto,_80rem)_14rem]',
                             'page-no-toc:lg:grid-cols-[minmax(auto,_48rem)_auto]',
-                            'page-no-toc:xl:grid-cols-[14rem_minmax(auto,_48rem)_14rem]'
+                            'page-no-toc:xl:grid-cols-[14rem_minmax(auto,_48rem)_14rem]',
+                            '[body:has(.site-full-width,.page-no-toc)_&]:lg:grid-cols-[minmax(auto,_90rem)_auto]',
+                            '[body:has(.site-full-width,.page-no-toc)_&]:xl:grid-cols-[14rem_minmax(auto,_90rem)_14rem]'
                         )}
                     >
                         {
@@ -102,7 +106,7 @@ export function Footer(props: { context: GitBookSiteContext }) {
                                         'col-span-2 page-has-toc:lg:col-span-1 page-has-toc:lg:col-start-2 page-no-toc:xl:col-span-1 page-no-toc:xl:col-start-2'
                                     )}
                                 >
-                                    <div className="mx-auto flex max-w-3xl flex-col gap-10 sm:flex-row sm:gap-6">
+                                    <div className="mx-auto flex max-w-3xl site-full-width:max-w-screen-2xl flex-col gap-10 sm:flex-row sm:gap-6">
                                         {partition(customization.footer.groups, FOOTER_COLUMNS).map(
                                             (column, columnIndex) => (
                                                 <div
diff --git a/packages/gitbook/src/components/Header/Header.tsx b/packages/gitbook/src/components/Header/Header.tsx
index 35c2583c41..304e853139 100644
--- a/packages/gitbook/src/components/Header/Header.tsx
+++ b/packages/gitbook/src/components/Header/Header.tsx
@@ -104,11 +104,9 @@ export function Header(props: { context: GitBookSiteContext; withTopHeader?: boo
                                           'lg:basis-40',
                                           'md:max-w-[40%]',
                                           'lg:max-w-lg',
-                                          'lg:ml-[max(calc((100%-18rem-48rem-3rem)/2),1.5rem)]', // container (100%) - sidebar (18rem) - content (48rem) - margin (3rem)
+                                          'lg:ml-[max(calc((100%-18rem-48rem)/2),1.5rem)]', // container (100%) - sidebar (18rem) - content (48rem)
                                           'xl:ml-[max(calc((100%-18rem-48rem-14rem-3rem)/2),1.5rem)]', // container (100%) - sidebar (18rem) - content (48rem) - outline (14rem) - margin (3rem)
                                           'page-no-toc:lg:ml-[max(calc((100%-18rem-48rem-18rem-3rem)/2),0rem)]',
-                                          'page-full-width:lg:ml-[max(calc((100%-18rem-103rem-3rem)/2),1.5rem)]',
-                                          'page-full-width:2xl:ml-[max(calc((100%-18rem-96rem-14rem+3rem)/2),1.5rem)]',
                                           'md:mr-auto',
                                           'order-last',
                                           'md:order-[unset]',
@@ -195,7 +193,6 @@ export function Header(props: { context: GitBookSiteContext; withTopHeader?: boo
                         <div
                             className={tcls(
                                 CONTAINER_STYLE,
-                                'page-default-width:max-w-[unset]',
                                 'grow',
                                 'flex',
                                 'items-end',
diff --git a/packages/gitbook/src/components/PageAside/PageAside.tsx b/packages/gitbook/src/components/PageAside/PageAside.tsx
index 06821d964c..d00af1bf2d 100644
--- a/packages/gitbook/src/components/PageAside/PageAside.tsx
+++ b/packages/gitbook/src/components/PageAside/PageAside.tsx
@@ -48,11 +48,9 @@ export function PageAside(props: {
                 'group/aside',
                 'hidden',
                 'xl:flex',
-                // 'page-no-toc:lg:flex',
                 'flex-col',
                 'basis-56',
-                // 'page-no-toc:basis-40',
-                // 'page-no-toc:xl:basis-56',
+                'xl:ml-12',
                 'grow-0',
                 'shrink-0',
                 'break-anywhere', // To prevent long words in headings from breaking the layout
diff --git a/packages/gitbook/src/components/PageBody/PageBody.tsx b/packages/gitbook/src/components/PageBody/PageBody.tsx
index 6d548dec95..801db03ce2 100644
--- a/packages/gitbook/src/components/PageBody/PageBody.tsx
+++ b/packages/gitbook/src/components/PageBody/PageBody.tsx
@@ -27,7 +27,9 @@ export function PageBody(props: {
     const { page, context, ancestors, document, withPageFeedback } = props;
     const { customization } = context;
 
-    const asFullWidth = document ? hasFullWidthBlock(document) : false;
+    const contentFullWidth = document ? hasFullWidthBlock(document) : false;
+    const pageFullWidth = page.id === 'wtthNFMqmEQmnt5LKR0q';
+    const asFullWidth = pageFullWidth || contentFullWidth;
     const language = getSpaceLanguage(customization);
     const updatedAt = page.updatedAt ?? page.createdAt;
 
@@ -36,15 +38,11 @@ export function PageBody(props: {
             <main
                 className={tcls(
                     'relative min-w-0 flex-1',
-                    'py-8 lg:px-12',
+                    'mx-auto max-w-screen-2xl py-8',
                     // Allow words to break if they are too long.
                     'break-anywhere',
-                    // When in api page mode without the aside, we align with the border of the main content
-                    'page-api-block:xl:max-2xl:pr-0',
-                    // Max size to ensure one column in api is aligned with rest of content (2 x 3xl) + (gap-3 + 2) * px-12
-                    'page-api-block:mx-auto page-api-block:max-w-screen-2xl',
-                    // page.layout.tableOfContents ? null : 'xl:ml-56',
-                    asFullWidth ? 'page-full-width' : 'page-default-width',
+                    pageFullWidth ? 'page-full-width 2xl:px-8' : 'page-default-width',
+                    asFullWidth ? 'site-full-width' : 'site-default-width',
                     page.layout.tableOfContents ? 'page-has-toc' : 'page-no-toc'
                 )}
             >
@@ -81,7 +79,7 @@ export function PageBody(props: {
                     <PageFooterNavigation context={context} page={page} />
                 ) : null}
 
-                <div className="mx-auto mt-6 page-api-block:ml-0 flex max-w-3xl flex-row flex-wrap items-center gap-4 text-tint contrast-more:text-tint-strong">
+                <div className="mx-auto mt-6 page-api-block:ml-0 flex max-w-3xl page-full-width:max-w-screen-2xl flex-row flex-wrap items-center gap-4 text-tint contrast-more:text-tint-strong">
                     {updatedAt ? (
                         <p className="mr-auto text-sm">
                             {t(language, 'page_last_modified', <DateRelative value={updatedAt} />)}
diff --git a/packages/gitbook/src/components/PageBody/PageCover.tsx b/packages/gitbook/src/components/PageBody/PageCover.tsx
index 67aabd2124..a4bb712618 100644
--- a/packages/gitbook/src/components/PageBody/PageCover.tsx
+++ b/packages/gitbook/src/components/PageBody/PageCover.tsx
@@ -34,14 +34,20 @@ export async function PageCover(props: {
                     ? [
                           'sm:-mx-6',
                           'md:-mx-8',
-                          '-lg:mr-8',
-                          'lg:ml-0',
+                          'lg:-mr-8',
+                          'lg:-ml-12',
                           !page.layout.tableOfContents &&
                           context.customization.header.preset !== 'none'
-                              ? 'xl:-ml-64'
+                              ? 'xl:-ml-[19rem]'
                               : null,
                       ]
-                    : ['sm:mx-auto', 'max-w-3xl', 'sm:rounded-md', 'mb-8']
+                    : [
+                          'sm:mx-auto',
+                          'max-w-3xl ',
+                          'page-full-width:max-w-screen-2xl',
+                          'sm:rounded-md',
+                          'mb-8',
+                      ]
             )}
         >
             <Image
diff --git a/packages/gitbook/src/components/PageBody/PageFooterNavigation.tsx b/packages/gitbook/src/components/PageBody/PageFooterNavigation.tsx
index b0190b6b18..a6be37d412 100644
--- a/packages/gitbook/src/components/PageBody/PageFooterNavigation.tsx
+++ b/packages/gitbook/src/components/PageBody/PageFooterNavigation.tsx
@@ -32,8 +32,8 @@ export async function PageFooterNavigation(props: {
                 'mt-6',
                 'gap-2',
                 'max-w-3xl',
+                'page-full-width:max-w-screen-2xl',
                 'mx-auto',
-                'page-api-block:ml-0',
                 'text-tint'
             )}
         >
diff --git a/packages/gitbook/src/components/PageBody/PageHeader.tsx b/packages/gitbook/src/components/PageBody/PageHeader.tsx
index b8b62f945d..7c1ad9cbf7 100644
--- a/packages/gitbook/src/components/PageBody/PageHeader.tsx
+++ b/packages/gitbook/src/components/PageBody/PageHeader.tsx
@@ -23,7 +23,14 @@ export async function PageHeader(props: {
 
     return (
         <header
-            className={tcls('max-w-3xl', 'mx-auto', 'mb-6', 'space-y-3', 'page-api-block:ml-0')}
+            className={tcls(
+                'max-w-3xl',
+                'page-full-width:max-w-screen-2xl',
+                'mx-auto',
+                'mb-6',
+                'space-y-3',
+                'page-api-block:ml-0'
+            )}
         >
             {ancestors.length > 0 && (
                 <nav>
diff --git a/packages/gitbook/src/components/PageBody/PreservePageLayout.tsx b/packages/gitbook/src/components/PageBody/PreservePageLayout.tsx
index 7094711fbb..06a801034c 100644
--- a/packages/gitbook/src/components/PageBody/PreservePageLayout.tsx
+++ b/packages/gitbook/src/components/PageBody/PreservePageLayout.tsx
@@ -3,12 +3,12 @@ import * as React from 'react';
 
 /**
  * This component preserves the layout of the page while loading a new one.
- * This approach is needed as page layout (full width block) is done using CSS (`body:has(.page-full-width)`),
+ * This approach is needed as page layout (full width block) is done using CSS (`body:has(.full-width)`),
  * which becomes false while transitioning between the 2 page states:
  *
- * 1. Page 1 with full width block: `body:has(.page-full-width)` is true
- * 2. Loading skeleton while transitioning to page 2: `body:has(.page-full-width)` is false
- * 3. Page 2 with full width block: `body:has(.page-full-width)` is true
+ * 1. Page 1 with full width block: `body:has(.site-full-width)` is true
+ * 2. Loading skeleton while transitioning to page 2: `body:has(.site-full-width)` is false
+ * 3. Page 2 with full width block: `body:has(.site-full-width)` is true
  *
  * This component ensures that the layout is preserved while transitioning between the 2 page states (in step 2).
  */
@@ -24,9 +24,9 @@ export function PreservePageLayout(props: { asFullWidth: boolean }) {
         }
 
         if (asFullWidth) {
-            header.classList.add('page-full-width');
+            header.classList.add('site-full-width');
         } else {
-            header.classList.remove('page-full-width');
+            header.classList.remove('site-full-width');
         }
     }, [asFullWidth]);
 
diff --git a/packages/gitbook/src/components/RootLayout/globals.css b/packages/gitbook/src/components/RootLayout/globals.css
index 2bb3e1a223..a46758c0ae 100644
--- a/packages/gitbook/src/components/RootLayout/globals.css
+++ b/packages/gitbook/src/components/RootLayout/globals.css
@@ -143,10 +143,6 @@
             margin-right: 0;
             width: calc(100% - var(--scrollbar-width));
         }
-        body:has(.page-full-width) .scroll-nojump {
-            margin-left: 0;
-            width: 100%;
-        }
     }
 
     .elevate-link {
diff --git a/packages/gitbook/src/components/SitePage/SitePageSkeleton.tsx b/packages/gitbook/src/components/SitePage/SitePageSkeleton.tsx
index 37d97167c9..7b840afbb0 100644
--- a/packages/gitbook/src/components/SitePage/SitePageSkeleton.tsx
+++ b/packages/gitbook/src/components/SitePage/SitePageSkeleton.tsx
@@ -19,7 +19,7 @@ export function SitePageSkeleton() {
                 'lg:items-start'
             )}
         >
-            <div className={tcls('flex-1', 'max-w-3xl', 'mx-auto', 'page-full-width:mx-0')}>
+            <div className={tcls('flex-1', 'max-w-3xl', 'mx-auto', 'site-full-width:mx-0')}>
                 <SkeletonHeading style={tcls('mb-8')} />
                 <SkeletonParagraph style={tcls('mb-4')} />
             </div>
diff --git a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx
index 6b9aefd929..cedc2eee89 100644
--- a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx
+++ b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx
@@ -73,6 +73,7 @@ export function SpaceLayout(props: {
                             'flex-col',
                             'lg:flex-row',
                             CONTAINER_STYLE,
+                            'site-full-width:max-w-full',
 
                             // Ensure the footer is display below the viewport even if the content is not enough
                             withFooter && 'min-h-[calc(100vh-64px)]',
diff --git a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx
index 1fb8b59442..7ca7a2c0c0 100644
--- a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx
+++ b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx
@@ -35,6 +35,7 @@ export function TableOfContents(props: {
                     'relative',
                     'z-[1]',
                     'lg:sticky',
+                    'lg:mr-12',
 
                     // Server-side static positioning
                     'lg:top-0',
diff --git a/packages/gitbook/src/components/layout.ts b/packages/gitbook/src/components/layout.ts
index 250d3366ab..6a9b39a982 100644
--- a/packages/gitbook/src/components/layout.ts
+++ b/packages/gitbook/src/components/layout.ts
@@ -14,7 +14,7 @@ export const CONTAINER_STYLE: ClassValue = [
     'md:px-8',
     'max-w-screen-2xl',
     'mx-auto',
-    'page-full-width:max-w-full',
+    // 'site-full-width:max-w-full',
 ];
 
 /**
diff --git a/packages/gitbook/tailwind.config.ts b/packages/gitbook/tailwind.config.ts
index 7981c337b2..192ad39eb8 100644
--- a/packages/gitbook/tailwind.config.ts
+++ b/packages/gitbook/tailwind.config.ts
@@ -447,6 +447,13 @@ const config: Config = {
             },
         },
         opacity: opacity(),
+        screens: {
+            sm: '640px',
+            md: '768px',
+            lg: '1024px',
+            xl: '1280px',
+            '2xl': '1536px',
+        },
     },
     plugins: [
         plugin(({ addVariant }) => {
@@ -514,8 +521,9 @@ const config: Config = {
             /**
              * Variant when the page contains a block that will be rendered in full-width mode.
              */
+            addVariant('site-full-width', 'body:has(.site-full-width) &');
+            addVariant('site-default-width', 'body:has(.site-default-width) &');
             addVariant('page-full-width', 'body:has(.page-full-width) &');
-            addVariant('page-default-width', 'body:has(.page-default-width) &');
 
             /**
              * Variant when the page is configured to hide the table of content.

From 902c3c6c1aef2a72040ea285716dfeb26f44f6de Mon Sep 17 00:00:00 2001
From: conico974 <nicodorseuil@yahoo.fr>
Date: Wed, 11 Jun 2025 14:05:30 +0200
Subject: [PATCH 092/127] apply customization for dynamic site context in v2
 (#3302)

Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
---
 .changeset/tidy-dots-suffer.md       | 5 +++++
 packages/gitbook-v2/src/app/utils.ts | 3 +++
 2 files changed, 8 insertions(+)
 create mode 100644 .changeset/tidy-dots-suffer.md

diff --git a/.changeset/tidy-dots-suffer.md b/.changeset/tidy-dots-suffer.md
new file mode 100644
index 0000000000..c622927590
--- /dev/null
+++ b/.changeset/tidy-dots-suffer.md
@@ -0,0 +1,5 @@
+---
+"gitbook-v2": patch
+---
+
+apply customization for dynamic context
diff --git a/packages/gitbook-v2/src/app/utils.ts b/packages/gitbook-v2/src/app/utils.ts
index da810b3b90..932902df0e 100644
--- a/packages/gitbook-v2/src/app/utils.ts
+++ b/packages/gitbook-v2/src/app/utils.ts
@@ -1,4 +1,5 @@
 import { getVisitorAuthClaims, getVisitorAuthClaimsFromToken } from '@/lib/adaptive';
+import { getDynamicCustomizationSettings } from '@/lib/customization';
 import type { SiteAPIToken } from '@gitbook/api';
 import { type SiteURLData, fetchSiteContextByURLLookup, getBaseContext } from '@v2/lib/context';
 import { jwtDecode } from 'jwt-decode';
@@ -67,6 +68,8 @@ export async function getDynamicSiteContext(params: RouteLayoutParams) {
         siteURLData
     );
 
+    context.customization = await getDynamicCustomizationSettings(context.customization);
+
     return {
         context,
         visitorAuthClaims: getVisitorAuthClaims(siteURLData),

From d41038195702f677882f3f2efb33eb1c77294b0c Mon Sep 17 00:00:00 2001
From: spastorelli <spastorelli@users.noreply.github.com>
Date: Wed, 11 Jun 2025 14:25:05 +0200
Subject: [PATCH 093/127] Add docs.testgitbook.com to ADAPTIVE_CONTENT_HOSTS
 list (#3303)

---
 .changeset/clever-jokes-yell.md       | 5 +++++
 packages/gitbook-v2/src/middleware.ts | 2 ++
 2 files changed, 7 insertions(+)
 create mode 100644 .changeset/clever-jokes-yell.md

diff --git a/.changeset/clever-jokes-yell.md b/.changeset/clever-jokes-yell.md
new file mode 100644
index 0000000000..87b2698636
--- /dev/null
+++ b/.changeset/clever-jokes-yell.md
@@ -0,0 +1,5 @@
+---
+"gitbook-v2": patch
+---
+
+Add docs.testgitbook.com to ADAPTIVE_CONTENT_HOSTS list
diff --git a/packages/gitbook-v2/src/middleware.ts b/packages/gitbook-v2/src/middleware.ts
index 49ffb9dba8..c11d466c78 100644
--- a/packages/gitbook-v2/src/middleware.ts
+++ b/packages/gitbook-v2/src/middleware.ts
@@ -41,6 +41,8 @@ const ADAPTIVE_CONTENT_HOSTS = [
     'docs.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) {

From b6b597564dc5ebe174d38582ee275ad8aa14c1b9 Mon Sep 17 00:00:00 2001
From: Zeno Kapitein <zeno@gitbook.io>
Date: Wed, 11 Jun 2025 14:44:07 +0200
Subject: [PATCH 094/127] Reverse order of feedback smileys (#3300)

---
 .changeset/tiny-zoos-scream.md                   |  5 +++++
 .../components/PageFeedback/PageFeedbackForm.tsx | 16 ++++++++--------
 2 files changed, 13 insertions(+), 8 deletions(-)
 create mode 100644 .changeset/tiny-zoos-scream.md

diff --git a/.changeset/tiny-zoos-scream.md b/.changeset/tiny-zoos-scream.md
new file mode 100644
index 0000000000..1c80e7684e
--- /dev/null
+++ b/.changeset/tiny-zoos-scream.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Reverse order of feedback smileys
diff --git a/packages/gitbook/src/components/PageFeedback/PageFeedbackForm.tsx b/packages/gitbook/src/components/PageFeedback/PageFeedbackForm.tsx
index 551a8b83f5..2c15f52831 100644
--- a/packages/gitbook/src/components/PageFeedback/PageFeedbackForm.tsx
+++ b/packages/gitbook/src/components/PageFeedback/PageFeedbackForm.tsx
@@ -64,10 +64,10 @@ export function PageFeedbackForm(props: {
                 <div className="rounded-full border border-tint-subtle bg-tint-base contrast-more:border-tint-12">
                     <div className="flex">
                         <RatingButton
-                            rating={PageFeedbackRating.Bad}
-                            label={tString(languages, 'was_this_helpful_negative')}
-                            onClick={() => onSubmitRating(PageFeedbackRating.Bad)}
-                            active={rating === PageFeedbackRating.Bad}
+                            rating={PageFeedbackRating.Good}
+                            label={tString(languages, 'was_this_helpful_positive')}
+                            onClick={() => onSubmitRating(PageFeedbackRating.Good)}
+                            active={rating === PageFeedbackRating.Good}
                             disabled={rating !== undefined}
                         />
                         <RatingButton
@@ -78,10 +78,10 @@ export function PageFeedbackForm(props: {
                             disabled={rating !== undefined}
                         />
                         <RatingButton
-                            rating={PageFeedbackRating.Good}
-                            label={tString(languages, 'was_this_helpful_positive')}
-                            onClick={() => onSubmitRating(PageFeedbackRating.Good)}
-                            active={rating === PageFeedbackRating.Good}
+                            rating={PageFeedbackRating.Bad}
+                            label={tString(languages, 'was_this_helpful_negative')}
+                            onClick={() => onSubmitRating(PageFeedbackRating.Bad)}
+                            active={rating === PageFeedbackRating.Bad}
                             disabled={rating !== undefined}
                         />
                     </div>

From 7a00880e5b57446ea3d99a8db355d9f5d34d21de Mon Sep 17 00:00:00 2001
From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com>
Date: Wed, 11 Jun 2025 15:34:52 +0200
Subject: [PATCH 095/127] Improve support for OAuth2 security type (#3304)

---
 .changeset/fifty-ducks-press.md               |   6 +
 .../components/DocumentView/OpenAPI/style.css |  20 +++
 .../react-openapi/src/OpenAPISchemaName.tsx   |  14 ++-
 .../react-openapi/src/OpenAPISecurities.tsx   | 118 ++++++++++++++++--
 packages/react-openapi/src/translations/de.ts |   1 +
 packages/react-openapi/src/translations/en.ts |   1 +
 packages/react-openapi/src/translations/es.ts |   1 +
 packages/react-openapi/src/translations/fr.ts |   1 +
 packages/react-openapi/src/translations/ja.ts |   1 +
 packages/react-openapi/src/translations/nl.ts |   1 +
 packages/react-openapi/src/translations/no.ts |   1 +
 .../react-openapi/src/translations/pt-br.ts   |   1 +
 packages/react-openapi/src/translations/zh.ts |   1 +
 13 files changed, 154 insertions(+), 13 deletions(-)
 create mode 100644 .changeset/fifty-ducks-press.md

diff --git a/.changeset/fifty-ducks-press.md b/.changeset/fifty-ducks-press.md
new file mode 100644
index 0000000000..a250d31243
--- /dev/null
+++ b/.changeset/fifty-ducks-press.md
@@ -0,0 +1,6 @@
+---
+'@gitbook/react-openapi': patch
+'gitbook': patch
+---
+
+Improve support for OAuth2 security type
diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
index 1a88b4faa6..c855ec9fd8 100644
--- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
+++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
@@ -310,6 +310,26 @@
     @apply py-2 border-b border-tint-subtle max-w-full flex-1;
 }
 
+.openapi-securities-oauth-flows {
+    @apply flex flex-col gap-2 divide-y divide-tint-subtle;
+}
+
+.openapi-securities-oauth-content {
+    @apply prose *:!prose-sm *:text-tint;
+}
+
+.openapi-securities-oauth-content.openapi-markdown code {
+    @apply text-xs;
+}
+
+.openapi-securities-oauth-content ul {
+    @apply !my-0;
+}
+
+.openapi-securities-url {
+    @apply ml-0.5 px-0.5 rounded hover:bg-tint transition-colors;
+}
+
 .openapi-securities-body {
     @apply flex flex-col gap-2;
 }
diff --git a/packages/react-openapi/src/OpenAPISchemaName.tsx b/packages/react-openapi/src/OpenAPISchemaName.tsx
index d24efe3f70..8994b4d71f 100644
--- a/packages/react-openapi/src/OpenAPISchemaName.tsx
+++ b/packages/react-openapi/src/OpenAPISchemaName.tsx
@@ -27,12 +27,14 @@ export function OpenAPISchemaName(props: OpenAPISchemaNameProps) {
                     {propertyName}
                 </span>
             ) : null}
-            <span>
-                {type ? <span className="openapi-schema-type">{type}</span> : null}
-                {additionalItems ? (
-                    <span className="openapi-schema-type">{additionalItems}</span>
-                ) : null}
-            </span>
+            {type || additionalItems ? (
+                <span>
+                    {type ? <span className="openapi-schema-type">{type}</span> : null}
+                    {additionalItems ? (
+                        <span className="openapi-schema-type">{additionalItems}</span>
+                    ) : null}
+                </span>
+            ) : null}
             {schema?.readOnly ? (
                 <span className="openapi-schema-readonly">
                     {t(context.translation, 'read_only')}
diff --git a/packages/react-openapi/src/OpenAPISecurities.tsx b/packages/react-openapi/src/OpenAPISecurities.tsx
index 3f86236506..8b007ff3c1 100644
--- a/packages/react-openapi/src/OpenAPISecurities.tsx
+++ b/packages/react-openapi/src/OpenAPISecurities.tsx
@@ -1,5 +1,7 @@
+import type { OpenAPIV3 } from '@gitbook/openapi-parser';
 import { InteractiveSection } from './InteractiveSection';
 import { Markdown } from './Markdown';
+import { OpenAPICopyButton } from './OpenAPICopyButton';
 import { OpenAPISchemaName } from './OpenAPISchemaName';
 import type { OpenAPIClientContext } from './context';
 import { t } from './translate';
@@ -105,13 +107,7 @@ function getLabelForType(security: OpenAPISecurityWithRequired, context: OpenAPI
                 />
             );
         case 'oauth2':
-            return (
-                <OpenAPISchemaName
-                    context={context}
-                    propertyName="OAuth2"
-                    required={security.required}
-                />
-            );
+            return <OpenAPISchemaOAuth2Flows context={context} security={security} />;
         case 'openIdConnect':
             return (
                 <OpenAPISchemaName
@@ -125,3 +121,111 @@ function getLabelForType(security: OpenAPISecurityWithRequired, context: OpenAPI
             return security.type;
     }
 }
+
+function OpenAPISchemaOAuth2Flows(props: {
+    context: OpenAPIClientContext;
+    security: OpenAPIV3.OAuth2SecurityScheme & { required?: boolean };
+}) {
+    const { context, security } = props;
+
+    const flows = Object.entries(security.flows ?? {});
+
+    return (
+        <div className="openapi-securities-oauth-flows">
+            {flows.map(([name, flow], index) => (
+                <OpenAPISchemaOAuth2Item
+                    key={index}
+                    flow={flow}
+                    name={name}
+                    context={context}
+                    security={security}
+                />
+            ))}
+        </div>
+    );
+}
+
+function OpenAPISchemaOAuth2Item(props: {
+    flow: NonNullable<OpenAPIV3.OAuth2SecurityScheme['flows']>[keyof NonNullable<
+        OpenAPIV3.OAuth2SecurityScheme['flows']
+    >];
+    name: string;
+    context: OpenAPIClientContext;
+    security: OpenAPIV3.OAuth2SecurityScheme & { required?: boolean };
+}) {
+    const { flow, context, security, name } = props;
+
+    if (!flow) {
+        return null;
+    }
+
+    const scopes = Object.entries(flow?.scopes ?? {});
+
+    return (
+        <div>
+            <OpenAPISchemaName
+                context={context}
+                propertyName="OAuth2"
+                type={name}
+                required={security.required}
+            />
+            <div className="openapi-securities-oauth-content openapi-markdown">
+                {security.description ? <Markdown source={security.description} /> : null}
+                {'authorizationUrl' in flow && flow.authorizationUrl ? (
+                    <span>
+                        Authorization URL:{' '}
+                        <OpenAPICopyButton
+                            value={flow.authorizationUrl}
+                            context={context}
+                            className="openapi-securities-url"
+                            withTooltip
+                        >
+                            {flow.authorizationUrl}
+                        </OpenAPICopyButton>
+                    </span>
+                ) : null}
+                {'tokenUrl' in flow && flow.tokenUrl ? (
+                    <span>
+                        Token URL:{' '}
+                        <OpenAPICopyButton
+                            value={flow.tokenUrl}
+                            context={context}
+                            className="openapi-securities-url"
+                            withTooltip
+                        >
+                            {flow.tokenUrl}
+                        </OpenAPICopyButton>
+                    </span>
+                ) : null}
+                {'refreshUrl' in flow && flow.refreshUrl ? (
+                    <span>
+                        Refresh URL:{' '}
+                        <OpenAPICopyButton
+                            value={flow.refreshUrl}
+                            context={context}
+                            className="openapi-securities-url"
+                            withTooltip
+                        >
+                            {flow.refreshUrl}
+                        </OpenAPICopyButton>
+                    </span>
+                ) : null}
+                {scopes.length ? (
+                    <div>
+                        {t(context.translation, 'available_scopes')}:{' '}
+                        <ul>
+                            {scopes.map(([key, value]) => (
+                                <li key={key}>
+                                    <OpenAPICopyButton value={key} context={context} withTooltip>
+                                        <code>{key}</code>
+                                    </OpenAPICopyButton>
+                                    : {value}
+                                </li>
+                            ))}
+                        </ul>
+                    </div>
+                ) : null}
+            </div>
+        </div>
+    );
+}
diff --git a/packages/react-openapi/src/translations/de.ts b/packages/react-openapi/src/translations/de.ts
index 8a5236c587..6229acd4fa 100644
--- a/packages/react-openapi/src/translations/de.ts
+++ b/packages/react-openapi/src/translations/de.ts
@@ -35,6 +35,7 @@ export const de = {
     show: 'Zeige ${1}',
     hide: 'Verstecke ${1}',
     available_items: 'Verfügbare Elemente',
+    available_scopes: 'Verfügbare scopes',
     properties: 'Eigenschaften',
     or: 'oder',
     and: 'und',
diff --git a/packages/react-openapi/src/translations/en.ts b/packages/react-openapi/src/translations/en.ts
index 4276595d54..3681626898 100644
--- a/packages/react-openapi/src/translations/en.ts
+++ b/packages/react-openapi/src/translations/en.ts
@@ -35,6 +35,7 @@ export const en = {
     show: 'Show ${1}',
     hide: 'Hide ${1}',
     available_items: 'Available items',
+    available_scopes: 'Available scopes',
     possible_values: 'Possible values',
     properties: 'Properties',
     or: 'or',
diff --git a/packages/react-openapi/src/translations/es.ts b/packages/react-openapi/src/translations/es.ts
index f5ac905f6a..f9e8c58f48 100644
--- a/packages/react-openapi/src/translations/es.ts
+++ b/packages/react-openapi/src/translations/es.ts
@@ -35,6 +35,7 @@ export const es = {
     show: 'Mostrar ${1}',
     hide: 'Ocultar ${1}',
     available_items: 'Elementos disponibles',
+    available_scopes: 'Scopes disponibles',
     properties: 'Propiedades',
     or: 'o',
     and: 'y',
diff --git a/packages/react-openapi/src/translations/fr.ts b/packages/react-openapi/src/translations/fr.ts
index b2e563c813..fde7a9222c 100644
--- a/packages/react-openapi/src/translations/fr.ts
+++ b/packages/react-openapi/src/translations/fr.ts
@@ -35,6 +35,7 @@ export const fr = {
     show: 'Afficher ${1}',
     hide: 'Masquer ${1}',
     available_items: 'Éléments disponibles',
+    available_scopes: 'Scopes disponibles',
     properties: 'Propriétés',
     or: 'ou',
     and: 'et',
diff --git a/packages/react-openapi/src/translations/ja.ts b/packages/react-openapi/src/translations/ja.ts
index e393dd6cbd..04d43f67ae 100644
--- a/packages/react-openapi/src/translations/ja.ts
+++ b/packages/react-openapi/src/translations/ja.ts
@@ -35,6 +35,7 @@ export const ja = {
     show: '${1}を表示',
     hide: '${1}を非表示',
     available_items: '利用可能なアイテム',
+    available_scopes: '利用可能なスコープ',
     properties: 'プロパティ',
     or: 'または',
     and: 'かつ',
diff --git a/packages/react-openapi/src/translations/nl.ts b/packages/react-openapi/src/translations/nl.ts
index 34867ccf15..2c57d7af4f 100644
--- a/packages/react-openapi/src/translations/nl.ts
+++ b/packages/react-openapi/src/translations/nl.ts
@@ -35,6 +35,7 @@ export const nl = {
     show: 'Toon ${1}',
     hide: 'Verberg ${1}',
     available_items: 'Beschikbare items',
+    available_scopes: 'Beschikbare scopes',
     properties: 'Eigenschappen',
     or: 'of',
     and: 'en',
diff --git a/packages/react-openapi/src/translations/no.ts b/packages/react-openapi/src/translations/no.ts
index 2701177932..9ef1b80048 100644
--- a/packages/react-openapi/src/translations/no.ts
+++ b/packages/react-openapi/src/translations/no.ts
@@ -35,6 +35,7 @@ export const no = {
     show: 'Vis ${1}',
     hide: 'Skjul ${1}',
     available_items: 'Tilgjengelige elementer',
+    available_scopes: 'Tilgjengelige scopes',
     properties: 'Egenskaper',
     or: 'eller',
     and: 'og',
diff --git a/packages/react-openapi/src/translations/pt-br.ts b/packages/react-openapi/src/translations/pt-br.ts
index 00e8ab3c27..2e9e7cb2d9 100644
--- a/packages/react-openapi/src/translations/pt-br.ts
+++ b/packages/react-openapi/src/translations/pt-br.ts
@@ -35,6 +35,7 @@ export const pt_br = {
     show: 'Mostrar ${1}',
     hide: 'Ocultar ${1}',
     available_items: 'Itens disponíveis',
+    available_scopes: 'Scopes disponíveis',
     properties: 'Propriedades',
     or: 'ou',
     and: 'e',
diff --git a/packages/react-openapi/src/translations/zh.ts b/packages/react-openapi/src/translations/zh.ts
index f8299d1993..f0e81f21bc 100644
--- a/packages/react-openapi/src/translations/zh.ts
+++ b/packages/react-openapi/src/translations/zh.ts
@@ -35,6 +35,7 @@ export const zh = {
     show: '显示${1}',
     hide: '隐藏${1}',
     available_items: '可用项',
+    available_scopes: '可用范围',
     properties: '属性',
     or: '或',
     and: '和',

From c3b620e975a74ea7fa79d37dc24873a5828d601c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Thu, 12 Jun 2025 09:30:08 +0200
Subject: [PATCH 096/127] Preserve current variant when navigating between
 sections (#3306)

---
 .changeset/hip-bobcats-cover.md               |  5 ++++
 .../src/components/Header/SpacesDropdown.tsx  | 19 ++----------
 .../SiteSections/encodeClientSiteSections.ts  | 29 +++++++++++++++---
 packages/gitbook/src/lib/sites.ts             | 30 +++++++++++++++++++
 4 files changed, 62 insertions(+), 21 deletions(-)
 create mode 100644 .changeset/hip-bobcats-cover.md

diff --git a/.changeset/hip-bobcats-cover.md b/.changeset/hip-bobcats-cover.md
new file mode 100644
index 0000000000..9bd789f85d
--- /dev/null
+++ b/.changeset/hip-bobcats-cover.md
@@ -0,0 +1,5 @@
+---
+"gitbook": minor
+---
+
+Best effort at preserving current variant when navigating between sections by matching the pathname against site spaces in the new section.
diff --git a/packages/gitbook/src/components/Header/SpacesDropdown.tsx b/packages/gitbook/src/components/Header/SpacesDropdown.tsx
index 1523774493..dd6887d955 100644
--- a/packages/gitbook/src/components/Header/SpacesDropdown.tsx
+++ b/packages/gitbook/src/components/Header/SpacesDropdown.tsx
@@ -1,8 +1,7 @@
 import type { SiteSpace } from '@gitbook/api';
 
+import { getSiteSpaceURL } from '@/lib/sites';
 import { tcls } from '@/lib/tailwind';
-
-import { joinPath } from '@/lib/paths';
 import type { GitBookSiteContext } from '@v2/lib/context';
 import { DropdownChevron, DropdownMenu } from './DropdownMenu';
 import { SpacesDropdownMenuItem } from './SpacesDropdownMenuItem';
@@ -14,7 +13,6 @@ export function SpacesDropdown(props: {
     className?: string;
 }) {
     const { context, siteSpace, siteSpaces, className } = props;
-    const { linker } = context;
 
     return (
         <DropdownMenu
@@ -73,9 +71,7 @@ export function SpacesDropdown(props: {
                     variantSpace={{
                         id: otherSiteSpace.id,
                         title: otherSiteSpace.title,
-                        url: otherSiteSpace.urls.published
-                            ? linker.toLinkForContent(otherSiteSpace.urls.published)
-                            : getFallbackSiteSpaceURL(otherSiteSpace, context),
+                        url: getSiteSpaceURL(context, otherSiteSpace),
                     }}
                     active={otherSiteSpace.id === siteSpace.id}
                 />
@@ -83,14 +79,3 @@ export function SpacesDropdown(props: {
         </DropdownMenu>
     );
 }
-
-/**
- * When the site is not published yet, `urls.published` is not available.
- * To ensure navigation works in preview, we compute a relative URL from the siteSpace path.
- */
-function getFallbackSiteSpaceURL(siteSpace: SiteSpace, context: GitBookSiteContext) {
-    const { linker, sections } = context;
-    return linker.toPathInSite(
-        sections?.current ? joinPath(sections.current.path, siteSpace.path) : siteSpace.path
-    );
-}
diff --git a/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts b/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts
index 49b73a8e1a..1c8cd09899 100644
--- a/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts
+++ b/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts
@@ -1,3 +1,4 @@
+import { getSectionURL, getSiteSpaceURL } from '@/lib/sites';
 import type { SiteSection, SiteSectionGroup } from '@gitbook/api';
 import type { GitBookSiteContext, SiteSections } from '@v2/lib/context';
 
@@ -46,15 +47,35 @@ export function encodeClientSiteSections(context: GitBookSiteContext, sections:
 }
 
 function encodeSection(context: GitBookSiteContext, section: SiteSection) {
-    const { linker } = context;
     return {
         id: section.id,
         title: section.title,
         description: section.description,
         icon: section.icon,
         object: section.object,
-        url: section.urls.published
-            ? linker.toLinkForContent(section.urls.published)
-            : linker.toPathInSite(section.path),
+        url: findBestTargetURL(context, section),
     };
 }
+
+/**
+ * Find the best default site space to navigate to for a givent section:
+ * 1. If we are on the default, continue on the default.
+ * 2. If a site space has the same path as the current one, return it.
+ * 3. Otherwise, return the default one.
+ */
+function findBestTargetURL(context: GitBookSiteContext, section: SiteSection) {
+    const { siteSpace: currentSiteSpace } = context;
+
+    if (section.siteSpaces.length === 1 || currentSiteSpace.default) {
+        return getSectionURL(context, section);
+    }
+
+    const bestMatch = section.siteSpaces.find(
+        (siteSpace) => siteSpace.path === currentSiteSpace.path
+    );
+    if (bestMatch) {
+        return getSiteSpaceURL(context, bestMatch);
+    }
+
+    return getSectionURL(context, section);
+}
diff --git a/packages/gitbook/src/lib/sites.ts b/packages/gitbook/src/lib/sites.ts
index 88d63e75d3..1827683e48 100644
--- a/packages/gitbook/src/lib/sites.ts
+++ b/packages/gitbook/src/lib/sites.ts
@@ -1,4 +1,6 @@
 import type { SiteSection, SiteSectionGroup, SiteSpace, SiteStructure } from '@gitbook/api';
+import type { GitBookSiteContext } from '@v2/lib/context';
+import { joinPath } from './paths';
 
 /**
  * Get all sections from a site structure.
@@ -64,6 +66,34 @@ export function findSiteSpaceById(siteStructure: SiteStructure, spaceId: string)
     return null;
 }
 
+/**
+ * Get the URL to navigate to for a section.
+ * When the site is not published yet, `urls.published` is not available.
+ * To ensure navigation works in preview, we compute a relative URL from the siteSection path.
+ */
+export function getSectionURL(context: GitBookSiteContext, section: SiteSection) {
+    const { linker } = context;
+    return section.urls.published
+        ? linker.toLinkForContent(section.urls.published)
+        : linker.toPathInSite(section.path);
+}
+
+/**
+ * Get the URL to navigate to for a site space.
+ * When the site is not published yet, `urls.published` is not available.
+ * To ensure navigation works in preview, we compute a relative URL from the siteSpace path.
+ */
+export function getSiteSpaceURL(context: GitBookSiteContext, siteSpace: SiteSpace) {
+    const { linker, sections } = context;
+    if (siteSpace.urls.published) {
+        return linker.toLinkForContent(siteSpace.urls.published);
+    }
+
+    return linker.toPathInSite(
+        sections?.current ? joinPath(sections.current.path, siteSpace.path) : siteSpace.path
+    );
+}
+
 function findSiteSpaceByIdInSections(sections: SiteSection[], spaceId: string): SiteSpace | null {
     for (const section of sections) {
         const siteSpace =

From 9316ccd1f4f0a52fe13849a2804a5f19dab67592 Mon Sep 17 00:00:00 2001
From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com>
Date: Thu, 12 Jun 2025 10:57:37 +0200
Subject: [PATCH 097/127] Update Models page styling (#3307)

---
 .changeset/fresh-shrimps-flow.md                         | 5 +++++
 .../src/components/DocumentView/OpenAPI/style.css        | 9 ++++++---
 2 files changed, 11 insertions(+), 3 deletions(-)
 create mode 100644 .changeset/fresh-shrimps-flow.md

diff --git a/.changeset/fresh-shrimps-flow.md b/.changeset/fresh-shrimps-flow.md
new file mode 100644
index 0000000000..a2e62c9480
--- /dev/null
+++ b/.changeset/fresh-shrimps-flow.md
@@ -0,0 +1,5 @@
+---
+'gitbook': patch
+---
+
+Update Models page styling
diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
index c855ec9fd8..337f0b2ed2 100644
--- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
+++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
@@ -6,7 +6,7 @@
 }
 
 .openapi-schemas {
-    @apply flex flex-col mb-14 flex-1;
+    @apply flex flex-col mb-14 gap-0 flex-1;
 }
 
 .openapi-schemas-title {
@@ -748,8 +748,12 @@ body:has(.openapi-select-popover) {
 }
 
 /* Disclosure */
+.openapi-schemas-disclosure {
+    @apply border-t border-x last:border-b border-tint-subtle !ring-0 first:!rounded-t-xl last:!rounded-b-xl !rounded-none;
+}
+
 .openapi-schemas-disclosure > .openapi-disclosure-trigger {
-    @apply flex items-center font-mono transition-all text-tint-strong !text-sm hover:bg-tint-subtle relative flex-1 gap-2.5 p-3 truncate -outline-offset-1;
+    @apply flex items-center font-mono transition-all font-normal text-tint-strong !text-sm hover:bg-tint-subtle relative flex-1 gap-2.5 p-5 truncate -outline-offset-1;
 }
 
 .openapi-schemas-disclosure > .openapi-disclosure-trigger,
@@ -793,7 +797,6 @@ body:has(.openapi-select-popover) {
     @apply bg-tint-subtle overflow-hidden;
 }
 
-.openapi-disclosure:has(> .openapi-disclosure-trigger:hover),
 .openapi-disclosure[data-expanded="true"] {
     @apply ring-1 shadow-sm;
 }

From fecdbe8dde38cd23e14508df47a8b3a0520da3d4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Thu, 12 Jun 2025 12:25:28 +0200
Subject: [PATCH 098/127] Support dynamic expressions (#3295)

---
 packages/gitbook-v2/src/lib/data/api.ts       | 45 +++++++++++++++++
 packages/gitbook-v2/src/lib/data/index.ts     |  2 +-
 packages/gitbook-v2/src/lib/data/pages.ts     | 48 +++++++++++++++++--
 packages/gitbook-v2/src/lib/data/types.ts     |  9 ++++
 packages/gitbook/package.json                 |  2 +-
 .../gitbook/src/components/PDF/PDFPage.tsx    |  5 +-
 .../src/components/SitePage/SitePage.tsx      |  2 +-
 packages/gitbook/src/lib/api.ts               | 33 +++++++++++++
 packages/gitbook/src/lib/references.tsx       |  2 +-
 packages/gitbook/src/lib/v1.ts                | 13 +++++
 packages/gitbook/src/lib/waitUntil.ts         | 10 ++++
 packages/gitbook/tests/preload-bun.ts         |  8 ++++
 12 files changed, 168 insertions(+), 11 deletions(-)
 create mode 100644 packages/gitbook/tests/preload-bun.ts

diff --git a/packages/gitbook-v2/src/lib/data/api.ts b/packages/gitbook-v2/src/lib/data/api.ts
index 52ba17c1a3..88c95c4016 100644
--- a/packages/gitbook-v2/src/lib/data/api.ts
+++ b/packages/gitbook-v2/src/lib/data/api.ts
@@ -113,6 +113,15 @@ export function createDataFetcher(
                 })
             );
         },
+        getRevisionPageDocument(params) {
+            return trace('getRevisionPageDocument', () =>
+                getRevisionPageDocument(input, {
+                    spaceId: params.spaceId,
+                    revisionId: params.revisionId,
+                    pageId: params.pageId,
+                })
+            );
+        },
         getReusableContent(params) {
             return trace('getReusableContent', () =>
                 getReusableContent(input, {
@@ -417,6 +426,42 @@ const getRevisionPageMarkdown = withCacheKey(
     )
 );
 
+const getRevisionPageDocument = withCacheKey(
+    withoutConcurrentExecution(
+        async (
+            _,
+            input: DataFetcherInput,
+            params: { spaceId: string; revisionId: string; pageId: string }
+        ) => {
+            'use cache';
+            return trace(
+                `getRevisionPageDocument(${params.spaceId}, ${params.revisionId}, ${params.pageId})`,
+                async () => {
+                    return wrapDataFetcherError(async () => {
+                        const api = apiClient(input);
+                        const res = await api.spaces.getPageDocumentInRevisionById(
+                            params.spaceId,
+                            params.revisionId,
+                            params.pageId,
+                            {
+                                evaluated: true,
+                            },
+                            {
+                                ...noCacheFetchOptions,
+                            }
+                        );
+
+                        cacheTag(...getCacheTagsFromResponse(res));
+                        cacheLife('max');
+
+                        return res.data;
+                    });
+                }
+            );
+        }
+    )
+);
+
 const getRevisionPageByPath = withCacheKey(
     withoutConcurrentExecution(
         async (
diff --git a/packages/gitbook-v2/src/lib/data/index.ts b/packages/gitbook-v2/src/lib/data/index.ts
index 2e37e2fbb4..93266d124e 100644
--- a/packages/gitbook-v2/src/lib/data/index.ts
+++ b/packages/gitbook-v2/src/lib/data/index.ts
@@ -1,7 +1,7 @@
 export * from './api';
 export * from './types';
-export * from './pages';
 export * from './urls';
 export * from './errors';
 export * from './lookup';
 export * from './visitor';
+export * from './pages';
diff --git a/packages/gitbook-v2/src/lib/data/pages.ts b/packages/gitbook-v2/src/lib/data/pages.ts
index 4324254d9b..642b486b82 100644
--- a/packages/gitbook-v2/src/lib/data/pages.ts
+++ b/packages/gitbook-v2/src/lib/data/pages.ts
@@ -1,15 +1,30 @@
-import type { JSONDocument, RevisionPageDocument, Space } from '@gitbook/api';
+import { waitUntil } from '@/lib/waitUntil';
+import type { JSONDocument, RevisionPageDocument } from '@gitbook/api';
+import type { GitBookSiteContext, GitBookSpaceContext } from '../context';
 import { getDataOrNull } from './errors';
-import type { GitBookDataFetcher } from './types';
 
 /**
  * Get the document for a page.
  */
 export async function getPageDocument(
-    dataFetcher: GitBookDataFetcher,
-    space: Space,
+    context: GitBookSpaceContext | GitBookSiteContext,
     page: RevisionPageDocument
 ): Promise<JSONDocument | null> {
+    const { dataFetcher, space } = context;
+
+    if (
+        'site' in context &&
+        (context.site.id === 'site_JOVzv' || context.site.id === 'site_IxAYj')
+    ) {
+        return getDataOrNull(
+            dataFetcher.getRevisionPageDocument({
+                spaceId: space.id,
+                revisionId: space.revision,
+                pageId: page.id,
+            })
+        );
+    }
+
     if (page.documentId) {
         return getDataOrNull(
             dataFetcher.getDocument({ spaceId: space.id, documentId: page.documentId })
@@ -26,5 +41,30 @@ export async function getPageDocument(
         );
     }
 
+    // Pre-fetch the document to start filling the cache before we migrate to this API.
+    if (isInPercentRollout(space.id, 10)) {
+        await waitUntil(
+            getDataOrNull(
+                dataFetcher.getRevisionPageDocument({
+                    spaceId: space.id,
+                    revisionId: space.revision,
+                    pageId: page.id,
+                })
+            )
+        );
+    }
+
     return null;
 }
+
+function isInPercentRollout(value: string, rollout: number) {
+    return getRandomPercent(value) < rollout;
+}
+
+function getRandomPercent(value: string) {
+    const hash = value.split('').reduce((acc, char) => {
+        return acc + char.charCodeAt(0);
+    }, 0);
+
+    return hash % 100;
+}
diff --git a/packages/gitbook-v2/src/lib/data/types.ts b/packages/gitbook-v2/src/lib/data/types.ts
index 178a0ba77d..8d987047e2 100644
--- a/packages/gitbook-v2/src/lib/data/types.ts
+++ b/packages/gitbook-v2/src/lib/data/types.ts
@@ -106,6 +106,15 @@ export interface GitBookDataFetcher {
         pageId: string;
     }): Promise<DataFetcherResponse<string>>;
 
+    /**
+     * Get the document of a page by its path.
+     */
+    getRevisionPageDocument(params: {
+        spaceId: string;
+        revisionId: string;
+        pageId: string;
+    }): Promise<DataFetcherResponse<api.JSONDocument>>;
+
     /**
      * Get a document by its space ID and document ID.
      */
diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json
index 666cd114d0..379f975a1e 100644
--- a/packages/gitbook/package.json
+++ b/packages/gitbook/package.json
@@ -10,7 +10,7 @@
         "typecheck": "tsc --noEmit",
         "e2e": "playwright test e2e/internal.spec.ts",
         "e2e-customers": "playwright test e2e/customers.spec.ts",
-        "unit": "bun test {src,packages}",
+        "unit": "bun test {src,packages} --preload ./tests/preload-bun.ts",
         "generate": "gitbook-icons ./public/~gitbook/static/icons custom-icons && gitbook-math ./public/~gitbook/static/math",
         "copy:icons": "gitbook-icons ./public/~gitbook/static/icons",
         "clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math"
diff --git a/packages/gitbook/src/components/PDF/PDFPage.tsx b/packages/gitbook/src/components/PDF/PDFPage.tsx
index 96cb784ccd..f70b340b41 100644
--- a/packages/gitbook/src/components/PDF/PDFPage.tsx
+++ b/packages/gitbook/src/components/PDF/PDFPage.tsx
@@ -9,7 +9,6 @@ import {
 } from '@gitbook/api';
 import { Icon } from '@gitbook/icons';
 import type { GitBookSiteContext, GitBookSpaceContext } from '@v2/lib/context';
-import { getPageDocument } from '@v2/lib/data';
 import type { GitBookLinker } from '@v2/lib/links';
 import type { Metadata } from 'next';
 import { notFound } from 'next/navigation';
@@ -29,6 +28,7 @@ import { PageControlButtons } from './PageControlButtons';
 import { PrintButton } from './PrintButton';
 import './pdf.css';
 import { sanitizeGitBookAppURL } from '@/lib/app';
+import { getPageDocument } from '@v2/lib/data';
 
 const DEFAULT_LIMIT = 100;
 
@@ -224,8 +224,7 @@ async function PDFPageDocument(props: {
     context: GitBookSpaceContext;
 }) {
     const { page, context } = props;
-    const { space } = context;
-    const document = await getPageDocument(context.dataFetcher, space, page);
+    const document = await getPageDocument(context, page);
 
     return (
         <PrintPage id={getPagePDFContainerId(page)}>
diff --git a/packages/gitbook/src/components/SitePage/SitePage.tsx b/packages/gitbook/src/components/SitePage/SitePage.tsx
index fe6934a529..9e7c08515f 100644
--- a/packages/gitbook/src/components/SitePage/SitePage.tsx
+++ b/packages/gitbook/src/components/SitePage/SitePage.tsx
@@ -62,7 +62,7 @@ export async function SitePage(props: SitePageProps) {
     const withSections = Boolean(sections && sections.list.length > 0);
     const headerOffset = { sectionsHeader: withSections, topHeader: withTopHeader };
 
-    const document = await getPageDocument(context.dataFetcher, context.space, page);
+    const document = await getPageDocument(context, page);
 
     return (
         <PageContextProvider pageId={page.id} spaceId={context.space.id} title={page.title}>
diff --git a/packages/gitbook/src/lib/api.ts b/packages/gitbook/src/lib/api.ts
index 310074450d..cb612664b0 100644
--- a/packages/gitbook/src/lib/api.ts
+++ b/packages/gitbook/src/lib/api.ts
@@ -493,6 +493,39 @@ export const getRevisionPageByPath = cache({
     },
 });
 
+/**
+ * Get a document from a page by its ID
+ */
+export const getRevisionPageDocument = cache({
+    name: 'api.getRevisionPageDocument.v1',
+    tag: (spaceId, revisionId) =>
+        getCacheTag({ tag: 'revision', space: spaceId, revision: revisionId }),
+    tagImmutable: true,
+    getKeySuffix: getAPIContextId,
+    get: async (
+        spaceId: string,
+        revisionId: string,
+        pageId: string,
+        options: CacheFunctionOptions
+    ) => {
+        const apiCtx = await api();
+        const response = await apiCtx.client.spaces.getPageDocumentInRevisionById(
+            spaceId,
+            revisionId,
+            pageId,
+            {
+                evaluated: true,
+            },
+            {
+                ...noCacheFetchOptions,
+                signal: options.signal,
+            }
+        );
+
+        return cacheResponse(response, cacheTtl_7days);
+    },
+});
+
 /**
  * Resolve a file by its ID.
  * It should not be used directly, use `getRevisionFile` instead.
diff --git a/packages/gitbook/src/lib/references.tsx b/packages/gitbook/src/lib/references.tsx
index 8cfef861d0..7362decbe6 100644
--- a/packages/gitbook/src/lib/references.tsx
+++ b/packages/gitbook/src/lib/references.tsx
@@ -155,7 +155,7 @@ export async function resolveContentRef(
                 });
 
                 if (resolveAnchorText) {
-                    const document = await getPageDocument(dataFetcher, space, page);
+                    const document = await getPageDocument(context, page);
                     if (document) {
                         const block = getBlockById(document, anchor);
                         if (block) {
diff --git a/packages/gitbook/src/lib/v1.ts b/packages/gitbook/src/lib/v1.ts
index eb2accdc24..db6118a84c 100644
--- a/packages/gitbook/src/lib/v1.ts
+++ b/packages/gitbook/src/lib/v1.ts
@@ -25,6 +25,7 @@ import {
     getRevision,
     getRevisionFile,
     getRevisionPageByPath,
+    getRevisionPageDocument,
     getRevisionPages,
     getSiteRedirectBySource,
     getSpace,
@@ -241,6 +242,18 @@ function getDataFetcherV1(apiTokenOverride?: string): GitBookDataFetcher {
             );
         },
 
+        getRevisionPageDocument(params) {
+            return withAPI(() =>
+                wrapDataFetcherError(async () => {
+                    return getRevisionPageDocument(
+                        params.spaceId,
+                        params.revisionId,
+                        params.pageId
+                    );
+                })
+            );
+        },
+
         getRevisionPageByPath(params) {
             return withAPI(() =>
                 wrapDataFetcherError(async () => {
diff --git a/packages/gitbook/src/lib/waitUntil.ts b/packages/gitbook/src/lib/waitUntil.ts
index 6e2940f17b..e81e662e2d 100644
--- a/packages/gitbook/src/lib/waitUntil.ts
+++ b/packages/gitbook/src/lib/waitUntil.ts
@@ -1,4 +1,6 @@
 import type { ExecutionContext, IncomingRequestCfProperties } from '@cloudflare/workers-types';
+import { getCloudflareContext as getCloudflareContextV2 } from '@v2/lib/data/cloudflare';
+import { isV2 } from './v2';
 
 let pendings: Array<Promise<unknown>> = [];
 
@@ -47,6 +49,14 @@ export async function waitUntil(promise: Promise<unknown>) {
         return;
     }
 
+    if (isV2()) {
+        const context = getCloudflareContextV2();
+        if (context) {
+            context.ctx.waitUntil(promise);
+            return;
+        }
+    }
+
     const cloudflareContext = await getGlobalContext();
     if ('waitUntil' in cloudflareContext) {
         cloudflareContext.waitUntil(promise);
diff --git a/packages/gitbook/tests/preload-bun.ts b/packages/gitbook/tests/preload-bun.ts
new file mode 100644
index 0000000000..3fc846ae26
--- /dev/null
+++ b/packages/gitbook/tests/preload-bun.ts
@@ -0,0 +1,8 @@
+import { mock } from 'bun:test';
+
+/**
+ * Mock the `server-only` module to avoid errors when running tests as it doesn't work well in Bun
+ */
+mock.module('server-only', () => {
+    return {};
+});

From da67711c90f93c281a7ca650fd78ac073d60db30 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Thu, 12 Jun 2025 23:37:38 +0200
Subject: [PATCH 099/127] Fix crash on Vercel (#3311)

---
 packages/gitbook-v2/src/lib/data/pages.ts |  2 +-
 packages/gitbook/src/lib/waitUntil.ts     | 33 ++++++++++++++++-------
 2 files changed, 24 insertions(+), 11 deletions(-)

diff --git a/packages/gitbook-v2/src/lib/data/pages.ts b/packages/gitbook-v2/src/lib/data/pages.ts
index 642b486b82..f4cbb1acab 100644
--- a/packages/gitbook-v2/src/lib/data/pages.ts
+++ b/packages/gitbook-v2/src/lib/data/pages.ts
@@ -42,7 +42,7 @@ export async function getPageDocument(
     }
 
     // Pre-fetch the document to start filling the cache before we migrate to this API.
-    if (isInPercentRollout(space.id, 10)) {
+    if (isInPercentRollout(space.id, 10) || process.env.VERCEL_ENV === 'preview') {
         await waitUntil(
             getDataOrNull(
                 dataFetcher.getRevisionPageDocument({
diff --git a/packages/gitbook/src/lib/waitUntil.ts b/packages/gitbook/src/lib/waitUntil.ts
index e81e662e2d..ed606e959d 100644
--- a/packages/gitbook/src/lib/waitUntil.ts
+++ b/packages/gitbook/src/lib/waitUntil.ts
@@ -1,5 +1,6 @@
 import type { ExecutionContext, IncomingRequestCfProperties } from '@cloudflare/workers-types';
 import { getCloudflareContext as getCloudflareContextV2 } from '@v2/lib/data/cloudflare';
+import { GITBOOK_RUNTIME } from '@v2/lib/env';
 import { isV2 } from './v2';
 
 let pendings: Array<Promise<unknown>> = [];
@@ -49,20 +50,32 @@ export async function waitUntil(promise: Promise<unknown>) {
         return;
     }
 
-    if (isV2()) {
-        const context = getCloudflareContextV2();
-        if (context) {
-            context.ctx.waitUntil(promise);
-            return;
+    if (GITBOOK_RUNTIME === 'cloudflare') {
+        if (isV2()) {
+            const context = getCloudflareContextV2();
+            if (context) {
+                context.ctx.waitUntil(promise);
+                return;
+            }
+        } else {
+            const cloudflareContext = await getGlobalContext();
+            if ('waitUntil' in cloudflareContext) {
+                cloudflareContext.waitUntil(promise);
+                return;
+            }
         }
     }
 
-    const cloudflareContext = await getGlobalContext();
-    if ('waitUntil' in cloudflareContext) {
-        cloudflareContext.waitUntil(promise);
-    } else {
-        await promise;
+    if (GITBOOK_RUNTIME === 'vercel' && isV2()) {
+        // @ts-expect-error - `after` is not exported by `next/server` in next 14
+        const { after } = await import('next/server');
+        if (typeof after === 'function') {
+            after(() => promise);
+            return;
+        }
     }
+
+    await promise;
 }
 
 /**

From 315717fa2d219f3571eaf3a7bbbef418066d1da0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Fri, 13 Jun 2025 11:17:33 +0200
Subject: [PATCH 100/127] Disable the background fetch of
 getRevisionPageDocument (#3313)

---
 packages/gitbook-v2/src/lib/data/pages.ts | 23 +++++++++++++----------
 1 file changed, 13 insertions(+), 10 deletions(-)

diff --git a/packages/gitbook-v2/src/lib/data/pages.ts b/packages/gitbook-v2/src/lib/data/pages.ts
index f4cbb1acab..b9e3cabc5e 100644
--- a/packages/gitbook-v2/src/lib/data/pages.ts
+++ b/packages/gitbook-v2/src/lib/data/pages.ts
@@ -42,16 +42,19 @@ export async function getPageDocument(
     }
 
     // Pre-fetch the document to start filling the cache before we migrate to this API.
-    if (isInPercentRollout(space.id, 10) || process.env.VERCEL_ENV === 'preview') {
-        await waitUntil(
-            getDataOrNull(
-                dataFetcher.getRevisionPageDocument({
-                    spaceId: space.id,
-                    revisionId: space.revision,
-                    pageId: page.id,
-                })
-            )
-        );
+    if (process.env.NODE_ENV === 'development') {
+        // Disable for now to investigate side-effects
+        if (isInPercentRollout(space.id, 10) || process.env.VERCEL_ENV === 'preview') {
+            await waitUntil(
+                getDataOrNull(
+                    dataFetcher.getRevisionPageDocument({
+                        spaceId: space.id,
+                        revisionId: space.revision,
+                        pageId: page.id,
+                    })
+                )
+            );
+        }
     }
 
     return null;

From 50e8b2ec52e74dcb00d895c25b43bcf7971b855f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Fri, 13 Jun 2025 11:30:18 +0200
Subject: [PATCH 101/127] Don't use next/server after (#3314)

---
 packages/gitbook/src/lib/waitUntil.ts | 13 +++----------
 1 file changed, 3 insertions(+), 10 deletions(-)

diff --git a/packages/gitbook/src/lib/waitUntil.ts b/packages/gitbook/src/lib/waitUntil.ts
index ed606e959d..dbaae41a0d 100644
--- a/packages/gitbook/src/lib/waitUntil.ts
+++ b/packages/gitbook/src/lib/waitUntil.ts
@@ -66,16 +66,9 @@ export async function waitUntil(promise: Promise<unknown>) {
         }
     }
 
-    if (GITBOOK_RUNTIME === 'vercel' && isV2()) {
-        // @ts-expect-error - `after` is not exported by `next/server` in next 14
-        const { after } = await import('next/server');
-        if (typeof after === 'function') {
-            after(() => promise);
-            return;
-        }
-    }
-
-    await promise;
+    await promise.catch((error) => {
+        console.error('Ignored error in waitUntil', error);
+    });
 }
 
 /**

From 28a9ee701e17a256387a301a078b5310c283d530 Mon Sep 17 00:00:00 2001
From: spastorelli <spastorelli@users.noreply.github.com>
Date: Fri, 13 Jun 2025 12:10:10 +0200
Subject: [PATCH 102/127] Add paypal.gitbook.com to ADAPTIVE_CONTENT_HOSTS list
 (#3315)

---
 packages/gitbook-v2/src/middleware.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/packages/gitbook-v2/src/middleware.ts b/packages/gitbook-v2/src/middleware.ts
index c11d466c78..0d57ce27e2 100644
--- a/packages/gitbook-v2/src/middleware.ts
+++ b/packages/gitbook-v2/src/middleware.ts
@@ -39,6 +39,7 @@ type URLWithMode = { url: URL; mode: 'url' | 'url-host' };
  */
 const ADAPTIVE_CONTENT_HOSTS = [
     'docs.gitbook.com',
+    'paypal.gitbook.com',
     'adaptive-docs.gitbook-staging.com',
     'enriched-content-playground.gitbook-staging.io',
     'docs.testgitbook.com',

From a28a9978517ae19c08b8dd3b61c98af2f7791d24 Mon Sep 17 00:00:00 2001
From: Zeno Kapitein <zeno@gitbook.io>
Date: Fri, 13 Jun 2025 15:42:00 +0200
Subject: [PATCH 103/127] Add margin to adjacent buttons (#3316)

---
 .changeset/purple-cougars-breathe.md          |  5 ++++
 .../components/DocumentView/InlineButton.tsx  | 30 +++++++++++--------
 2 files changed, 22 insertions(+), 13 deletions(-)
 create mode 100644 .changeset/purple-cougars-breathe.md

diff --git a/.changeset/purple-cougars-breathe.md b/.changeset/purple-cougars-breathe.md
new file mode 100644
index 0000000000..2c6d010d4e
--- /dev/null
+++ b/.changeset/purple-cougars-breathe.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Add margin to adjacent buttons
diff --git a/packages/gitbook/src/components/DocumentView/InlineButton.tsx b/packages/gitbook/src/components/DocumentView/InlineButton.tsx
index 9841c02626..a36cd74527 100644
--- a/packages/gitbook/src/components/DocumentView/InlineButton.tsx
+++ b/packages/gitbook/src/components/DocumentView/InlineButton.tsx
@@ -17,18 +17,22 @@ export async function InlineButton(props: InlineProps<api.DocumentInlineButton>)
     }
 
     return (
-        <Button
-            href={resolved.href}
-            label={inline.data.label}
-            // TODO: use a variant specifically for user-defined buttons.
-            variant={inline.data.kind}
-            insights={{
-                type: 'link_click',
-                link: {
-                    target: inline.data.ref,
-                    position: api.SiteInsightsLinkPosition.Content,
-                },
-            }}
-        />
+        // Set the leading to have some vertical space between adjacent buttons
+        <span className="inline-button leading-[3rem] [&:has(+.inline-button)]:mr-2">
+            <Button
+                href={resolved.href}
+                label={inline.data.label}
+                // TODO: use a variant specifically for user-defined buttons.
+                variant={inline.data.kind}
+                className="leading-normal"
+                insights={{
+                    type: 'link_click',
+                    link: {
+                        target: inline.data.ref,
+                        position: api.SiteInsightsLinkPosition.Content,
+                    },
+                }}
+            />
+        </span>
     );
 }

From 6294bbb53cffc9f7eb7487bd1a375b3c57622b17 Mon Sep 17 00:00:00 2001
From: conico974 <nicodorseuil@yahoo.fr>
Date: Fri, 13 Jun 2025 15:46:37 +0200
Subject: [PATCH 104/127] Fix error propagating (#3317)

Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
---
 .changeset/rotten-donuts-bow.md              |  5 +++++
 packages/gitbook-v2/src/app/global-error.tsx | 18 ++++++++++++++++++
 2 files changed, 23 insertions(+)
 create mode 100644 .changeset/rotten-donuts-bow.md
 create mode 100644 packages/gitbook-v2/src/app/global-error.tsx

diff --git a/.changeset/rotten-donuts-bow.md b/.changeset/rotten-donuts-bow.md
new file mode 100644
index 0000000000..d6ebe0650a
--- /dev/null
+++ b/.changeset/rotten-donuts-bow.md
@@ -0,0 +1,5 @@
+---
+"gitbook-v2": patch
+---
+
+add a global error boundary
diff --git a/packages/gitbook-v2/src/app/global-error.tsx b/packages/gitbook-v2/src/app/global-error.tsx
new file mode 100644
index 0000000000..abf0186a40
--- /dev/null
+++ b/packages/gitbook-v2/src/app/global-error.tsx
@@ -0,0 +1,18 @@
+'use client';
+
+import NextError from 'next/error';
+
+export default function GlobalError({
+    error,
+}: {
+    error: Error & { digest?: string };
+}) {
+    console.error('Global error:', error);
+    return (
+        <html lang="en">
+            <body>
+                <NextError statusCode={undefined as any} />
+            </body>
+        </html>
+    );
+}

From 8a3910e208d83a0048e61f06e09e26cac263aef3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Fri, 13 Jun 2025 15:55:04 +0200
Subject: [PATCH 105/127] Experiment with hiding sections without a matching
 site space (#3318)

---
 .../SiteSections/encodeClientSiteSections.ts  | 48 +++++++++++++++++--
 1 file changed, 43 insertions(+), 5 deletions(-)

diff --git a/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts b/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts
index 1c8cd09899..891936b6fc 100644
--- a/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts
+++ b/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts
@@ -1,5 +1,5 @@
 import { getSectionURL, getSiteSpaceURL } from '@/lib/sites';
-import type { SiteSection, SiteSectionGroup } from '@gitbook/api';
+import type { SiteSection, SiteSectionGroup, SiteSpace } from '@gitbook/api';
 import type { GitBookSiteContext, SiteSections } from '@v2/lib/context';
 
 export type ClientSiteSections = {
@@ -33,10 +33,14 @@ export function encodeClientSiteSections(context: GitBookSiteContext, sections:
                 title: item.title,
                 icon: item.icon,
                 object: item.object,
-                sections: item.sections.map((section) => encodeSection(context, section)),
+                sections: item.sections
+                    .filter((section) => shouldIncludeSection(context, section))
+                    .map((section) => encodeSection(context, section)),
             });
         } else {
-            clientSections.push(encodeSection(context, item));
+            if (shouldIncludeSection(context, item)) {
+                clientSections.push(encodeSection(context, item));
+            }
         }
     }
 
@@ -57,6 +61,33 @@ function encodeSection(context: GitBookSiteContext, section: SiteSection) {
     };
 }
 
+/**
+ * Test if a section should be included in the list of sections.
+ */
+function shouldIncludeSection(context: GitBookSiteContext, section: SiteSection) {
+    if (context.site.id !== 'site_JOVzv') {
+        return true;
+    }
+
+    // Testing for a new mode of navigation where the multi-variants section are hidden
+    // if they do not include an equivalent of the current site space.
+
+    // TODO: replace with a proper flag on the section
+    const withNavigateOnlyIfEquivalent = section.id === 'sitesc_4jvEm';
+
+    if (!withNavigateOnlyIfEquivalent) {
+        return true;
+    }
+
+    const { siteSpace: currentSiteSpace } = context;
+    if (section.siteSpaces.length === 1) {
+        return true;
+    }
+    return section.siteSpaces.some((siteSpace) =>
+        areSiteSpacesEquivalent(siteSpace, currentSiteSpace)
+    );
+}
+
 /**
  * Find the best default site space to navigate to for a givent section:
  * 1. If we are on the default, continue on the default.
@@ -70,8 +101,8 @@ function findBestTargetURL(context: GitBookSiteContext, section: SiteSection) {
         return getSectionURL(context, section);
     }
 
-    const bestMatch = section.siteSpaces.find(
-        (siteSpace) => siteSpace.path === currentSiteSpace.path
+    const bestMatch = section.siteSpaces.find((siteSpace) =>
+        areSiteSpacesEquivalent(siteSpace, currentSiteSpace)
     );
     if (bestMatch) {
         return getSiteSpaceURL(context, bestMatch);
@@ -79,3 +110,10 @@ function findBestTargetURL(context: GitBookSiteContext, section: SiteSection) {
 
     return getSectionURL(context, section);
 }
+
+/**
+ * Test if 2 site spaces are equivalent.
+ */
+function areSiteSpacesEquivalent(siteSpace1: SiteSpace, siteSpace2: SiteSpace) {
+    return siteSpace1.path === siteSpace2.path;
+}

From 87218062becd2272d66c7c93f0237461c6479a2a Mon Sep 17 00:00:00 2001
From: conico974 <nicodorseuil@yahoo.fr>
Date: Sat, 14 Jun 2025 11:10:26 +0200
Subject: [PATCH 106/127] Use Promise.all whenever possible (#3319)

Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
---
 .../src/components/Ads/AdClassicRendering.tsx | 12 ++--
 .../gitbook/src/components/Ads/renderAd.tsx   |  6 +-
 .../DocumentView/Table/RecordCard.tsx         | 16 +++---
 .../src/components/PageBody/PageCover.tsx     |  6 +-
 .../src/components/SiteLayout/SiteLayout.tsx  | 55 +++++++++++--------
 packages/gitbook/src/routes/ogimage.tsx       | 10 ++--
 6 files changed, 59 insertions(+), 46 deletions(-)

diff --git a/packages/gitbook/src/components/Ads/AdClassicRendering.tsx b/packages/gitbook/src/components/Ads/AdClassicRendering.tsx
index e6d263bf6a..82613af491 100644
--- a/packages/gitbook/src/components/Ads/AdClassicRendering.tsx
+++ b/packages/gitbook/src/components/Ads/AdClassicRendering.tsx
@@ -19,14 +19,14 @@ export async function AdClassicRendering({
     insightsAd: SiteInsightsAd | null;
     context: GitBookBaseContext;
 }) {
-    const smallImgSrc =
+    const [smallImgSrc, logoSrc] = await Promise.all([
         'smallImage' in ad
-            ? await getResizedImageURL(context.imageResizer, ad.smallImage, { width: 192, dpr: 2 })
-            : null;
-    const logoSrc =
+            ? getResizedImageURL(context.imageResizer, ad.smallImage, { width: 192, dpr: 2 })
+            : null,
         'logo' in ad
-            ? await getResizedImageURL(context.imageResizer, ad.logo, { width: 192 - 48, dpr: 2 })
-            : null;
+            ? getResizedImageURL(context.imageResizer, ad.logo, { width: 192 - 48, dpr: 2 })
+            : null,
+    ]);
     return (
         <Link
             rel="sponsored noopener"
diff --git a/packages/gitbook/src/components/Ads/renderAd.tsx b/packages/gitbook/src/components/Ads/renderAd.tsx
index 146f0d2530..96ff7bd3ba 100644
--- a/packages/gitbook/src/components/Ads/renderAd.tsx
+++ b/packages/gitbook/src/components/Ads/renderAd.tsx
@@ -43,10 +43,12 @@ interface FetchPlaceholderAdOptions {
  * and properly access user-agent and IP.
  */
 export async function renderAd(options: FetchAdOptions) {
-    const context = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
+    const [context, result] = await Promise.all([
+        isV2() ? getServerActionBaseContext() : getV1BaseContext(),
+        options.source === 'live' ? fetchAd(options) : getPlaceholderAd(),
+    ]);
 
     const mode = options.source === 'live' ? options.mode : 'classic';
-    const result = options.source === 'live' ? await fetchAd(options) : await getPlaceholderAd();
     if (!result || !result.ad.description || !result.ad.statlink) {
         return null;
     }
diff --git a/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx b/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx
index 4bc1e0ce85..c32fb01e9d 100644
--- a/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx
+++ b/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx
@@ -23,18 +23,18 @@ export async function RecordCard(
     const coverFile = view.coverDefinition
         ? getRecordValue<string[]>(record[1], view.coverDefinition)?.[0]
         : null;
-    const cover =
-        coverFile && context.contentContext
-            ? await resolveContentRef({ kind: 'file', file: coverFile }, context.contentContext)
-            : null;
-
     const targetRef = view.targetDefinition
         ? (record[1].values[view.targetDefinition] as ContentRef)
         : null;
-    const target =
+
+    const [cover, target] = await Promise.all([
+        coverFile && context.contentContext
+            ? resolveContentRef({ kind: 'file', file: coverFile }, context.contentContext)
+            : null,
         targetRef && context.contentContext
-            ? await resolveContentRef(targetRef, context.contentContext)
-            : null;
+            ? resolveContentRef(targetRef, context.contentContext)
+            : null,
+    ]);
 
     const coverIsSquareOrPortrait =
         cover?.file?.dimensions &&
diff --git a/packages/gitbook/src/components/PageBody/PageCover.tsx b/packages/gitbook/src/components/PageBody/PageCover.tsx
index a4bb712618..3b37dfd389 100644
--- a/packages/gitbook/src/components/PageBody/PageCover.tsx
+++ b/packages/gitbook/src/components/PageBody/PageCover.tsx
@@ -21,8 +21,10 @@ export async function PageCover(props: {
     context: GitBookSiteContext;
 }) {
     const { as, page, cover, context } = props;
-    const resolved = cover.ref ? await resolveContentRef(cover.ref, context) : null;
-    const resolvedDark = cover.refDark ? await resolveContentRef(cover.refDark, context) : null;
+    const [resolved, resolvedDark] = await Promise.all([
+        cover.ref ? resolveContentRef(cover.ref, context) : null,
+        cover.refDark ? resolveContentRef(cover.refDark, context) : null,
+    ]);
 
     return (
         <div
diff --git a/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx b/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx
index dda7095266..83211354f1 100644
--- a/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx
+++ b/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx
@@ -102,30 +102,37 @@ export async function generateSiteLayoutMetadata(context: GitBookSiteContext): P
     const customIcon = 'icon' in customization.favicon ? customization.favicon.icon : null;
 
     const faviconSize = 48;
-    const icons = [
-        {
-            url: customIcon?.light
-                ? await getResizedImageURL(imageResizer, customIcon.light, {
-                      width: faviconSize,
-                      height: faviconSize,
-                  })
-                : linker.toAbsoluteURL(
-                      linker.toPathInSpace('~gitbook/icon?size=small&theme=light')
-                  ),
-            type: 'image/png',
-            media: '(prefers-color-scheme: light)',
-        },
-        {
-            url: customIcon?.dark
-                ? await getResizedImageURL(imageResizer, customIcon.dark, {
-                      width: faviconSize,
-                      height: faviconSize,
-                  })
-                : linker.toAbsoluteURL(linker.toPathInSpace('~gitbook/icon?size=small&theme=dark')),
-            type: 'image/png',
-            media: '(prefers-color-scheme: dark)',
-        },
-    ];
+    const icons = await Promise.all(
+        [
+            {
+                url: customIcon?.light
+                    ? getResizedImageURL(imageResizer, customIcon.light, {
+                          width: faviconSize,
+                          height: faviconSize,
+                      })
+                    : linker.toAbsoluteURL(
+                          linker.toPathInSpace('~gitbook/icon?size=small&theme=light')
+                      ),
+                type: 'image/png',
+                media: '(prefers-color-scheme: light)',
+            },
+            {
+                url: customIcon?.dark
+                    ? getResizedImageURL(imageResizer, customIcon.dark, {
+                          width: faviconSize,
+                          height: faviconSize,
+                      })
+                    : linker.toAbsoluteURL(
+                          linker.toPathInSpace('~gitbook/icon?size=small&theme=dark')
+                      ),
+                type: 'image/png',
+                media: '(prefers-color-scheme: dark)',
+            },
+        ].map(async (icon) => ({
+            ...icon,
+            url: await icon.url,
+        }))
+    );
 
     return {
         title: site.title,
diff --git a/packages/gitbook/src/routes/ogimage.tsx b/packages/gitbook/src/routes/ogimage.tsx
index e55dcfc2dd..e213ee2238 100644
--- a/packages/gitbook/src/routes/ogimage.tsx
+++ b/packages/gitbook/src/routes/ogimage.tsx
@@ -45,7 +45,7 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
             : '';
 
     // Load the fonts
-    const { fontFamily, fonts } = await (async () => {
+    const fontLoader = async () => {
         // google fonts
         if (typeof customization.styling.font === 'string') {
             const fontFamily = customization.styling.font ?? CustomizationDefaultFont.Inter;
@@ -85,7 +85,7 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
         ).filter(filterOutNullable);
 
         return { fontFamily: 'CustomFont', fonts };
-    })();
+    };
 
     const theme = customization.themes.default;
     const useLightTheme = theme === 'light';
@@ -139,7 +139,7 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
             break;
     }
 
-    const favicon = await (async () => {
+    const faviconLoader = async () => {
         if ('icon' in customization.favicon)
             return (
                 <img
@@ -164,7 +164,9 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
             )
         );
         return <img src={src} alt="Icon" width={40} height={40} tw="mr-4" />;
-    })();
+    };
+
+    const [favicon, { fontFamily, fonts }] = await Promise.all([faviconLoader(), fontLoader()]);
 
     return new ImageResponse(
         <div

From 2863fe0dc17e2bf20dc8cb4137f72480024cc73a Mon Sep 17 00:00:00 2001
From: Taran Vohra <taranvohra@outlook.com>
Date: Mon, 16 Jun 2025 09:59:55 +0530
Subject: [PATCH 107/127] 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 4c999bd7a4..be8c21f1d6 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<T> = ReturnType<typeof tryCatch<T>>;
-
-async function lookupPublishedContentByUrl(input: {
-    url: string;
-    fetchLookupAPIResult: (args: {
-        url: string;
-        signal: AbortSignal;
-    }) => TryCatch<Awaited<ReturnType<GitBookAPI['urls']['resolvePublishedContentByUrl']>>>;
-}): Promise<DataFetcherResponse<PublishedSiteContentLookup>> {
+export async function lookupPublishedContentByUrl(
+    input: LookupPublishedContentByUrlInput
+): Promise<DataFetcherResponse<PublishedSiteContentLookup>> {
     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 0d57ce27e2..d413d6a435 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 <utkuufuk@gmail.com>
Date: Mon, 16 Jun 2025 12:31:15 +0200
Subject: [PATCH 108/127] 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 0000000000..ec33c1efcf
--- /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 8a9f5c2fdc..9b13767ffd 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 960b1eb38a..a0f467f80f 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
                         <HighlightQuery query={query} text={item.title} />
                     </p>
                 ) : null}
-                {item.body ? (
-                    <p className={tcls('text-sm', 'line-clamp-3', 'relative')}>
-                        <HighlightQuery query={query} text={item.body} />
-                    </p>
-                ) : null}
+                {item.body ? highlightQueryInBody(item.body, query) : null}
             </div>
             <div
                 className={tcls(
@@ -90,3 +86,14 @@ export const SearchSectionResultItem = React.forwardRef(function SearchSectionRe
         </Link>
     );
 });
+
+function highlightQueryInBody(body: string, query: string) {
+    const idx = body.indexOf(query);
+
+    // Ensure the query to be highlighted is visible in the body.
+    return (
+        <p className={tcls('text-sm', 'line-clamp-3', 'relative')}>
+            <HighlightQuery query={query} text={idx < 20 ? body : `...${body.slice(idx - 15)}`} />
+        </p>
+    );
+}

From 72cd0e59e683ace3d269f0e59338b41c36b88651 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Mon, 16 Jun 2025 13:37:45 +0200
Subject: [PATCH 109/127] 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     | 979 +++++++++-----------
 packages/gitbook-v2/src/lib/data/memoize.ts |  92 --
 7 files changed, 609 insertions(+), 612 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 0000000000..ada33d1c55
--- /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 3ff6d61c4c..b4d2b49d1f 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 5723cdbc79..cfa6e71b91 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 0000000000..735b1fb06d
--- /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 0000000000..e05753907a
--- /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<Args extends any[], Return>(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(): <T>(value: T) => T {
+    const reverseIndex = new WeakMap<object, string>();
+    const refIndex = new Map<string, object>();
+
+    return <T>(value: T) => {
+        if (isPrimitive(value)) {
+            return value;
+        }
+
+        const objectValue = value as object;
+        const index = reverseIndex.get(objectValue);
+        if (index !== undefined) {
+            return refIndex.get(index) as T;
+        }
+
+        const hash = identify(objectValue);
+        reverseIndex.set(objectValue, hash);
+
+        const existing = refIndex.get(hash);
+        if (existing !== undefined) {
+            return existing as T;
+        }
+
+        // first time we've seen this shape
+        refIndex.set(hash, objectValue);
+        return value;
+    };
+}
+
+function isPrimitive(value: any): boolean {
+    return value === null || typeof value !== 'object';
+}
diff --git a/packages/gitbook-v2/src/lib/data/api.ts b/packages/gitbook-v2/src/lib/data/api.ts
index 88c95c4016..d36c2445f5 100644
--- a/packages/gitbook-v2/src/lib/data/api.ts
+++ b/packages/gitbook-v2/src/lib/data/api.ts
@@ -8,8 +8,8 @@ import {
 import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-tags';
 import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env';
 import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache';
+import { cache } from '../cache';
 import { DataFetcherError, wrapDataFetcherError } from './errors';
-import { withCacheKey, withoutConcurrentExecution } from './memoize';
 import type { GitBookDataFetcher } from './types';
 
 interface DataFetcherInput {
@@ -204,307 +204,316 @@ export function createDataFetcher(
     };
 }
 
-const getUserById = withCacheKey(
-    withoutConcurrentExecution(async (_, input: DataFetcherInput, params: { userId: string }) => {
+const getUserById = cache(async (input: DataFetcherInput, params: { userId: string }) => {
+    'use cache';
+    return trace(`getUserById(${params.userId})`, async () => {
+        return wrapDataFetcherError(async () => {
+            const api = apiClient(input);
+            const res = await api.users.getUserById(params.userId, {
+                ...noCacheFetchOptions,
+            });
+            cacheTag(...getCacheTagsFromResponse(res));
+            cacheLife('days');
+            return res.data;
+        });
+    });
+});
+
+const getSpace = cache(
+    async (input: DataFetcherInput, params: { spaceId: string; shareKey: string | undefined }) => {
         'use cache';
-        return trace(`getUserById(${params.userId})`, async () => {
+        cacheTag(
+            getCacheTag({
+                tag: 'space',
+                space: params.spaceId,
+            })
+        );
+
+        return trace(`getSpace(${params.spaceId}, ${params.shareKey})`, async () => {
             return wrapDataFetcherError(async () => {
                 const api = apiClient(input);
-                const res = await api.users.getUserById(params.userId, {
-                    ...noCacheFetchOptions,
-                });
+                const res = await api.spaces.getSpaceById(
+                    params.spaceId,
+                    {
+                        shareKey: params.shareKey,
+                    },
+                    {
+                        ...noCacheFetchOptions,
+                    }
+                );
                 cacheTag(...getCacheTagsFromResponse(res));
                 cacheLife('days');
                 return res.data;
             });
         });
-    })
+    }
 );
 
-const getSpace = withCacheKey(
-    withoutConcurrentExecution(
-        async (
-            _,
-            input: DataFetcherInput,
-            params: { spaceId: string; shareKey: string | undefined }
-        ) => {
-            'use cache';
-            cacheTag(
-                getCacheTag({
-                    tag: 'space',
-                    space: params.spaceId,
-                })
-            );
+const getChangeRequest = cache(
+    async (input: DataFetcherInput, params: { spaceId: string; changeRequestId: string }) => {
+        'use cache';
+        cacheTag(
+            getCacheTag({
+                tag: 'change-request',
+                space: params.spaceId,
+                changeRequest: params.changeRequestId,
+            })
+        );
+
+        return trace(`getChangeRequest(${params.spaceId}, ${params.changeRequestId})`, async () => {
+            return wrapDataFetcherError(async () => {
+                const api = apiClient(input);
+                const res = await api.spaces.getChangeRequestById(
+                    params.spaceId,
+                    params.changeRequestId,
+                    {
+                        ...noCacheFetchOptions,
+                    }
+                );
+                cacheTag(...getCacheTagsFromResponse(res));
+                cacheLife('minutes');
+                return res.data;
+            });
+        });
+    }
+);
 
-            return trace(`getSpace(${params.spaceId}, ${params.shareKey})`, async () => {
+const getRevision = cache(
+    async (
+        input: DataFetcherInput,
+        params: { spaceId: string; revisionId: string; metadata: boolean }
+    ) => {
+        'use cache';
+        return trace(`getRevision(${params.spaceId}, ${params.revisionId})`, async () => {
+            return wrapDataFetcherError(async () => {
+                const api = apiClient(input);
+                const res = await api.spaces.getRevisionById(
+                    params.spaceId,
+                    params.revisionId,
+                    {
+                        metadata: params.metadata,
+                    },
+                    {
+                        ...noCacheFetchOptions,
+                    }
+                );
+                cacheTag(...getCacheTagsFromResponse(res));
+                cacheLife('max');
+                return res.data;
+            });
+        });
+    }
+);
+
+const getRevisionPages = cache(
+    async (
+        input: DataFetcherInput,
+        params: { spaceId: string; revisionId: string; metadata: boolean }
+    ) => {
+        'use cache';
+        return trace(`getRevisionPages(${params.spaceId}, ${params.revisionId})`, async () => {
+            return wrapDataFetcherError(async () => {
+                const api = apiClient(input);
+                const res = await api.spaces.listPagesInRevisionById(
+                    params.spaceId,
+                    params.revisionId,
+                    {
+                        metadata: params.metadata,
+                    },
+                    {
+                        ...noCacheFetchOptions,
+                    }
+                );
+                cacheTag(...getCacheTagsFromResponse(res));
+                cacheLife('max');
+                return res.data.pages;
+            });
+        });
+    }
+);
+
+const getRevisionFile = cache(
+    async (
+        input: DataFetcherInput,
+        params: { spaceId: string; revisionId: string; fileId: string }
+    ) => {
+        'use cache';
+        return trace(
+            `getRevisionFile(${params.spaceId}, ${params.revisionId}, ${params.fileId})`,
+            async () => {
                 return wrapDataFetcherError(async () => {
                     const api = apiClient(input);
-                    const res = await api.spaces.getSpaceById(
+                    const res = await api.spaces.getFileInRevisionById(
                         params.spaceId,
-                        {
-                            shareKey: params.shareKey,
-                        },
+                        params.revisionId,
+                        params.fileId,
+                        {},
                         {
                             ...noCacheFetchOptions,
                         }
                     );
                     cacheTag(...getCacheTagsFromResponse(res));
-                    cacheLife('days');
+                    cacheLife('max');
                     return res.data;
                 });
-            });
-        }
-    )
-);
-
-const getChangeRequest = withCacheKey(
-    withoutConcurrentExecution(
-        async (
-            _,
-            input: DataFetcherInput,
-            params: { spaceId: string; changeRequestId: string }
-        ) => {
-            'use cache';
-            cacheTag(
-                getCacheTag({
-                    tag: 'change-request',
-                    space: params.spaceId,
-                    changeRequest: params.changeRequestId,
-                })
-            );
-
-            return trace(
-                `getChangeRequest(${params.spaceId}, ${params.changeRequestId})`,
-                async () => {
-                    return wrapDataFetcherError(async () => {
-                        const api = apiClient(input);
-                        const res = await api.spaces.getChangeRequestById(
-                            params.spaceId,
-                            params.changeRequestId,
-                            {
-                                ...noCacheFetchOptions,
-                            }
-                        );
-                        cacheTag(...getCacheTagsFromResponse(res));
-                        cacheLife('minutes');
-                        return res.data;
-                    });
-                }
-            );
-        }
-    )
+            }
+        );
+    }
 );
 
-const getRevision = withCacheKey(
-    withoutConcurrentExecution(
-        async (
-            _,
-            input: DataFetcherInput,
-            params: { spaceId: string; revisionId: string; metadata: boolean }
-        ) => {
-            'use cache';
-            return trace(`getRevision(${params.spaceId}, ${params.revisionId})`, async () => {
+const getRevisionPageMarkdown = cache(
+    async (
+        input: DataFetcherInput,
+        params: { spaceId: string; revisionId: string; pageId: string }
+    ) => {
+        'use cache';
+        return trace(
+            `getRevisionPageMarkdown(${params.spaceId}, ${params.revisionId}, ${params.pageId})`,
+            async () => {
                 return wrapDataFetcherError(async () => {
                     const api = apiClient(input);
-                    const res = await api.spaces.getRevisionById(
+                    const res = await api.spaces.getPageInRevisionById(
                         params.spaceId,
                         params.revisionId,
+                        params.pageId,
                         {
-                            metadata: params.metadata,
+                            format: 'markdown',
                         },
                         {
                             ...noCacheFetchOptions,
                         }
                     );
+
                     cacheTag(...getCacheTagsFromResponse(res));
                     cacheLife('max');
-                    return res.data;
+
+                    if (!('markdown' in res.data)) {
+                        throw new DataFetcherError('Page is not a document', 404);
+                    }
+                    return res.data.markdown;
                 });
-            });
-        }
-    )
+            }
+        );
+    }
 );
 
-const getRevisionPages = withCacheKey(
-    withoutConcurrentExecution(
-        async (
-            _,
-            input: DataFetcherInput,
-            params: { spaceId: string; revisionId: string; metadata: boolean }
-        ) => {
-            'use cache';
-            return trace(`getRevisionPages(${params.spaceId}, ${params.revisionId})`, async () => {
+const getRevisionPageDocument = cache(
+    async (
+        input: DataFetcherInput,
+        params: { spaceId: string; revisionId: string; pageId: string }
+    ) => {
+        'use cache';
+        return trace(
+            `getRevisionPageDocument(${params.spaceId}, ${params.revisionId}, ${params.pageId})`,
+            async () => {
                 return wrapDataFetcherError(async () => {
                     const api = apiClient(input);
-                    const res = await api.spaces.listPagesInRevisionById(
+                    const res = await api.spaces.getPageDocumentInRevisionById(
                         params.spaceId,
                         params.revisionId,
+                        params.pageId,
                         {
-                            metadata: params.metadata,
+                            evaluated: true,
                         },
                         {
                             ...noCacheFetchOptions,
                         }
                     );
+
                     cacheTag(...getCacheTagsFromResponse(res));
                     cacheLife('max');
-                    return res.data.pages;
-                });
-            });
-        }
-    )
-);
 
-const getRevisionFile = withCacheKey(
-    withoutConcurrentExecution(
-        async (
-            _,
-            input: DataFetcherInput,
-            params: { spaceId: string; revisionId: string; fileId: string }
-        ) => {
-            'use cache';
-            return trace(
-                `getRevisionFile(${params.spaceId}, ${params.revisionId}, ${params.fileId})`,
-                async () => {
-                    return wrapDataFetcherError(async () => {
-                        const api = apiClient(input);
-                        const res = await api.spaces.getFileInRevisionById(
-                            params.spaceId,
-                            params.revisionId,
-                            params.fileId,
-                            {},
-                            {
-                                ...noCacheFetchOptions,
-                            }
-                        );
-                        cacheTag(...getCacheTagsFromResponse(res));
-                        cacheLife('max');
-                        return res.data;
-                    });
-                }
-            );
-        }
-    )
+                    return res.data;
+                });
+            }
+        );
+    }
 );
 
-const getRevisionPageMarkdown = withCacheKey(
-    withoutConcurrentExecution(
-        async (
-            _,
-            input: DataFetcherInput,
-            params: { spaceId: string; revisionId: string; pageId: string }
-        ) => {
-            'use cache';
-            return trace(
-                `getRevisionPageMarkdown(${params.spaceId}, ${params.revisionId}, ${params.pageId})`,
-                async () => {
-                    return wrapDataFetcherError(async () => {
-                        const api = apiClient(input);
-                        const res = await api.spaces.getPageInRevisionById(
-                            params.spaceId,
-                            params.revisionId,
-                            params.pageId,
-                            {
-                                format: 'markdown',
-                            },
-                            {
-                                ...noCacheFetchOptions,
-                            }
-                        );
-
-                        cacheTag(...getCacheTagsFromResponse(res));
-                        cacheLife('max');
-
-                        if (!('markdown' in res.data)) {
-                            throw new DataFetcherError('Page is not a document', 404);
+const getRevisionPageByPath = cache(
+    async (
+        input: DataFetcherInput,
+        params: { spaceId: string; revisionId: string; path: string }
+    ) => {
+        'use cache';
+        return trace(
+            `getRevisionPageByPath(${params.spaceId}, ${params.revisionId}, ${params.path})`,
+            async () => {
+                const encodedPath = encodeURIComponent(params.path);
+                return wrapDataFetcherError(async () => {
+                    const api = apiClient(input);
+                    const res = await api.spaces.getPageInRevisionByPath(
+                        params.spaceId,
+                        params.revisionId,
+                        encodedPath,
+                        {},
+                        {
+                            ...noCacheFetchOptions,
                         }
-                        return res.data.markdown;
-                    });
-                }
-            );
-        }
-    )
+                    );
+                    cacheTag(...getCacheTagsFromResponse(res));
+                    cacheLife('max');
+                    return res.data;
+                });
+            }
+        );
+    }
 );
 
-const getRevisionPageDocument = withCacheKey(
-    withoutConcurrentExecution(
-        async (
-            _,
-            input: DataFetcherInput,
-            params: { spaceId: string; revisionId: string; pageId: string }
-        ) => {
-            'use cache';
-            return trace(
-                `getRevisionPageDocument(${params.spaceId}, ${params.revisionId}, ${params.pageId})`,
-                async () => {
-                    return wrapDataFetcherError(async () => {
-                        const api = apiClient(input);
-                        const res = await api.spaces.getPageDocumentInRevisionById(
-                            params.spaceId,
-                            params.revisionId,
-                            params.pageId,
-                            {
-                                evaluated: true,
-                            },
-                            {
-                                ...noCacheFetchOptions,
-                            }
-                        );
-
-                        cacheTag(...getCacheTagsFromResponse(res));
-                        cacheLife('max');
-
-                        return res.data;
-                    });
-                }
-            );
-        }
-    )
+const getDocument = cache(
+    async (input: DataFetcherInput, params: { spaceId: string; documentId: string }) => {
+        'use cache';
+        return trace(`getDocument(${params.spaceId}, ${params.documentId})`, async () => {
+            return wrapDataFetcherError(async () => {
+                const api = apiClient(input);
+                const res = await api.spaces.getDocumentById(
+                    params.spaceId,
+                    params.documentId,
+                    {},
+                    {
+                        ...noCacheFetchOptions,
+                    }
+                );
+                cacheTag(...getCacheTagsFromResponse(res));
+                cacheLife('max');
+                return res.data;
+            });
+        });
+    }
 );
 
-const getRevisionPageByPath = withCacheKey(
-    withoutConcurrentExecution(
-        async (
-            _,
-            input: DataFetcherInput,
-            params: { spaceId: string; revisionId: string; path: string }
-        ) => {
-            'use cache';
-            return trace(
-                `getRevisionPageByPath(${params.spaceId}, ${params.revisionId}, ${params.path})`,
-                async () => {
-                    const encodedPath = encodeURIComponent(params.path);
-                    return wrapDataFetcherError(async () => {
-                        const api = apiClient(input);
-                        const res = await api.spaces.getPageInRevisionByPath(
-                            params.spaceId,
-                            params.revisionId,
-                            encodedPath,
-                            {},
-                            {
-                                ...noCacheFetchOptions,
-                            }
-                        );
-                        cacheTag(...getCacheTagsFromResponse(res));
-                        cacheLife('max');
-                        return res.data;
-                    });
-                }
-            );
+const getComputedDocument = cache(
+    async (
+        input: DataFetcherInput,
+        params: {
+            spaceId: string;
+            organizationId: string;
+            source: ComputedContentSource;
+            seed: string;
         }
-    )
-);
-
-const getDocument = withCacheKey(
-    withoutConcurrentExecution(
-        async (_, input: DataFetcherInput, params: { spaceId: string; documentId: string }) => {
-            'use cache';
-            return trace(`getDocument(${params.spaceId}, ${params.documentId})`, async () => {
+    ) => {
+        'use cache';
+        cacheTag(
+            ...getComputedContentSourceCacheTags(
+                {
+                    spaceId: params.spaceId,
+                    organizationId: params.organizationId,
+                },
+                params.source
+            )
+        );
+
+        return trace(
+            `getComputedDocument(${params.spaceId}, ${params.organizationId}, ${params.source.type}, ${params.seed})`,
+            async () => {
                 return wrapDataFetcherError(async () => {
                     const api = apiClient(input);
-                    const res = await api.spaces.getDocumentById(
+                    const res = await api.spaces.getComputedDocument(
                         params.spaceId,
-                        params.documentId,
+                        {
+                            source: params.source,
+                            seed: params.seed,
+                        },
                         {},
                         {
                             ...noCacheFetchOptions,
@@ -514,319 +523,251 @@ const getDocument = withCacheKey(
                     cacheLife('max');
                     return res.data;
                 });
-            });
-        }
-    )
-);
-
-const getComputedDocument = withCacheKey(
-    withoutConcurrentExecution(
-        async (
-            _,
-            input: DataFetcherInput,
-            params: {
-                spaceId: string;
-                organizationId: string;
-                source: ComputedContentSource;
-                seed: string;
             }
-        ) => {
-            'use cache';
-            cacheTag(
-                ...getComputedContentSourceCacheTags(
-                    {
-                        spaceId: params.spaceId,
-                        organizationId: params.organizationId,
-                    },
-                    params.source
-                )
-            );
-
-            return trace(
-                `getComputedDocument(${params.spaceId}, ${params.organizationId}, ${params.source.type}, ${params.seed})`,
-                async () => {
-                    return wrapDataFetcherError(async () => {
-                        const api = apiClient(input);
-                        const res = await api.spaces.getComputedDocument(
-                            params.spaceId,
-                            {
-                                source: params.source,
-                                seed: params.seed,
-                            },
-                            {},
-                            {
-                                ...noCacheFetchOptions,
-                            }
-                        );
-                        cacheTag(...getCacheTagsFromResponse(res));
-                        cacheLife('max');
-                        return res.data;
-                    });
-                }
-            );
-        }
-    )
-);
-
-const getReusableContent = withCacheKey(
-    withoutConcurrentExecution(
-        async (
-            _,
-            input: DataFetcherInput,
-            params: { spaceId: string; revisionId: string; reusableContentId: string }
-        ) => {
-            'use cache';
-            return trace(
-                `getReusableContent(${params.spaceId}, ${params.revisionId}, ${params.reusableContentId})`,
-                async () => {
-                    return wrapDataFetcherError(async () => {
-                        const api = apiClient(input);
-                        const res = await api.spaces.getReusableContentInRevisionById(
-                            params.spaceId,
-                            params.revisionId,
-                            params.reusableContentId,
-                            {},
-                            {
-                                ...noCacheFetchOptions,
-                            }
-                        );
-                        cacheTag(...getCacheTagsFromResponse(res));
-                        cacheLife('max');
-                        return res.data;
-                    });
-                }
-            );
-        }
-    )
-);
-
-const getLatestOpenAPISpecVersionContent = withCacheKey(
-    withoutConcurrentExecution(
-        async (_, input: DataFetcherInput, params: { organizationId: string; slug: string }) => {
-            'use cache';
-            cacheTag(
-                getCacheTag({
-                    tag: 'openapi',
-                    organization: params.organizationId,
-                    openAPISpec: params.slug,
-                })
-            );
-
-            return trace(
-                `getLatestOpenAPISpecVersionContent(${params.organizationId}, ${params.slug})`,
-                async () => {
-                    return wrapDataFetcherError(async () => {
-                        const api = apiClient(input);
-                        const res = await api.orgs.getLatestOpenApiSpecVersionContent(
-                            params.organizationId,
-                            params.slug,
-                            {
-                                ...noCacheFetchOptions,
-                            }
-                        );
-                        cacheTag(...getCacheTagsFromResponse(res));
-                        cacheLife('max');
-                        return res.data;
-                    });
-                }
-            );
-        }
-    )
+        );
+    }
 );
 
-const getPublishedContentSite = withCacheKey(
-    withoutConcurrentExecution(
-        async (
-            _,
-            input: DataFetcherInput,
-            params: { organizationId: string; siteId: string; siteShareKey: string | undefined }
-        ) => {
-            'use cache';
-            cacheTag(
-                getCacheTag({
-                    tag: 'site',
-                    site: params.siteId,
-                })
-            );
-
-            return trace(
-                `getPublishedContentSite(${params.organizationId}, ${params.siteId}, ${params.siteShareKey})`,
-                async () => {
-                    return wrapDataFetcherError(async () => {
-                        const api = apiClient(input);
-                        const res = await api.orgs.getPublishedContentSite(
-                            params.organizationId,
-                            params.siteId,
-                            {
-                                shareKey: params.siteShareKey,
-                            },
-                            {
-                                ...noCacheFetchOptions,
-                            }
-                        );
-                        cacheTag(...getCacheTagsFromResponse(res));
-                        cacheLife('days');
-                        return res.data;
-                    });
-                }
-            );
-        }
-    )
+const getReusableContent = cache(
+    async (
+        input: DataFetcherInput,
+        params: { spaceId: string; revisionId: string; reusableContentId: string }
+    ) => {
+        'use cache';
+        return trace(
+            `getReusableContent(${params.spaceId}, ${params.revisionId}, ${params.reusableContentId})`,
+            async () => {
+                return wrapDataFetcherError(async () => {
+                    const api = apiClient(input);
+                    const res = await api.spaces.getReusableContentInRevisionById(
+                        params.spaceId,
+                        params.revisionId,
+                        params.reusableContentId,
+                        {},
+                        {
+                            ...noCacheFetchOptions,
+                        }
+                    );
+                    cacheTag(...getCacheTagsFromResponse(res));
+                    cacheLife('max');
+                    return res.data;
+                });
+            }
+        );
+    }
 );
 
-const getSiteRedirectBySource = withCacheKey(
-    withoutConcurrentExecution(
-        async (
-            _,
-            input: DataFetcherInput,
-            params: {
-                organizationId: string;
-                siteId: string;
-                siteShareKey: string | undefined;
-                source: string;
+const getLatestOpenAPISpecVersionContent = cache(
+    async (input: DataFetcherInput, params: { organizationId: string; slug: string }) => {
+        'use cache';
+        cacheTag(
+            getCacheTag({
+                tag: 'openapi',
+                organization: params.organizationId,
+                openAPISpec: params.slug,
+            })
+        );
+
+        return trace(
+            `getLatestOpenAPISpecVersionContent(${params.organizationId}, ${params.slug})`,
+            async () => {
+                return wrapDataFetcherError(async () => {
+                    const api = apiClient(input);
+                    const res = await api.orgs.getLatestOpenApiSpecVersionContent(
+                        params.organizationId,
+                        params.slug,
+                        {
+                            ...noCacheFetchOptions,
+                        }
+                    );
+                    cacheTag(...getCacheTagsFromResponse(res));
+                    cacheLife('max');
+                    return res.data;
+                });
             }
-        ) => {
-            'use cache';
-            cacheTag(
-                getCacheTag({
-                    tag: 'site',
-                    site: params.siteId,
-                })
-            );
-
-            return trace(
-                `getSiteRedirectBySource(${params.organizationId}, ${params.siteId}, ${params.siteShareKey}, ${params.source})`,
-                async () => {
-                    return wrapDataFetcherError(async () => {
-                        const api = apiClient(input);
-                        const res = await api.orgs.getSiteRedirectBySource(
-                            params.organizationId,
-                            params.siteId,
-                            {
-                                shareKey: params.siteShareKey,
-                                source: params.source,
-                            },
-                            {
-                                ...noCacheFetchOptions,
-                            }
-                        );
-                        cacheTag(...getCacheTagsFromResponse(res));
-                        cacheLife('days');
-                        return res.data;
-                    });
-                }
-            );
-        }
-    )
+        );
+    }
 );
 
-const getEmbedByUrl = withCacheKey(
-    withoutConcurrentExecution(
-        async (_, input: DataFetcherInput, params: { spaceId: string; url: string }) => {
-            'use cache';
-            cacheTag(
-                getCacheTag({
-                    tag: 'space',
-                    space: params.spaceId,
-                })
-            );
-
-            return trace(`getEmbedByUrl(${params.spaceId}, ${params.url})`, async () => {
+const getPublishedContentSite = cache(
+    async (
+        input: DataFetcherInput,
+        params: { organizationId: string; siteId: string; siteShareKey: string | undefined }
+    ) => {
+        'use cache';
+        cacheTag(
+            getCacheTag({
+                tag: 'site',
+                site: params.siteId,
+            })
+        );
+
+        return trace(
+            `getPublishedContentSite(${params.organizationId}, ${params.siteId}, ${params.siteShareKey})`,
+            async () => {
                 return wrapDataFetcherError(async () => {
                     const api = apiClient(input);
-                    const res = await api.spaces.getEmbedByUrlInSpace(
-                        params.spaceId,
+                    const res = await api.orgs.getPublishedContentSite(
+                        params.organizationId,
+                        params.siteId,
                         {
-                            url: params.url,
+                            shareKey: params.siteShareKey,
                         },
                         {
                             ...noCacheFetchOptions,
                         }
                     );
                     cacheTag(...getCacheTagsFromResponse(res));
-                    cacheLife('weeks');
+                    cacheLife('days');
                     return res.data;
                 });
-            });
-        }
-    )
+            }
+        );
+    }
 );
 
-const searchSiteContent = withCacheKey(
-    withoutConcurrentExecution(
-        async (
-            _,
-            input: DataFetcherInput,
-            params: Parameters<GitBookDataFetcher['searchSiteContent']>[0]
-        ) => {
-            'use cache';
-            cacheTag(
-                getCacheTag({
-                    tag: 'site',
-                    site: params.siteId,
-                })
-            );
-
-            return trace(
-                `searchSiteContent(${params.organizationId}, ${params.siteId}, ${params.query})`,
-                async () => {
-                    return wrapDataFetcherError(async () => {
-                        const { organizationId, siteId, query, scope } = params;
-                        const api = apiClient(input);
-                        const res = await api.orgs.searchSiteContent(
-                            organizationId,
-                            siteId,
-                            {
-                                query,
-                                ...scope,
-                            },
-                            {},
-                            {
-                                ...noCacheFetchOptions,
-                            }
-                        );
-                        cacheTag(...getCacheTagsFromResponse(res));
-                        cacheLife('hours');
-                        return res.data.items;
-                    });
-                }
-            );
+const getSiteRedirectBySource = cache(
+    async (
+        input: DataFetcherInput,
+        params: {
+            organizationId: string;
+            siteId: string;
+            siteShareKey: string | undefined;
+            source: string;
         }
-    )
+    ) => {
+        'use cache';
+        cacheTag(
+            getCacheTag({
+                tag: 'site',
+                site: params.siteId,
+            })
+        );
+
+        return trace(
+            `getSiteRedirectBySource(${params.organizationId}, ${params.siteId}, ${params.siteShareKey}, ${params.source})`,
+            async () => {
+                return wrapDataFetcherError(async () => {
+                    const api = apiClient(input);
+                    const res = await api.orgs.getSiteRedirectBySource(
+                        params.organizationId,
+                        params.siteId,
+                        {
+                            shareKey: params.siteShareKey,
+                            source: params.source,
+                        },
+                        {
+                            ...noCacheFetchOptions,
+                        }
+                    );
+                    cacheTag(...getCacheTagsFromResponse(res));
+                    cacheLife('days');
+                    return res.data;
+                });
+            }
+        );
+    }
 );
 
-const renderIntegrationUi = withCacheKey(
-    withoutConcurrentExecution(
-        async (
-            _,
-            input: DataFetcherInput,
-            params: { integrationName: string; request: RenderIntegrationUI }
-        ) => {
-            'use cache';
-            cacheTag(
-                getCacheTag({
-                    tag: 'integration',
-                    integration: params.integrationName,
-                })
-            );
+const getEmbedByUrl = cache(
+    async (input: DataFetcherInput, params: { spaceId: string; url: string }) => {
+        'use cache';
+        cacheTag(
+            getCacheTag({
+                tag: 'space',
+                space: params.spaceId,
+            })
+        );
+
+        return trace(`getEmbedByUrl(${params.spaceId}, ${params.url})`, async () => {
+            return wrapDataFetcherError(async () => {
+                const api = apiClient(input);
+                const res = await api.spaces.getEmbedByUrlInSpace(
+                    params.spaceId,
+                    {
+                        url: params.url,
+                    },
+                    {
+                        ...noCacheFetchOptions,
+                    }
+                );
+                cacheTag(...getCacheTagsFromResponse(res));
+                cacheLife('weeks');
+                return res.data;
+            });
+        });
+    }
+);
 
-            return trace(`renderIntegrationUi(${params.integrationName})`, async () => {
+const searchSiteContent = cache(
+    async (
+        input: DataFetcherInput,
+        params: Parameters<GitBookDataFetcher['searchSiteContent']>[0]
+    ) => {
+        'use cache';
+        cacheTag(
+            getCacheTag({
+                tag: 'site',
+                site: params.siteId,
+            })
+        );
+
+        return trace(
+            `searchSiteContent(${params.organizationId}, ${params.siteId}, ${params.query})`,
+            async () => {
                 return wrapDataFetcherError(async () => {
+                    const { organizationId, siteId, query, scope } = params;
                     const api = apiClient(input);
-                    const res = await api.integrations.renderIntegrationUiWithPost(
-                        params.integrationName,
-                        params.request,
+                    const res = await api.orgs.searchSiteContent(
+                        organizationId,
+                        siteId,
+                        {
+                            query,
+                            ...scope,
+                        },
+                        {},
                         {
                             ...noCacheFetchOptions,
                         }
                     );
                     cacheTag(...getCacheTagsFromResponse(res));
-                    cacheLife('days');
-                    return res.data;
+                    cacheLife('hours');
+                    return res.data.items;
                 });
+            }
+        );
+    }
+);
+
+const renderIntegrationUi = cache(
+    async (
+        input: DataFetcherInput,
+        params: { integrationName: string; request: RenderIntegrationUI }
+    ) => {
+        'use cache';
+        cacheTag(
+            getCacheTag({
+                tag: 'integration',
+                integration: params.integrationName,
+            })
+        );
+
+        return trace(`renderIntegrationUi(${params.integrationName})`, async () => {
+            return wrapDataFetcherError(async () => {
+                const api = apiClient(input);
+                const res = await api.integrations.renderIntegrationUiWithPost(
+                    params.integrationName,
+                    params.request,
+                    {
+                        ...noCacheFetchOptions,
+                    }
+                );
+                cacheTag(...getCacheTagsFromResponse(res));
+                cacheLife('days');
+                return res.data;
             });
-        }
-    )
+        });
+    }
 );
 
 async function* streamAIResponse(
diff --git a/packages/gitbook-v2/src/lib/data/memoize.ts b/packages/gitbook-v2/src/lib/data/memoize.ts
deleted file mode 100644
index 5110d015fa..0000000000
--- a/packages/gitbook-v2/src/lib/data/memoize.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import { cache } from 'react';
-
-// This is used to create a context specific to the current request.
-// This version works both in cloudflare and in vercel.
-const getRequestContext = cache(() => ({}));
-
-/**
- * Wrap a function by preventing concurrent executions of the same function.
- * With a logic to work per-request in Cloudflare Workers.
- */
-export function withoutConcurrentExecution<ArgsType extends any[], ReturnType>(
-    wrapped: (key: string, ...args: ArgsType) => Promise<ReturnType>
-): (cacheKey: string, ...args: ArgsType) => Promise<ReturnType> {
-    const globalPromiseCache = new WeakMap<object, Map<string, Promise<ReturnType>>>();
-
-    return (key: string, ...args: ArgsType) => {
-        const globalContext = getRequestContext();
-
-        /**
-         * Cache storage that is scoped to the current request when executed in Cloudflare Workers,
-         * to avoid "Cannot perform I/O on behalf of a different request" errors.
-         */
-        const promiseCache =
-            globalPromiseCache.get(globalContext) ?? new Map<string, Promise<ReturnType>>();
-        globalPromiseCache.set(globalContext, promiseCache);
-
-        const concurrent = promiseCache.get(key);
-        if (concurrent) {
-            return concurrent;
-        }
-
-        const promise = (async () => {
-            try {
-                const result = await wrapped(key, ...args);
-                return result;
-            } finally {
-                promiseCache.delete(key);
-            }
-        })();
-
-        promiseCache.set(key, promise);
-
-        return promise;
-    };
-}
-
-/**
- * Wrap a function by passing it a cache key that is computed from the function arguments.
- */
-export function withCacheKey<ArgsType extends any[], ReturnType>(
-    wrapped: (cacheKey: string, ...args: ArgsType) => Promise<ReturnType>
-): (...args: ArgsType) => Promise<ReturnType> {
-    return (...args: ArgsType) => {
-        const cacheKey = getCacheKey(args);
-        return wrapped(cacheKey, ...args);
-    };
-}
-
-/**
- * Compute a cache key from the function arguments.
- */
-function getCacheKey(args: any[]) {
-    return JSON.stringify(deepSortValue(args));
-}
-
-function deepSortValue(value: unknown): unknown {
-    if (
-        typeof value === 'string' ||
-        typeof value === 'number' ||
-        typeof value === 'boolean' ||
-        value === null ||
-        value === undefined
-    ) {
-        return value;
-    }
-
-    if (Array.isArray(value)) {
-        return value.map(deepSortValue);
-    }
-
-    if (value && typeof value === 'object') {
-        return Object.entries(value)
-            .map(([key, subValue]) => {
-                return [key, deepSortValue(subValue)] as const;
-            })
-            .sort((a, b) => {
-                return a[0].localeCompare(b[0]);
-            });
-    }
-
-    return value;
-}

From d99da6a3ae7f128116bbec9ade8dcbe01f94eb31 Mon Sep 17 00:00:00 2001
From: Utku Ufuk <utkuufuk@gmail.com>
Date: Mon, 16 Jun 2025 14:33:00 +0200
Subject: [PATCH 110/127] Ignore case while pattern-matching to highlight
 search results (#3326)

---
 .changeset/slimy-cows-press.md                               | 5 +++++
 .../src/components/Search/SearchSectionResultItem.tsx        | 4 ++--
 2 files changed, 7 insertions(+), 2 deletions(-)
 create mode 100644 .changeset/slimy-cows-press.md

diff --git a/.changeset/slimy-cows-press.md b/.changeset/slimy-cows-press.md
new file mode 100644
index 0000000000..17ec44ca8a
--- /dev/null
+++ b/.changeset/slimy-cows-press.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Ignore case while highlighting search results.
diff --git a/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx b/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx
index a0f467f80f..8ccda684d2 100644
--- a/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx
+++ b/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx
@@ -88,12 +88,12 @@ export const SearchSectionResultItem = React.forwardRef(function SearchSectionRe
 });
 
 function highlightQueryInBody(body: string, query: string) {
-    const idx = body.indexOf(query);
+    const idx = body.toLocaleLowerCase().indexOf(query.toLocaleLowerCase());
 
     // Ensure the query to be highlighted is visible in the body.
     return (
         <p className={tcls('text-sm', 'line-clamp-3', 'relative')}>
-            <HighlightQuery query={query} text={idx < 20 ? body : `...${body.slice(idx - 15)}`} />
+            <HighlightQuery query={query} text={idx < 20 ? body : `...${body.slice(idx - 10)}`} />
         </p>
     );
 }

From 382a19885be802945ee309e71d4699ccd720c94f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Greg=20Berg=C3=A9?= <berge.greg@gmail.com>
Date: Mon, 16 Jun 2025 15:52:42 +0200
Subject: [PATCH 111/127] Hide empty section groups (#3327)

---
 .../SiteSections/encodeClientSiteSections.ts  | 35 +++++++++++++------
 1 file changed, 24 insertions(+), 11 deletions(-)

diff --git a/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts b/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts
index 891936b6fc..98c22bbb66 100644
--- a/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts
+++ b/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts
@@ -1,6 +1,7 @@
 import { getSectionURL, getSiteSpaceURL } from '@/lib/sites';
 import type { SiteSection, SiteSectionGroup, SiteSpace } from '@gitbook/api';
 import type { GitBookSiteContext, SiteSections } from '@v2/lib/context';
+import assertNever from 'assert-never';
 
 export type ClientSiteSections = {
     list: (ClientSiteSection | ClientSiteSectionGroup)[];
@@ -27,20 +28,32 @@ export function encodeClientSiteSections(context: GitBookSiteContext, sections:
     const clientSections: (ClientSiteSection | ClientSiteSectionGroup)[] = [];
 
     for (const item of list) {
-        if (item.object === 'site-section-group') {
-            clientSections.push({
-                id: item.id,
-                title: item.title,
-                icon: item.icon,
-                object: item.object,
-                sections: item.sections
+        switch (item.object) {
+            case 'site-section-group': {
+                const sections = item.sections
                     .filter((section) => shouldIncludeSection(context, section))
-                    .map((section) => encodeSection(context, section)),
-            });
-        } else {
-            if (shouldIncludeSection(context, item)) {
+                    .map((section) => encodeSection(context, section));
+
+                // Skip empty groups
+                if (sections.length === 0) {
+                    continue;
+                }
+
+                clientSections.push({
+                    id: item.id,
+                    title: item.title,
+                    icon: item.icon,
+                    object: item.object,
+                    sections,
+                });
+                continue;
+            }
+            case 'site-section': {
                 clientSections.push(encodeSection(context, item));
+                continue;
             }
+            default:
+                assertNever(item, 'Unknown site section object type');
         }
     }
 

From b7a0db3339dcdfb882a5e46e90a989bb1eb088a0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Mon, 16 Jun 2025 16:59:54 +0200
Subject: [PATCH 112/127] Fix rendering of ogimage with SVG logos (#3328)

---
 .changeset/green-clouds-cough.md        |  5 ++
 bun.lock                                |  3 +
 packages/gitbook/package.json           |  3 +-
 packages/gitbook/src/routes/ogimage.tsx | 85 +++++++++++--------------
 4 files changed, 48 insertions(+), 48 deletions(-)
 create mode 100644 .changeset/green-clouds-cough.md

diff --git a/.changeset/green-clouds-cough.md b/.changeset/green-clouds-cough.md
new file mode 100644
index 0000000000..0988603814
--- /dev/null
+++ b/.changeset/green-clouds-cough.md
@@ -0,0 +1,5 @@
+---
+"gitbook": minor
+---
+
+Fix rendering of ogimage with SVG logos.
diff --git a/bun.lock b/bun.lock
index b4d2b49d1f..1c192ca717 100644
--- a/bun.lock
+++ b/bun.lock
@@ -87,6 +87,7 @@
         "classnames": "^2.5.1",
         "event-iterator": "^2.0.0",
         "framer-motion": "^10.16.14",
+        "image-size": "^2.0.2",
         "js-cookie": "^3.0.5",
         "jsontoxml": "^1.0.1",
         "jwt-decode": "^4.0.0",
@@ -2067,6 +2068,8 @@
 
     "ignore": ["ignore@7.0.3", "", {}, "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA=="],
 
+    "image-size": ["image-size@2.0.2", "", { "bin": { "image-size": "bin/image-size.js" } }, "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w=="],
+
     "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
 
     "import-lazy": ["import-lazy@2.1.0", "", {}, "sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A=="],
diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json
index 379f975a1e..2be76259d9 100644
--- a/packages/gitbook/package.json
+++ b/packages/gitbook/package.json
@@ -81,7 +81,8 @@
         "usehooks-ts": "^3.1.0",
         "zod": "^3.24.2",
         "zod-to-json-schema": "^3.24.5",
-        "zustand": "^5.0.3"
+        "zustand": "^5.0.3",
+        "image-size": "^2.0.2"
     },
     "devDependencies": {
         "@argos-ci/playwright": "^5.0.3",
diff --git a/packages/gitbook/src/routes/ogimage.tsx b/packages/gitbook/src/routes/ogimage.tsx
index e213ee2238..653fc8fc3b 100644
--- a/packages/gitbook/src/routes/ogimage.tsx
+++ b/packages/gitbook/src/routes/ogimage.tsx
@@ -1,6 +1,7 @@
 import { CustomizationDefaultFont, CustomizationHeaderPreset } from '@gitbook/api';
 import { colorContrast } from '@gitbook/colors';
 import { type FontWeight, getDefaultFont } from '@gitbook/fonts';
+import { imageSize } from 'image-size';
 import { redirect } from 'next/navigation';
 import { ImageResponse } from 'next/og';
 
@@ -156,14 +157,14 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
                     {String.fromCodePoint(Number.parseInt(`0x${customization.favicon.emoji}`))}
                 </span>
             );
-        const src = await readSelfImage(
+        const iconImage = await fetchImage(
             linker.toAbsoluteURL(
                 linker.toPathInSpace(
                     `~gitbook/icon?size=medium&theme=${customization.themes.default}`
                 )
             )
         );
-        return <img src={src} alt="Icon" width={40} height={40} tw="mr-4" />;
+        return <img {...iconImage} alt="Icon" width={40} height={40} tw="mr-4" />;
     };
 
     const [favicon, { fontFamily, fonts }] = await Promise.all([faviconLoader(), fontLoader()]);
@@ -187,21 +188,23 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
             {/* Grid */}
             <img
                 tw="absolute inset-0 w-[100vw] h-[100vh]"
-                src={await readStaticImage(gridAsset)}
+                src={(await fetchStaticImage(gridAsset)).src}
                 alt="Grid"
             />
 
             {/* Logo */}
             {customization.header.logo ? (
-                <img
-                    alt="Logo"
-                    height={60}
-                    src={
-                        useLightTheme
-                            ? customization.header.logo.light
-                            : customization.header.logo.dark
-                    }
-                />
+                <div tw="flex flex-row">
+                    <img
+                        {...(await fetchImage(
+                            useLightTheme
+                                ? customization.header.logo.light
+                                : customization.header.logo.dark
+                        ))}
+                        alt="Logo"
+                        tw="h-[60px]"
+                    />
+                </div>
             ) : (
                 <div tw="flex">
                     {favicon}
@@ -289,34 +292,6 @@ async function loadCustomFont(input: { url: string; weight: 400 | 700 }) {
     };
 }
 
-/**
- * Temporary function to log some data on Cloudflare.
- * TODO: remove this when we found the issue
- */
-function logOnCloudflareOnly(message: string) {
-    if (process.env.DEBUG_CLOUDFLARE === 'true') {
-        // biome-ignore lint/suspicious/noConsole: <explanation>
-        console.log(message);
-    }
-}
-
-/**
- * Read an image from a response as a base64 encoded string.
- */
-async function readImage(response: Response) {
-    const contentType = response.headers.get('content-type');
-    if (!contentType || !contentType.startsWith('image/')) {
-        logOnCloudflareOnly(`Invalid content type: ${contentType}, 
-            status: ${response.status}
-            rayId: ${response.headers.get('cf-ray')}`);
-        throw new Error(`Invalid content type: ${contentType}`);
-    }
-
-    const arrayBuffer = await response.arrayBuffer();
-    const base64 = Buffer.from(arrayBuffer).toString('base64');
-    return `data:${contentType};base64,${base64}`;
-}
-
 // biome-ignore lint/suspicious/noExplicitAny: <explanation>
 const staticCache = new Map<string, any>();
 
@@ -335,16 +310,32 @@ async function getWithCache<T>(key: string, fn: () => Promise<T>) {
 /**
  * Read a static image and cache it in memory.
  */
-async function readStaticImage(url: string) {
-    logOnCloudflareOnly(`Reading static image: ${url}, cache size: ${staticCache.size}`);
-    return getWithCache(`static-image:${url}`, () => readSelfImage(url));
+async function fetchStaticImage(url: string) {
+    return getWithCache(`static-image:${url}`, () => fetchImage(url));
 }
 
 /**
- * Read an image from GitBook itself.
+ * Fetch an image from a URL and return a base64 encoded string.
+ * We do this as @vercel/og is otherwise failing on SVG images referenced by a URL.
  */
-async function readSelfImage(url: string) {
+async function fetchImage(url: string) {
     const response = await fetch(url);
-    const image = await readImage(response);
-    return image;
+
+    const contentType = response.headers.get('content-type');
+    if (!contentType || !contentType.startsWith('image/')) {
+        throw new Error(`Invalid content type: ${contentType}`);
+    }
+
+    const arrayBuffer = await response.arrayBuffer();
+    const buffer = Buffer.from(arrayBuffer);
+    const base64 = buffer.toString('base64');
+    const src = `data:${contentType};base64,${base64}`;
+
+    try {
+        const { width, height } = imageSize(buffer);
+        return { src, width, height };
+    } catch (error) {
+        console.error(`Error reading image size: ${error}`);
+        return { src };
+    }
 }

From af98402655945a47473828cce1b8b33e4b858683 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Mon, 16 Jun 2025 19:15:22 +0200
Subject: [PATCH 113/127] Add support for inline icons (#3329)

---
 .changeset/curly-rules-learn.md               |  5 +++
 bun.lock                                      |  4 +--
 package.json                                  |  2 +-
 packages/gitbook/e2e/internal.spec.ts         |  5 +++
 .../DocumentView/CodeBlock/highlight.ts       | 33 +++++++++++++------
 .../src/components/DocumentView/Inline.tsx    |  3 ++
 .../components/DocumentView/InlineIcon.tsx    | 10 ++++++
 7 files changed, 49 insertions(+), 13 deletions(-)
 create mode 100644 .changeset/curly-rules-learn.md
 create mode 100644 packages/gitbook/src/components/DocumentView/InlineIcon.tsx

diff --git a/.changeset/curly-rules-learn.md b/.changeset/curly-rules-learn.md
new file mode 100644
index 0000000000..47400e9194
--- /dev/null
+++ b/.changeset/curly-rules-learn.md
@@ -0,0 +1,5 @@
+---
+"gitbook": minor
+---
+
+Add support for inline icons.
diff --git a/bun.lock b/bun.lock
index 1c192ca717..94b4fc3a54 100644
--- a/bun.lock
+++ b/bun.lock
@@ -285,7 +285,7 @@
     "react-dom": "^19.0.0",
   },
   "catalog": {
-    "@gitbook/api": "^0.120.0",
+    "@gitbook/api": "^0.121.0",
   },
   "packages": {
     "@ai-sdk/provider": ["@ai-sdk/provider@1.1.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew=="],
@@ -650,7 +650,7 @@
 
     "@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="],
 
-    "@gitbook/api": ["@gitbook/api@0.120.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-FiRmPiSBwobMxmNjd14QkkOdM95BAPLDDRShgpS9Vsd8lHjNMyZfrJKVJTsJUuFcgYoi4cqNw9yu/TiUBUgv3g=="],
+    "@gitbook/api": ["@gitbook/api@0.121.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-o4/N24RM0Rg8S/3yPDjPmt6TbQF+1iZmg9q9QKxOxMqpQ2bZmMUqS7dSkeqEbEBMALx/m/x0xQlJbEJGbOwteg=="],
 
     "@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"],
 
diff --git a/package.json b/package.json
index 1faa750ba8..1cf5326c1d 100644
--- a/package.json
+++ b/package.json
@@ -36,7 +36,7 @@
     "workspaces": {
         "packages": ["packages/*"],
         "catalog": {
-            "@gitbook/api": "^0.120.0"
+            "@gitbook/api": "^0.121.0"
         }
     },
     "patchedDependencies": {
diff --git a/packages/gitbook/e2e/internal.spec.ts b/packages/gitbook/e2e/internal.spec.ts
index 3efe00bd27..3a884ae5ea 100644
--- a/packages/gitbook/e2e/internal.spec.ts
+++ b/packages/gitbook/e2e/internal.spec.ts
@@ -603,6 +603,11 @@ const testCases: TestsCase[] = [
                 url: 'blocks/emojis',
                 run: waitForCookiesDialog,
             },
+            {
+                name: 'Icons',
+                url: 'blocks/icons',
+                run: waitForCookiesDialog,
+            },
             {
                 name: 'Links',
                 url: 'blocks/links',
diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts b/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts
index eb02b5b928..89d00dafba 100644
--- a/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts
+++ b/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts
@@ -12,6 +12,7 @@ import {
 import { createJavaScriptRegexEngine } from 'shiki/engine/javascript';
 import { type BundledLanguage, bundledLanguages } from 'shiki/langs';
 
+import { nullIfNever } from '@/lib/typescript';
 import { plainHighlight } from './plain-highlight';
 
 export type HighlightLine = {
@@ -263,16 +264,28 @@ function getPlainCodeBlockLine(
         if (node.object === 'text') {
             content += cleanupLine(node.leaves.map((leaf) => leaf.text).join(''));
         } else {
-            const start = index + content.length;
-            content += getPlainCodeBlockLine(node, index + content.length, inlines);
-            const end = index + content.length;
-
-            if (inlines) {
-                inlines.push({
-                    inline: node,
-                    start,
-                    end,
-                });
+            switch (node.type) {
+                case 'annotation': {
+                    const start = index + content.length;
+                    content += getPlainCodeBlockLine(node, index + content.length, inlines);
+                    const end = index + content.length;
+
+                    if (inlines) {
+                        inlines.push({
+                            inline: node,
+                            start,
+                            end,
+                        });
+                    }
+                    break;
+                }
+                case 'expression': {
+                    break;
+                }
+                default: {
+                    nullIfNever(node);
+                    break;
+                }
             }
         }
     }
diff --git a/packages/gitbook/src/components/DocumentView/Inline.tsx b/packages/gitbook/src/components/DocumentView/Inline.tsx
index 1e1b091cef..84b56decfc 100644
--- a/packages/gitbook/src/components/DocumentView/Inline.tsx
+++ b/packages/gitbook/src/components/DocumentView/Inline.tsx
@@ -5,6 +5,7 @@ import { Annotation } from './Annotation/Annotation';
 import type { DocumentContextProps } from './DocumentView';
 import { Emoji } from './Emoji';
 import { InlineButton } from './InlineButton';
+import { InlineIcon } from './InlineIcon';
 import { InlineImage } from './InlineImage';
 import { InlineLink } from './InlineLink';
 import { InlineMath } from './Math';
@@ -47,6 +48,8 @@ export function Inline<T extends DocumentInline>(props: InlineProps<T>) {
             return <InlineImage {...contextProps} inline={inline} />;
         case 'button':
             return <InlineButton {...contextProps} inline={inline} />;
+        case 'icon':
+            return <InlineIcon {...contextProps} inline={inline} />;
         case 'expression':
             // The GitBook API should take care of evaluating expressions.
             // We should never need to render them.
diff --git a/packages/gitbook/src/components/DocumentView/InlineIcon.tsx b/packages/gitbook/src/components/DocumentView/InlineIcon.tsx
new file mode 100644
index 0000000000..0eca373f69
--- /dev/null
+++ b/packages/gitbook/src/components/DocumentView/InlineIcon.tsx
@@ -0,0 +1,10 @@
+import type { DocumentInlineIcon } from '@gitbook/api';
+
+import { Icon, type IconName } from '@gitbook/icons';
+import type { InlineProps } from './Inline';
+
+export async function InlineIcon(props: InlineProps<DocumentInlineIcon>) {
+    const { inline } = props;
+
+    return <Icon icon={inline.data.icon as IconName} className="inline size-[1em]" />;
+}

From 11a6511b7aa7d1052c887417d97d4eede6104daa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Mon, 16 Jun 2025 20:06:33 +0200
Subject: [PATCH 114/127] Fix crash when integration script fails to render
 block (#3332)

---
 .changeset/famous-melons-compete.md           |  5 +++
 .changeset/orange-ears-drop.md                |  5 +++
 packages/gitbook-v2/src/lib/data/errors.ts    | 34 ++++++++++++---
 .../Integration/IntegrationBlock.tsx          | 34 +++++++++------
 .../DocumentView/Integration/render.ts        | 31 ++++++++++++++
 .../Integration/server-actions.tsx            | 21 ++++++----
 packages/react-contentkit/src/ContentKit.tsx  | 41 ++++++++++++-------
 7 files changed, 129 insertions(+), 42 deletions(-)
 create mode 100644 .changeset/famous-melons-compete.md
 create mode 100644 .changeset/orange-ears-drop.md
 create mode 100644 packages/gitbook/src/components/DocumentView/Integration/render.ts

diff --git a/.changeset/famous-melons-compete.md b/.changeset/famous-melons-compete.md
new file mode 100644
index 0000000000..17512b6f49
--- /dev/null
+++ b/.changeset/famous-melons-compete.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Fix crash when integration script fails to render block.
diff --git a/.changeset/orange-ears-drop.md b/.changeset/orange-ears-drop.md
new file mode 100644
index 0000000000..12dd7fae5d
--- /dev/null
+++ b/.changeset/orange-ears-drop.md
@@ -0,0 +1,5 @@
+---
+"@gitbook/react-contentkit": patch
+---
+
+Add basic error handling when transitioning between states.
diff --git a/packages/gitbook-v2/src/lib/data/errors.ts b/packages/gitbook-v2/src/lib/data/errors.ts
index 784eebdbb5..4059fb5c94 100644
--- a/packages/gitbook-v2/src/lib/data/errors.ts
+++ b/packages/gitbook-v2/src/lib/data/errors.ts
@@ -47,11 +47,7 @@ export function getDataOrNull<T>(
         return response.then((result) => getDataOrNull(result, ignoreErrors));
     }
 
-    if (response.error) {
-        if (ignoreErrors.includes(response.error.code)) return null;
-        throw new DataFetcherError(response.error.message, response.error.code);
-    }
-    return response.data;
+    return ignoreDataFetcherErrors(response, ignoreErrors).data ?? null;
 }
 
 /**
@@ -93,6 +89,34 @@ export async function wrapDataFetcherError<T>(
     }
 }
 
+/**
+ * Ignore some data fetcher errors.
+ */
+export function ignoreDataFetcherErrors<T>(
+    response: DataFetcherResponse<T>,
+    ignoreErrors?: number[]
+): DataFetcherResponse<T>;
+export function ignoreDataFetcherErrors<T>(
+    response: Promise<DataFetcherResponse<T>>,
+    ignoreErrors?: number[]
+): Promise<DataFetcherResponse<T>>;
+export function ignoreDataFetcherErrors<T>(
+    response: DataFetcherResponse<T> | Promise<DataFetcherResponse<T>>,
+    ignoreErrors: number[] = [404]
+): DataFetcherResponse<T> | Promise<DataFetcherResponse<T>> {
+    if (response instanceof Promise) {
+        return response.then((result) => ignoreDataFetcherErrors(result, ignoreErrors));
+    }
+
+    if (response.error) {
+        if (ignoreErrors.includes(response.error.code)) {
+            return response;
+        }
+        throw new DataFetcherError(response.error.message, response.error.code);
+    }
+    return response;
+}
+
 /**
  * Get a data fetcher exposable error from a JS error.
  */
diff --git a/packages/gitbook/src/components/DocumentView/Integration/IntegrationBlock.tsx b/packages/gitbook/src/components/DocumentView/Integration/IntegrationBlock.tsx
index 6ebec6f030..a26aa3bfd2 100644
--- a/packages/gitbook/src/components/DocumentView/Integration/IntegrationBlock.tsx
+++ b/packages/gitbook/src/components/DocumentView/Integration/IntegrationBlock.tsx
@@ -5,8 +5,8 @@ import { GITBOOK_INTEGRATIONS_HOST } from '@v2/lib/env';
 
 import type { BlockProps } from '../Block';
 import './contentkit.css';
-import { getDataOrNull } from '@v2/lib/data';
 import { contentKitServerContext } from './contentkit';
+import { fetchSafeIntegrationUI } from './render';
 import { renderIntegrationUi } from './server-actions';
 
 export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegration>) {
@@ -16,8 +16,6 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio
         throw new Error('integration block requires a content.spaceId');
     }
 
-    const { dataFetcher } = context.contentContext;
-
     const initialInput: RenderIntegrationUI = {
         componentId: block.data.block,
         props: block.data.props,
@@ -30,17 +28,27 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio
         },
     };
 
-    const initialOutput = await getDataOrNull(
-        dataFetcher.renderIntegrationUi({
-            integrationName: block.data.integration,
-            request: initialInput,
-        }),
+    const initialResponse = await fetchSafeIntegrationUI(context.contentContext, {
+        integrationName: block.data.integration,
+        request: initialInput,
+    });
 
-        // The API can respond with a 400 error if the integration is not installed
-        // and 404 if the integration is not found.
-        [404, 400]
-    );
-    if (!initialOutput || initialOutput.type === 'complete') {
+    if (initialResponse.error) {
+        if (initialResponse.error.code === 404) {
+            return null;
+        }
+
+        return (
+            <div className={tcls(style)}>
+                <pre>
+                    Unexpected error with integration {block.data.integration}:{' '}
+                    {initialResponse.error.message}
+                </pre>
+            </div>
+        );
+    }
+    const initialOutput = initialResponse.data;
+    if (initialOutput.type === 'complete') {
         return null;
     }
 
diff --git a/packages/gitbook/src/components/DocumentView/Integration/render.ts b/packages/gitbook/src/components/DocumentView/Integration/render.ts
new file mode 100644
index 0000000000..a9a86c9e59
--- /dev/null
+++ b/packages/gitbook/src/components/DocumentView/Integration/render.ts
@@ -0,0 +1,31 @@
+import type { RenderIntegrationUI } from '@gitbook/api';
+import type { GitBookBaseContext } from '@v2/lib/context';
+import { ignoreDataFetcherErrors } from '@v2/lib/data';
+
+/**
+ * Render an integration UI while ignoring some errors.
+ */
+export async function fetchSafeIntegrationUI(
+    context: GitBookBaseContext,
+    {
+        integrationName,
+        request,
+    }: {
+        integrationName: string;
+        request: RenderIntegrationUI;
+    }
+) {
+    const output = await ignoreDataFetcherErrors(
+        context.dataFetcher.renderIntegrationUi({
+            integrationName,
+            request,
+        }),
+
+        // The API can respond with a 400 error if the integration is not installed
+        // and 404 if the integration is not found.
+        // The API can also respond with a 502 error if the integration is not generating a proper response.
+        [404, 400, 502]
+    );
+
+    return output;
+}
diff --git a/packages/gitbook/src/components/DocumentView/Integration/server-actions.tsx b/packages/gitbook/src/components/DocumentView/Integration/server-actions.tsx
index 54180e5c89..91d1e0e73c 100644
--- a/packages/gitbook/src/components/DocumentView/Integration/server-actions.tsx
+++ b/packages/gitbook/src/components/DocumentView/Integration/server-actions.tsx
@@ -4,9 +4,9 @@ import { getV1BaseContext } from '@/lib/v1';
 import { isV2 } from '@/lib/v2';
 import type { RenderIntegrationUI } from '@gitbook/api';
 import { ContentKitOutput } from '@gitbook/react-contentkit';
-import { throwIfDataError } from '@v2/lib/data';
 import { getServerActionBaseContext } from '@v2/lib/server-actions';
 import { contentKitServerContext } from './contentkit';
+import { fetchSafeIntegrationUI } from './render';
 
 /**
  * Server action to render an integration UI request from <ContentKit />.
@@ -22,16 +22,19 @@ export async function renderIntegrationUi({
     request: RenderIntegrationUI;
 }) {
     const serverAction = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
+    const output = await fetchSafeIntegrationUI(serverAction, {
+        integrationName: renderContext.integrationName,
+        request,
+    });
 
-    const output = await throwIfDataError(
-        serverAction.dataFetcher.renderIntegrationUi({
-            integrationName: renderContext.integrationName,
-            request,
-        })
-    );
+    if (output.error) {
+        return {
+            error: output.error.message,
+        };
+    }
 
     return {
-        children: <ContentKitOutput output={output} context={contentKitServerContext} />,
-        output: output,
+        children: <ContentKitOutput output={output.data} context={contentKitServerContext} />,
+        output: output.data,
     };
 }
diff --git a/packages/react-contentkit/src/ContentKit.tsx b/packages/react-contentkit/src/ContentKit.tsx
index 9d53c70de2..4f413f367a 100644
--- a/packages/react-contentkit/src/ContentKit.tsx
+++ b/packages/react-contentkit/src/ContentKit.tsx
@@ -38,10 +38,18 @@ export function ContentKit<RenderContext>(props: {
     render: (input: {
         renderContext: RenderContext;
         request: RequestRenderIntegrationUI;
-    }) => Promise<{
-        children: React.ReactNode;
-        output: ContentKitRenderOutput;
-    }>;
+    }) => Promise<
+        | {
+              error?: undefined;
+              children: React.ReactNode;
+              output: ContentKitRenderOutput;
+          }
+        | {
+              error: string;
+              children?: undefined;
+              output?: undefined;
+          }
+    >;
     /** Callback when an action is triggered */
     onAction?: (action: ContentKitAction) => void;
     /** Callback when the flow is completed */
@@ -98,17 +106,18 @@ export function ContentKit<RenderContext>(props: {
                 request: newInput,
             });
             const output = result.output;
+            if (output) {
+                if (output.type === 'complete') {
+                    return onComplete?.(output.returnValue);
+                }
 
-            if (output.type === 'complete') {
-                return onComplete?.(output.returnValue);
+                setCurrent((prev) => ({
+                    input: newInput,
+                    children: result.children,
+                    output: output,
+                    state: prev.state,
+                }));
             }
-
-            setCurrent((prev) => ({
-                input: newInput,
-                children: result.children,
-                output: output,
-                state: prev.state,
-            }));
         },
         [setCurrent, current, render, onComplete]
     );
@@ -147,8 +156,10 @@ export function ContentKit<RenderContext>(props: {
                             renderContext,
                             request: modalInput,
                         });
-
-                        if (result.output.type === 'element' || !result.output.type) {
+                        if (
+                            result.output &&
+                            (result.output.type === 'element' || !result.output.type)
+                        ) {
                             setSubView({
                                 mode: 'modal',
                                 initialInput: modalInput,

From dfa8a37be19c1868bb1c833631f199b4f52b3305 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Tue, 17 Jun 2025 00:16:52 +0200
Subject: [PATCH 115/127] Don't cache unexpected API errors for more than a few
 minutes (#3334)

---
 .changeset/real-walls-glow.md              |   5 +
 packages/gitbook-v2/src/lib/data/api.ts    | 203 +++++++++++----------
 packages/gitbook-v2/src/lib/data/errors.ts |  17 ++
 3 files changed, 125 insertions(+), 100 deletions(-)
 create mode 100644 .changeset/real-walls-glow.md

diff --git a/.changeset/real-walls-glow.md b/.changeset/real-walls-glow.md
new file mode 100644
index 0000000000..277c39ca9f
--- /dev/null
+++ b/.changeset/real-walls-glow.md
@@ -0,0 +1,5 @@
+---
+"gitbook-v2": patch
+---
+
+Don't cache unexpected API errors for more than a few minutes.
diff --git a/packages/gitbook-v2/src/lib/data/api.ts b/packages/gitbook-v2/src/lib/data/api.ts
index d36c2445f5..8e6535331a 100644
--- a/packages/gitbook-v2/src/lib/data/api.ts
+++ b/packages/gitbook-v2/src/lib/data/api.ts
@@ -9,7 +9,7 @@ import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-t
 import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env';
 import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache';
 import { cache } from '../cache';
-import { DataFetcherError, wrapDataFetcherError } from './errors';
+import { DataFetcherError, wrapCacheDataFetcherError } from './errors';
 import type { GitBookDataFetcher } from './types';
 
 interface DataFetcherInput {
@@ -206,8 +206,8 @@ export function createDataFetcher(
 
 const getUserById = cache(async (input: DataFetcherInput, params: { userId: string }) => {
     'use cache';
-    return trace(`getUserById(${params.userId})`, async () => {
-        return wrapDataFetcherError(async () => {
+    return wrapCacheDataFetcherError(async () => {
+        return trace(`getUserById(${params.userId})`, async () => {
             const api = apiClient(input);
             const res = await api.users.getUserById(params.userId, {
                 ...noCacheFetchOptions,
@@ -229,8 +229,8 @@ const getSpace = cache(
             })
         );
 
-        return trace(`getSpace(${params.spaceId}, ${params.shareKey})`, async () => {
-            return wrapDataFetcherError(async () => {
+        return wrapCacheDataFetcherError(async () => {
+            return trace(`getSpace(${params.spaceId}, ${params.shareKey})`, async () => {
                 const api = apiClient(input);
                 const res = await api.spaces.getSpaceById(
                     params.spaceId,
@@ -260,20 +260,23 @@ const getChangeRequest = cache(
             })
         );
 
-        return trace(`getChangeRequest(${params.spaceId}, ${params.changeRequestId})`, async () => {
-            return wrapDataFetcherError(async () => {
-                const api = apiClient(input);
-                const res = await api.spaces.getChangeRequestById(
-                    params.spaceId,
-                    params.changeRequestId,
-                    {
-                        ...noCacheFetchOptions,
-                    }
-                );
-                cacheTag(...getCacheTagsFromResponse(res));
-                cacheLife('minutes');
-                return res.data;
-            });
+        return wrapCacheDataFetcherError(async () => {
+            return trace(
+                `getChangeRequest(${params.spaceId}, ${params.changeRequestId})`,
+                async () => {
+                    const api = apiClient(input);
+                    const res = await api.spaces.getChangeRequestById(
+                        params.spaceId,
+                        params.changeRequestId,
+                        {
+                            ...noCacheFetchOptions,
+                        }
+                    );
+                    cacheTag(...getCacheTagsFromResponse(res));
+                    cacheLife('minutes');
+                    return res.data;
+                }
+            );
         });
     }
 );
@@ -284,8 +287,8 @@ const getRevision = cache(
         params: { spaceId: string; revisionId: string; metadata: boolean }
     ) => {
         'use cache';
-        return trace(`getRevision(${params.spaceId}, ${params.revisionId})`, async () => {
-            return wrapDataFetcherError(async () => {
+        return wrapCacheDataFetcherError(async () => {
+            return trace(`getRevision(${params.spaceId}, ${params.revisionId})`, async () => {
                 const api = apiClient(input);
                 const res = await api.spaces.getRevisionById(
                     params.spaceId,
@@ -311,8 +314,8 @@ const getRevisionPages = cache(
         params: { spaceId: string; revisionId: string; metadata: boolean }
     ) => {
         'use cache';
-        return trace(`getRevisionPages(${params.spaceId}, ${params.revisionId})`, async () => {
-            return wrapDataFetcherError(async () => {
+        return wrapCacheDataFetcherError(async () => {
+            return trace(`getRevisionPages(${params.spaceId}, ${params.revisionId})`, async () => {
                 const api = apiClient(input);
                 const res = await api.spaces.listPagesInRevisionById(
                     params.spaceId,
@@ -338,10 +341,10 @@ const getRevisionFile = cache(
         params: { spaceId: string; revisionId: string; fileId: string }
     ) => {
         'use cache';
-        return trace(
-            `getRevisionFile(${params.spaceId}, ${params.revisionId}, ${params.fileId})`,
-            async () => {
-                return wrapDataFetcherError(async () => {
+        return wrapCacheDataFetcherError(async () => {
+            return trace(
+                `getRevisionFile(${params.spaceId}, ${params.revisionId}, ${params.fileId})`,
+                async () => {
                     const api = apiClient(input);
                     const res = await api.spaces.getFileInRevisionById(
                         params.spaceId,
@@ -355,9 +358,9 @@ const getRevisionFile = cache(
                     cacheTag(...getCacheTagsFromResponse(res));
                     cacheLife('max');
                     return res.data;
-                });
-            }
-        );
+                }
+            );
+        });
     }
 );
 
@@ -367,10 +370,10 @@ const getRevisionPageMarkdown = cache(
         params: { spaceId: string; revisionId: string; pageId: string }
     ) => {
         'use cache';
-        return trace(
-            `getRevisionPageMarkdown(${params.spaceId}, ${params.revisionId}, ${params.pageId})`,
-            async () => {
-                return wrapDataFetcherError(async () => {
+        return wrapCacheDataFetcherError(async () => {
+            return trace(
+                `getRevisionPageMarkdown(${params.spaceId}, ${params.revisionId}, ${params.pageId})`,
+                async () => {
                     const api = apiClient(input);
                     const res = await api.spaces.getPageInRevisionById(
                         params.spaceId,
@@ -391,9 +394,9 @@ const getRevisionPageMarkdown = cache(
                         throw new DataFetcherError('Page is not a document', 404);
                     }
                     return res.data.markdown;
-                });
-            }
-        );
+                }
+            );
+        });
     }
 );
 
@@ -403,10 +406,10 @@ const getRevisionPageDocument = cache(
         params: { spaceId: string; revisionId: string; pageId: string }
     ) => {
         'use cache';
-        return trace(
-            `getRevisionPageDocument(${params.spaceId}, ${params.revisionId}, ${params.pageId})`,
-            async () => {
-                return wrapDataFetcherError(async () => {
+        return wrapCacheDataFetcherError(async () => {
+            return trace(
+                `getRevisionPageDocument(${params.spaceId}, ${params.revisionId}, ${params.pageId})`,
+                async () => {
                     const api = apiClient(input);
                     const res = await api.spaces.getPageDocumentInRevisionById(
                         params.spaceId,
@@ -424,9 +427,9 @@ const getRevisionPageDocument = cache(
                     cacheLife('max');
 
                     return res.data;
-                });
-            }
-        );
+                }
+            );
+        });
     }
 );
 
@@ -436,11 +439,11 @@ const getRevisionPageByPath = cache(
         params: { spaceId: string; revisionId: string; path: string }
     ) => {
         'use cache';
-        return trace(
-            `getRevisionPageByPath(${params.spaceId}, ${params.revisionId}, ${params.path})`,
-            async () => {
-                const encodedPath = encodeURIComponent(params.path);
-                return wrapDataFetcherError(async () => {
+        return wrapCacheDataFetcherError(async () => {
+            return trace(
+                `getRevisionPageByPath(${params.spaceId}, ${params.revisionId}, ${params.path})`,
+                async () => {
+                    const encodedPath = encodeURIComponent(params.path);
                     const api = apiClient(input);
                     const res = await api.spaces.getPageInRevisionByPath(
                         params.spaceId,
@@ -454,17 +457,17 @@ const getRevisionPageByPath = cache(
                     cacheTag(...getCacheTagsFromResponse(res));
                     cacheLife('max');
                     return res.data;
-                });
-            }
-        );
+                }
+            );
+        });
     }
 );
 
 const getDocument = cache(
     async (input: DataFetcherInput, params: { spaceId: string; documentId: string }) => {
         'use cache';
-        return trace(`getDocument(${params.spaceId}, ${params.documentId})`, async () => {
-            return wrapDataFetcherError(async () => {
+        return wrapCacheDataFetcherError(async () => {
+            return trace(`getDocument(${params.spaceId}, ${params.documentId})`, async () => {
                 const api = apiClient(input);
                 const res = await api.spaces.getDocumentById(
                     params.spaceId,
@@ -503,10 +506,10 @@ const getComputedDocument = cache(
             )
         );
 
-        return trace(
-            `getComputedDocument(${params.spaceId}, ${params.organizationId}, ${params.source.type}, ${params.seed})`,
-            async () => {
-                return wrapDataFetcherError(async () => {
+        return wrapCacheDataFetcherError(async () => {
+            return trace(
+                `getComputedDocument(${params.spaceId}, ${params.organizationId}, ${params.source.type}, ${params.seed})`,
+                async () => {
                     const api = apiClient(input);
                     const res = await api.spaces.getComputedDocument(
                         params.spaceId,
@@ -522,9 +525,9 @@ const getComputedDocument = cache(
                     cacheTag(...getCacheTagsFromResponse(res));
                     cacheLife('max');
                     return res.data;
-                });
-            }
-        );
+                }
+            );
+        });
     }
 );
 
@@ -534,10 +537,10 @@ const getReusableContent = cache(
         params: { spaceId: string; revisionId: string; reusableContentId: string }
     ) => {
         'use cache';
-        return trace(
-            `getReusableContent(${params.spaceId}, ${params.revisionId}, ${params.reusableContentId})`,
-            async () => {
-                return wrapDataFetcherError(async () => {
+        return wrapCacheDataFetcherError(async () => {
+            return trace(
+                `getReusableContent(${params.spaceId}, ${params.revisionId}, ${params.reusableContentId})`,
+                async () => {
                     const api = apiClient(input);
                     const res = await api.spaces.getReusableContentInRevisionById(
                         params.spaceId,
@@ -551,9 +554,9 @@ const getReusableContent = cache(
                     cacheTag(...getCacheTagsFromResponse(res));
                     cacheLife('max');
                     return res.data;
-                });
-            }
-        );
+                }
+            );
+        });
     }
 );
 
@@ -568,10 +571,10 @@ const getLatestOpenAPISpecVersionContent = cache(
             })
         );
 
-        return trace(
-            `getLatestOpenAPISpecVersionContent(${params.organizationId}, ${params.slug})`,
-            async () => {
-                return wrapDataFetcherError(async () => {
+        return wrapCacheDataFetcherError(async () => {
+            return trace(
+                `getLatestOpenAPISpecVersionContent(${params.organizationId}, ${params.slug})`,
+                async () => {
                     const api = apiClient(input);
                     const res = await api.orgs.getLatestOpenApiSpecVersionContent(
                         params.organizationId,
@@ -583,9 +586,9 @@ const getLatestOpenAPISpecVersionContent = cache(
                     cacheTag(...getCacheTagsFromResponse(res));
                     cacheLife('max');
                     return res.data;
-                });
-            }
-        );
+                }
+            );
+        });
     }
 );
 
@@ -602,10 +605,10 @@ const getPublishedContentSite = cache(
             })
         );
 
-        return trace(
-            `getPublishedContentSite(${params.organizationId}, ${params.siteId}, ${params.siteShareKey})`,
-            async () => {
-                return wrapDataFetcherError(async () => {
+        return wrapCacheDataFetcherError(async () => {
+            return trace(
+                `getPublishedContentSite(${params.organizationId}, ${params.siteId}, ${params.siteShareKey})`,
+                async () => {
                     const api = apiClient(input);
                     const res = await api.orgs.getPublishedContentSite(
                         params.organizationId,
@@ -620,9 +623,9 @@ const getPublishedContentSite = cache(
                     cacheTag(...getCacheTagsFromResponse(res));
                     cacheLife('days');
                     return res.data;
-                });
-            }
-        );
+                }
+            );
+        });
     }
 );
 
@@ -644,10 +647,10 @@ const getSiteRedirectBySource = cache(
             })
         );
 
-        return trace(
-            `getSiteRedirectBySource(${params.organizationId}, ${params.siteId}, ${params.siteShareKey}, ${params.source})`,
-            async () => {
-                return wrapDataFetcherError(async () => {
+        return wrapCacheDataFetcherError(async () => {
+            return trace(
+                `getSiteRedirectBySource(${params.organizationId}, ${params.siteId}, ${params.siteShareKey}, ${params.source})`,
+                async () => {
                     const api = apiClient(input);
                     const res = await api.orgs.getSiteRedirectBySource(
                         params.organizationId,
@@ -663,9 +666,9 @@ const getSiteRedirectBySource = cache(
                     cacheTag(...getCacheTagsFromResponse(res));
                     cacheLife('days');
                     return res.data;
-                });
-            }
-        );
+                }
+            );
+        });
     }
 );
 
@@ -679,8 +682,8 @@ const getEmbedByUrl = cache(
             })
         );
 
-        return trace(`getEmbedByUrl(${params.spaceId}, ${params.url})`, async () => {
-            return wrapDataFetcherError(async () => {
+        return wrapCacheDataFetcherError(async () => {
+            return trace(`getEmbedByUrl(${params.spaceId}, ${params.url})`, async () => {
                 const api = apiClient(input);
                 const res = await api.spaces.getEmbedByUrlInSpace(
                     params.spaceId,
@@ -712,10 +715,10 @@ const searchSiteContent = cache(
             })
         );
 
-        return trace(
-            `searchSiteContent(${params.organizationId}, ${params.siteId}, ${params.query})`,
-            async () => {
-                return wrapDataFetcherError(async () => {
+        return wrapCacheDataFetcherError(async () => {
+            return trace(
+                `searchSiteContent(${params.organizationId}, ${params.siteId}, ${params.query})`,
+                async () => {
                     const { organizationId, siteId, query, scope } = params;
                     const api = apiClient(input);
                     const res = await api.orgs.searchSiteContent(
@@ -733,9 +736,9 @@ const searchSiteContent = cache(
                     cacheTag(...getCacheTagsFromResponse(res));
                     cacheLife('hours');
                     return res.data.items;
-                });
-            }
-        );
+                }
+            );
+        });
     }
 );
 
@@ -752,8 +755,8 @@ const renderIntegrationUi = cache(
             })
         );
 
-        return trace(`renderIntegrationUi(${params.integrationName})`, async () => {
-            return wrapDataFetcherError(async () => {
+        return wrapCacheDataFetcherError(async () => {
+            return trace(`renderIntegrationUi(${params.integrationName})`, async () => {
                 const api = apiClient(input);
                 const res = await api.integrations.renderIntegrationUiWithPost(
                     params.integrationName,
diff --git a/packages/gitbook-v2/src/lib/data/errors.ts b/packages/gitbook-v2/src/lib/data/errors.ts
index 4059fb5c94..c4d983ec2a 100644
--- a/packages/gitbook-v2/src/lib/data/errors.ts
+++ b/packages/gitbook-v2/src/lib/data/errors.ts
@@ -1,4 +1,5 @@
 import { GitBookAPIError } from '@gitbook/api';
+import { unstable_cacheLife as cacheLife } from 'next/cache';
 import type { DataFetcherErrorData, DataFetcherResponse } from './types';
 
 export class DataFetcherError extends Error {
@@ -89,6 +90,22 @@ export async function wrapDataFetcherError<T>(
     }
 }
 
+/**
+ * Wrap an async execution to handle errors and return a DataFetcherResponse.
+ * This should be used inside 'use cache' functions.
+ */
+export async function wrapCacheDataFetcherError<T>(
+    fn: () => Promise<T>
+): Promise<DataFetcherResponse<T>> {
+    const result = await wrapDataFetcherError(fn);
+    if (result.error && result.error.code >= 500) {
+        // We don't want to cache errors for too long.
+        // as the API might
+        cacheLife('minutes');
+    }
+    return result;
+}
+
 /**
  * Ignore some data fetcher errors.
  */

From 6859f7d2391482323667c170b5fb144d1b2fd49f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Tue, 17 Jun 2025 00:31:07 +0200
Subject: [PATCH 116/127] Fix rendering of ogimage when logo or icon are AVIF
 images (#3336)

Co-authored-by: Steven H <steven@gitbook.io>
---
 .changeset/pretty-balloons-fold.md      |  5 ++
 packages/gitbook/src/routes/ogimage.tsx | 80 ++++++++++++++++++-------
 2 files changed, 64 insertions(+), 21 deletions(-)
 create mode 100644 .changeset/pretty-balloons-fold.md

diff --git a/.changeset/pretty-balloons-fold.md b/.changeset/pretty-balloons-fold.md
new file mode 100644
index 0000000000..b501879843
--- /dev/null
+++ b/.changeset/pretty-balloons-fold.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Fix rendering of ogimage when logo or icon are AVIF images.
diff --git a/packages/gitbook/src/routes/ogimage.tsx b/packages/gitbook/src/routes/ogimage.tsx
index 653fc8fc3b..4035f54bef 100644
--- a/packages/gitbook/src/routes/ogimage.tsx
+++ b/packages/gitbook/src/routes/ogimage.tsx
@@ -8,6 +8,7 @@ import { ImageResponse } from 'next/og';
 import { type PageParams, fetchPageData } from '@/components/SitePage';
 import { getFontSourcesToPreload } from '@/fonts/custom';
 import { getAssetURL } from '@/lib/assets';
+import { getExtension } from '@/lib/paths';
 import { filterOutNullable } from '@/lib/typescript';
 import { getCacheTag } from '@gitbook/cache-tags';
 import type { GitBookSiteContext } from '@v2/lib/context';
@@ -32,7 +33,6 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
     }
 
     // Compute all text to load only the necessary fonts
-    const contentTitle = customization.header.logo ? '' : site.title;
     const pageTitle = page
         ? page.title.length > 64
             ? `${page.title.slice(0, 64)}...`
@@ -52,7 +52,7 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
             const fontFamily = customization.styling.font ?? CustomizationDefaultFont.Inter;
 
             const regularText = pageDescription;
-            const boldText = `${contentTitle}${pageTitle}`;
+            const boldText = `${site.title} ${pageTitle}`;
 
             const fonts = (
                 await Promise.all([
@@ -164,10 +164,28 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
                 )
             )
         );
+        if (!iconImage) {
+            throw new Error('Icon image should always be fetchable');
+        }
+
         return <img {...iconImage} alt="Icon" width={40} height={40} tw="mr-4" />;
     };
 
-    const [favicon, { fontFamily, fonts }] = await Promise.all([faviconLoader(), fontLoader()]);
+    const logoLoader = async () => {
+        if (!customization.header.logo) {
+            return null;
+        }
+
+        return await fetchImage(
+            useLightTheme ? customization.header.logo.light : customization.header.logo.dark
+        );
+    };
+
+    const [favicon, logo, { fontFamily, fonts }] = await Promise.all([
+        faviconLoader(),
+        logoLoader(),
+        fontLoader(),
+    ]);
 
     return new ImageResponse(
         <div
@@ -193,22 +211,14 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
             />
 
             {/* Logo */}
-            {customization.header.logo ? (
+            {logo ? (
                 <div tw="flex flex-row">
-                    <img
-                        {...(await fetchImage(
-                            useLightTheme
-                                ? customization.header.logo.light
-                                : customization.header.logo.dark
-                        ))}
-                        alt="Logo"
-                        tw="h-[60px]"
-                    />
+                    <img {...logo} alt="Logo" tw="h-[60px]" />
                 </div>
             ) : (
                 <div tw="flex">
                     {favicon}
-                    <h3 tw="text-4xl my-0 font-bold">{contentTitle}</h3>
+                    <h3 tw="text-4xl my-0 font-bold">{site.title}</h3>
                 </div>
             )}
 
@@ -295,7 +305,9 @@ async function loadCustomFont(input: { url: string; weight: 400 | 700 }) {
 // biome-ignore lint/suspicious/noExplicitAny: <explanation>
 const staticCache = new Map<string, any>();
 
-// Do we need to limit the in-memory cache size? I think given the usage, we should be fine.
+/**
+ * Get or initialize a value in the static cache.
+ */
 async function getWithCache<T>(key: string, fn: () => Promise<T>) {
     const cached = staticCache.get(key) as T;
     if (cached) {
@@ -311,19 +323,46 @@ async function getWithCache<T>(key: string, fn: () => Promise<T>) {
  * Read a static image and cache it in memory.
  */
 async function fetchStaticImage(url: string) {
-    return getWithCache(`static-image:${url}`, () => fetchImage(url));
+    return getWithCache(`static-image:${url}`, async () => {
+        const image = await fetchImage(url);
+        if (!image) {
+            throw new Error('Failed to fetch static image');
+        }
+
+        return image;
+    });
 }
 
+/**
+ * @vercel/og supports the following image formats:
+ * Extracted from https://github.com/vercel/next.js/blob/canary/packages/next/src/compiled/%40vercel/og/index.node.js
+ */
+const UNSUPPORTED_IMAGE_EXTENSIONS = ['.avif', '.webp'];
+const SUPPORTED_IMAGE_TYPES = [
+    'image/png',
+    'image/apng',
+    'image/jpeg',
+    'image/gif',
+    'image/svg+xml',
+];
+
 /**
  * Fetch an image from a URL and return a base64 encoded string.
  * We do this as @vercel/og is otherwise failing on SVG images referenced by a URL.
  */
 async function fetchImage(url: string) {
+    // Skip early some images to avoid fetching them
+    const parsedURL = new URL(url);
+    if (UNSUPPORTED_IMAGE_EXTENSIONS.includes(getExtension(parsedURL.pathname).toLowerCase())) {
+        return null;
+    }
+
     const response = await fetch(url);
 
+    // Filter out unsupported image types
     const contentType = response.headers.get('content-type');
-    if (!contentType || !contentType.startsWith('image/')) {
-        throw new Error(`Invalid content type: ${contentType}`);
+    if (!contentType || !SUPPORTED_IMAGE_TYPES.some((type) => contentType.includes(type))) {
+        return null;
     }
 
     const arrayBuffer = await response.arrayBuffer();
@@ -334,8 +373,7 @@ async function fetchImage(url: string) {
     try {
         const { width, height } = imageSize(buffer);
         return { src, width, height };
-    } catch (error) {
-        console.error(`Error reading image size: ${error}`);
-        return { src };
+    } catch {
+        return null;
     }
 }

From 500c8cb649018c639c013a1c4850aaf000528122 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Tue, 17 Jun 2025 13:04:50 +0200
Subject: [PATCH 117/127] Don't crash ogimage generation on RTL text (#3341)

---
 .changeset/pink-windows-wonder.md       |  5 +++++
 bun.lock                                |  3 +++
 packages/gitbook/package.json           |  3 ++-
 packages/gitbook/src/routes/ogimage.tsx | 23 ++++++++++++++++++++---
 4 files changed, 30 insertions(+), 4 deletions(-)
 create mode 100644 .changeset/pink-windows-wonder.md

diff --git a/.changeset/pink-windows-wonder.md b/.changeset/pink-windows-wonder.md
new file mode 100644
index 0000000000..d8e9560dcc
--- /dev/null
+++ b/.changeset/pink-windows-wonder.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Don't crash ogimage generation on RTL text, as a workaround until we can support it.
diff --git a/bun.lock b/bun.lock
index 94b4fc3a54..8afc0e702e 100644
--- a/bun.lock
+++ b/bun.lock
@@ -85,6 +85,7 @@
         "assert-never": "^1.2.1",
         "bun-types": "^1.1.20",
         "classnames": "^2.5.1",
+        "direction": "^2.0.1",
         "event-iterator": "^2.0.0",
         "framer-motion": "^10.16.14",
         "image-size": "^2.0.2",
@@ -1708,6 +1709,8 @@
 
     "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="],
 
+    "direction": ["direction@2.0.1", "", { "bin": { "direction": "cli.js" } }, "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA=="],
+
     "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
 
     "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json
index 2be76259d9..e549013162 100644
--- a/packages/gitbook/package.json
+++ b/packages/gitbook/package.json
@@ -82,7 +82,8 @@
         "zod": "^3.24.2",
         "zod-to-json-schema": "^3.24.5",
         "zustand": "^5.0.3",
-        "image-size": "^2.0.2"
+        "image-size": "^2.0.2",
+        "direction": "^2.0.1"
     },
     "devDependencies": {
         "@argos-ci/playwright": "^5.0.3",
diff --git a/packages/gitbook/src/routes/ogimage.tsx b/packages/gitbook/src/routes/ogimage.tsx
index 4035f54bef..6ce776c699 100644
--- a/packages/gitbook/src/routes/ogimage.tsx
+++ b/packages/gitbook/src/routes/ogimage.tsx
@@ -1,6 +1,7 @@
 import { CustomizationDefaultFont, CustomizationHeaderPreset } from '@gitbook/api';
 import { colorContrast } from '@gitbook/colors';
 import { type FontWeight, getDefaultFont } from '@gitbook/fonts';
+import { direction } from 'direction';
 import { imageSize } from 'image-size';
 import { redirect } from 'next/navigation';
 import { ImageResponse } from 'next/og';
@@ -218,7 +219,7 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
             ) : (
                 <div tw="flex">
                     {favicon}
-                    <h3 tw="text-4xl my-0 font-bold">{site.title}</h3>
+                    <h3 tw="text-4xl my-0 font-bold">{transformText(site.title)}</h3>
                 </div>
             )}
 
@@ -227,10 +228,12 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
                 <h1
                     tw={`text-8xl my-0 tracking-tight leading-none text-left text-[${colors.title}] font-bold`}
                 >
-                    {pageTitle}
+                    {transformText(pageTitle)}
                 </h1>
                 {pageDescription ? (
-                    <h2 tw="text-4xl mb-0 mt-8 w-[75%] font-normal">{pageDescription}</h2>
+                    <h2 tw="text-4xl mb-0 mt-8 w-[75%] font-normal">
+                        {transformText(pageDescription)}
+                    </h2>
                 ) : null}
             </div>
         </div>,
@@ -377,3 +380,17 @@ async function fetchImage(url: string) {
         return null;
     }
 }
+
+/**
+ * @vercel/og doesn't support RTL text, so we need to transform with a HACK for now.
+ * We can remove it once support has been added.
+ * https://github.com/vercel/satori/issues/74
+ */
+function transformText(text: string) {
+    const dir = direction(text);
+    if (dir !== 'rtl') {
+        return text;
+    }
+
+    return '';
+}

From ff6d1150a5930ffd79144e05882dd9c5cea78684 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Tue, 17 Jun 2025 16:12:58 +0200
Subject: [PATCH 118/127] Use `getRevisionPageDocument` on v2 (#3342)

---
 packages/gitbook-v2/src/lib/data/pages.ts | 35 ++---------------------
 1 file changed, 2 insertions(+), 33 deletions(-)

diff --git a/packages/gitbook-v2/src/lib/data/pages.ts b/packages/gitbook-v2/src/lib/data/pages.ts
index b9e3cabc5e..319c0a9170 100644
--- a/packages/gitbook-v2/src/lib/data/pages.ts
+++ b/packages/gitbook-v2/src/lib/data/pages.ts
@@ -1,4 +1,4 @@
-import { waitUntil } from '@/lib/waitUntil';
+import { isV2 } from '@/lib/v2';
 import type { JSONDocument, RevisionPageDocument } from '@gitbook/api';
 import type { GitBookSiteContext, GitBookSpaceContext } from '../context';
 import { getDataOrNull } from './errors';
@@ -12,10 +12,7 @@ export async function getPageDocument(
 ): Promise<JSONDocument | null> {
     const { dataFetcher, space } = context;
 
-    if (
-        'site' in context &&
-        (context.site.id === 'site_JOVzv' || context.site.id === 'site_IxAYj')
-    ) {
+    if (isV2()) {
         return getDataOrNull(
             dataFetcher.getRevisionPageDocument({
                 spaceId: space.id,
@@ -41,33 +38,5 @@ export async function getPageDocument(
         );
     }
 
-    // Pre-fetch the document to start filling the cache before we migrate to this API.
-    if (process.env.NODE_ENV === 'development') {
-        // Disable for now to investigate side-effects
-        if (isInPercentRollout(space.id, 10) || process.env.VERCEL_ENV === 'preview') {
-            await waitUntil(
-                getDataOrNull(
-                    dataFetcher.getRevisionPageDocument({
-                        spaceId: space.id,
-                        revisionId: space.revision,
-                        pageId: page.id,
-                    })
-                )
-            );
-        }
-    }
-
     return null;
 }
-
-function isInPercentRollout(value: string, rollout: number) {
-    return getRandomPercent(value) < rollout;
-}
-
-function getRandomPercent(value: string) {
-    const hash = value.split('').reduce((acc, char) => {
-        return acc + char.charCodeAt(0);
-    }, 0);
-
-    return hash % 100;
-}

From 67998b6f15b6977c7cc20dafe6a97f3156e95ffc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Tue, 17 Jun 2025 19:10:15 +0200
Subject: [PATCH 119/127] Fix ogimage generation failing with some JPEG images
 (#3345)

---
 .changeset/rare-pens-whisper.md               |  5 ++++
 .../src/lib/images/resizer/types.ts           |  2 +-
 packages/gitbook/src/routes/ogimage.tsx       | 25 ++++++++++++++++---
 3 files changed, 27 insertions(+), 5 deletions(-)
 create mode 100644 .changeset/rare-pens-whisper.md

diff --git a/.changeset/rare-pens-whisper.md b/.changeset/rare-pens-whisper.md
new file mode 100644
index 0000000000..aed0712ba4
--- /dev/null
+++ b/.changeset/rare-pens-whisper.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Fix ogimage generation failing with some JPEG images.
diff --git a/packages/gitbook-v2/src/lib/images/resizer/types.ts b/packages/gitbook-v2/src/lib/images/resizer/types.ts
index 8bbe697f90..9cb370586f 100644
--- a/packages/gitbook-v2/src/lib/images/resizer/types.ts
+++ b/packages/gitbook-v2/src/lib/images/resizer/types.ts
@@ -13,7 +13,7 @@ export interface CloudflareImageJsonFormat {
  * https://developers.cloudflare.com/images/image-resizing/resize-with-workers/
  */
 export interface CloudflareImageOptions {
-    format?: 'webp' | 'avif' | 'json' | 'jpeg';
+    format?: 'webp' | 'avif' | 'json' | 'jpeg' | 'png';
     fit?: 'scale-down' | 'contain' | 'cover' | 'crop' | 'pad';
     width?: number;
     height?: number;
diff --git a/packages/gitbook/src/routes/ogimage.tsx b/packages/gitbook/src/routes/ogimage.tsx
index 6ce776c699..ce3f8a7d67 100644
--- a/packages/gitbook/src/routes/ogimage.tsx
+++ b/packages/gitbook/src/routes/ogimage.tsx
@@ -13,7 +13,13 @@ import { getExtension } from '@/lib/paths';
 import { filterOutNullable } from '@/lib/typescript';
 import { getCacheTag } from '@gitbook/cache-tags';
 import type { GitBookSiteContext } from '@v2/lib/context';
-import { getResizedImageURL } from '@v2/lib/images';
+import {
+    type ResizeImageOptions,
+    SizableImageAction,
+    checkIsSizableImageURL,
+    getResizedImageURL,
+    resizeImage,
+} from '@v2/lib/images';
 
 /**
  * Render the OpenGraph image for a site content.
@@ -178,7 +184,10 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
         }
 
         return await fetchImage(
-            useLightTheme ? customization.header.logo.light : customization.header.logo.dark
+            useLightTheme ? customization.header.logo.light : customization.header.logo.dark,
+            {
+                height: 60,
+            }
         );
     };
 
@@ -353,14 +362,22 @@ const SUPPORTED_IMAGE_TYPES = [
  * Fetch an image from a URL and return a base64 encoded string.
  * We do this as @vercel/og is otherwise failing on SVG images referenced by a URL.
  */
-async function fetchImage(url: string) {
+async function fetchImage(url: string, options?: ResizeImageOptions) {
     // Skip early some images to avoid fetching them
     const parsedURL = new URL(url);
     if (UNSUPPORTED_IMAGE_EXTENSIONS.includes(getExtension(parsedURL.pathname).toLowerCase())) {
         return null;
     }
 
-    const response = await fetch(url);
+    // We use the image resizer to normalize the image format to PNG.
+    // as @vercel/og can sometimes fail on some JPEG images.
+    const response =
+        checkIsSizableImageURL(url) !== SizableImageAction.Resize
+            ? await fetch(url)
+            : await resizeImage(url, {
+                  ...options,
+                  format: 'png',
+              });
 
     // Filter out unsupported image types
     const contentType = response.headers.get('content-type');

From 88a35ed057935489519086295379eb2aef065cf9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Tue, 17 Jun 2025 19:23:52 +0200
Subject: [PATCH 120/127] Fix crash when integration is triggering invalid
 requests (#3346)

---
 .changeset/khaki-bees-count.md                        |  5 +++++
 .../src/components/DocumentView/Integration/render.ts | 11 +++++++----
 2 files changed, 12 insertions(+), 4 deletions(-)
 create mode 100644 .changeset/khaki-bees-count.md

diff --git a/.changeset/khaki-bees-count.md b/.changeset/khaki-bees-count.md
new file mode 100644
index 0000000000..a481690e24
--- /dev/null
+++ b/.changeset/khaki-bees-count.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Fix crash when integration is triggering invalid requests.
diff --git a/packages/gitbook/src/components/DocumentView/Integration/render.ts b/packages/gitbook/src/components/DocumentView/Integration/render.ts
index a9a86c9e59..4e683e0dcd 100644
--- a/packages/gitbook/src/components/DocumentView/Integration/render.ts
+++ b/packages/gitbook/src/components/DocumentView/Integration/render.ts
@@ -21,10 +21,13 @@ export async function fetchSafeIntegrationUI(
             request,
         }),
 
-        // The API can respond with a 400 error if the integration is not installed
-        // and 404 if the integration is not found.
-        // The API can also respond with a 502 error if the integration is not generating a proper response.
-        [404, 400, 502]
+        // The API can respond with certain errors that are expected to happen.
+        [
+            404, // Integration has been uninstalled
+            400, // Integration is rejecting its own request
+            422, // Integration is triggering an invalid request, failing at the validation step
+            502, // Integration is failing in an unexpected way
+        ]
     );
 
     return output;

From a3a944d7ddde5290fe9f070c0461c5884c75db49 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Tue, 17 Jun 2025 19:38:47 +0200
Subject: [PATCH 121/127] Fix crash during rendering of ogimage for VA sites
 with default icon (#3347)

---
 .changeset/thick-cups-shout.md          |  5 +++
 packages/gitbook/src/routes/icon.tsx    | 53 +++++++++++++++++--------
 packages/gitbook/src/routes/ogimage.tsx | 51 +++++++++++++-----------
 3 files changed, 69 insertions(+), 40 deletions(-)
 create mode 100644 .changeset/thick-cups-shout.md

diff --git a/.changeset/thick-cups-shout.md b/.changeset/thick-cups-shout.md
new file mode 100644
index 0000000000..3aabb9ad40
--- /dev/null
+++ b/.changeset/thick-cups-shout.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Fix crash during rendering of ogimage for VA sites with default icon.
diff --git a/packages/gitbook/src/routes/icon.tsx b/packages/gitbook/src/routes/icon.tsx
index 03a6f35224..ab3429d631 100644
--- a/packages/gitbook/src/routes/icon.tsx
+++ b/packages/gitbook/src/routes/icon.tsx
@@ -24,6 +24,11 @@ const SIZES = {
     },
 };
 
+type RenderIconOptions = {
+    size: keyof typeof SIZES;
+    theme: 'light' | 'dark';
+};
+
 /**
  * Generate an icon for a site content.
  */
@@ -31,7 +36,7 @@ export async function serveIcon(context: GitBookSiteContext, req: Request) {
     const options = getOptions(req.url);
     const size = SIZES[options.size];
 
-    const { site, customization } = context;
+    const { customization } = context;
     const customIcon = 'icon' in customization.favicon ? customization.favicon.icon : null;
 
     // If the site has a custom icon, redirect to it
@@ -45,17 +50,45 @@ export async function serveIcon(context: GitBookSiteContext, req: Request) {
         );
     }
 
+    return new ImageResponse(<SiteDefaultIcon context={context} options={options} />, {
+        width: size.width,
+        height: size.height,
+        headers: {
+            'cache-tag': [
+                getCacheTag({
+                    tag: 'site',
+                    site: context.site.id,
+                }),
+            ].join(','),
+        },
+    });
+}
+
+/**
+ * Render the icon as a React node.
+ */
+export function SiteDefaultIcon(props: {
+    context: GitBookSiteContext;
+    options: RenderIconOptions;
+    style?: React.CSSProperties;
+    tw?: string;
+}) {
+    const { context, options, style, tw } = props;
+    const size = SIZES[options.size];
+
+    const { site, customization } = context;
     const contentTitle = site.title;
 
-    return new ImageResponse(
+    return (
         <div
-            tw={tcls(options.theme === 'light' ? 'bg-white' : 'bg-black', size.boxStyle)}
+            tw={tcls(options.theme === 'light' ? 'bg-white' : 'bg-black', size.boxStyle, tw)}
             style={{
                 width: '100%',
                 height: '100%',
                 display: 'flex',
                 alignItems: 'center',
                 justifyContent: 'center',
+                ...style,
             }}
         >
             <h2
@@ -70,19 +103,7 @@ export async function serveIcon(context: GitBookSiteContext, req: Request) {
                     ? getEmojiForCode(customization.favicon.emoji)
                     : contentTitle.slice(0, 1).toUpperCase()}
             </h2>
-        </div>,
-        {
-            width: size.width,
-            height: size.height,
-            headers: {
-                'cache-tag': [
-                    getCacheTag({
-                        tag: 'site',
-                        site: context.site.id,
-                    }),
-                ].join(','),
-            },
-        }
+        </div>
     );
 }
 
diff --git a/packages/gitbook/src/routes/ogimage.tsx b/packages/gitbook/src/routes/ogimage.tsx
index ce3f8a7d67..f79b86948f 100644
--- a/packages/gitbook/src/routes/ogimage.tsx
+++ b/packages/gitbook/src/routes/ogimage.tsx
@@ -20,13 +20,14 @@ import {
     getResizedImageURL,
     resizeImage,
 } from '@v2/lib/images';
+import { SiteDefaultIcon } from './icon';
 
 /**
  * Render the OpenGraph image for a site content.
  */
 export async function serveOGImage(baseContext: GitBookSiteContext, params: PageParams) {
     const { context, pageTarget } = await fetchPageData(baseContext, params);
-    const { customization, site, linker, imageResizer } = context;
+    const { customization, site, imageResizer } = context;
     const page = pageTarget?.page;
 
     // If user configured a custom social preview, we redirect to it.
@@ -148,34 +149,36 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
     }
 
     const faviconLoader = async () => {
+        if (customization.header.logo) {
+            // Don't load the favicon if we have a logo
+            // as it'll not be used.
+            return null;
+        }
+
+        const faviconSize = {
+            width: 48,
+            height: 48,
+        };
+
         if ('icon' in customization.favicon)
             return (
                 <img
-                    src={customization.favicon.icon[theme]}
-                    width={40}
-                    height={40}
-                    tw="mr-4"
+                    {...(await fetchImage(customization.favicon.icon[theme], faviconSize))}
+                    {...faviconSize}
                     alt="Icon"
                 />
             );
-        if ('emoji' in customization.favicon)
-            return (
-                <span tw="text-4xl mr-4">
-                    {String.fromCodePoint(Number.parseInt(`0x${customization.favicon.emoji}`))}
-                </span>
-            );
-        const iconImage = await fetchImage(
-            linker.toAbsoluteURL(
-                linker.toPathInSpace(
-                    `~gitbook/icon?size=medium&theme=${customization.themes.default}`
-                )
-            )
-        );
-        if (!iconImage) {
-            throw new Error('Icon image should always be fetchable');
-        }
 
-        return <img {...iconImage} alt="Icon" width={40} height={40} tw="mr-4" />;
+        return (
+            <SiteDefaultIcon
+                context={context}
+                options={{
+                    size: 'small',
+                    theme,
+                }}
+                style={faviconSize}
+            />
+        );
     };
 
     const logoLoader = async () => {
@@ -226,9 +229,9 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
                     <img {...logo} alt="Logo" tw="h-[60px]" />
                 </div>
             ) : (
-                <div tw="flex">
+                <div tw="flex flex-row items-center">
                     {favicon}
-                    <h3 tw="text-4xl my-0 font-bold">{transformText(site.title)}</h3>
+                    <h3 tw="text-4xl ml-4 my-0 font-bold">{transformText(site.title)}</h3>
                 </div>
             )}
 

From 8f7c304d5852ecdeb556085f211c4369bd861b2c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com>
Date: Tue, 17 Jun 2025 20:25:21 +0200
Subject: [PATCH 122/127] Fix crash when rendering ogimage with invalid icon
 (#3348)

---
 packages/gitbook/src/routes/ogimage.tsx | 14 ++++++--------
 1 file changed, 6 insertions(+), 8 deletions(-)

diff --git a/packages/gitbook/src/routes/ogimage.tsx b/packages/gitbook/src/routes/ogimage.tsx
index f79b86948f..4d8a83b2c3 100644
--- a/packages/gitbook/src/routes/ogimage.tsx
+++ b/packages/gitbook/src/routes/ogimage.tsx
@@ -160,14 +160,12 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
             height: 48,
         };
 
-        if ('icon' in customization.favicon)
-            return (
-                <img
-                    {...(await fetchImage(customization.favicon.icon[theme], faviconSize))}
-                    {...faviconSize}
-                    alt="Icon"
-                />
-            );
+        if ('icon' in customization.favicon) {
+            const faviconImage = await fetchImage(customization.favicon.icon[theme], faviconSize);
+            if (faviconImage) {
+                return <img {...faviconImage} {...faviconSize} alt="Icon" />;
+            }
+        }
 
         return (
             <SiteDefaultIcon

From b4918f60cebccb4660a2a32a738f513eba83f0e3 Mon Sep 17 00:00:00 2001
From: conico974 <nicodorseuil@yahoo.fr>
Date: Tue, 17 Jun 2025 22:31:32 +0200
Subject: [PATCH 123/127] fix variant space change when pathname equals
 space-path (#3343)

Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
---
 .../src/components/Header/SpacesDropdown.tsx  | 21 ++++-----
 .../Header/SpacesDropdownMenuItem.tsx         | 45 +++++++++++++++++--
 2 files changed, 50 insertions(+), 16 deletions(-)

diff --git a/packages/gitbook/src/components/Header/SpacesDropdown.tsx b/packages/gitbook/src/components/Header/SpacesDropdown.tsx
index dd6887d955..87ad44b5d7 100644
--- a/packages/gitbook/src/components/Header/SpacesDropdown.tsx
+++ b/packages/gitbook/src/components/Header/SpacesDropdown.tsx
@@ -4,7 +4,7 @@ import { getSiteSpaceURL } from '@/lib/sites';
 import { tcls } from '@/lib/tailwind';
 import type { GitBookSiteContext } from '@v2/lib/context';
 import { DropdownChevron, DropdownMenu } from './DropdownMenu';
-import { SpacesDropdownMenuItem } from './SpacesDropdownMenuItem';
+import { SpacesDropdownMenuItems } from './SpacesDropdownMenuItem';
 
 export function SpacesDropdown(props: {
     context: GitBookSiteContext;
@@ -65,17 +65,14 @@ export function SpacesDropdown(props: {
                 </div>
             }
         >
-            {siteSpaces.map((otherSiteSpace, index) => (
-                <SpacesDropdownMenuItem
-                    key={`${otherSiteSpace.id}-${index}`}
-                    variantSpace={{
-                        id: otherSiteSpace.id,
-                        title: otherSiteSpace.title,
-                        url: getSiteSpaceURL(context, otherSiteSpace),
-                    }}
-                    active={otherSiteSpace.id === siteSpace.id}
-                />
-            ))}
+            <SpacesDropdownMenuItems
+                slimSpaces={siteSpaces.map((space) => ({
+                    id: space.id,
+                    title: space.title,
+                    url: getSiteSpaceURL(context, space),
+                }))}
+                curPath={siteSpace.path}
+            />
         </DropdownMenu>
     );
 }
diff --git a/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx b/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx
index 70859abfae..2044d0fc6f 100644
--- a/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx
+++ b/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx
@@ -6,12 +6,28 @@ import { joinPath } from '@/lib/paths';
 import { useCurrentPagePath } from '../hooks';
 import { DropdownMenuItem } from './DropdownMenu';
 
-function useVariantSpaceHref(variantSpaceUrl: string) {
+interface VariantSpace {
+    id: Space['id'];
+    title: Space['title'];
+    url: string;
+}
+
+// When switching to a different variant space, we reconstruct the URL by swapping the space path.
+function useVariantSpaceHref(variantSpaceUrl: string, currentSpacePath: string, active = false) {
     const currentPathname = useCurrentPagePath();
 
+    // We need to ensure that the variant space URL is not the same as the current space path.
+    // If it is, we return only the variant space URL to redirect to the root of the variant space.
+    // This is necessary in case the currentPathname is the same as the variantSpaceUrl,
+    // otherwise we would redirect to the same space if the variant space that we are switching to is the default one.
+    if (!active && currentPathname.startsWith(`${currentSpacePath}/`)) {
+        return variantSpaceUrl;
+    }
+
     if (URL.canParse(variantSpaceUrl)) {
         const targetUrl = new URL(variantSpaceUrl);
         targetUrl.pathname = joinPath(targetUrl.pathname, currentPathname);
+
         targetUrl.searchParams.set('fallback', 'true');
 
         return targetUrl.toString();
@@ -22,11 +38,12 @@ function useVariantSpaceHref(variantSpaceUrl: string) {
 }
 
 export function SpacesDropdownMenuItem(props: {
-    variantSpace: { id: Space['id']; title: Space['title']; url: string };
+    variantSpace: VariantSpace;
     active: boolean;
+    currentSpacePath: string;
 }) {
-    const { variantSpace, active } = props;
-    const variantHref = useVariantSpaceHref(variantSpace.url);
+    const { variantSpace, active, currentSpacePath } = props;
+    const variantHref = useVariantSpaceHref(variantSpace.url, currentSpacePath, active);
 
     return (
         <DropdownMenuItem key={variantSpace.id} href={variantHref} active={active}>
@@ -34,3 +51,23 @@ export function SpacesDropdownMenuItem(props: {
         </DropdownMenuItem>
     );
 }
+
+export function SpacesDropdownMenuItems(props: {
+    slimSpaces: VariantSpace[];
+    curPath: string;
+}) {
+    const { slimSpaces, curPath } = props;
+
+    return (
+        <>
+            {slimSpaces.map((space) => (
+                <SpacesDropdownMenuItem
+                    key={space.id}
+                    variantSpace={space}
+                    active={false}
+                    currentSpacePath={curPath}
+                />
+            ))}
+        </>
+    );
+}

From 392f59450caee52d07bc6b59a0c2934df1cd23e1 Mon Sep 17 00:00:00 2001
From: Steven H <steven@gitbook.io>
Date: Wed, 18 Jun 2025 12:15:45 +0100
Subject: [PATCH 124/127] Improve performance of InlineLinkTooltip (#3339)

---
 .changeset/soft-walls-change.md               |   6 +
 .../components/DocumentView/DocumentView.tsx  |   8 +
 .../components/DocumentView/InlineLink.tsx    |  61 -------
 .../DocumentView/InlineLink/InlineLink.tsx    | 153 ++++++++++++++++++
 .../InlineLink/InlineLinkTooltip.tsx          |  73 +++++++++
 .../InlineLinkTooltipImpl.tsx}                | 101 ++++--------
 .../DocumentView/InlineLink/index.ts          |   1 +
 .../gitbook/src/components/PDF/PDFPage.tsx    |   1 +
 .../src/components/PageBody/PageBody.tsx      |  14 +-
 .../src/components/Search/server-actions.tsx  |   1 +
 packages/gitbook/src/lib/document.ts          |  35 ++++
 11 files changed, 325 insertions(+), 129 deletions(-)
 create mode 100644 .changeset/soft-walls-change.md
 delete mode 100644 packages/gitbook/src/components/DocumentView/InlineLink.tsx
 create mode 100644 packages/gitbook/src/components/DocumentView/InlineLink/InlineLink.tsx
 create mode 100644 packages/gitbook/src/components/DocumentView/InlineLink/InlineLinkTooltip.tsx
 rename packages/gitbook/src/components/DocumentView/{InlineLinkTooltip.tsx => InlineLink/InlineLinkTooltipImpl.tsx} (64%)
 create mode 100644 packages/gitbook/src/components/DocumentView/InlineLink/index.ts

diff --git a/.changeset/soft-walls-change.md b/.changeset/soft-walls-change.md
new file mode 100644
index 0000000000..2086de0317
--- /dev/null
+++ b/.changeset/soft-walls-change.md
@@ -0,0 +1,6 @@
+---
+"gitbook": patch
+"gitbook-v2": patch
+---
+
+Fix InlineLinkTooltip having a negative impact on performance, especially on larger pages.
diff --git a/packages/gitbook/src/components/DocumentView/DocumentView.tsx b/packages/gitbook/src/components/DocumentView/DocumentView.tsx
index 724a7f2b05..1ff8542eed 100644
--- a/packages/gitbook/src/components/DocumentView/DocumentView.tsx
+++ b/packages/gitbook/src/components/DocumentView/DocumentView.tsx
@@ -28,6 +28,14 @@ export interface DocumentContext {
      * @default true
      */
     wrapBlocksInSuspense?: boolean;
+
+    /**
+     * True if link previews should be rendered.
+     * This is used to limit the number of link previews rendered in a document.
+     * If false, no link previews will be rendered.
+     * @default false
+     */
+    shouldRenderLinkPreviews?: boolean;
 }
 
 export interface DocumentContextProps {
diff --git a/packages/gitbook/src/components/DocumentView/InlineLink.tsx b/packages/gitbook/src/components/DocumentView/InlineLink.tsx
deleted file mode 100644
index f5bd7f798b..0000000000
--- a/packages/gitbook/src/components/DocumentView/InlineLink.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import { type DocumentInlineLink, SiteInsightsLinkPosition } from '@gitbook/api';
-
-import { resolveContentRef } from '@/lib/references';
-import { Icon } from '@gitbook/icons';
-import { StyledLink } from '../primitives';
-import type { InlineProps } from './Inline';
-import { InlineLinkTooltip } from './InlineLinkTooltip';
-import { Inlines } from './Inlines';
-
-export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
-    const { inline, document, context, ancestorInlines } = props;
-
-    const resolved = context.contentContext
-        ? await resolveContentRef(inline.data.ref, context.contentContext, {
-              // We don't want to resolve the anchor text here, as it can be very expensive and will block rendering if there is a lot of anchors link.
-              resolveAnchorText: false,
-          })
-        : null;
-
-    if (!context.contentContext || !resolved) {
-        return (
-            <span title="Broken link" className="underline">
-                <Inlines
-                    context={context}
-                    document={document}
-                    nodes={inline.nodes}
-                    ancestorInlines={[...ancestorInlines, inline]}
-                />
-            </span>
-        );
-    }
-    const isExternal = inline.data.ref.kind === 'url';
-
-    return (
-        <InlineLinkTooltip inline={inline} context={context.contentContext} resolved={resolved}>
-            <StyledLink
-                href={resolved.href}
-                insights={{
-                    type: 'link_click',
-                    link: {
-                        target: inline.data.ref,
-                        position: SiteInsightsLinkPosition.Content,
-                    },
-                }}
-            >
-                <Inlines
-                    context={context}
-                    document={document}
-                    nodes={inline.nodes}
-                    ancestorInlines={[...ancestorInlines, inline]}
-                />
-                {isExternal ? (
-                    <Icon
-                        icon="arrow-up-right"
-                        className="ml-0.5 inline size-3 links-accent:text-tint-subtle"
-                    />
-                ) : null}
-            </StyledLink>
-        </InlineLinkTooltip>
-    );
-}
diff --git a/packages/gitbook/src/components/DocumentView/InlineLink/InlineLink.tsx b/packages/gitbook/src/components/DocumentView/InlineLink/InlineLink.tsx
new file mode 100644
index 0000000000..8ad287a351
--- /dev/null
+++ b/packages/gitbook/src/components/DocumentView/InlineLink/InlineLink.tsx
@@ -0,0 +1,153 @@
+import { type DocumentInlineLink, SiteInsightsLinkPosition } from '@gitbook/api';
+
+import { getSpaceLanguage, tString } from '@/intl/server';
+import { languages } from '@/intl/translations';
+import { type ResolvedContentRef, resolveContentRef } from '@/lib/references';
+import { Icon } from '@gitbook/icons';
+import type { GitBookAnyContext } from '@v2/lib/context';
+import { StyledLink } from '../../primitives';
+import type { InlineProps } from '../Inline';
+import { Inlines } from '../Inlines';
+import { InlineLinkTooltip } from './InlineLinkTooltip';
+
+export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
+    const { inline, document, context, ancestorInlines } = props;
+
+    const resolved = context.contentContext
+        ? await resolveContentRef(inline.data.ref, context.contentContext, {
+              // We don't want to resolve the anchor text here, as it can be very expensive and will block rendering if there is a lot of anchors link.
+              resolveAnchorText: false,
+          })
+        : null;
+
+    if (!context.contentContext || !resolved) {
+        return (
+            <span title="Broken link" className="underline">
+                <Inlines
+                    context={context}
+                    document={document}
+                    nodes={inline.nodes}
+                    ancestorInlines={[...ancestorInlines, inline]}
+                />
+            </span>
+        );
+    }
+    const isExternal = inline.data.ref.kind === 'url';
+    const content = (
+        <StyledLink
+            href={resolved.href}
+            insights={{
+                type: 'link_click',
+                link: {
+                    target: inline.data.ref,
+                    position: SiteInsightsLinkPosition.Content,
+                },
+            }}
+        >
+            <Inlines
+                context={context}
+                document={document}
+                nodes={inline.nodes}
+                ancestorInlines={[...ancestorInlines, inline]}
+            />
+            {isExternal ? (
+                <Icon
+                    icon="arrow-up-right"
+                    className="ml-0.5 inline size-3 links-accent:text-tint-subtle"
+                />
+            ) : null}
+        </StyledLink>
+    );
+
+    if (context.shouldRenderLinkPreviews) {
+        return (
+            <InlineLinkTooltipWrapper
+                inline={inline}
+                context={context.contentContext}
+                resolved={resolved}
+            >
+                {content}
+            </InlineLinkTooltipWrapper>
+        );
+    }
+
+    return content;
+}
+
+/**
+ * An SSR component that renders a link with a tooltip.
+ * Essentially it pulls the minimum amount of props from the context to render the tooltip.
+ */
+function InlineLinkTooltipWrapper(props: {
+    inline: DocumentInlineLink;
+    context: GitBookAnyContext;
+    children: React.ReactNode;
+    resolved: ResolvedContentRef;
+}) {
+    const { inline, context, resolved, children } = props;
+
+    let breadcrumbs = resolved.ancestors ?? [];
+    const language =
+        'customization' in context ? getSpaceLanguage(context.customization) : languages.en;
+    const isExternal = inline.data.ref.kind === 'url';
+    const isSamePage = inline.data.ref.kind === 'anchor' && inline.data.ref.page === undefined;
+    if (isExternal) {
+        breadcrumbs = [
+            {
+                label: tString(language, 'link_tooltip_external_link'),
+            },
+        ];
+    }
+    if (isSamePage) {
+        breadcrumbs = [
+            {
+                label: tString(language, 'link_tooltip_page_anchor'),
+                icon: <Icon icon="arrow-down-short-wide" className="size-3" />,
+            },
+        ];
+        resolved.subText = undefined;
+    }
+
+    const aiSummary: { pageId: string; spaceId: string } | undefined = (() => {
+        if (isExternal) {
+            return;
+        }
+
+        if (isSamePage) {
+            return;
+        }
+
+        if (!('customization' in context) || !context.customization.ai?.pageLinkSummaries.enabled) {
+            return;
+        }
+
+        if (!('page' in context) || !('page' in inline.data.ref)) {
+            return;
+        }
+
+        if (inline.data.ref.kind === 'page' || inline.data.ref.kind === 'anchor') {
+            return {
+                pageId: resolved.page?.id ?? inline.data.ref.page ?? context.page.id,
+                spaceId: inline.data.ref.space ?? context.space.id,
+            };
+        }
+    })();
+
+    return (
+        <InlineLinkTooltip
+            breadcrumbs={breadcrumbs}
+            isExternal={isExternal}
+            isSamePage={isSamePage}
+            aiSummary={aiSummary}
+            openInNewTabLabel={tString(language, 'open_in_new_tab')}
+            target={{
+                href: resolved.href,
+                text: resolved.text,
+                subText: resolved.subText,
+                icon: resolved.icon,
+            }}
+        >
+            {children}
+        </InlineLinkTooltip>
+    );
+}
diff --git a/packages/gitbook/src/components/DocumentView/InlineLink/InlineLinkTooltip.tsx b/packages/gitbook/src/components/DocumentView/InlineLink/InlineLinkTooltip.tsx
new file mode 100644
index 0000000000..45be0c7173
--- /dev/null
+++ b/packages/gitbook/src/components/DocumentView/InlineLink/InlineLinkTooltip.tsx
@@ -0,0 +1,73 @@
+'use client';
+import dynamic from 'next/dynamic';
+import React from 'react';
+
+const LoadingValueContext = React.createContext<React.ReactNode>(null);
+
+// To avoid polluting the RSC payload with the tooltip implementation,
+// we lazily load it on the client side. This way, the tooltip is only loaded
+// when the user interacts with the link, and it doesn't block the initial render.
+
+const InlineLinkTooltipImpl = dynamic(
+    () => import('./InlineLinkTooltipImpl').then((mod) => mod.InlineLinkTooltipImpl),
+    {
+        // Disable server-side rendering for this component, it's only
+        // visible on user interaction.
+        ssr: false,
+        loading: () => {
+            // The fallback should be the children (the content of the link),
+            // but as next/dynamic is aiming for feature parity with React.lazy,
+            // it doesn't support passing children to the loading component.
+            // https://github.com/vercel/next.js/issues/7906
+            const children = React.useContext(LoadingValueContext);
+            return <>{children}</>;
+        },
+    }
+);
+
+/**
+ * Tooltip for inline links. It's lazily loaded to avoid blocking the initial render
+ * and polluting the RSC payload.
+ *
+ * The link text and href have already been rendered on the server for good SEO,
+ * so we can be as lazy as possible with the tooltip.
+ */
+export function InlineLinkTooltip(props: {
+    isSamePage: boolean;
+    isExternal: boolean;
+    aiSummary?: { pageId: string; spaceId: string };
+    breadcrumbs: Array<{ href?: string; label: string; icon?: React.ReactNode }>;
+    target: {
+        href: string;
+        text: string;
+        subText?: string;
+        icon?: React.ReactNode;
+    };
+    openInNewTabLabel: string;
+    children: React.ReactNode;
+}) {
+    const { children, ...rest } = props;
+    const [shouldLoad, setShouldLoad] = React.useState(false);
+
+    // Once the browser is idle, we set shouldLoad to true.
+    // NOTE: to be slightly more performant, we could load when a link is hovered.
+    // But I found this was too much of a delay for the tooltip to appear.
+    // Loading on idle is a good compromise, as it allows the initial render to be fast,
+    // while still loading the tooltip in the background and not polluting the RSC payload.
+    React.useEffect(() => {
+        if ('requestIdleCallback' in window) {
+            (window as globalThis.Window).requestIdleCallback(() => setShouldLoad(true));
+        } else {
+            // fallback for old browsers
+            setTimeout(() => setShouldLoad(true), 2000);
+        }
+    }, []);
+
+    return shouldLoad ? (
+        <LoadingValueContext.Provider value={children}>
+            <InlineLinkTooltipImpl {...rest}>{children}</InlineLinkTooltipImpl>
+        </LoadingValueContext.Provider>
+    ) : (
+        children
+    );
+}
diff --git a/packages/gitbook/src/components/DocumentView/InlineLinkTooltip.tsx b/packages/gitbook/src/components/DocumentView/InlineLink/InlineLinkTooltipImpl.tsx
similarity index 64%
rename from packages/gitbook/src/components/DocumentView/InlineLinkTooltip.tsx
rename to packages/gitbook/src/components/DocumentView/InlineLink/InlineLinkTooltipImpl.tsx
index 1a5cefa1e3..333184a2cd 100644
--- a/packages/gitbook/src/components/DocumentView/InlineLinkTooltip.tsx
+++ b/packages/gitbook/src/components/DocumentView/InlineLink/InlineLinkTooltipImpl.tsx
@@ -1,55 +1,27 @@
-import type { DocumentInlineLink } from '@gitbook/api';
-
-import type { ResolvedContentRef } from '@/lib/references';
-
-import { getSpaceLanguage } from '@/intl/server';
-import { tString } from '@/intl/translate';
-import { languages } from '@/intl/translations';
-import { getNodeText } from '@/lib/document';
+'use client';
 import { tcls } from '@/lib/tailwind';
 import { Icon } from '@gitbook/icons';
 import * as Tooltip from '@radix-ui/react-tooltip';
-import type { GitBookAnyContext } from '@v2/lib/context';
 import { Fragment } from 'react';
-import { AIPageLinkSummary } from '../Adaptive/AIPageLinkSummary';
-import { Button, StyledLink } from '../primitives';
+import { AIPageLinkSummary } from '../../Adaptive';
+import { Button, StyledLink } from '../../primitives';
 
-export async function InlineLinkTooltip(props: {
-    inline: DocumentInlineLink;
-    context: GitBookAnyContext;
+export function InlineLinkTooltipImpl(props: {
+    isSamePage: boolean;
+    isExternal: boolean;
+    aiSummary?: { pageId: string; spaceId: string };
+    breadcrumbs: Array<{ href?: string; label: string; icon?: React.ReactNode }>;
+    target: {
+        href: string;
+        text: string;
+        subText?: string;
+        icon?: React.ReactNode;
+    };
+    openInNewTabLabel: string;
     children: React.ReactNode;
-    resolved: ResolvedContentRef;
 }) {
-    const { inline, context, resolved, children } = props;
-
-    let breadcrumbs = resolved.ancestors;
-    const language =
-        'customization' in context ? getSpaceLanguage(context.customization) : languages.en;
-    const isExternal = inline.data.ref.kind === 'url';
-    const isSamePage = inline.data.ref.kind === 'anchor' && inline.data.ref.page === undefined;
-    if (isExternal) {
-        breadcrumbs = [
-            {
-                label: tString(language, 'link_tooltip_external_link'),
-            },
-        ];
-    }
-    if (isSamePage) {
-        breadcrumbs = [
-            {
-                label: tString(language, 'link_tooltip_page_anchor'),
-                icon: <Icon icon="arrow-down-short-wide" className="size-3" />,
-            },
-        ];
-        resolved.subText = undefined;
-    }
-
-    const hasAISummary =
-        !isExternal &&
-        !isSamePage &&
-        'customization' in context &&
-        context.customization.ai?.pageLinkSummaries.enabled &&
-        (inline.data.ref.kind === 'page' || inline.data.ref.kind === 'anchor');
+    const { isSamePage, isExternal, aiSummary, openInNewTabLabel, target, breadcrumbs, children } =
+        props;
 
     return (
         <Tooltip.Provider delayDuration={200}>
@@ -100,15 +72,15 @@ export async function InlineLinkTooltip(props: {
                                                 isExternal && 'text-sm [overflow-wrap:anywhere]'
                                             )}
                                         >
-                                            {resolved.icon ? (
+                                            {target.icon ? (
                                                 <div className="mt-1 text-tint-subtle empty:hidden">
-                                                    {resolved.icon}
+                                                    {target.icon}
                                                 </div>
                                             ) : null}
-                                            <h5 className="font-semibold">{resolved.text}</h5>
+                                            <h5 className="font-semibold">{target.text}</h5>
                                         </div>
                                     </div>
-                                    {!isSamePage && resolved.href ? (
+                                    {!isSamePage && target.href ? (
                                         <Button
                                             className={tcls(
                                                 '-mx-2 -my-2 ml-auto',
@@ -117,40 +89,35 @@ export async function InlineLinkTooltip(props: {
                                                     : null
                                             )}
                                             variant="blank"
-                                            href={resolved.href}
+                                            href={target.href}
                                             target="_blank"
-                                            label={tString(language, 'open_in_new_tab')}
+                                            label={openInNewTabLabel}
                                             size="small"
                                             icon="arrow-up-right-from-square"
                                             iconOnly={true}
                                         />
                                     ) : null}
                                 </div>
-                                {resolved.subText ? (
-                                    <p className="mt-1 text-sm text-tint">{resolved.subText}</p>
+                                {target.subText ? (
+                                    <p className="mt-1 text-sm text-tint">{target.subText}</p>
                                 ) : null}
                             </div>
 
-                            {hasAISummary && 'page' in context && 'page' in inline.data.ref ? (
+                            {aiSummary ? (
                                 <div className="border-tint-subtle border-t bg-tint p-4">
                                     <AIPageLinkSummary
-                                        targetPageId={
-                                            resolved.page?.id ??
-                                            inline.data.ref.page ??
-                                            context.page.id
-                                        }
-                                        targetSpaceId={inline.data.ref.space ?? context.space.id}
-                                        linkTitle={getNodeText(inline)}
-                                        linkPreview={`**${resolved.text}**: ${resolved.subText}`}
-                                        showTrademark={
-                                            'customization' in context &&
-                                            context.customization.trademark.enabled
-                                        }
+                                        targetPageId={aiSummary.pageId}
+                                        targetSpaceId={aiSummary.spaceId}
+                                        showTrademark
                                     />
                                 </div>
                             ) : null}
                         </div>
-                        <Tooltip.Arrow className={hasAISummary ? 'fill-tint-3' : 'fill-tint-1'} />
+                        <Tooltip.Arrow
+                            className={
+                                typeof aiSummary !== 'undefined' ? 'fill-tint-3' : 'fill-tint-1'
+                            }
+                        />
                     </Tooltip.Content>
                 </Tooltip.Portal>
             </Tooltip.Root>
diff --git a/packages/gitbook/src/components/DocumentView/InlineLink/index.ts b/packages/gitbook/src/components/DocumentView/InlineLink/index.ts
new file mode 100644
index 0000000000..1d7f30120b
--- /dev/null
+++ b/packages/gitbook/src/components/DocumentView/InlineLink/index.ts
@@ -0,0 +1 @@
+export * from './InlineLink';
diff --git a/packages/gitbook/src/components/PDF/PDFPage.tsx b/packages/gitbook/src/components/PDF/PDFPage.tsx
index f70b340b41..cc9bf28077 100644
--- a/packages/gitbook/src/components/PDF/PDFPage.tsx
+++ b/packages/gitbook/src/components/PDF/PDFPage.tsx
@@ -245,6 +245,7 @@ async function PDFPageDocument(props: {
                             page,
                         },
                         getId: (id) => getPagePDFContainerId(page, id),
+                        shouldRenderLinkPreviews: false, // We don't want to render link previews in the PDF.
                     }}
                     // We consider all pages as offscreen in PDF mode
                     // to ensure we can efficiently render as many pages as possible
diff --git a/packages/gitbook/src/components/PageBody/PageBody.tsx b/packages/gitbook/src/components/PageBody/PageBody.tsx
index 801db03ce2..9b9e744743 100644
--- a/packages/gitbook/src/components/PageBody/PageBody.tsx
+++ b/packages/gitbook/src/components/PageBody/PageBody.tsx
@@ -4,7 +4,7 @@ import React from 'react';
 
 import { getSpaceLanguage } from '@/intl/server';
 import { t } from '@/intl/translate';
-import { hasFullWidthBlock, isNodeEmpty } from '@/lib/document';
+import { hasFullWidthBlock, hasMoreThan, isNodeEmpty } from '@/lib/document';
 import type { AncestorRevisionPage } from '@/lib/pages';
 import { tcls } from '@/lib/tailwind';
 import { DocumentView, DocumentViewSkeleton } from '../DocumentView';
@@ -17,6 +17,8 @@ import { PageFooterNavigation } from './PageFooterNavigation';
 import { PageHeader } from './PageHeader';
 import { PreservePageLayout } from './PreservePageLayout';
 
+const LINK_PREVIEW_MAX_COUNT = 100;
+
 export function PageBody(props: {
     context: GitBookSiteContext;
     page: RevisionPageDocument;
@@ -28,6 +30,15 @@ export function PageBody(props: {
     const { customization } = context;
 
     const contentFullWidth = document ? hasFullWidthBlock(document) : false;
+
+    // Render link previews only if there are less than LINK_PREVIEW_MAX_COUNT links in the document.
+    const shouldRenderLinkPreviews = document
+        ? !hasMoreThan(
+              document,
+              (inline) => inline.object === 'inline' && inline.type === 'link',
+              LINK_PREVIEW_MAX_COUNT
+          )
+        : false;
     const pageFullWidth = page.id === 'wtthNFMqmEQmnt5LKR0q';
     const asFullWidth = pageFullWidth || contentFullWidth;
     const language = getSpaceLanguage(customization);
@@ -68,6 +79,7 @@ export function PageBody(props: {
                             context={{
                                 mode: 'default',
                                 contentContext: context,
+                                shouldRenderLinkPreviews,
                             }}
                         />
                     </React.Suspense>
diff --git a/packages/gitbook/src/components/Search/server-actions.tsx b/packages/gitbook/src/components/Search/server-actions.tsx
index b45bc0aba1..9bd8257c16 100644
--- a/packages/gitbook/src/components/Search/server-actions.tsx
+++ b/packages/gitbook/src/components/Search/server-actions.tsx
@@ -345,6 +345,7 @@ async function transformAnswer(
                         mode: 'default',
                         contentContext: undefined,
                         wrapBlocksInSuspense: false,
+                        shouldRenderLinkPreviews: false, // We don't want to render link previews in the AI answer.
                     }}
                     style={['space-y-5']}
                 />
diff --git a/packages/gitbook/src/lib/document.ts b/packages/gitbook/src/lib/document.ts
index 33618f76c1..8d9fabc1e5 100644
--- a/packages/gitbook/src/lib/document.ts
+++ b/packages/gitbook/src/lib/document.ts
@@ -30,6 +30,41 @@ export function hasFullWidthBlock(document: JSONDocument): boolean {
     return false;
 }
 
+/**
+ * Returns true if the document has more than `limit` blocks and/or inlines that match the `check` predicate.
+ */
+export function hasMoreThan(
+    document: JSONDocument | DocumentBlock,
+    check: (block: DocumentBlock | DocumentInline) => boolean,
+    limit = 1
+): boolean {
+    let count = 0;
+
+    function traverse(node: JSONDocument | DocumentBlock | DocumentFragment): boolean {
+        for (const child of 'nodes' in node ? node.nodes : []) {
+            if (child.object === 'text') continue;
+
+            if (check(child)) {
+                count++;
+                if (count > limit) return true;
+            }
+
+            if (child.object === 'block' && 'nodes' in child) {
+                if (traverse(child)) return true;
+            }
+
+            if (child.object === 'block' && 'fragments' in child) {
+                for (const fragment of child.fragments) {
+                    if (traverse(fragment)) return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    return traverse(document);
+}
+
 /**
  * Get the text of a block/inline.
  */

From 73e0cbb2d659a4236114cd550bbb6cc5efcb61e9 Mon Sep 17 00:00:00 2001
From: Steven H <steven@gitbook.io>
Date: Wed, 18 Jun 2025 13:26:20 +0100
Subject: [PATCH 125/127] Fix an issue where PDF export URLs were not bringing
 their query params. (#3351)

---
 .changeset/rich-buses-hunt.md                 |   5 +
 packages/gitbook-v2/src/middleware.ts         |   2 +
 packages/gitbook/e2e/pdf.spec.ts              | 170 ++++++++++++++++++
 packages/gitbook/e2e/util.ts                  |   2 +-
 packages/gitbook/package.json                 |   2 +-
 .../gitbook/src/components/PDF/PDFPage.tsx    |   2 +-
 6 files changed, 180 insertions(+), 3 deletions(-)
 create mode 100644 .changeset/rich-buses-hunt.md
 create mode 100644 packages/gitbook/e2e/pdf.spec.ts

diff --git a/.changeset/rich-buses-hunt.md b/.changeset/rich-buses-hunt.md
new file mode 100644
index 0000000000..fbc59d232d
--- /dev/null
+++ b/.changeset/rich-buses-hunt.md
@@ -0,0 +1,5 @@
+---
+"gitbook-v2": patch
+---
+
+Fix an issue where PDF export URLs were not keeping their query params.
diff --git a/packages/gitbook-v2/src/middleware.ts b/packages/gitbook-v2/src/middleware.ts
index d413d6a435..4bcbf77aea 100644
--- a/packages/gitbook-v2/src/middleware.ts
+++ b/packages/gitbook-v2/src/middleware.ts
@@ -259,6 +259,8 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
         console.log(`rewriting ${request.nextUrl.toString()} to ${route}`);
 
         const rewrittenURL = new URL(`/${route}`, request.nextUrl.toString());
+        rewrittenURL.search = request.nextUrl.search; // Preserve the original search params
+
         const response = NextResponse.rewrite(rewrittenURL, {
             request: {
                 headers: requestHeaders,
diff --git a/packages/gitbook/e2e/pdf.spec.ts b/packages/gitbook/e2e/pdf.spec.ts
new file mode 100644
index 0000000000..4798a2a047
--- /dev/null
+++ b/packages/gitbook/e2e/pdf.spec.ts
@@ -0,0 +1,170 @@
+import { argosScreenshot } from '@argos-ci/playwright';
+import { expect, test } from '@playwright/test';
+import { getContentTestURL } from '../tests/utils';
+import { waitForIcons } from './util';
+
+test.describe('PDF export', () => {
+    test('export all pages as PDF (e2e)', async ({ page }) => {
+        // Set the header to disable the Vercel toolbar
+        // But only on the main document as it'd cause CORS issues on other resources
+        await page.route('**/*', async (route, request) => {
+            if (request.resourceType() === 'document') {
+                await route.continue({
+                    headers: {
+                        ...request.headers(),
+                        'x-vercel-skip-toolbar': '1',
+                    },
+                });
+            } else {
+                await route.continue();
+            }
+        });
+
+        await page.goto(
+            getContentTestURL(
+                'https://gitbook-open-e2e-sites.gitbook.io/gitbook-doc/~gitbook/pdf?limit=10'
+            )
+        );
+
+        const printBtn = page.getByTestId('print-button');
+        await expect(printBtn).toBeVisible();
+
+        await argosScreenshot(page, 'pdf - all pages', {
+            viewports: ['macbook-13'],
+            argosCSS: `
+                            /* Hide Intercom */
+                            .intercom-lightweight-app {
+                                display: none !important;
+                            }
+                            `,
+            threshold: undefined,
+            fullPage: true,
+            beforeScreenshot: async ({ runStabilization }) => {
+                await runStabilization();
+                await waitForIcons(page);
+            },
+        });
+    });
+
+    test('export all pages as PDF (GitBook docs)', async ({ page }) => {
+        // Set the header to disable the Vercel toolbar
+        // But only on the main document as it'd cause CORS issues on other resources
+        await page.route('**/*', async (route, request) => {
+            if (request.resourceType() === 'document') {
+                await route.continue({
+                    headers: {
+                        ...request.headers(),
+                        'x-vercel-skip-toolbar': '1',
+                    },
+                });
+            } else {
+                await route.continue();
+            }
+        });
+
+        await page.goto(getContentTestURL('https://gitbook.com/docs/~gitbook/pdf?limit=10'));
+
+        const printBtn = page.getByTestId('print-button');
+        await expect(printBtn).toBeVisible();
+
+        await argosScreenshot(page, 'pdf - all pages', {
+            viewports: ['macbook-13'],
+            argosCSS: `
+                            /* Hide Intercom */
+                            .intercom-lightweight-app {
+                                display: none !important;
+                            }
+                            `,
+            threshold: undefined,
+            fullPage: true,
+            beforeScreenshot: async ({ runStabilization }) => {
+                await runStabilization();
+                await waitForIcons(page);
+            },
+        });
+    });
+
+    test('export a single page as PDF (e2e)', async ({ page }) => {
+        // Set the header to disable the Vercel toolbar
+        // But only on the main document as it'd cause CORS issues on other resources
+        await page.route('**/*', async (route, request) => {
+            if (request.resourceType() === 'document') {
+                await route.continue({
+                    headers: {
+                        ...request.headers(),
+                        'x-vercel-skip-toolbar': '1',
+                    },
+                });
+            } else {
+                await route.continue();
+            }
+        });
+
+        await page.goto(
+            getContentTestURL(
+                'https://gitbook-open-e2e-sites.gitbook.io/gitbook-doc/~gitbook/pdf?page=Bw7LjWwgTjV8nIV4s7rs&only=yes&limit=2'
+            )
+        );
+
+        const printBtn = page.getByTestId('print-button');
+        await expect(printBtn).toBeVisible();
+
+        await argosScreenshot(page, 'pdf - all pages', {
+            viewports: ['macbook-13'],
+            argosCSS: `
+                            /* Hide Intercom */
+                            .intercom-lightweight-app {
+                                display: none !important;
+                            }
+                            `,
+            threshold: undefined,
+            fullPage: true,
+            beforeScreenshot: async ({ runStabilization }) => {
+                await runStabilization();
+                await waitForIcons(page);
+            },
+        });
+    });
+
+    test('export a single page as PDF (GitBook docs)', async ({ page }) => {
+        // Set the header to disable the Vercel toolbar
+        // But only on the main document as it'd cause CORS issues on other resources
+        await page.route('**/*', async (route, request) => {
+            if (request.resourceType() === 'document') {
+                await route.continue({
+                    headers: {
+                        ...request.headers(),
+                        'x-vercel-skip-toolbar': '1',
+                    },
+                });
+            } else {
+                await route.continue();
+            }
+        });
+
+        await page.goto(
+            getContentTestURL(
+                'https://gitbook.com/docs/~gitbook/pdf?page=DfnNkU49mvLe2ythHAyx&only=yes&limit=2'
+            )
+        );
+
+        const printBtn = page.getByTestId('print-button');
+        await expect(printBtn).toBeVisible();
+
+        await argosScreenshot(page, 'pdf - all pages', {
+            viewports: ['macbook-13'],
+            argosCSS: `
+                            /* Hide Intercom */
+                            .intercom-lightweight-app {
+                                display: none !important;
+                            }
+                            `,
+            threshold: undefined,
+            fullPage: true,
+            beforeScreenshot: async ({ runStabilization }) => {
+                await runStabilization();
+                await waitForIcons(page);
+            },
+        });
+    });
+});
diff --git a/packages/gitbook/e2e/util.ts b/packages/gitbook/e2e/util.ts
index f54b70fbe2..937b776d70 100644
--- a/packages/gitbook/e2e/util.ts
+++ b/packages/gitbook/e2e/util.ts
@@ -346,7 +346,7 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
 /**
  * Wait for all icons present on the page to be loaded.
  */
-async function waitForIcons(page: Page) {
+export async function waitForIcons(page: Page) {
     await page.waitForFunction(() => {
         const urlStates: Record<
             string,
diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json
index e549013162..a430218cfb 100644
--- a/packages/gitbook/package.json
+++ b/packages/gitbook/package.json
@@ -8,7 +8,7 @@
         "build:cloudflare": "next-on-pages --custom-entrypoint=./src/cloudflare-entrypoint.ts",
         "start": "next start",
         "typecheck": "tsc --noEmit",
-        "e2e": "playwright test e2e/internal.spec.ts",
+        "e2e": "playwright test e2e/internal.spec.ts e2e/pdf.spec.ts",
         "e2e-customers": "playwright test e2e/customers.spec.ts",
         "unit": "bun test {src,packages} --preload ./tests/preload-bun.ts",
         "generate": "gitbook-icons ./public/~gitbook/static/icons custom-icons && gitbook-math ./public/~gitbook/static/math",
diff --git a/packages/gitbook/src/components/PDF/PDFPage.tsx b/packages/gitbook/src/components/PDF/PDFPage.tsx
index cc9bf28077..d2bc5c91a6 100644
--- a/packages/gitbook/src/components/PDF/PDFPage.tsx
+++ b/packages/gitbook/src/components/PDF/PDFPage.tsx
@@ -53,7 +53,7 @@ export async function PDFPage(props: {
 }) {
     const baseContext = props.context;
     const searchParams = new URLSearchParams(props.searchParams);
-    const pdfParams = getPDFSearchParams(new URLSearchParams(searchParams));
+    const pdfParams = getPDFSearchParams(searchParams);
 
     const customization =
         'customization' in baseContext ? baseContext.customization : defaultCustomization();

From dae019c11501477fcf21313ec8c8ff9b35f6230c Mon Sep 17 00:00:00 2001
From: Zeno Kapitein <zeno@gitbook.io>
Date: Wed, 18 Jun 2025 17:48:49 +0200
Subject: [PATCH 126/127] Consistently show variant selector in section bar if
 site has sections (#3349)

---
 .changeset/slimy-hornets-share.md                           | 5 +++++
 packages/gitbook/src/components/Header/Header.tsx           | 2 +-
 packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx | 2 +-
 3 files changed, 7 insertions(+), 2 deletions(-)
 create mode 100644 .changeset/slimy-hornets-share.md

diff --git a/.changeset/slimy-hornets-share.md b/.changeset/slimy-hornets-share.md
new file mode 100644
index 0000000000..bfeef22793
--- /dev/null
+++ b/.changeset/slimy-hornets-share.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Consistently show variant selector in section bar if site has sections
diff --git a/packages/gitbook/src/components/Header/Header.tsx b/packages/gitbook/src/components/Header/Header.tsx
index 304e853139..f3bd145d56 100644
--- a/packages/gitbook/src/components/Header/Header.tsx
+++ b/packages/gitbook/src/components/Header/Header.tsx
@@ -202,7 +202,7 @@ export function Header(props: { context: GitBookSiteContext; withTopHeader?: boo
                             {siteSpaces.length > 1 && (
                                 <div
                                     id="variants"
-                                    className="my-2 mr-5 page-no-toc:flex hidden grow border-tint border-r pr-5 *:grow only:mr-0 only:border-none only:pr-0 sm:max-w-64"
+                                    className="my-2 mr-5 grow border-tint border-r pr-5 *:grow only:mr-0 only:border-none only:pr-0 sm:max-w-64"
                                 >
                                     <SpacesDropdown
                                         context={context}
diff --git a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx
index cedc2eee89..f69de405e3 100644
--- a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx
+++ b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx
@@ -125,7 +125,7 @@ export function SpaceLayout(props: {
                                             sections={encodeClientSiteSections(context, sections)}
                                         />
                                     )}
-                                    {isMultiVariants && (
+                                    {isMultiVariants && !sections && (
                                         <SpacesDropdown
                                             context={context}
                                             siteSpace={siteSpace}

From 8d3c6562a266fc24d20a94af7f2f9c1e629970d6 Mon Sep 17 00:00:00 2001
From: conico974 <nicodorseuil@yahoo.fr>
Date: Wed, 18 Jun 2025 19:02:32 +0200
Subject: [PATCH 127/127] Implement a StylesProvider (#3352)

Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
---
 .../Announcement/AnnouncementBanner.tsx       |  5 +-
 .../DocumentView/Table/RecordCard.tsx         | 30 +-------
 .../components/DocumentView/Table/styles.ts   | 26 +++++++
 .../TableOfContents/PageLinkItem.tsx          | 20 +----
 .../TableOfContents/ToggleableLinkItem.tsx    | 40 +---------
 .../src/components/TableOfContents/styles.ts  | 56 ++++++++++++++
 .../src/components/primitives/Button.tsx      | 50 +++----------
 .../src/components/primitives/Card.tsx        | 23 +-----
 .../src/components/primitives/Link.tsx        | 34 +++++++--
 .../components/primitives/StyleProvider.tsx   | 32 ++++++++
 .../src/components/primitives/StyledLink.tsx  | 29 ++------
 .../src/components/primitives/styles.ts       | 74 +++++++++++++++++++
 12 files changed, 246 insertions(+), 173 deletions(-)
 create mode 100644 packages/gitbook/src/components/DocumentView/Table/styles.ts
 create mode 100644 packages/gitbook/src/components/TableOfContents/styles.ts
 create mode 100644 packages/gitbook/src/components/primitives/StyleProvider.tsx
 create mode 100644 packages/gitbook/src/components/primitives/styles.ts

diff --git a/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx b/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx
index 2b07504f3f..decd177a13 100644
--- a/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx
+++ b/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx
@@ -7,7 +7,8 @@ import { tcls } from '@/lib/tailwind';
 import { type CustomizationAnnouncement, SiteInsightsLinkPosition } from '@gitbook/api';
 import { Icon, type IconName } from '@gitbook/icons';
 import { CONTAINER_STYLE } from '../layout';
-import { Link, linkStyles } from '../primitives';
+import { Link } from '../primitives';
+import { LinkStyles } from '../primitives/styles';
 import { ANNOUNCEMENT_CSS_CLASS, ANNOUNCEMENT_STORAGE_KEY } from './constants';
 
 /**
@@ -58,7 +59,7 @@ export function AnnouncementBanner(props: {
                         <div>
                             {announcement.message}
                             {hasLink ? (
-                                <div className={tcls(linkStyles, style.link, 'ml-1 inline')}>
+                                <div className={tcls(LinkStyles, style.link, 'ml-1 inline')}>
                                     {contentRef?.icon ? (
                                         <span className="mr-1 ml-2 *:inline">
                                             {contentRef?.icon}
diff --git a/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx b/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx
index c32fb01e9d..49aa7a1bc5 100644
--- a/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx
+++ b/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx
@@ -7,10 +7,11 @@ import {
 import { LinkBox, LinkOverlay } from '@/components/primitives';
 import { Image } from '@/components/utils';
 import { resolveContentRef } from '@/lib/references';
-import { type ClassValue, tcls } from '@/lib/tailwind';
+import { tcls } from '@/lib/tailwind';
 
 import { RecordColumnValue } from './RecordColumnValue';
 import type { TableRecordKV, TableViewProps } from './Table';
+import { RecordCardStyles } from './styles';
 import { getRecordValue } from './utils';
 
 export async function RecordCard(
@@ -143,35 +144,12 @@ export async function RecordCard(
         </div>
     );
 
-    const style = [
-        'group',
-        'grid',
-        'shadow-1xs',
-        'shadow-tint-9/1',
-        'depth-flat:shadow-none',
-        'rounded',
-        'straight-corners:rounded-none',
-        'circular-corners:rounded-xl',
-        'dark:shadow-transparent',
-
-        'before:pointer-events-none',
-        'before:grid-area-1-1',
-        'before:transition-shadow',
-        'before:w-full',
-        'before:h-full',
-        'before:rounded-[inherit]',
-        'before:ring-1',
-        'before:ring-tint-12/2',
-        'before:z-10',
-        'before:relative',
-    ] as ClassValue;
-
     if (target && targetRef) {
         return (
             // We don't use `Link` directly here because we could end up in a situation where
             // a link is rendered inside a link, which is not allowed in HTML.
             // It causes an hydration error in React.
-            <LinkBox href={target.href} className={tcls(style, 'hover:before:ring-tint-12/5')}>
+            <LinkBox href={target.href} classNames={['RecordCardStyles']}>
                 <LinkOverlay
                     href={target.href}
                     insights={{
@@ -187,5 +165,5 @@ export async function RecordCard(
         );
     }
 
-    return <div className={tcls(style)}>{body}</div>;
+    return <div className={tcls(RecordCardStyles)}>{body}</div>;
 }
diff --git a/packages/gitbook/src/components/DocumentView/Table/styles.ts b/packages/gitbook/src/components/DocumentView/Table/styles.ts
new file mode 100644
index 0000000000..3e33b06837
--- /dev/null
+++ b/packages/gitbook/src/components/DocumentView/Table/styles.ts
@@ -0,0 +1,26 @@
+import type { ClassValue } from '@/lib/tailwind';
+
+export const RecordCardStyles = [
+    'group',
+    'grid',
+    'shadow-1xs',
+    'shadow-tint-9/1',
+    'depth-flat:shadow-none',
+    'rounded',
+    'straight-corners:rounded-none',
+    'circular-corners:rounded-xl',
+    'dark:shadow-transparent',
+
+    'before:pointer-events-none',
+    'before:grid-area-1-1',
+    'before:transition-shadow',
+    'before:w-full',
+    'before:h-full',
+    'before:rounded-[inherit]',
+    'before:ring-1',
+    'before:ring-tint-12/2',
+    'before:z-10',
+    'before:relative',
+
+    'hover:before:ring-tint-12/5',
+] as ClassValue;
diff --git a/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx b/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx
index 35f350a0c3..c00d1c6216 100644
--- a/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx
+++ b/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx
@@ -17,25 +17,7 @@ export async function PageLinkItem(props: { page: RevisionPageLink; context: Git
         <li className={tcls('flex', 'flex-col')}>
             <Link
                 href={resolved?.href ?? '#'}
-                className={tcls(
-                    'flex',
-                    'justify-start',
-                    'items-center',
-                    'gap-3',
-                    'p-1.5',
-                    'pl-3',
-                    'text-sm',
-                    'transition-colors',
-                    'duration-100',
-                    'text-tint-strong/7',
-                    'rounded-md',
-                    'straight-corners:rounded-none',
-                    'circular-corners:rounded-xl',
-                    'before:content-none',
-                    'font-normal',
-                    'hover:bg-tint',
-                    'hover:text-tint-strong'
-                )}
+                classNames={['PageLinkItemStyles']}
                 insights={{
                     type: 'link_click',
                     link: {
diff --git a/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx b/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx
index b118501f4c..681df69080 100644
--- a/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx
+++ b/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx
@@ -66,42 +66,10 @@ function LinkItem(
             href={href}
             insights={insights}
             aria-current={isActive ? 'page' : undefined}
-            className={tcls(
-                'group/toclink toclink relative transition-colors',
-                'flex flex-row justify-between',
-                'circular-corners:rounded-2xl rounded-md straight-corners:rounded-none p-1.5 pl-3',
-                'text-balance font-normal text-sm text-tint-strong/7 hover:bg-tint-hover hover:text-tint-strong contrast-more:text-tint-strong',
-                'hover:contrast-more:text-tint-strong hover:contrast-more:ring-1 hover:contrast-more:ring-tint-12',
-                'before:contents[] before:-left-px before:absolute before:inset-y-0',
-                'sidebar-list-line:rounded-l-none sidebar-list-line:before:w-px sidebar-list-default:[&+div_a]:rounded-l-none [&+div_a]:pl-5 sidebar-list-default:[&+div_a]:before:w-px',
-
-                isActive && [
-                    'font-semibold',
-                    'sidebar-list-line:before:w-0.5',
-
-                    'before:bg-primary-solid',
-                    'text-primary-subtle',
-                    'contrast-more:text-primary',
-
-                    'sidebar-list-pill:bg-primary',
-                    '[html.sidebar-list-pill.theme-muted_&]:bg-primary-hover',
-                    '[html.sidebar-list-pill.theme-bold.tint_&]:bg-primary-hover',
-                    '[html.sidebar-filled.sidebar-list-pill.theme-muted_&]:bg-primary',
-                    '[html.sidebar-filled.sidebar-list-pill.theme-bold.tint_&]:bg-primary',
-
-                    'hover:bg-primary-hover',
-                    'hover:text-primary',
-                    'hover:before:bg-primary-solid-hover',
-                    'sidebar-list-pill:hover:bg-primary-hover',
-
-                    'contrast-more:text-primary',
-                    'contrast-more:hover:text-primary-strong',
-                    'contrast-more:bg-primary',
-                    'contrast-more:ring-1',
-                    'contrast-more:ring-primary',
-                    'contrast-more:hover:ring-primary-hover',
-                ]
-            )}
+            classNames={[
+                'ToggleableLinkItemStyles',
+                ...(isActive ? ['ToggleableLinkItemActiveStyles' as const] : []),
+            ]}
         >
             {children}
         </Link>
diff --git a/packages/gitbook/src/components/TableOfContents/styles.ts b/packages/gitbook/src/components/TableOfContents/styles.ts
new file mode 100644
index 0000000000..3ceb636941
--- /dev/null
+++ b/packages/gitbook/src/components/TableOfContents/styles.ts
@@ -0,0 +1,56 @@
+export const PageLinkItemStyles = [
+    'flex',
+    'justify-start',
+    'items-center',
+    'gap-3',
+    'p-1.5',
+    'pl-3',
+    'text-sm',
+    'transition-colors',
+    'duration-100',
+    'text-tint-strong/7',
+    'rounded-md',
+    'straight-corners:rounded-none',
+    'circular-corners:rounded-xl',
+    'before:content-none',
+    'font-normal',
+    'hover:bg-tint',
+    'hover:text-tint-strong',
+];
+
+export const ToggleableLinkItemStyles = [
+    'group/toclink toclink relative transition-colors',
+    'flex flex-row justify-between',
+    'circular-corners:rounded-2xl rounded-md straight-corners:rounded-none p-1.5 pl-3',
+    'text-balance font-normal text-sm text-tint-strong/7 hover:bg-tint-hover hover:text-tint-strong contrast-more:text-tint-strong',
+    'hover:contrast-more:text-tint-strong hover:contrast-more:ring-1 hover:contrast-more:ring-tint-12',
+    'before:contents[] before:-left-px before:absolute before:inset-y-0',
+    'sidebar-list-line:rounded-l-none sidebar-list-line:before:w-px sidebar-list-default:[&+div_a]:rounded-l-none [&+div_a]:pl-5 sidebar-list-default:[&+div_a]:before:w-px',
+];
+
+export const ToggleableLinkItemActiveStyles = [
+    'font-semibold',
+    'sidebar-list-line:before:w-0.5',
+
+    'before:bg-primary-solid',
+    'text-primary-subtle',
+    'contrast-more:text-primary',
+
+    'sidebar-list-pill:bg-primary',
+    '[html.sidebar-list-pill.theme-muted_&]:bg-primary-hover',
+    '[html.sidebar-list-pill.theme-bold.tint_&]:bg-primary-hover',
+    '[html.sidebar-filled.sidebar-list-pill.theme-muted_&]:bg-primary',
+    '[html.sidebar-filled.sidebar-list-pill.theme-bold.tint_&]:bg-primary',
+
+    'hover:bg-primary-hover',
+    'hover:text-primary',
+    'hover:before:bg-primary-solid-hover',
+    'sidebar-list-pill:hover:bg-primary-hover',
+
+    'contrast-more:text-primary',
+    'contrast-more:hover:text-primary-strong',
+    'contrast-more:bg-primary',
+    'contrast-more:ring-1',
+    'contrast-more:ring-primary',
+    'contrast-more:hover:ring-primary-hover',
+];
diff --git a/packages/gitbook/src/components/primitives/Button.tsx b/packages/gitbook/src/components/primitives/Button.tsx
index 58697d5b7f..4ccc10b6b7 100644
--- a/packages/gitbook/src/components/primitives/Button.tsx
+++ b/packages/gitbook/src/components/primitives/Button.tsx
@@ -6,6 +6,7 @@ import { type ClassValue, tcls } from '@/lib/tailwind';
 
 import { Icon, type IconName } from '@gitbook/icons';
 import { Link, type LinkInsightsProps } from './Link';
+import { useClassnames } from './StyleProvider';
 
 type ButtonProps = {
     href?: string;
@@ -18,7 +19,7 @@ type ButtonProps = {
 } & LinkInsightsProps &
     HTMLAttributes<HTMLElement>;
 
-const variantClasses = {
+export const variantClasses = {
     primary: [
         'bg-primary-solid',
         'text-contrast-primary-solid',
@@ -69,49 +70,15 @@ export function Button({
 
     const sizeClasses = sizes[size] || sizes.default;
 
-    const domClassName = tcls(
-        'button',
-        'inline-flex',
-        'items-center',
-        'gap-2',
-        'rounded-md',
-        'straight-corners:rounded-none',
-        'circular-corners:rounded-full',
-        // 'place-self-start',
-
-        'ring-1',
-        'ring-tint',
-        'hover:ring-tint-hover',
-
-        'shadow-sm',
-        'shadow-tint',
-        'dark:shadow-tint-1',
-        'hover:shadow-md',
-        'active:shadow-none',
-        'depth-flat:shadow-none',
-
-        'contrast-more:ring-tint-12',
-        'contrast-more:hover:ring-2',
-        'contrast-more:hover:ring-tint-12',
-
-        'hover:scale-104',
-        'depth-flat:hover:scale-100',
-        'active:scale-100',
-        'transition-all',
-
-        'grow-0',
-        'shrink-0',
-        'truncate',
-        variantClasses[variant],
-        sizeClasses,
-        className
-    );
+    const domClassName = tcls(variantClasses[variant], sizeClasses, className);
+    const buttonOnlyClassNames = useClassnames(['ButtonStyles']);
 
     if (href) {
         return (
             <Link
                 href={href}
                 className={domClassName}
+                classNames={['ButtonStyles']}
                 insights={insights}
                 aria-label={label}
                 target={target}
@@ -124,7 +91,12 @@ export function Button({
     }
 
     return (
-        <button type="button" className={domClassName} aria-label={label} {...rest}>
+        <button
+            type="button"
+            className={tcls(domClassName, buttonOnlyClassNames)}
+            aria-label={label}
+            {...rest}
+        >
             {icon ? <Icon icon={icon} className={tcls('size-[1em]')} /> : null}
             {iconOnly ? null : label}
         </button>
diff --git a/packages/gitbook/src/components/primitives/Card.tsx b/packages/gitbook/src/components/primitives/Card.tsx
index a796ba9eff..1195c78476 100644
--- a/packages/gitbook/src/components/primitives/Card.tsx
+++ b/packages/gitbook/src/components/primitives/Card.tsx
@@ -17,28 +17,7 @@ export async function Card(
     const { title, leadingIcon, href, preTitle, postTitle, style, insights } = props;
 
     return (
-        <Link
-            href={href}
-            className={tcls(
-                'group',
-                'flex',
-                'flex-row',
-                'justify-between',
-                'items-center',
-                'gap-4',
-                'ring-1',
-                'ring-tint-subtle',
-                'rounded',
-                'straight-corners:rounded-none',
-                'circular-corners:rounded-2xl',
-                'px-5',
-                'py-3',
-                'transition-shadow',
-                'hover:ring-primary-hover',
-                style
-            )}
-            insights={insights}
-        >
+        <Link href={href} className={tcls(style)} classNames={['CardStyles']} insights={insights}>
             {leadingIcon}
             <span className={tcls('flex', 'flex-col', 'flex-1')}>
                 {preTitle ? (
diff --git a/packages/gitbook/src/components/primitives/Link.tsx b/packages/gitbook/src/components/primitives/Link.tsx
index ff9f5ebde1..e15a01ca11 100644
--- a/packages/gitbook/src/components/primitives/Link.tsx
+++ b/packages/gitbook/src/components/primitives/Link.tsx
@@ -5,6 +5,7 @@ import React from 'react';
 
 import { tcls } from '@/lib/tailwind';
 import { type TrackEventInput, useTrackEvent } from '../Insights';
+import { type DesignTokenName, useClassnames } from './StyleProvider';
 
 // Props from Next, which includes NextLinkProps and all the things anchor elements support.
 type BaseLinkProps = Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof NextLinkProps> &
@@ -25,6 +26,8 @@ export type LinkProps = Omit<BaseLinkProps, 'href'> &
     LinkInsightsProps & {
         /** Enforce href is passed as a string (not a URL). */
         href: string;
+        /** This is a temporary solution designed to reduce the number of tailwind class passed to the client */
+        classNames?: DesignTokenName[];
     };
 
 /**
@@ -35,8 +38,9 @@ export const Link = React.forwardRef(function Link(
     props: LinkProps,
     ref: React.Ref<HTMLAnchorElement>
 ) {
-    const { href, prefetch, children, insights, ...domProps } = props;
+    const { href, prefetch, children, insights, classNames, className, ...domProps } = props;
     const trackEvent = useTrackEvent();
+    const forwardedClassNames = useClassnames(classNames || []);
 
     // Use a real anchor tag for external links,s and a Next.js Link for internal links.
     // If we use a NextLink for external links, Nextjs won't rerender the top-level layouts.
@@ -63,14 +67,27 @@ export const Link = React.forwardRef(function Link(
     // as this will be rendered on the server and it could result in a mismatch.
     if (isExternalLink(href)) {
         return (
-            <a ref={ref} {...domProps} href={href} onClick={onClick}>
+            <a
+                ref={ref}
+                className={tcls(...forwardedClassNames, className)}
+                {...domProps}
+                href={href}
+                onClick={onClick}
+            >
                 {children}
             </a>
         );
     }
 
     return (
-        <NextLink ref={ref} href={href} prefetch={prefetch} {...domProps} onClick={onClick}>
+        <NextLink
+            ref={ref}
+            href={href}
+            prefetch={prefetch}
+            className={tcls(...forwardedClassNames, className)}
+            {...domProps}
+            onClick={onClick}
+        >
             {children}
         </NextLink>
     );
@@ -81,12 +98,17 @@ export const Link = React.forwardRef(function Link(
  * It is used to create a clickable area that can contain other elements.
  */
 export const LinkBox = React.forwardRef(function LinkBox(
-    props: React.BaseHTMLAttributes<HTMLDivElement>,
+    props: React.BaseHTMLAttributes<HTMLDivElement> & { classNames?: DesignTokenName[] },
     ref: React.Ref<HTMLDivElement>
 ) {
-    const { children, className, ...domProps } = props;
+    const { children, className, classNames, ...domProps } = props;
+    const forwardedClassNames = useClassnames(classNames || []);
     return (
-        <div ref={ref} {...domProps} className={tcls('elevate-link relative', className)}>
+        <div
+            ref={ref}
+            {...domProps}
+            className={tcls('elevate-link relative', className, forwardedClassNames)}
+        >
             {children}
         </div>
     );
diff --git a/packages/gitbook/src/components/primitives/StyleProvider.tsx b/packages/gitbook/src/components/primitives/StyleProvider.tsx
new file mode 100644
index 0000000000..a1e1b1fe03
--- /dev/null
+++ b/packages/gitbook/src/components/primitives/StyleProvider.tsx
@@ -0,0 +1,32 @@
+'use client';
+import type { ClassValue } from '@/lib/tailwind';
+
+import { RecordCardStyles } from '../DocumentView/Table/styles';
+import {
+    PageLinkItemStyles,
+    ToggleableLinkItemActiveStyles,
+    ToggleableLinkItemStyles,
+} from '../TableOfContents/styles';
+import { ButtonStyles, CardStyles, LinkStyles } from './styles';
+
+const styles = {
+    LinkStyles,
+    CardStyles,
+    ButtonStyles,
+    RecordCardStyles,
+    PageLinkItemStyles,
+    ToggleableLinkItemStyles,
+    ToggleableLinkItemActiveStyles,
+};
+
+export type DesignTokenName = keyof typeof styles;
+
+/**
+ * Get the class names for the given design token names.
+ * TODO: remove this once we figure out a better solution. Likely with TW4.
+ * @param names The design token names to get class names for.
+ * @returns The class names for the given design token names.
+ */
+export function useClassnames(names: DesignTokenName[]): ClassValue[] {
+    return names.flatMap((name) => styles[name] || []);
+}
diff --git a/packages/gitbook/src/components/primitives/StyledLink.tsx b/packages/gitbook/src/components/primitives/StyledLink.tsx
index 3fb346f5f6..f3b391b67e 100644
--- a/packages/gitbook/src/components/primitives/StyledLink.tsx
+++ b/packages/gitbook/src/components/primitives/StyledLink.tsx
@@ -1,35 +1,18 @@
-import { type ClassValue, tcls } from '@/lib/tailwind';
+import type { ClassValue } from '@/lib/tailwind';
 
 import { Link, type LinkProps } from '../primitives/Link';
-
-export const linkStyles = [
-    'underline',
-    'decoration-[max(0.07em,1px)]', // Set the underline to be proportional to the font size, with a minimum. The default is too thin.
-    'underline-offset-2',
-    'links-accent:underline-offset-4',
-
-    'links-default:decoration-primary/6',
-    'links-default:text-primary-subtle',
-    'links-default:hover:text-primary-strong',
-    'links-default:contrast-more:text-primary',
-    'links-default:contrast-more:hover:text-primary-strong',
-
-    'links-accent:decoration-primary-subtle',
-    'links-accent:hover:decoration-[3px]',
-    'links-accent:hover:[text-decoration-skip-ink:none]',
-
-    'transition-all',
-    'duration-100',
-];
+import type { DesignTokenName } from './StyleProvider';
 
 /**
  * Styled version of Link component.
  */
 export function StyledLink(props: Omit<LinkProps, 'style'> & { className?: ClassValue }) {
-    const { className, ...rest } = props;
+    const { classNames, ...rest } = props;
+
+    const classNamesToForward: DesignTokenName[] = [...(classNames || []), 'LinkStyles'];
 
     return (
-        <Link {...rest} className={tcls(linkStyles, className)}>
+        <Link {...rest} classNames={classNamesToForward}>
             {props.children}
         </Link>
     );
diff --git a/packages/gitbook/src/components/primitives/styles.ts b/packages/gitbook/src/components/primitives/styles.ts
new file mode 100644
index 0000000000..9d465aac4a
--- /dev/null
+++ b/packages/gitbook/src/components/primitives/styles.ts
@@ -0,0 +1,74 @@
+import type { ClassValue } from '@/lib/tailwind';
+
+export const ButtonStyles = [
+    'button',
+    'inline-flex',
+    'items-center',
+    'gap-2',
+    'rounded-md',
+    'straight-corners:rounded-none',
+    'circular-corners:rounded-full',
+    // 'place-self-start',
+
+    'ring-1',
+    'ring-tint',
+    'hover:ring-tint-hover',
+
+    'shadow-sm',
+    'shadow-tint',
+    'dark:shadow-tint-1',
+    'hover:shadow-md',
+    'active:shadow-none',
+    'depth-flat:shadow-none',
+
+    'contrast-more:ring-tint-12',
+    'contrast-more:hover:ring-2',
+    'contrast-more:hover:ring-tint-12',
+
+    'hover:scale-104',
+    'depth-flat:hover:scale-100',
+    'active:scale-100',
+    'transition-all',
+
+    'grow-0',
+    'shrink-0',
+    'truncate',
+] as ClassValue[];
+
+export const CardStyles = [
+    'group',
+    'flex',
+    'flex-row',
+    'justify-between',
+    'items-center',
+    'gap-4',
+    'ring-1',
+    'ring-tint-subtle',
+    'rounded',
+    'straight-corners:rounded-none',
+    'circular-corners:rounded-2xl',
+    'px-5',
+    'py-3',
+    'transition-shadow',
+    'hover:ring-primary-hover',
+] as ClassValue[];
+
+export const LinkStyles = [
+    'underline',
+    'decoration-[max(0.07em,1px)]', // Set the underline to be proportional to the font size, with a minimum. The default is too thin.
+    'underline-offset-2',
+    'links-accent:underline-offset-4',
+
+    'links-default:decoration-primary/6',
+    'links-default:text-primary-subtle',
+    'links-default:hover:text-primary-strong',
+    'links-default:contrast-more:text-primary',
+    'links-default:contrast-more:hover:text-primary-strong',
+
+    'links-accent:decoration-primary-subtle',
+    'links-accent:hover:decoration-[3px]',
+    'links-accent:hover:[text-decoration-skip-ink:none]',
+
+    'transition-all',
+    'duration-100',
+] as ClassValue[];