From 3c0e49d56bfbf98523cef3eaf37de4295a1f7974 Mon Sep 17 00:00:00 2001 From: conico974 Date: Fri, 17 Oct 2025 19:42:16 +0200 Subject: [PATCH 01/48] Fix cache race condition (#3738) --- packages/gitbook/open-next.config.ts | 6 ++++-- .../{ => incrementalCache}/incrementalCache.ts | 13 +------------ .../gitbook/openNext/incrementalCache/middleware.ts | 11 +++++++++++ .../gitbook/openNext/incrementalCache/server.ts | 12 ++++++++++++ 4 files changed, 28 insertions(+), 14 deletions(-) rename packages/gitbook/openNext/{ => incrementalCache}/incrementalCache.ts (90%) create mode 100644 packages/gitbook/openNext/incrementalCache/middleware.ts create mode 100644 packages/gitbook/openNext/incrementalCache/server.ts diff --git a/packages/gitbook/open-next.config.ts b/packages/gitbook/open-next.config.ts index 187230911..f4f5c25df 100644 --- a/packages/gitbook/open-next.config.ts +++ b/packages/gitbook/open-next.config.ts @@ -7,7 +7,8 @@ export default { converter: 'edge', proxyExternalRequest: 'fetch', queue: () => import('./openNext/queue/middleware').then((m) => m.default), - incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default), + incrementalCache: () => + import('./openNext/incrementalCache/server').then((m) => m.default), tagCache: () => import('./openNext/tagCache/middleware').then((m) => m.default), }, }, @@ -18,7 +19,8 @@ export default { converter: 'edge', proxyExternalRequest: 'fetch', queue: () => import('./openNext/queue/middleware').then((m) => m.default), - incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default), + incrementalCache: () => + import('./openNext/incrementalCache/middleware').then((m) => m.default), tagCache: () => import('./openNext/tagCache/middleware').then((m) => m.default), }, }, diff --git a/packages/gitbook/openNext/incrementalCache.ts b/packages/gitbook/openNext/incrementalCache/incrementalCache.ts similarity index 90% rename from packages/gitbook/openNext/incrementalCache.ts rename to packages/gitbook/openNext/incrementalCache/incrementalCache.ts index 9fde1fc41..11c0f8977 100644 --- a/packages/gitbook/openNext/incrementalCache.ts +++ b/packages/gitbook/openNext/incrementalCache/incrementalCache.ts @@ -7,8 +7,6 @@ import type { } from '@opennextjs/aws/types/overrides.js'; import { getCloudflareContext } from '@opennextjs/cloudflare'; -import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache'; - import type { DurableObjectNamespace, Rpc } from '@cloudflare/workers-types'; export const BINDING_NAME = 'NEXT_INC_CACHE_R2_BUCKET'; @@ -23,7 +21,7 @@ export type KeyOptions = { * It is very similar to the `R2IncrementalCache` in the `@opennextjs/cloudflare` package, but it has an additional * R2WriteBuffer Durable Object to handle writes to R2. Given how we set up cache, we often end up writing to the same key too fast. */ -class GitbookIncrementalCache implements IncrementalCache { +export class GitbookIncrementalCache implements IncrementalCache { name = 'GitbookIncrementalCache'; async get( @@ -137,12 +135,3 @@ class GitbookIncrementalCache implements IncrementalCache { ); } } - -export default withRegionalCache(new GitbookIncrementalCache(), { - mode: 'long-lived', - // We can do it because we use our own logic to invalidate the cache - bypassTagCacheOnCacheHit: true, - defaultLongLivedTtlSec: 60 * 60 * 24 /* 24 hours */, - // We don't want to update the cache entry on every cache hit - shouldLazilyUpdateOnCacheHit: false, -}); diff --git a/packages/gitbook/openNext/incrementalCache/middleware.ts b/packages/gitbook/openNext/incrementalCache/middleware.ts new file mode 100644 index 000000000..2204ae4c6 --- /dev/null +++ b/packages/gitbook/openNext/incrementalCache/middleware.ts @@ -0,0 +1,11 @@ +import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache'; +import { GitbookIncrementalCache } from './incrementalCache'; + +export default withRegionalCache(new GitbookIncrementalCache(), { + mode: 'long-lived', + // We can do it because we use our own logic to invalidate the cache + bypassTagCacheOnCacheHit: true, + defaultLongLivedTtlSec: 60 * 60 * 24 /* 24 hours */, + // We don't want to update the cache entry on every cache hit + shouldLazilyUpdateOnCacheHit: false, +}); diff --git a/packages/gitbook/openNext/incrementalCache/server.ts b/packages/gitbook/openNext/incrementalCache/server.ts new file mode 100644 index 000000000..87c496325 --- /dev/null +++ b/packages/gitbook/openNext/incrementalCache/server.ts @@ -0,0 +1,12 @@ +import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache'; +import { GitbookIncrementalCache } from './incrementalCache'; + +export default withRegionalCache(new GitbookIncrementalCache(), { + mode: 'long-lived', + // Because of a race condition, the middleware may have populated the cache entry before `cache.match` had time to run on the server. + // TODO: We should bypass the incremental cache entirely when the interceptor has caught the request. Should be done in OpenNext. + bypassTagCacheOnCacheHit: false, + defaultLongLivedTtlSec: 60 * 60 * 24 /* 24 hours */, + // We don't want to update the cache entry on every cache hit + shouldLazilyUpdateOnCacheHit: false, +}); From 1dcf4df51f5f4c8f611712b1e752e9c5ccbc2e29 Mon Sep 17 00:00:00 2001 From: conico974 Date: Fri, 17 Oct 2025 21:20:33 +0200 Subject: [PATCH 02/48] Fix table of contents display issue (#3740) --- .../src/components/TableOfContents/ToggleableLinkItem.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx b/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx index 9ab582ea0..5656ba3cd 100644 --- a/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx +++ b/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx @@ -180,6 +180,7 @@ function Toggler(props: { const show = { opacity: 1, height: 'auto', + display: 'inherit', }; const hide = { opacity: 0, From c32d4494f2ba6218803c17e3752322e51592007a Mon Sep 17 00:00:00 2001 From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com> Date: Sun, 19 Oct 2025 21:44:26 +0200 Subject: [PATCH 03/48] Fix OpenAPI path display (#3743) --- .changeset/solid-things-own.md | 5 +++++ .../src/components/DocumentView/OpenAPI/style.css | 10 +++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 .changeset/solid-things-own.md diff --git a/.changeset/solid-things-own.md b/.changeset/solid-things-own.md new file mode 100644 index 000000000..3f7263a42 --- /dev/null +++ b/.changeset/solid-things-own.md @@ -0,0 +1,5 @@ +--- +'gitbook': patch +--- + +Fix OpenAPI path display diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css index a4b9f28f3..5fac6a4f3 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css @@ -454,6 +454,10 @@ @apply flex items-center font-mono text-[0.813rem] gap-1 h-fit *:truncate overflow-x-auto min-w-0 max-w-full font-normal text-tint-strong; } +.openapi-codesample-header-content .openapi-path-title { + @apply block; +} + .openapi-codesample-header-content .openapi-path .openapi-path-variable { @apply text-[0.813rem]; } @@ -486,7 +490,7 @@ } .openapi-path-title { - @apply flex-1 relative font-normal text-left overflow-x-auto font-mono text-tint-strong/10; + @apply flex-1 relative font-normal items-center gap-y-1 flex flex-wrap text-left overflow-x-auto font-mono text-tint-strong/10; @apply whitespace-nowrap md:whitespace-normal; scrollbar-width: none; -ms-overflow-style: none; @@ -606,7 +610,7 @@ body:has(.openapi-select-popover) { } .openapi-select-unstyled > button { - @apply p-1; + @apply p-1 *:truncate max-w-full; } .openapi-select > button[data-focused="true"] { @@ -998,7 +1002,7 @@ body:has(.openapi-select-popover) { } .openapi-path-copy-button-icon { - @apply size-6 flex opacity-0 transition-all; + @apply size-6 opacity-0 transition-all hidden sm:flex; } .openapi-path:hover .openapi-path-copy-button-icon { From 3355960d95a4ee0e3fde28c4a0205c00b8f800d5 Mon Sep 17 00:00:00 2001 From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com> Date: Tue, 21 Oct 2025 10:32:17 +0200 Subject: [PATCH 04/48] Fix ToggeableLinkItem display (#3744) Co-authored-by: Zeno Kapitein --- .changeset/quiet-icons-deny.md | 5 ++++ .../TableOfContents/ToggleableLinkItem.tsx | 25 ++++++++++--------- 2 files changed, 18 insertions(+), 12 deletions(-) create mode 100644 .changeset/quiet-icons-deny.md diff --git a/.changeset/quiet-icons-deny.md b/.changeset/quiet-icons-deny.md new file mode 100644 index 000000000..9345f6b8f --- /dev/null +++ b/.changeset/quiet-icons-deny.md @@ -0,0 +1,5 @@ +--- +'gitbook': patch +--- + +Fix ToggeableLinkItem display diff --git a/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx b/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx index 5656ba3cd..4d222da04 100644 --- a/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx +++ b/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx @@ -1,7 +1,7 @@ 'use client'; import { Icon } from '@gitbook/icons'; -import { motion } from 'motion/react'; +import { AnimatePresence, motion } from 'motion/react'; import React, { useRef } from 'react'; import { tcls } from '@/lib/tailwind'; @@ -180,14 +180,10 @@ function Toggler(props: { const show = { opacity: 1, height: 'auto', - display: 'inherit', }; const hide = { opacity: 0, height: 0, - transitionEnd: { - display: 'none', - }, }; function Descendants(props: { @@ -196,12 +192,17 @@ function Descendants(props: { }) { const { isVisible, children } = props; return ( - - {children} - + + {isVisible ? ( + + {children} + + ) : null} + ); } From d159788d0b27deec46bec4ab037f5eddbda7b460 Mon Sep 17 00:00:00 2001 From: conico974 Date: Tue, 21 Oct 2025 12:39:23 +0200 Subject: [PATCH 05/48] Add end-to-end tests for table of contents navigation (#3741) --- packages/gitbook/e2e/internal.spec.ts | 52 +++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/packages/gitbook/e2e/internal.spec.ts b/packages/gitbook/e2e/internal.spec.ts index f008550d4..110996338 100644 --- a/packages/gitbook/e2e/internal.spec.ts +++ b/packages/gitbook/e2e/internal.spec.ts @@ -228,6 +228,58 @@ const testCases: TestsCase[] = [ ); }, }, + { + name: 'Expandable TOC navigation', + url: '', + run: async (page) => { + await waitForCookiesDialog(page); + + // Verify "Navigation" link is not visible initially + const navigationLink = page.getByRole('link', { name: 'Navigation' }); + await expect(navigationLink).not.toBeVisible(); + + // Find and click the chevron element that is next to "Editor" in the TOC + // It is a span inside the link + const editorChevron = page + .getByRole('link', { name: 'Editor' }) + .locator('span'); + await editorChevron.click(); + + // Verify "Navigation" link becomes visible after expansion + await expect(navigationLink).toBeVisible(); + }, + }, + { + name: 'Expandable nested TOC navigation', + url: '', + screenshot: false, + run: async (page) => { + await waitForCookiesDialog(page); + + // Verify "Spaces" link is not visible initially + const navigationLink = page.getByRole('link', { name: 'Spaces' }); + await expect(navigationLink).not.toBeVisible(); + + // Find and click the chevron element that is next to "Editor" in the TOC + // It is a span inside the link + const editorChevron = page + .getByRole('link', { name: 'Editor' }) + .locator('span'); + await editorChevron.click(); + + // At this stage the link should still not be visible + await expect(navigationLink).not.toBeVisible(); + + // Then we click 'Content Structure' chevron to expand further + const contentStructureChevron = page + .getByRole('link', { name: 'Content Structure' }) + .locator('span'); + await contentStructureChevron.click(); + + // Verify "Spaces" link becomes visible after expansion + await expect(navigationLink).toBeVisible(); + }, + }, ...searchTestCases, { name: 'Not found', From d436b04ff4545be24611b870b4d40e003c611fcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= Date: Tue, 21 Oct 2025 18:30:33 +0200 Subject: [PATCH 06/48] Fix jump when clicking tabs (#3746) --- .../DocumentView/Tabs/DynamicTabs.tsx | 18 +++++-------- .../src/components/hooks/usePrevious.ts | 12 +++++++++ .../src/components/hooks/useScrollPage.ts | 26 ++++++++++++------- 3 files changed, 35 insertions(+), 21 deletions(-) create mode 100644 packages/gitbook/src/components/hooks/usePrevious.ts diff --git a/packages/gitbook/src/components/DocumentView/Tabs/DynamicTabs.tsx b/packages/gitbook/src/components/DocumentView/Tabs/DynamicTabs.tsx index ad0dd304f..baa8506b1 100644 --- a/packages/gitbook/src/components/DocumentView/Tabs/DynamicTabs.tsx +++ b/packages/gitbook/src/components/DocumentView/Tabs/DynamicTabs.tsx @@ -2,12 +2,7 @@ import React, { memo, useCallback, useMemo, type ComponentPropsWithRef } from 'react'; -import { - NavigationStatusContext, - useHash, - useIsMounted, - useListOverflow, -} from '@/components/hooks'; +import { useHash, useIsMounted, useListOverflow } from '@/components/hooks'; import { DropdownMenu, DropdownMenuItem } from '@/components/primitives'; import { useLanguage } from '@/intl/client'; import { tString } from '@/intl/translate'; @@ -74,7 +69,6 @@ export function DynamicTabs(props: { }) { const { id, tabs, className } = props; const router = useRouter(); - const { onNavigationClick } = React.useContext(NavigationStatusContext); const hash = useHash(); const [tabsState, setTabsState] = useTabsState(); @@ -106,8 +100,7 @@ export function DynamicTabs(props: { const href = `#${tab.id}`; if (window.location.hash !== href) { - router.replace(href); - onNavigationClick(href); + router.replace(href, { scroll: false }); } setTabsState((prev) => { @@ -128,7 +121,7 @@ export function DynamicTabs(props: { }; }); }, - [onNavigationClick, router, setTabsState, tabs, id] + [router, setTabsState, tabs, id] ); // When the hash changes, we try to select the tab containing the targetted element. @@ -184,7 +177,10 @@ const TabPanel = memo(function TabPanel(props: { role="tabpanel" id={tab.id} aria-labelledby={getTabButtonId(tab.id)} - className={tcls('p-4', isActive ? null : 'hidden')} + className={tcls( + 'scroll-mt-[calc(var(--content-scroll-margin)+var(--spacing)*12)] p-4', + isActive ? null : 'hidden' + )} > {tab.body} diff --git a/packages/gitbook/src/components/hooks/usePrevious.ts b/packages/gitbook/src/components/hooks/usePrevious.ts new file mode 100644 index 000000000..5a3adfa6f --- /dev/null +++ b/packages/gitbook/src/components/hooks/usePrevious.ts @@ -0,0 +1,12 @@ +import * as React from 'react'; + +/** + * Returns the value of the previous render. + */ +export function usePrevious(value: T): T | undefined { + const ref = React.useRef(undefined); + React.useLayoutEffect(() => { + ref.current = value; + }); + return ref.current; +} diff --git a/packages/gitbook/src/components/hooks/useScrollPage.ts b/packages/gitbook/src/components/hooks/useScrollPage.ts index 1b1219a29..100174f62 100644 --- a/packages/gitbook/src/components/hooks/useScrollPage.ts +++ b/packages/gitbook/src/components/hooks/useScrollPage.ts @@ -4,6 +4,7 @@ import { usePathname } from 'next/navigation'; import React from 'react'; import { useHash } from './useHash'; +import { usePrevious } from './usePrevious'; /** * Scroll the page to an anchor point or @@ -12,8 +13,17 @@ import { useHash } from './useHash'; */ export function useScrollPage() { const hash = useHash(); + const previousHash = usePrevious(hash); const pathname = usePathname(); + const previousPathname = usePrevious(pathname); React.useLayoutEffect(() => { + // If there is no change in pathname or hash, do nothing + if (previousHash === hash && previousPathname === pathname) { + return; + } + + // If there is a hash + // - Triggered by a change of hash or pathname if (hash) { const element = document.getElementById(hash); if (element) { @@ -22,16 +32,12 @@ export function useScrollPage() { behavior: 'smooth', }); } - } else { + return; + } + + // If there was a hash but not anymore, scroll to top + if (previousHash && !hash) { window.scrollTo(0, 0); } - return () => { - if (hash) { - const element = document.getElementById(hash); - if (element) { - element.style.scrollMarginTop = ''; - } - } - }; - }, [hash, pathname]); + }, [hash, previousHash, pathname, previousPathname]); } From 6b588af07d2fda31a33cacfe904d5e1937607f2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= Date: Wed, 22 Oct 2025 14:43:28 +0200 Subject: [PATCH 07/48] Clarify CONTRIBUTING.md (#3742) --- .github/CONTRIBUTING.md | 7 +++---- package.json | 3 +++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index f5a5d0c97..d5c6f5a21 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -54,10 +54,9 @@ After forking this repository, you'll want to [create a branch](https://docs.git #### 3. Install dependencies and run the project locally ##### Prerequisites: -- Node.js (Version: >= 22.3) - - Use `nvm` for easy Node management -- [Bun](https://bun.sh/) (Version: >=1.2.15) - - We use a text-based lockfile which isn't supported below 1.2.15 + +- [Node.js](https://nodejs.org/en) (see "engines" in `package.json`) +- [Bun](https://bun.sh/) (see "packageManager" in `package.json`) ##### Setup steps: diff --git a/package.json b/package.json index 4d9be922b..2baf43e0e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,9 @@ { "name": "gitbook", "version": "0.1.0", + "engines": { + "node": "^22.3.0" + }, "devDependencies": { "@biomejs/biome": "^1.9.4", "@changesets/cli": "^2.29.7", From 3164f1a99d12f4230798035d4f8f5239307914e2 Mon Sep 17 00:00:00 2001 From: conico974 Date: Wed, 22 Oct 2025 15:09:42 +0200 Subject: [PATCH 08/48] Fix preloading images (#3747) --- .../src/components/DocumentView/Drawing.tsx | 1 + .../src/components/DocumentView/Embed.tsx | 1 + .../src/components/DocumentView/Images.tsx | 3 +- .../components/DocumentView/InlineImage.tsx | 3 +- .../DocumentView/Table/RecordCard.tsx | 3 +- .../DocumentView/Table/RecordColumnValue.tsx | 4 +-- .../gitbook/src/components/Footer/Footer.tsx | 2 +- .../components/Header/CurrentContentIcon.tsx | 1 + .../src/components/Header/HeaderLogo.tsx | 2 +- .../gitbook/src/components/utils/Image.tsx | 31 ++----------------- 10 files changed, 13 insertions(+), 38 deletions(-) diff --git a/packages/gitbook/src/components/DocumentView/Drawing.tsx b/packages/gitbook/src/components/DocumentView/Drawing.tsx index ca565a690..25a9a8c03 100644 --- a/packages/gitbook/src/components/DocumentView/Drawing.tsx +++ b/packages/gitbook/src/components/DocumentView/Drawing.tsx @@ -31,6 +31,7 @@ export async function Drawing(props: BlockProps) { alt="Drawing" sizes={imageBlockSizes} zoom + loading="lazy" /> ); diff --git a/packages/gitbook/src/components/DocumentView/Embed.tsx b/packages/gitbook/src/components/DocumentView/Embed.tsx index 0b433b689..3977d30af 100644 --- a/packages/gitbook/src/components/DocumentView/Embed.tsx +++ b/packages/gitbook/src/components/DocumentView/Embed.tsx @@ -60,6 +60,7 @@ export async function Embed(props: BlockProps) { sources={{ light: { src: embed.icon } }} sizes={[{ width: 20 }]} resize={context.contentContext.imageResizer} + loading="lazy" /> ) : null } diff --git a/packages/gitbook/src/components/DocumentView/Images.tsx b/packages/gitbook/src/components/DocumentView/Images.tsx index 2d39a7d94..27db314d2 100644 --- a/packages/gitbook/src/components/DocumentView/Images.tsx +++ b/packages/gitbook/src/components/DocumentView/Images.tsx @@ -135,8 +135,7 @@ async function ImageBlock(props: { } : null, }} - priority={isEstimatedOffscreen ? 'lazy' : 'high'} - preload + loading={isEstimatedOffscreen ? 'lazy' : 'eager'} zoom inlineStyle={{ maxWidth: '100%', diff --git a/packages/gitbook/src/components/DocumentView/InlineImage.tsx b/packages/gitbook/src/components/DocumentView/InlineImage.tsx index 894c56b57..2fe6d8111 100644 --- a/packages/gitbook/src/components/DocumentView/InlineImage.tsx +++ b/packages/gitbook/src/components/DocumentView/InlineImage.tsx @@ -49,8 +49,7 @@ export async function InlineImage(props: InlineProps) { } : null, }} - priority="lazy" - preload + loading="lazy" style={[size === 'line' ? ['max-h-lh', 'h-lh', 'w-auto'] : null]} inline zoom={!isInLink} diff --git a/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx b/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx index 57d814d93..68ddb254d 100644 --- a/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx +++ b/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx @@ -120,8 +120,7 @@ export async function RecordCard( : ['h-auto', 'aspect-video'], objectFits )} - priority={isOffscreen ? 'lazy' : 'high'} - preload + loading={isOffscreen ? 'lazy' : 'eager'} /> ) : null}
( }, }, }} - priority="lazy" + loading="lazy" /> ) : ( ( size: image.file?.dimensions, }, }} - priority="lazy" + loading="lazy" /> {image.text} diff --git a/packages/gitbook/src/components/Footer/Footer.tsx b/packages/gitbook/src/components/Footer/Footer.tsx index c084a4571..1d41bff22 100644 --- a/packages/gitbook/src/components/Footer/Footer.tsx +++ b/packages/gitbook/src/components/Footer/Footer.tsx @@ -64,7 +64,7 @@ export function Footer(props: { context: GitBookSiteContext }) { } : null, }} - priority="lazy" + loading="lazy" style={[ 'w-auto', 'max-w-40', diff --git a/packages/gitbook/src/components/Header/CurrentContentIcon.tsx b/packages/gitbook/src/components/Header/CurrentContentIcon.tsx index 71bcd375c..39e0f4664 100644 --- a/packages/gitbook/src/components/Header/CurrentContentIcon.tsx +++ b/packages/gitbook/src/components/Header/CurrentContentIcon.tsx @@ -51,6 +51,7 @@ export function CurrentContentIcon( }, } } + preload {...imageProps} /> ); diff --git a/packages/gitbook/src/components/Header/HeaderLogo.tsx b/packages/gitbook/src/components/Header/HeaderLogo.tsx index ccea4b963..4239cb5f3 100644 --- a/packages/gitbook/src/components/Header/HeaderLogo.tsx +++ b/packages/gitbook/src/components/Header/HeaderLogo.tsx @@ -46,7 +46,7 @@ export async function HeaderLogo(props: HeaderLogoProps) { width: 260, }, ]} - priority="high" + preload style={tcls( 'overflow-hidden', 'shrink', diff --git a/packages/gitbook/src/components/utils/Image.tsx b/packages/gitbook/src/components/utils/Image.tsx index 87debcaa7..2cd0f42bf 100644 --- a/packages/gitbook/src/components/utils/Image.tsx +++ b/packages/gitbook/src/components/utils/Image.tsx @@ -3,8 +3,6 @@ import type { ImageResizer } from '@/lib/images'; import ReactDOM from 'react-dom'; import { type ClassValue, tcls } from '@/lib/tailwind'; - -import { checkIsHttpURL } from '@/lib/urls'; import { ZoomImage } from './ZoomImage'; import type { PolymorphicComponentProp } from './types'; @@ -60,16 +58,6 @@ interface ImageCommonProps { */ zoom?: boolean; - /** - * Priority of the image. - * - `lazy` will load the image only when it's visible in the viewport. - * - `eager` will load the image as soon as possible, even if it's not visible; but will not preload. - * - `high` will preload. - * - * @default normal - */ - priority?: 'lazy' | 'normal' | 'high'; - /** * Force preloading the image. * Even if the priority is set to `lazy`, the image will be preloaded. @@ -139,7 +127,7 @@ export function Image( inline={inline} // We don't want to preload the dark image, because it's not visible // TODO: adapt based on the default theme - priority="lazy" + loading="lazy" className={tcls( rest.className, 'hidden', @@ -198,11 +186,12 @@ async function ImagePictureSized( style: _style, alt, quality = 100, - priority = 'normal', inline = false, zoom = false, resize = false, preload = false, + loading, + fetchPriority, inlineStyle, ...rest } = props; @@ -212,9 +201,6 @@ async function ImagePictureSized( } const attrs = await getImageAttributes({ sizes, source, quality, resize }); - const canBeFetched = checkIsHttpURL(attrs.src); - const fetchPriority = canBeFetched ? getFetchPriority(priority) : undefined; - const loading = priority === 'lazy' ? 'lazy' : undefined; const aspectRatioStyle = source.aspectRatio ? { aspectRatio: source.aspectRatio } : {}; const style = { ...aspectRatioStyle, ...inlineStyle }; @@ -310,14 +296,3 @@ export async function getImageAttributes(params: { ...source.size, }; } - -function getFetchPriority(priority: ImageCommonProps['priority']) { - switch (priority) { - case 'lazy': - return 'low'; - case 'high': - return 'high'; - default: - return undefined; - } -} From 1b0613e482d94e6d7b41cd1b1ffe83804d0d6d09 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein Date: Wed, 22 Oct 2025 22:00:52 +0200 Subject: [PATCH 09/48] Tweak language dropdown styling (#3748) --- .changeset/quiet-comics-act.md | 5 ++++ .../gitbook/src/components/Header/Header.tsx | 13 +++++---- .../SiteSections/SiteSectionTabs.tsx | 7 +++-- .../components/primitives/ScrollContainer.tsx | 27 ++++++++++++++++--- 4 files changed, 39 insertions(+), 13 deletions(-) create mode 100644 .changeset/quiet-comics-act.md diff --git a/.changeset/quiet-comics-act.md b/.changeset/quiet-comics-act.md new file mode 100644 index 000000000..b3ed43a64 --- /dev/null +++ b/.changeset/quiet-comics-act.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Tweak language dropdown styling diff --git a/packages/gitbook/src/components/Header/Header.tsx b/packages/gitbook/src/components/Header/Header.tsx index 38529ac07..999c4d1fd 100644 --- a/packages/gitbook/src/components/Header/Header.tsx +++ b/packages/gitbook/src/components/Header/Header.tsx @@ -188,13 +188,12 @@ export function Header(props: {
{withVariants === 'translations' ? ( -
- -
+ ) : null}
diff --git a/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx b/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx index dce92e949..9bf7be4a4 100644 --- a/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx +++ b/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx @@ -67,15 +67,18 @@ export function SiteSectionTabs(props: { className={tcls( 'grow', 'md:-ml-8 -ml-4 sm:-ml-6', - !children ? 'md:-mr-8 -mr-4 sm:-mr-6' : '' + !children + ? 'md:-mr-8 -mr-4 sm:-mr-6' + : 'after:contents[] after:absolute after:inset-y-2 after:right-0 after:border-transparent after:border-r after:transition-colors' )} activeId={currentSection.id} + trailingEdgeScrollClassName={children ? 'after:border-tint' : ''} > ; export function ScrollContainer(props: ScrollContainerProps) { - const { children, className, orientation, activeId, ...rest } = props; + const { + children, + className, + orientation, + activeId, + leadingEdgeScrollClassName, + trailingEdgeScrollClassName, + ...rest + } = props; const containerRef = React.useRef(null); @@ -110,7 +124,12 @@ export function ScrollContainer(props: ScrollContainerProps) { return (
0 ? leadingEdgeScrollClassName : '', + scrollPosition < scrollSize ? trailingEdgeScrollClassName : '' + )} {...rest} > {/* Scrollable content */} @@ -140,7 +159,7 @@ export function ScrollContainer(props: ScrollContainerProps) { icon={orientation === 'horizontal' ? 'chevron-left' : 'chevron-up'} iconOnly size="xsmall" - variant="secondary" + variant="header" tabIndex={-1} className={tcls( orientation === 'horizontal' @@ -158,7 +177,7 @@ export function ScrollContainer(props: ScrollContainerProps) { icon={orientation === 'horizontal' ? 'chevron-right' : 'chevron-down'} iconOnly size="xsmall" - variant="secondary" + variant="header" tabIndex={-1} className={tcls( orientation === 'horizontal' From 758b34d46ba3c6cefeaba27fc02e97506a2eb0a5 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein Date: Fri, 24 Oct 2025 10:58:51 +0200 Subject: [PATCH 10/48] Move the "Ask " to the bottom of search results if query is not a question (#3751) --- .changeset/lazy-snails-warn.md | 5 +++++ packages/gitbook/src/components/Search/useSearchResults.ts | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 .changeset/lazy-snails-warn.md diff --git a/.changeset/lazy-snails-warn.md b/.changeset/lazy-snails-warn.md new file mode 100644 index 000000000..e2467abfe --- /dev/null +++ b/.changeset/lazy-snails-warn.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Move the "Ask " to the bottom of search results if query is not a question diff --git a/packages/gitbook/src/components/Search/useSearchResults.ts b/packages/gitbook/src/components/Search/useSearchResults.ts index 563339d27..25880f123 100644 --- a/packages/gitbook/src/components/Search/useSearchResults.ts +++ b/packages/gitbook/src/components/Search/useSearchResults.ts @@ -13,6 +13,7 @@ import { import { type Assistant, useAI } from '@/components/AI'; import { useTrackEvent } from '../Insights'; +import { isQuestion } from './isQuestion'; import type { SearchScope } from './useSearch'; export type ResultType = @@ -198,13 +199,16 @@ function withAskTriggers( return without; } + const queryIsQuestion = isQuestion(query); + return [ + ...(queryIsQuestion ? [] : (without ?? [])), ...assistants.map((assistant, index) => ({ type: 'question' as const, id: `question-${index}`, query, assistant, })), - ...(without ?? []), + ...(!queryIsQuestion ? [] : (without ?? [])), ]; } From 8fdc5c89dc92d79f31ae5012e1395f1034268e2a Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Fri, 24 Oct 2025 10:29:46 +0100 Subject: [PATCH 11/48] Fix: height issues with webframe (#3749) --- .changeset/breezy-nights-repair.md | 5 ++++ packages/gitbook/e2e/internal.spec.ts | 5 +++- .../react-contentkit/src/ElementWebframe.tsx | 29 +++++++++---------- 3 files changed, 23 insertions(+), 16 deletions(-) create mode 100644 .changeset/breezy-nights-repair.md diff --git a/.changeset/breezy-nights-repair.md b/.changeset/breezy-nights-repair.md new file mode 100644 index 000000000..1f4aa422c --- /dev/null +++ b/.changeset/breezy-nights-repair.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Fix webframe height issue diff --git a/packages/gitbook/e2e/internal.spec.ts b/packages/gitbook/e2e/internal.spec.ts index 110996338..34088e136 100644 --- a/packages/gitbook/e2e/internal.spec.ts +++ b/packages/gitbook/e2e/internal.spec.ts @@ -779,7 +779,10 @@ const testCases: TestsCase[] = [ url: 'blocks/integrations', run: async (page) => { await waitForCookiesDialog(page); - const mermaidIframe = page.locator('iframe[title*="mermaid"]').contentFrame(); + const mermaidIframe = page + .locator('iframe[title*="mermaid"]') + .first() + .contentFrame(); await expect(mermaidIframe.getByText('Mermaid', { exact: true })).toBeVisible(); await expect(mermaidIframe.getByText('Diagram', { exact: true })).toBeVisible(); }, diff --git a/packages/react-contentkit/src/ElementWebframe.tsx b/packages/react-contentkit/src/ElementWebframe.tsx index 8dfedbfa0..e0474087a 100644 --- a/packages/react-contentkit/src/ElementWebframe.tsx +++ b/packages/react-contentkit/src/ElementWebframe.tsx @@ -2,13 +2,14 @@ import type { ContentKitWebFrame } from '@gitbook/api'; import React from 'react'; -import { useResizeObserver } from 'usehooks-ts'; import { Icon } from '@gitbook/icons'; import { useContentKitClientContext } from './context'; import { resolveDynamicBinding } from './dynamic'; import type { ContentKitClientElementProps } from './types'; +const MIN_HEIGHT = 32; // minimum height for the iframe in pixels + export function ElementWebframe(props: ContentKitClientElementProps) { const { element } = props; @@ -159,21 +160,22 @@ export function ElementWebframe(props: ContentKitClientElementProps (size.height ?? 0) - ? Math.min(Math.round(liveWidth / aspectRatio), size.height ?? 32) - : 'auto'; + const aspectRatio = + size.aspectRatio || + element.aspectRatio || + (iframeRef.current?.clientWidth && height + ? iframeRef.current.clientWidth / height + : undefined); if (!mounted) { return ; } + // only use height measurement if no aspect ratio is defined + const useHeightMeasurement = !aspectRatio && height; + return (