From 65fd3cc10168a228182542f71cf377db69898218 Mon Sep 17 00:00:00 2001 From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com> Date: Thu, 2 Jul 2026 11:37:03 +0200 Subject: [PATCH 01/46] Gate AI features server-actions (#4353) --- .../[siteData]/~gitbook/embed/assistant/page.tsx | 4 ++-- .../[siteURL]/[siteData]/~gitbook/embed/page.tsx | 4 ++-- .../[siteData]/~gitbook/embed/assistant/page.tsx | 4 ++-- .../[siteURL]/[siteData]/~gitbook/embed/page.tsx | 4 ++-- .../gitbook/src/components/AI/server-actions/chat.ts | 8 +++++++- packages/gitbook/src/components/AI/useAI.tsx | 7 ++++--- .../AIChat/AskAITextSelection/AskAITextSelection.tsx | 4 ++-- .../DocumentView/CodeBlock/AskAICodeButton.tsx | 5 ++--- .../components/Embeddable/EmbeddableIframeAPI.tsx | 6 ++---- .../gitbook/src/components/Search/server-actions.tsx | 10 ++++++++++ .../src/components/SpaceLayout/SpaceLayout.tsx | 9 +++------ .../gitbook/src/components/utils/isAIChatEnabled.ts | 12 +++++++++--- packages/gitbook/src/routes/llms.ts | 5 +++++ packages/gitbook/src/routes/markdownAsk.ts | 5 +++++ packages/gitbook/src/routes/markdownPage.ts | 5 +++++ 15 files changed, 62 insertions(+), 30 deletions(-) diff --git a/packages/gitbook/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/page.tsx b/packages/gitbook/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/page.tsx index 59736ceef..274f55334 100644 --- a/packages/gitbook/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/page.tsx +++ b/packages/gitbook/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/page.tsx @@ -1,7 +1,7 @@ import type { RouteLayoutParams } from '@/app/utils'; import { EmbeddableAssistantPage } from '@/components/Embeddable'; +import { isAIChatEnabled } from '@/components/utils/isAIChatEnabled'; import { getEmbeddableDynamicContext } from '@/lib/embeddable'; -import { CustomizationAIMode } from '@gitbook/api'; import { redirect } from 'next/navigation'; type PageProps = { @@ -15,7 +15,7 @@ export default async function Page(props: PageProps) { const { context } = await getEmbeddableDynamicContext(params); // If the assistant is not enabled, redirect to the docs - if (context.customization.ai.mode !== CustomizationAIMode.Assistant) { + if (!isAIChatEnabled(context.customization.ai.mode)) { redirect(`${context.linker.toPathInSite('~gitbook/embed/page/')}`); } diff --git a/packages/gitbook/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/embed/page.tsx b/packages/gitbook/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/embed/page.tsx index b847b5cf7..4abe02997 100644 --- a/packages/gitbook/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/embed/page.tsx +++ b/packages/gitbook/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/embed/page.tsx @@ -1,6 +1,6 @@ import type { RouteLayoutParams } from '@/app/utils'; +import { isAIChatEnabled } from '@/components/utils/isAIChatEnabled'; import { getEmbeddableDynamicContext } from '@/lib/embeddable'; -import { CustomizationAIMode } from '@gitbook/api'; import { redirect } from 'next/navigation'; type PageProps = { @@ -13,7 +13,7 @@ export default async function Page(props: PageProps) { const baseURL = context.linker.toPathInSite('~gitbook/embed/'); // If assistant is enabled, redirect to assistant, otherwise to docs - if (context.customization.ai.mode === CustomizationAIMode.Assistant) { + if (isAIChatEnabled(context.customization.ai.mode)) { redirect(`${baseURL}/assistant`); } else { redirect(`${baseURL}/page/`); diff --git a/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/page.tsx b/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/page.tsx index 9b8b40a76..b5e5cfa42 100644 --- a/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/page.tsx +++ b/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/page.tsx @@ -1,7 +1,7 @@ import type { RouteParams } from '@/app/utils'; import { EmbeddableAssistantPage } from '@/components/Embeddable'; +import { isAIChatEnabled } from '@/components/utils/isAIChatEnabled'; import { getEmbeddableStaticContext } from '@/lib/embeddable'; -import { CustomizationAIMode } from '@gitbook/api'; import { redirect } from 'next/navigation'; export const dynamic = 'force-static'; @@ -15,7 +15,7 @@ export default async function Page(props: PageProps) { const { context } = await getEmbeddableStaticContext(params); // If the assistant is not enabled, redirect to the docs - if (context.customization.ai.mode !== CustomizationAIMode.Assistant) { + if (!isAIChatEnabled(context.customization.ai.mode)) { redirect(`${context.linker.toPathInSite('~gitbook/embed/page/')}`); } diff --git a/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/page.tsx b/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/page.tsx index 2f667d280..0198f23f2 100644 --- a/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/page.tsx +++ b/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/page.tsx @@ -1,6 +1,6 @@ import type { RouteLayoutParams } from '@/app/utils'; +import { isAIChatEnabled } from '@/components/utils/isAIChatEnabled'; import { getEmbeddableStaticContext } from '@/lib/embeddable'; -import { CustomizationAIMode } from '@gitbook/api'; import { redirect } from 'next/navigation'; export const dynamic = 'force-static'; @@ -15,7 +15,7 @@ export default async function Page(props: PageProps) { const baseURL = context.linker.toPathInSite('~gitbook/embed/'); // If assistant is enabled, redirect to assistant, otherwise to docs - if (context.customization.ai.mode === CustomizationAIMode.Assistant) { + if (isAIChatEnabled(context.customization.ai.mode)) { redirect(`${baseURL}/assistant`); } else { redirect(`${baseURL}/page/`); diff --git a/packages/gitbook/src/components/AI/server-actions/chat.ts b/packages/gitbook/src/components/AI/server-actions/chat.ts index c5f65b285..beca9500b 100644 --- a/packages/gitbook/src/components/AI/server-actions/chat.ts +++ b/packages/gitbook/src/components/AI/server-actions/chat.ts @@ -1,6 +1,7 @@ 'use server'; +import { isAIChatEnabled } from '@/components/utils/isAIChatEnabled'; import { getSiteURLDataFromMiddleware } from '@/lib/middleware'; -import { getServerActionBaseContext } from '@/lib/server-actions'; +import { fetchServerActionSiteContext, getServerActionBaseContext } from '@/lib/server-actions'; import { traceErrorOnly } from '@/lib/tracing'; import { type AIMessageContext, @@ -39,6 +40,11 @@ export async function* streamAIChatResponse({ isEmbeddable: options?.asEmbeddable, }); + const siteContext = await fetchServerActionSiteContext(context); + if (!isAIChatEnabled(siteContext.customization.ai.mode)) { + throw new Error('The AI Assistant is not enabled for this site.'); + } + const siteURLData = await getSiteURLDataFromMiddleware(); const api = await context.dataFetcher.api(); diff --git a/packages/gitbook/src/components/AI/useAI.tsx b/packages/gitbook/src/components/AI/useAI.tsx index 9c6c81e06..e8322c1a2 100644 --- a/packages/gitbook/src/components/AI/useAI.tsx +++ b/packages/gitbook/src/components/AI/useAI.tsx @@ -1,10 +1,11 @@ 'use client'; -import { CustomizationAIMode } from '@gitbook/api'; +import type { CustomizationAIMode } from '@gitbook/api'; import { Icon, type IconName } from '@gitbook/icons'; import * as React from 'react'; import type { ReactNode } from 'react'; +import { isAIChatEnabled, isAISearchEnabled } from '@/components/utils/isAIChatEnabled'; import { tString, useLanguage } from '@/intl/client'; import type { GitBookAssistant } from '@gitbook/browser-types'; import { useAIChatController, useAIChatState } from '.'; @@ -89,7 +90,7 @@ export function useAI(): AIContext { const assistants: Assistant[] = []; - if (config.aiMode === CustomizationAIMode.Assistant) { + if (isAIChatEnabled(config.aiMode)) { assistants.push({ id: 'gitbook-assistant', label: config.assistantName ?? getAIChatName(language, config.trademark), @@ -110,7 +111,7 @@ export function useAI(): AIContext { ui: true, mode: 'sidebar', }); - } else if (config.aiMode === CustomizationAIMode.Search) { + } else if (isAISearchEnabled(config.aiMode)) { assistants.push({ id: 'gitbook-ai-search', label: tString(language, 'ai_chat_context_badge'), diff --git a/packages/gitbook/src/components/AIChat/AskAITextSelection/AskAITextSelection.tsx b/packages/gitbook/src/components/AIChat/AskAITextSelection/AskAITextSelection.tsx index 94056b6ee..ee3d00909 100644 --- a/packages/gitbook/src/components/AIChat/AskAITextSelection/AskAITextSelection.tsx +++ b/packages/gitbook/src/components/AIChat/AskAITextSelection/AskAITextSelection.tsx @@ -1,6 +1,5 @@ 'use client'; -import { CustomizationAIMode } from '@gitbook/api'; import fnv1a from '@sindresorhus/fnv1a'; import { AnimatePresence, motion } from 'motion/react'; import * as React from 'react'; @@ -10,6 +9,7 @@ import { useAIChatController, useAIConfig } from '@/components/AI'; import { useIsMobile } from '@/components/hooks/useIsMobile'; import { useIsMounted } from '@/components/hooks/useIsMounted'; import { Button } from '@/components/primitives'; +import { isAIChatEnabled } from '@/components/utils/isAIChatEnabled'; import { t, useLanguage } from '@/intl/client'; import { AIChatIcon } from '../AIChatIcon'; @@ -31,7 +31,7 @@ export function AskAITextSelection() { const isMobile = useIsMobile(); const isMounted = useIsMounted(); - const enabled = config.aiMode === CustomizationAIMode.Assistant && !isMobile; + const enabled = isAIChatEnabled(config.aiMode) && !isMobile; const toolbarRef = React.useRef(null); const { selection, clear } = useStableTextSelection({ diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/AskAICodeButton.tsx b/packages/gitbook/src/components/DocumentView/CodeBlock/AskAICodeButton.tsx index 10a784439..850988337 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/AskAICodeButton.tsx +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/AskAICodeButton.tsx @@ -1,10 +1,9 @@ 'use client'; -import { CustomizationAIMode } from '@gitbook/api'; - import { useAIChatController, useAIConfig } from '@/components/AI'; import { AIChatIcon } from '@/components/AIChat'; import { Button } from '@/components/primitives'; +import { isAIChatEnabled } from '@/components/utils/isAIChatEnabled'; import { t, useLanguage } from '@/intl/client'; import { type ClassValue, tcls } from '@/lib/tailwind'; @@ -22,7 +21,7 @@ export function AskAICodeButton(props: { const config = useAIConfig(); const chatController = useAIChatController(); - if (config.aiMode !== CustomizationAIMode.Assistant) { + if (!isAIChatEnabled(config.aiMode)) { return null; } diff --git a/packages/gitbook/src/components/Embeddable/EmbeddableIframeAPI.tsx b/packages/gitbook/src/components/Embeddable/EmbeddableIframeAPI.tsx index f4e116e23..b2b612ad5 100644 --- a/packages/gitbook/src/components/Embeddable/EmbeddableIframeAPI.tsx +++ b/packages/gitbook/src/components/Embeddable/EmbeddableIframeAPI.tsx @@ -4,8 +4,8 @@ import type { GitBookEmbeddableConfiguration, ParentToFrameMessage } from '@gitb import React, { useEffect, useRef } from 'react'; import { useAI, useAIChatController } from '@/components/AI'; +import { isAIChatEnabled } from '@/components/utils/isAIChatEnabled'; import { tString, useLanguage } from '@/intl/client'; -import { CustomizationAIMode } from '@gitbook/api'; import { useRouter } from 'next/navigation'; import { createStore, useStore } from 'zustand'; import { integrationsAssistantTools } from '../Integrations'; @@ -186,9 +186,7 @@ export function EmbeddableIframeTabs(props: { const router = useRouter(); const enabledTabs = [ - config.aiMode === CustomizationAIMode.Assistant && - assistants[0] && - tabs.includes('assistant') + isAIChatEnabled(config.aiMode) && assistants[0] && tabs.includes('assistant') ? { key: 'assistant', label: assistants[0].label, diff --git a/packages/gitbook/src/components/Search/server-actions.tsx b/packages/gitbook/src/components/Search/server-actions.tsx index 1dca529a2..41e065e06 100644 --- a/packages/gitbook/src/components/Search/server-actions.tsx +++ b/packages/gitbook/src/components/Search/server-actions.tsx @@ -1,5 +1,6 @@ 'use server'; +import { isAIEnabled, isAISearchEnabled } from '@/components/utils/isAIChatEnabled'; import type { GitBookSiteContext } from '@/lib/context'; import { resolvePageId } from '@/lib/pages'; import { fetchServerActionSiteContext, getServerActionBaseContext } from '@/lib/server-actions'; @@ -55,6 +56,10 @@ export async function streamAskQuestion({ await getServerActionBaseContext({ isEmbeddable: asEmbeddable }) ); + if (!isAISearchEnabled(context.customization.ai.mode)) { + throw new Error('AI Search is not enabled for this site.'); + } + const apiClient = await context.dataFetcher.api(); const stream = apiClient.orgs.streamAskInSite( @@ -151,6 +156,11 @@ export async function streamRecommendedQuestions(args: { siteSpaceId?: string }) >(); (async () => { + const siteContext = await fetchServerActionSiteContext(context); + if (!isAIEnabled(siteContext.customization.ai.mode)) { + throw new Error('AI is not enabled for this site.'); + } + const apiClient = await context.dataFetcher.api(); const apiStream = apiClient.orgs.streamRecommendedQuestionsInSite( siteURLData.organization, diff --git a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx index 5c095579c..ae3e69456 100644 --- a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx +++ b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx @@ -1,14 +1,11 @@ import type { GitBookSiteContext } from '@/lib/context'; -import { - CustomizationAIMode, - CustomizationHeaderPreset, - CustomizationSearchStyle, -} from '@gitbook/api'; +import { CustomizationHeaderPreset, CustomizationSearchStyle } from '@gitbook/api'; import type React from 'react'; import { Footer } from '@/components/Footer'; import { Header, HeaderLogo } from '@/components/Header'; import { TableOfContents } from '@/components/TableOfContents'; +import { isAIChatEnabled } from '@/components/utils/isAIChatEnabled'; import type { VisitorAuthClaims } from '@/lib/adaptive'; import { GITBOOK_APP_URL } from '@/lib/env'; import { tcls } from '@/lib/tailwind'; @@ -128,7 +125,7 @@ export function SpaceLayout(props: SpaceLayoutProps) {
- {customization.ai?.mode === CustomizationAIMode.Assistant ? ( + {isAIChatEnabled(customization.ai?.mode) ? ( <> diff --git a/packages/gitbook/src/components/utils/isAIChatEnabled.ts b/packages/gitbook/src/components/utils/isAIChatEnabled.ts index c23b60181..6d0051fbb 100644 --- a/packages/gitbook/src/components/utils/isAIChatEnabled.ts +++ b/packages/gitbook/src/components/utils/isAIChatEnabled.ts @@ -1,5 +1,11 @@ -import type { GitBookSiteContext } from '@/lib/context'; import { CustomizationAIMode } from '@gitbook/api'; -export const isAIChatEnabled = (context: GitBookSiteContext) => - context.customization.ai.mode === CustomizationAIMode.Assistant; +// `ai.mode` is exclusive: Assistant (chat) and Search (ask) are distinct features; None means no AI. +export const isAIChatEnabled = (mode: CustomizationAIMode | undefined) => + mode === CustomizationAIMode.Assistant; + +export const isAISearchEnabled = (mode: CustomizationAIMode | undefined) => + mode === CustomizationAIMode.Search; + +export const isAIEnabled = (mode: CustomizationAIMode | undefined) => + isAIChatEnabled(mode) || isAISearchEnabled(mode); diff --git a/packages/gitbook/src/routes/llms.ts b/packages/gitbook/src/routes/llms.ts index d9c68b6cc..18faaea93 100644 --- a/packages/gitbook/src/routes/llms.ts +++ b/packages/gitbook/src/routes/llms.ts @@ -1,3 +1,4 @@ +import { isAIEnabled } from '@/components/utils/isAIChatEnabled'; import { type GitBookSiteContext, checkIsRootSiteContext } from '@/lib/context'; import { throwIfDataError } from '@/lib/data'; import { type GitBookLinker, linkerWithMarkdownPages } from '@/lib/links'; @@ -203,6 +204,10 @@ export async function getMarkdownForPagesTree( } function renderAskFooter(context: GitBookSiteContext) { + if (!isAIEnabled(context.customization.ai.mode)) { + return ''; + } + return `\n\n---\n\n# Agent Instructions This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com. diff --git a/packages/gitbook/src/routes/markdownAsk.ts b/packages/gitbook/src/routes/markdownAsk.ts index dca64d5f5..87508cc49 100644 --- a/packages/gitbook/src/routes/markdownAsk.ts +++ b/packages/gitbook/src/routes/markdownAsk.ts @@ -1,3 +1,4 @@ +import { isAIEnabled } from '@/components/utils/isAIChatEnabled'; import type { GitBookSiteContext } from '@/lib/context'; import { throwIfDataError } from '@/lib/data'; import { linkerWithMarkdownPages } from '@/lib/links'; @@ -27,6 +28,10 @@ export async function serveAskMarkdown( rawQuestion: string, options: ServeAskMarkdownOptions = {} ) { + if (!isAIEnabled(context.customization.ai.mode)) { + return new Response('Not Found', { status: 404 }); + } + return serveMarkdown(async () => { const question = rawQuestion.trim(); const goal = options.goal?.trim() || undefined; diff --git a/packages/gitbook/src/routes/markdownPage.ts b/packages/gitbook/src/routes/markdownPage.ts index 21070b231..039e80a50 100644 --- a/packages/gitbook/src/routes/markdownPage.ts +++ b/packages/gitbook/src/routes/markdownPage.ts @@ -1,3 +1,4 @@ +import { isAIEnabled } from '@/components/utils/isAIChatEnabled'; import type { GitBookSiteContext } from '@/lib/context'; import { getExposableError } from '@/lib/data'; import { linkerWithMarkdownPages } from '@/lib/links'; @@ -100,6 +101,10 @@ function renderAskFooter( context: GitBookSiteContext, pageLookup: ResolvedPagePath ) { + if (!isAIEnabled(context.customization.ai.mode)) { + return ''; + } + const pageUrl = context.linker.toAbsoluteURL( context.linker.toPathForPage({ page: pageLookup.page, From c923c654bd6f4ffd8f94619adecb300586243147 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein Date: Thu, 2 Jul 2026 12:19:24 +0200 Subject: [PATCH 02/46] Fix centered paragraphs shifting left with the paragraph ask button (#4354) Co-authored-by: Claude Opus 4.8 --- .changeset/fix-ask-button-centered-paragraph.md | 5 +++++ .../gitbook/src/components/DocumentView/Paragraph.tsx | 10 +++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-ask-button-centered-paragraph.md diff --git a/.changeset/fix-ask-button-centered-paragraph.md b/.changeset/fix-ask-button-centered-paragraph.md new file mode 100644 index 000000000..1f89e6c26 --- /dev/null +++ b/.changeset/fix-ask-button-centered-paragraph.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Fix center- and end-aligned paragraphs shifting left on wide/no-TOC pages when the AI Assistant is enabled, caused by the per-paragraph ask-button wrapper not inheriting the block alignment. diff --git a/packages/gitbook/src/components/DocumentView/Paragraph.tsx b/packages/gitbook/src/components/DocumentView/Paragraph.tsx index d2f475acf..b58c2988a 100644 --- a/packages/gitbook/src/components/DocumentView/Paragraph.tsx +++ b/packages/gitbook/src/components/DocumentView/Paragraph.tsx @@ -33,8 +33,16 @@ export function Paragraph(props: BlockProps) { const text = aiAssistantEnabled ? getNodeText(block) : ''; if (aiAssistantEnabled && text.trim()) { + // The wrapper is now the flex child, so it must carry the block alignment (notably + // `self-center`/`self-end`) — otherwise centered paragraphs pin left on wide/no-TOC pages. return ( -
+
{paragraph}
From 578a3a829b64d87af8f8b225877f623fc00b6f5c Mon Sep 17 00:00:00 2001 From: Zeno Kapitein Date: Thu, 2 Jul 2026 14:06:41 +0200 Subject: [PATCH 03/46] Fix section tab dropdowns clipping their contents during animation (#4356) Co-authored-by: Claude Opus 4.8 --- .changeset/section-dropdown-clipping.md | 5 +++++ .../components/SiteSections/SiteSectionTabs.tsx | 16 +++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) create mode 100644 .changeset/section-dropdown-clipping.md diff --git a/.changeset/section-dropdown-clipping.md b/.changeset/section-dropdown-clipping.md new file mode 100644 index 000000000..a0fd5393d --- /dev/null +++ b/.changeset/section-dropdown-clipping.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Fix site section dropdowns not being bounded by their container during the open/resize animation, causing the contents to visually clip. diff --git a/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx b/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx index 0f6fa0c6e..8636c53c1 100644 --- a/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx +++ b/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx @@ -165,20 +165,26 @@ export function SiteSectionTabs(props: {
From eaef259a80007f46a7223dbf3aa1f60f88cbcc76 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein Date: Thu, 2 Jul 2026 19:34:09 +0200 Subject: [PATCH 04/46] Refresh e2e customer list (#4360) Co-authored-by: Claude Opus 4.8 --- .github/workflows/deploy-preview.yaml | 4 +- packages/gitbook/e2e/customers.spec.ts | 455 ++++++++++++++++++++++--- 2 files changed, 410 insertions(+), 49 deletions(-) diff --git a/.github/workflows/deploy-preview.yaml b/.github/workflows/deploy-preview.yaml index 726580de7..44269f1d1 100644 --- a/.github/workflows/deploy-preview.yaml +++ b/.github/workflows/deploy-preview.yaml @@ -168,7 +168,7 @@ jobs: name: Visual Testing Customers v2 needs: deploy-v2-vercel if: ${{ always() && needs.deploy-v2-vercel.result == 'success' }} - timeout-minutes: 15 + timeout-minutes: 30 steps: - name: Checkout uses: actions/checkout@v4 @@ -192,7 +192,7 @@ jobs: name: Visual Testing Customers v2 (Cloudflare) needs: deploy-v2-cloudflare if: ${{ always() && needs.deploy-v2-cloudflare.result == 'success' }} - timeout-minutes: 15 + timeout-minutes: 30 steps: - name: Checkout uses: actions/checkout@v4 diff --git a/packages/gitbook/e2e/customers.spec.ts b/packages/gitbook/e2e/customers.spec.ts index 207189d66..422e99ee4 100644 --- a/packages/gitbook/e2e/customers.spec.ts +++ b/packages/gitbook/e2e/customers.spec.ts @@ -40,11 +40,6 @@ const testCases: TestsCase[] = [ contentBaseURL: 'https://asiksupport-stg.dto.kemkes.go.id', tests: [{ name: 'Home', url: '/' }], }, - { - name: 'jasons-tutorials.gitbook.io', - contentBaseURL: 'https://jasons-tutorials.gitbook.io', - tests: [{ name: 'Home', url: '/' }], - }, { name: 'faq.deltaemulator.com', contentBaseURL: 'https://faq.deltaemulator.com', @@ -70,11 +65,6 @@ const testCases: TestsCase[] = [ contentBaseURL: 'https://docs.gmgn.ai', tests: [{ name: 'Home', url: '/' }], }, - // { - // name: 'docs.spicychat.ai', - // contentBaseURL: 'https://docs.spicychat.ai', - // tests: [{ name: 'Home', url: '/' }], - // }, { name: 'docs.portainer.io', contentBaseURL: 'https://docs.portainer.io', @@ -110,27 +100,11 @@ const testCases: TestsCase[] = [ contentBaseURL: 'https://docs.midas.app', tests: [{ name: 'Home', url: '/' }], }, - // Disabling for now, one of the image is making the tests timeout - // { - // name: 'docs.keeper.io', - // contentBaseURL: 'https://docs.keeper.io', - // tests: [{ name: 'Home', url: '/en', run: waitForCookiesDialog }], - // }, { name: 'adiblar.gitbook.io', contentBaseURL: 'https://adiblar.gitbook.io', tests: [{ name: 'Home', url: '/' }], }, - // { - // name: 'docs.gradient.network', - // contentBaseURL: 'https://docs.gradient.network', - // tests: [{ name: 'Home', url: '/' }], - // }, - // { - // name: 'mygate-network.gitbook.io', - // contentBaseURL: 'https://mygate-network.gitbook.io', - // tests: [{ name: 'Home', url: '/' }], - // }, { name: 'treasurenft.gitbook.io', contentBaseURL: 'https://treasurenft.gitbook.io', @@ -166,16 +140,6 @@ const testCases: TestsCase[] = [ contentBaseURL: 'https://wiki.redmodding.org', tests: [{ name: 'Home', url: '/' }], }, - // { - // name: 'docs.cherry-ai.com', - // contentBaseURL: 'https://docs.cherry-ai.com', - // tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], - // }, - { - name: 'docs.snyk.io', - contentBaseURL: 'https://docs.snyk.io', - tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], - }, { name: 'docs.realapp.link', contentBaseURL: 'https://docs.realapp.link', @@ -208,11 +172,6 @@ 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: 'chartschool.stockcharts.com', contentBaseURL: 'https://chartschool.stockcharts.com', @@ -223,12 +182,6 @@ const testCases: TestsCase[] = [ contentBaseURL: 'https://docs.soniclabs.com', tests: [{ name: 'Home', url: '/' }], }, - // This one redirects to binance now - // { - // name: 'docs.meshchain.ai', - // contentBaseURL: 'https://docs.meshchain.ai', - // tests: [{ name: 'Home', url: '/' }], - // }, { name: 'docs.thousandeyes.com', contentBaseURL: 'https://docs.thousandeyes.com', @@ -256,6 +209,414 @@ const testCases: TestsCase[] = [ { name: 'OG Image', url: '/~gitbook/ogimage/h17zQIFwy3MaafVNmItO', mode: 'image' }, ], }, + + // Additional customer docs sites. + { + name: 'unsloth.ai/docs', + contentBaseURL: 'https://unsloth.ai', + tests: [{ name: 'Home', url: '/docs' }], + }, + { + name: 'mariadb.com/docs', + contentBaseURL: 'https://mariadb.com', + tests: [{ name: 'Home', url: '/docs' }], + }, + { + name: 'docs.n8n.io', + contentBaseURL: 'https://docs.n8n.io', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'docs.cherry-ai.com', + contentBaseURL: 'https://docs.cherry-ai.com', + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], + }, + { + name: 'library.zoom.com', + contentBaseURL: 'https://library.zoom.com', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'help.verkada.com', + contentBaseURL: 'https://help.verkada.com', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'docs.overleaf.com', + contentBaseURL: 'https://docs.overleaf.com', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'wiki.tiltedphoques.com/tilted-online', + contentBaseURL: 'https://wiki.tiltedphoques.com', + tests: [{ name: 'Home', url: '/tilted-online' }], + }, + { + name: 'handbook.musescore.org', + contentBaseURL: 'https://handbook.musescore.org', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'kakaobusiness.gitbook.io/main', + contentBaseURL: 'https://kakaobusiness.gitbook.io', + tests: [{ name: 'Home', url: '/main' }], + }, + { + name: 'docs.maestro.dev', + contentBaseURL: 'https://docs.maestro.dev', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'developers.oxylabs.io', + contentBaseURL: 'https://developers.oxylabs.io', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'docs.parallels.com/landing', + contentBaseURL: 'https://docs.parallels.com', + tests: [{ name: 'Home', url: '/landing' }], + }, + { + name: 'help.impact.com', + contentBaseURL: 'https://help.impact.com', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'docs.9proxy.com', + contentBaseURL: 'https://docs.9proxy.com', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'vimeo.com/legal', + contentBaseURL: 'https://vimeo.com', + tests: [{ name: 'Home', url: '/legal' }], + }, + { + name: 'help.platipomiru.com', + contentBaseURL: 'https://help.platipomiru.com', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'help.aikido.dev', + contentBaseURL: 'https://help.aikido.dev', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'doc.demarche.numerique.gouv.fr', + contentBaseURL: 'https://doc.demarche.numerique.gouv.fr', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'docs.adapta.org', + contentBaseURL: 'https://docs.adapta.org', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'www.xabuxa.com', + contentBaseURL: 'https://www.xabuxa.com', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'docs.triumpharcade.com', + contentBaseURL: 'https://docs.triumpharcade.com', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'docs.nats.io', + contentBaseURL: 'https://docs.nats.io', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'help.glpi-project.org', + contentBaseURL: 'https://help.glpi-project.org', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'docs.waydro.id', + contentBaseURL: 'https://docs.waydro.id', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'bellingcat.gitbook.io/toolkit', + contentBaseURL: 'https://bellingcat.gitbook.io', + tests: [{ name: 'Home', url: '/toolkit' }], + }, + { + name: 'wiki.project-fika.com', + contentBaseURL: 'https://wiki.project-fika.com', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'docs.pinot.apache.org', + contentBaseURL: 'https://docs.pinot.apache.org', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'docs.devolutions.net', + contentBaseURL: 'https://docs.devolutions.net', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'guides.gresb.com', + contentBaseURL: 'https://guides.gresb.com', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'docs.prestashop-project.org/welcome', + contentBaseURL: 'https://docs.prestashop-project.org', + tests: [{ name: 'Home', url: '/welcome' }], + }, + { + name: 'help.researchgate.net', + contentBaseURL: 'https://help.researchgate.net', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'docs.verifone.com', + contentBaseURL: 'https://docs.verifone.com', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'docs.roboflow.com', + contentBaseURL: 'https://docs.roboflow.com', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'www.netexec.wiki', + contentBaseURL: 'https://www.netexec.wiki', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'guide.strikepack.com', + contentBaseURL: 'https://guide.strikepack.com', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'gitbook.com/docs', + contentBaseURL: 'https://gitbook.com', + tests: [ + { name: 'Home', url: '/docs' }, + { + name: 'OpenAPI', + url: '/docs/developers/gitbook-api/api-reference/docs-sites/site-ai-ask', + }, + ], + }, + { + name: 'documentation.gravitee.io', + contentBaseURL: 'https://documentation.gravitee.io', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'faq.wanttopay.net/wanttopay-app', + contentBaseURL: 'https://faq.wanttopay.net', + tests: [{ name: 'Home', url: '/wanttopay-app' }], + }, + { + name: 'guide.prismlive.com', + contentBaseURL: 'https://guide.prismlive.com', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'docs.ionos.com/cloud', + contentBaseURL: 'https://docs.ionos.com', + tests: [{ name: 'Home', url: '/cloud' }], + }, + { + name: 'support.evite.com', + contentBaseURL: 'https://support.evite.com', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'knowledge.illumina.com', + contentBaseURL: 'https://knowledge.illumina.com', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'wiki.retrobat.org', + contentBaseURL: 'https://wiki.retrobat.org', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'wiki.polymaker.com', + contentBaseURL: 'https://wiki.polymaker.com', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'docs.ducks-services.com', + contentBaseURL: 'https://docs.ducks-services.com', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'docs.hex-rays.com', + contentBaseURL: 'https://docs.hex-rays.com', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'whitepaper.interlinklabs.ai', + contentBaseURL: 'https://whitepaper.interlinklabs.ai', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'help.openloyalty.io', + contentBaseURL: 'https://help.openloyalty.io', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'retrozia.gitbook.io/retrozia', + contentBaseURL: 'https://retrozia.gitbook.io', + tests: [{ name: 'Home', url: '/retrozia' }], + }, + { + name: 'helpcenter.channable.com', + contentBaseURL: 'https://helpcenter.channable.com', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'developerdocs.instructure.com', + contentBaseURL: 'https://developerdocs.instructure.com', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'legal.jagex.com', + contentBaseURL: 'https://legal.jagex.com', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'manual.edgetx.org', + contentBaseURL: 'https://manual.edgetx.org', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'docs.cortex.io', + contentBaseURL: 'https://docs.cortex.io', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'docs.mufy.ai', + contentBaseURL: 'https://docs.mufy.ai', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'docs.ndi.video/all', + contentBaseURL: 'https://docs.ndi.video', + tests: [{ name: 'Home', url: '/all' }], + }, + { + name: 'docs.sevenpens.com/drawtab', + contentBaseURL: 'https://docs.sevenpens.com', + tests: [{ name: 'Home', url: '/drawtab' }], + }, + { + name: 'manuals.i-reporter.jp', + contentBaseURL: 'https://manuals.i-reporter.jp', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'docs.holybro.com', + contentBaseURL: 'https://docs.holybro.com', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'help.tokenpocket.pro/en', + contentBaseURL: 'https://help.tokenpocket.pro', + tests: [{ name: 'Home', url: '/en' }], + }, + { + name: 'docs.bullmq.io', + contentBaseURL: 'https://docs.bullmq.io', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'tools.osintnewsletter.com', + contentBaseURL: 'https://tools.osintnewsletter.com', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'wiki.mmorealms.gg', + contentBaseURL: 'https://wiki.mmorealms.gg', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'docs.vectra.ai', + contentBaseURL: 'https://docs.vectra.ai', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'docs.cipp.app', + contentBaseURL: 'https://docs.cipp.app', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'sinfa-com-co.gitbook.io/manual-de-usuario', + contentBaseURL: 'https://sinfa-com-co.gitbook.io', + tests: [{ name: 'Home', url: '/manual-de-usuario' }], + }, + { + name: 'support.skylum.com', + contentBaseURL: 'https://support.skylum.com', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'docs.jgscripts.com', + contentBaseURL: 'https://docs.jgscripts.com', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'docs.patchmypc.com', + contentBaseURL: 'https://docs.patchmypc.com', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'guide.cryosparc.com', + contentBaseURL: 'https://guide.cryosparc.com', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'guides.stellaraio.com/stellar', + contentBaseURL: 'https://guides.stellaraio.com', + tests: [{ name: 'Home', url: '/stellar' }], + }, + { + name: 'docs.iyzico.com', + contentBaseURL: 'https://docs.iyzico.com', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'help.wotnot.io', + contentBaseURL: 'https://help.wotnot.io', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'docs.sportmonks.com/v3', + contentBaseURL: 'https://docs.sportmonks.com', + tests: [{ name: 'Home', url: '/v3' }], + }, + { + name: 'docs.payments.thalescloud.io', + contentBaseURL: 'https://docs.payments.thalescloud.io', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'doc.anytype.io/anytype-docs', + contentBaseURL: 'https://doc.anytype.io', + tests: [{ name: 'Home', url: '/anytype-docs' }], + }, + { + name: 'help.blotato.com', + contentBaseURL: 'https://help.blotato.com', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'docs.cartographer3d.com', + contentBaseURL: 'https://docs.cartographer3d.com', + tests: [{ name: 'Home', url: '/' }], + }, + { + name: 'docs.acestudio.ai', + contentBaseURL: 'https://docs.acestudio.ai', + tests: [{ name: 'Home', url: '/' }], + }, ]; runTestCases(testCases); From 6102413d6b3d953c3ec0114c6a02b8be93fa6d95 Mon Sep 17 00:00:00 2001 From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com> Date: Thu, 2 Jul 2026 23:19:07 +0200 Subject: [PATCH 05/46] Reuse shared AI mode helpers across AI actions and UI (#4355) --- .github/composite/deploy-cloudflare/action.yaml | 1 + .github/composite/deploy-vercel/action.yaml | 1 + packages/gitbook/next.config.mjs | 1 + packages/gitbook/src/lib/server-actions.ts | 6 +++++- packages/gitbook/src/middleware.ts | 14 ++++++++++++-- 5 files changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/composite/deploy-cloudflare/action.yaml b/.github/composite/deploy-cloudflare/action.yaml index 8e5ee4823..136ad52b1 100644 --- a/.github/composite/deploy-cloudflare/action.yaml +++ b/.github/composite/deploy-cloudflare/action.yaml @@ -66,6 +66,7 @@ runs: env: GITBOOK_RUNTIME: cloudflare GITBOOK_BLOCK_SEARCH_INDEXATION: ${{ inputs.environment == 'preview' && 'true' || '' }} + GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE: ${{ inputs.environment == 'preview' && 'true' || '' }} shell: bash - name: Upload the DO worker diff --git a/.github/composite/deploy-vercel/action.yaml b/.github/composite/deploy-vercel/action.yaml index bb7b81750..8c62577c7 100644 --- a/.github/composite/deploy-vercel/action.yaml +++ b/.github/composite/deploy-vercel/action.yaml @@ -72,6 +72,7 @@ runs: echo "GITBOOK_HEAD_SHA=$HEAD_SHA" >> .vercel/.env.${{ inputs.environment }}.local echo "GITBOOK_RUNTIME=vercel" >> .vercel/.env.${{ inputs.environment }}.local echo "GITBOOK_BLOCK_SEARCH_INDEXATION=true" >> .vercel/.env.${{ inputs.environment }}.local + echo "GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE=true" >> .vercel/.env.${{ inputs.environment }}.local echo "--- .vercel/.env.${{ inputs.environment }}.local after inject ---" cat .vercel/.env.${{ inputs.environment }}.local - name: Build Project Artifacts diff --git a/packages/gitbook/next.config.mjs b/packages/gitbook/next.config.mjs index 3db64e6a3..309e7d0e5 100644 --- a/packages/gitbook/next.config.mjs +++ b/packages/gitbook/next.config.mjs @@ -61,6 +61,7 @@ const nextConfig = { GITBOOK_FONTS_URL: process.env.GITBOOK_FONTS_URL, GITBOOK_RUNTIME: process.env.GITBOOK_RUNTIME, GITBOOK_BLOCK_SEARCH_INDEXATION: process.env.GITBOOK_BLOCK_SEARCH_INDEXATION, + GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE: process.env.GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE, // Next.js envs NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY, diff --git a/packages/gitbook/src/lib/server-actions.ts b/packages/gitbook/src/lib/server-actions.ts index 0e8c8ed93..40bc0282e 100644 --- a/packages/gitbook/src/lib/server-actions.ts +++ b/packages/gitbook/src/lib/server-actions.ts @@ -1,4 +1,5 @@ import { type GitBookBaseContext, fetchSiteContextByURLLookup, getBaseContext } from './context'; +import { getDynamicCustomizationSettings } from './customization'; import { getEmbeddableLinker } from './embeddable-linker'; import { getSiteURLDataFromMiddleware, @@ -37,5 +38,8 @@ export async function getServerActionBaseContext(options?: { isEmbeddable?: bool */ export async function fetchServerActionSiteContext(baseContext: GitBookBaseContext) { const siteURLData = await getSiteURLDataFromMiddleware(); - return fetchSiteContextByURLLookup(baseContext, siteURLData); + const context = await fetchSiteContextByURLLookup(baseContext, siteURLData); + // Apply the preview customization override + context.customization = await getDynamicCustomizationSettings(context.customization); + return context; } diff --git a/packages/gitbook/src/middleware.ts b/packages/gitbook/src/middleware.ts index 7fd652585..38071e2be 100644 --- a/packages/gitbook/src/middleware.ts +++ b/packages/gitbook/src/middleware.ts @@ -393,12 +393,22 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) { ); requestHeaders.set(MiddlewareHeaders.SiteURLData, JSON.stringify(stableSiteURLData)); - // Preview of customization/theme + // Preview of customization/theme. + // The customization override is only honored for a legitimate preview + // (backend-authoritative `siteURLData.preview`), so an anonymous public request can't + // force customization — e.g. ai.mode — via the query string or a forged cookie. + // Preview/test deployments opt in via GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE to drive it in e2e. + const allowCustomizationOverride = + siteURLData.preview || process.env.GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE === 'true'; const customizationCookie = request.cookies.get(MiddlewareHeaders.Customization); const customization = siteRequestURL.searchParams.get('customization') ?? (customizationCookie ? decodeURIComponent(customizationCookie.value) : undefined); - if (customization && validateSerializedCustomization(customization)) { + if ( + allowCustomizationOverride && + customization && + validateSerializedCustomization(customization) + ) { routeType = 'dynamic'; // 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)); From 62ca6d3ce53528df0d369555c5554e76fa1dd49a Mon Sep 17 00:00:00 2001 From: Zeno Kapitein Date: Fri, 3 Jul 2026 12:36:07 +0200 Subject: [PATCH 06/46] Show a "Back to [space]" shortcut when a link leads to another space (#4358) Co-authored-by: Claude Opus 4.8 --- .changeset/back-to-space-toc.md | 5 + .../Header/SpacesDropdownMenuItem.tsx | 19 +- .../src/components/SitePage/SitePage.tsx | 27 +-- .../SiteSections/SiteSectionList.tsx | 3 +- .../SiteSections/SiteSectionTabs.tsx | 2 + .../TableOfContents/BackToSpaceButton.tsx | 56 ++++++ .../TableOfContents/TableOfContents.tsx | 8 + .../gitbook/src/components/hooks/index.ts | 1 + .../components/hooks/useBackToSpace.test.ts | 104 ++++++++++ .../src/components/hooks/useBackToSpace.tsx | 178 ++++++++++++++++++ packages/gitbook/src/intl/translations/ar.ts | 1 + packages/gitbook/src/intl/translations/bg.ts | 1 + packages/gitbook/src/intl/translations/cs.ts | 1 + packages/gitbook/src/intl/translations/da.ts | 1 + packages/gitbook/src/intl/translations/de.ts | 1 + packages/gitbook/src/intl/translations/el.ts | 1 + packages/gitbook/src/intl/translations/en.ts | 1 + packages/gitbook/src/intl/translations/es.ts | 1 + packages/gitbook/src/intl/translations/et.ts | 1 + packages/gitbook/src/intl/translations/fi.ts | 1 + packages/gitbook/src/intl/translations/fr.ts | 1 + packages/gitbook/src/intl/translations/he.ts | 1 + packages/gitbook/src/intl/translations/hi.ts | 1 + packages/gitbook/src/intl/translations/hr.ts | 1 + packages/gitbook/src/intl/translations/hu.ts | 1 + packages/gitbook/src/intl/translations/id.ts | 1 + packages/gitbook/src/intl/translations/it.ts | 1 + packages/gitbook/src/intl/translations/ja.ts | 1 + packages/gitbook/src/intl/translations/ko.ts | 1 + packages/gitbook/src/intl/translations/lt.ts | 1 + packages/gitbook/src/intl/translations/lv.ts | 1 + packages/gitbook/src/intl/translations/ms.ts | 1 + packages/gitbook/src/intl/translations/nl.ts | 1 + packages/gitbook/src/intl/translations/no.ts | 1 + packages/gitbook/src/intl/translations/pl.ts | 1 + .../gitbook/src/intl/translations/pt-br.ts | 1 + packages/gitbook/src/intl/translations/pt.ts | 1 + packages/gitbook/src/intl/translations/ro.ts | 1 + packages/gitbook/src/intl/translations/ru.ts | 1 + packages/gitbook/src/intl/translations/sk.ts | 1 + packages/gitbook/src/intl/translations/sl.ts | 1 + packages/gitbook/src/intl/translations/sv.ts | 1 + packages/gitbook/src/intl/translations/th.ts | 1 + packages/gitbook/src/intl/translations/tr.ts | 1 + packages/gitbook/src/intl/translations/uk.ts | 1 + packages/gitbook/src/intl/translations/vi.ts | 1 + packages/gitbook/src/intl/translations/yue.ts | 1 + .../gitbook/src/intl/translations/zh-tw.ts | 1 + packages/gitbook/src/intl/translations/zh.ts | 1 + packages/gitbook/src/lib/sites.ts | 27 +++ 50 files changed, 440 insertions(+), 29 deletions(-) create mode 100644 .changeset/back-to-space-toc.md create mode 100644 packages/gitbook/src/components/TableOfContents/BackToSpaceButton.tsx create mode 100644 packages/gitbook/src/components/hooks/useBackToSpace.test.ts create mode 100644 packages/gitbook/src/components/hooks/useBackToSpace.tsx diff --git a/.changeset/back-to-space-toc.md b/.changeset/back-to-space-toc.md new file mode 100644 index 000000000..fc502837a --- /dev/null +++ b/.changeset/back-to-space-toc.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Show a "Back to [space]" shortcut at the top of the table of contents when a reader follows a link into a different space, so they don't lose track of where they were browsing from. diff --git a/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx b/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx index 68c28b8fc..6ca5aa50a 100644 --- a/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx +++ b/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx @@ -1,7 +1,11 @@ 'use client'; import { joinPath } from '@/lib/paths'; -import { useCurrentPageMetadata, useCurrentPagePath } from '../hooks'; +import { + markSpaceNavigationFromPickerOnClick, + useCurrentPageMetadata, + useCurrentPagePath, +} from '../hooks'; import { DropdownMenuItem } from '../primitives/DropdownMenu'; interface VariantSpace { @@ -60,7 +64,18 @@ export function SpacesDropdownMenuItem(props: { const variantHref = useVariantSpaceHref(variantSpace, currentSpacePath, active); return ( - + { + // Switching variant/translation through the picker shouldn't offer to + // navigate "back" to the space we're leaving. + if (!active) { + markSpaceNavigationFromPickerOnClick(event); + } + }} + > {variantSpace.title} ); diff --git a/packages/gitbook/src/components/SitePage/SitePage.tsx b/packages/gitbook/src/components/SitePage/SitePage.tsx index 073717f49..684115f3b 100644 --- a/packages/gitbook/src/components/SitePage/SitePage.tsx +++ b/packages/gitbook/src/components/SitePage/SitePage.tsx @@ -25,7 +25,7 @@ import { } from '@/lib/icons/inline'; import { getResizedImageURL } from '@/lib/images'; import { resolveContentRef } from '@/lib/references'; -import { getLocalizedTitle } from '@/lib/sites'; +import { getSiteStructureTitle } from '@/lib/sites'; import { tcls } from '@/lib/tailwind'; import { getPageRSSURL } from '@/routes/rss'; import { PageContextProvider } from '../PageContext'; @@ -149,31 +149,6 @@ export async function generateSitePageViewport(context: GitBookSiteContext): Pro }; } -/** - * A string concatenation of the site structure (sections and variants) titles. - */ -function getSiteStructureTitle(context: GitBookSiteContext): string | null { - const { visibleSections: sections, siteSpace, visibleSiteSpaces: siteSpaces } = context; - const currentLanguage = context.locale; - - const title = []; - if ( - sections && - sections.current.default === false && // Only if the current section is not the default one - sections.list.filter((section) => section.object === 'site-section').length > 1 // Only if there are multiple sections - ) { - title.push(getLocalizedTitle(sections.current, currentLanguage)); - } - if ( - siteSpaces.length > 1 && // Only if there are multiple variants - siteSpace.default === false && // Only if the variant is not the default one - siteSpaces.filter((space) => space.space.language === siteSpace.space.language).length > 1 // Only if there are multiple variants *for the current language*. This filters out spaces that are "just" translations of each other, not versions. - ) { - title.push(getLocalizedTitle(siteSpace, siteSpace.space.language)); - } - return title.join(' '); -} - export async function generateSitePageMetadata(props: SitePageProps): Promise { const { context, pageTarget, pageMetaLinks } = await getPageDataWithFallback({ context: props.context, diff --git a/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx b/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx index 399525a07..213325f44 100644 --- a/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx +++ b/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { type ClassValue, tcls } from '@/lib/tailwind'; import { findSectionInGroup } from '@/lib/utils'; -import { useToggleAnimation } from '../hooks'; +import { markSpaceNavigationFromPickerOnClick, useToggleAnimation } from '../hooks'; import { Link, ToggleChevron } from '../primitives'; import { ScrollContainer } from '../primitives/ScrollContainer'; import { SectionIcon } from './SectionIcon'; @@ -31,6 +31,7 @@ export function SiteSectionList(props: { sections: ClientSiteSections; className sectionsAndGroups.length > 0 && (
+ ); +} diff --git a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx index 25d2c5b95..dcc7edcdf 100644 --- a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx +++ b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx @@ -2,9 +2,11 @@ import type { GitBookSiteContext } from '@/lib/context'; import { SiteInsightsTrademarkPlacement } from '@gitbook/api'; import type React from 'react'; +import { getSiteStructureTitle } from '@/lib/sites'; import { tcls } from '@/lib/tailwind'; import { ScrollContainer } from '../primitives/ScrollContainer'; import { SideSheet } from '../primitives/SideSheet'; +import { BackToSpaceButton } from './BackToSpaceButton'; import { PagesList } from './PagesList'; import { TableOfContentsScript } from './TableOfContentsScript'; import { Trademark } from './Trademark'; @@ -119,6 +121,12 @@ export async function TableOfContents(props: { '[html.sidebar-filled.circular-corners_&]:layout-wide:rounded-4xl' )} > + {innerHeader} { + it('offers nothing on the first navigation of a session', () => { + expect( + resolveBackToSpace({ + current: gettingStarted, + lastLocation: null, + storedBack: null, + fromPicker: false, + }) + ).toBeNull(); + }); + + it('offers to go back after following a link into a different space', () => { + expect( + resolveBackToSpace({ + current: programmersGuide, + lastLocation: gettingStarted, + storedBack: null, + fromPicker: false, + }) + ).toEqual(gettingStarted); + }); + + it('keeps the back target while browsing within the destination space', () => { + const deeperInGuide: SpaceLocation = { + ...programmersGuide, + url: '/docs/programmers-guide/chapter-2', + }; + expect( + resolveBackToSpace({ + current: deeperInGuide, + lastLocation: programmersGuide, + storedBack: gettingStarted, + fromPicker: false, + }) + ).toEqual(gettingStarted); + }); + + it('does not offer to go back when the space was changed through the picker', () => { + expect( + resolveBackToSpace({ + current: programmersGuide, + lastLocation: gettingStarted, + storedBack: null, + fromPicker: true, + }) + ).toBeNull(); + }); + + it('clears the back target once the reader returns to the space they came from', () => { + expect( + resolveBackToSpace({ + current: gettingStarted, + lastLocation: programmersGuide, + storedBack: gettingStarted, + fromPicker: false, + }) + ).toBeNull(); + }); + + it('points back to the most recent space when chaining cross-space links', () => { + // Was in Getting Started (stored back), followed a link from the Programmer's + // Guide into the API Reference: back should now point to the Programmer's Guide. + expect( + resolveBackToSpace({ + current: apiReference, + lastLocation: programmersGuide, + storedBack: gettingStarted, + fromPicker: false, + }) + ).toEqual(programmersGuide); + }); + + it('resets the back target when switching space through the picker even if one was set', () => { + expect( + resolveBackToSpace({ + current: apiReference, + lastLocation: programmersGuide, + storedBack: gettingStarted, + fromPicker: true, + }) + ).toBeNull(); + }); +}); diff --git a/packages/gitbook/src/components/hooks/useBackToSpace.tsx b/packages/gitbook/src/components/hooks/useBackToSpace.tsx new file mode 100644 index 000000000..481deffb8 --- /dev/null +++ b/packages/gitbook/src/components/hooks/useBackToSpace.tsx @@ -0,0 +1,178 @@ +'use client'; + +import { usePathname } from 'next/navigation'; +import * as React from 'react'; + +import { + getSessionStorageItem, + removeSessionStorageItem, + setSessionStorageItem, +} from '@/lib/browser'; + +/** + * A location visited by the reader, scoped to the space it belongs to. + */ +export type SpaceLocation = { + /** Identifier of the space. */ + spaceId: string; + /** Localized title of the space, used as the label of the "back" shortcut. */ + spaceTitle: string; + /** Path (with query string) of the last page visited in the space. */ + url: string; +}; + +// Keys used to persist the navigation state for the current tab session. +const LAST_LOCATION_KEY = 'gitbook-space-navigation:last'; +const BACK_TO_SPACE_KEY = 'gitbook-space-navigation:back'; +const FROM_PICKER_KEY = 'gitbook-space-navigation:from-picker'; + +/** + * Mark that the navigation about to happen was initiated from the section/variant + * picker (spaces dropdown, translations dropdown or section switcher). + * + * Such navigations are deliberate moves within the site's own structure and should + * not surface a "Back to space" shortcut. Set synchronously when a picker link is + * activated, so the flag is in place before the destination page reads it. + */ +function markSpaceNavigationFromPicker() { + setSessionStorageItem(FROM_PICKER_KEY, true); +} + +/** + * Whether a click opens its link in a new tab or window instead of navigating the + * current tab (a modifier key or a non-primary button). These must not mark a picker + * navigation: the current tab stays put, so the flag would otherwise linger and be + * mis-attributed to a later, unrelated navigation. + */ +function opensInNewTab(event: React.MouseEvent): boolean { + return event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0; +} + +/** + * Click handler for the section/variant/translation pickers that marks the upcoming + * navigation as picker-initiated, so arriving in the target space doesn't surface a + * "Back to space" shortcut. + * + * Attach it with `onClickCapture` on a picker container (section list/tabs) or as the + * `onClick` of a picker menu item (spaces dropdown). It intentionally does nothing for + * clicks that leave the current tab unchanged: re-selecting the already-active item, + * or opening the link in a new tab/window. + */ +export function markSpaceNavigationFromPickerOnClick(event: React.MouseEvent) { + if (opensInNewTab(event)) { + return; + } + + const target = event.target; + if (!(target instanceof Element)) { + return; + } + + const link = target.closest('a[href]'); + if (!link) { + return; + } + + // Skip the currently active item: re-selecting it doesn't navigate. The sidebar + // section list marks the active link with `aria-current`, while the header section + // tabs (rendered via the Button primitive) use `aria-pressed`. + if ( + link.getAttribute('aria-current') === 'page' || + link.getAttribute('aria-pressed') === 'true' + ) { + return; + } + + markSpaceNavigationFromPicker(); +} + +/** + * Determine which space (if any) the reader should be offered to navigate back to, + * given the current location and the state persisted from the previous navigation. + * + * This is the pure decision logic behind {@link useBackToSpace}, extracted so it can + * be unit-tested without a DOM. + */ +export function resolveBackToSpace(input: { + current: SpaceLocation; + lastLocation: SpaceLocation | null; + storedBack: SpaceLocation | null; + fromPicker: boolean; +}): SpaceLocation | null { + const { current, lastLocation, storedBack, fromPicker } = input; + + // No previous navigation this session, or we're still browsing the same space: + // keep whatever "back" target we already had (it persists while browsing). + if (!lastLocation || lastLocation.spaceId === current.spaceId) { + return storedBack && storedBack.spaceId !== current.spaceId ? storedBack : null; + } + + // The reader moved to a different space since the last navigation. + + // Switching space through the picker is a deliberate navigation within the + // site structure: reset the context instead of offering to go back. + if (fromPicker) { + return null; + } + + // The reader returned to the space they originally came from. + if (storedBack && storedBack.spaceId === current.spaceId) { + return null; + } + + // The reader followed a link into a different space: offer to go back to the + // page they were on. + return lastLocation; +} + +/** + * Track navigation between spaces and return the space to offer navigating back to. + * + * When a reader follows a link (in the ToC or in the content) into a space different + * from the one they were browsing, we remember where they came from so a "Back to + * [space]" shortcut can be surfaced. The shortcut persists while they browse the + * destination space and is cleared once they return or switch space through the + * section/variant picker. + * + * The state is kept in `sessionStorage` so it survives both client-side and full-page + * navigations within the tab, without leaking across tabs or sessions. + */ +export function useBackToSpace(current: { + spaceId: string; + spaceTitle: string; +}): SpaceLocation | null { + const { spaceId, spaceTitle } = current; + const pathname = usePathname(); + const [backToSpace, setBackToSpace] = React.useState(null); + + React.useEffect(() => { + const currentLocation: SpaceLocation = { + spaceId, + spaceTitle, + // `pathname` (from Next) is the app-relative path used for client navigation, + // paired with the current query string so we return to the exact same page. + url: `${pathname}${window.location.search}`, + }; + + const fromPicker = getSessionStorageItem(FROM_PICKER_KEY, false); + removeSessionStorageItem(FROM_PICKER_KEY); + + const back = resolveBackToSpace({ + current: currentLocation, + lastLocation: getSessionStorageItem(LAST_LOCATION_KEY, null), + storedBack: getSessionStorageItem(BACK_TO_SPACE_KEY, null), + fromPicker, + }); + + if (back) { + setSessionStorageItem(BACK_TO_SPACE_KEY, back); + } else { + removeSessionStorageItem(BACK_TO_SPACE_KEY); + } + setSessionStorageItem(LAST_LOCATION_KEY, currentLocation); + + setBackToSpace(back); + }, [pathname, spaceId, spaceTitle]); + + return backToSpace; +} diff --git a/packages/gitbook/src/intl/translations/ar.ts b/packages/gitbook/src/intl/translations/ar.ts index 769ce44ca..16a2e7d5c 100644 --- a/packages/gitbook/src/intl/translations/ar.ts +++ b/packages/gitbook/src/intl/translations/ar.ts @@ -44,6 +44,7 @@ export const ar: TranslationLanguage = { on_this_page: 'في هذه الصفحة', next_page: 'التالي', previous_page: 'السابق', + toc_back_to_space: 'العودة إلى ${1}', page_last_modified: 'آخر تحديث ${1}', was_this_helpful: 'هل كان هذا مفيدا؟', was_this_helpful_positive: 'نعم، كان مفيدا!', diff --git a/packages/gitbook/src/intl/translations/bg.ts b/packages/gitbook/src/intl/translations/bg.ts index 8bb82d0df..9dc337a1a 100644 --- a/packages/gitbook/src/intl/translations/bg.ts +++ b/packages/gitbook/src/intl/translations/bg.ts @@ -45,6 +45,7 @@ export const bg: TranslationLanguage = { on_this_page: 'На тази страница', next_page: 'Следваща', previous_page: 'Предишна', + toc_back_to_space: 'Обратно към ${1}', page_last_modified: 'Последна актуализация ${1}', was_this_helpful: 'Беше ли полезно?', was_this_helpful_positive: 'Да, беше!', diff --git a/packages/gitbook/src/intl/translations/cs.ts b/packages/gitbook/src/intl/translations/cs.ts index e4fc09a43..c44ffe3af 100644 --- a/packages/gitbook/src/intl/translations/cs.ts +++ b/packages/gitbook/src/intl/translations/cs.ts @@ -44,6 +44,7 @@ export const cs: TranslationLanguage = { on_this_page: 'Na této stránce', next_page: 'Další', previous_page: 'Předchozí', + toc_back_to_space: 'Zpět na ${1}', page_last_modified: 'Poslední aktualizace ${1}', was_this_helpful: 'Bylo to užitečné?', was_this_helpful_positive: 'Ano, bylo!', diff --git a/packages/gitbook/src/intl/translations/da.ts b/packages/gitbook/src/intl/translations/da.ts index b3130fd83..287328a5e 100644 --- a/packages/gitbook/src/intl/translations/da.ts +++ b/packages/gitbook/src/intl/translations/da.ts @@ -44,6 +44,7 @@ export const da: TranslationLanguage = { on_this_page: 'På denne side', next_page: 'Næste', previous_page: 'Forrige', + toc_back_to_space: 'Tilbage til ${1}', page_last_modified: 'Sidst opdateret ${1}', was_this_helpful: 'Var dette nyttigt?', was_this_helpful_positive: 'Ja, det var det!', diff --git a/packages/gitbook/src/intl/translations/de.ts b/packages/gitbook/src/intl/translations/de.ts index fdfb1b07e..9f6d330f6 100644 --- a/packages/gitbook/src/intl/translations/de.ts +++ b/packages/gitbook/src/intl/translations/de.ts @@ -46,6 +46,7 @@ export const de: TranslationLanguage = { on_this_page: 'Auf dieser Seite', next_page: 'Nächste', previous_page: 'Vorherige', + toc_back_to_space: 'Zurück zu ${1}', page_last_modified: 'Zuletzt aktualisiert ${1}', was_this_helpful: 'War das hilfreich?', was_this_helpful_positive: 'Ja, das war es!', diff --git a/packages/gitbook/src/intl/translations/el.ts b/packages/gitbook/src/intl/translations/el.ts index 32296d8db..3d734b40a 100644 --- a/packages/gitbook/src/intl/translations/el.ts +++ b/packages/gitbook/src/intl/translations/el.ts @@ -45,6 +45,7 @@ export const el: TranslationLanguage = { on_this_page: 'Σε αυτήν τη σελίδα', next_page: 'Επόμενη', previous_page: 'Προηγούμενη', + toc_back_to_space: 'Επιστροφή σε ${1}', page_last_modified: 'Τελευταία ενημέρωση ${1}', was_this_helpful: 'Ήταν χρήσιμο αυτό;', was_this_helpful_positive: 'Ναι, ήταν!', diff --git a/packages/gitbook/src/intl/translations/en.ts b/packages/gitbook/src/intl/translations/en.ts index e26f18458..93cc868b7 100644 --- a/packages/gitbook/src/intl/translations/en.ts +++ b/packages/gitbook/src/intl/translations/en.ts @@ -42,6 +42,7 @@ export const en = { on_this_page: 'On this page', next_page: 'Next', previous_page: 'Previous', + toc_back_to_space: 'Back to ${1}', page_last_modified: 'Last updated ${1}', was_this_helpful: 'Was this helpful?', was_this_helpful_positive: 'Yes, it was!', diff --git a/packages/gitbook/src/intl/translations/es.ts b/packages/gitbook/src/intl/translations/es.ts index 4557282ef..be02ee437 100644 --- a/packages/gitbook/src/intl/translations/es.ts +++ b/packages/gitbook/src/intl/translations/es.ts @@ -46,6 +46,7 @@ export const es: TranslationLanguage = { on_this_page: 'En esta página', next_page: 'Siguiente', previous_page: 'Anterior', + toc_back_to_space: 'Volver a ${1}', page_last_modified: 'Última actualización ${1}', was_this_helpful: '¿Te fue útil?', was_this_helpful_positive: '¡Sí, lo fue!', diff --git a/packages/gitbook/src/intl/translations/et.ts b/packages/gitbook/src/intl/translations/et.ts index 49e53b025..bdfe26619 100644 --- a/packages/gitbook/src/intl/translations/et.ts +++ b/packages/gitbook/src/intl/translations/et.ts @@ -44,6 +44,7 @@ export const et: TranslationLanguage = { on_this_page: 'Sellel lehel', next_page: 'Järgmine', previous_page: 'Eelmine', + toc_back_to_space: 'Tagasi: ${1}', page_last_modified: 'Viimati uuendatud ${1}', was_this_helpful: 'Kas sellest oli abi?', was_this_helpful_positive: 'Jah, oli!', diff --git a/packages/gitbook/src/intl/translations/fi.ts b/packages/gitbook/src/intl/translations/fi.ts index 6f573bec3..03e3de395 100644 --- a/packages/gitbook/src/intl/translations/fi.ts +++ b/packages/gitbook/src/intl/translations/fi.ts @@ -45,6 +45,7 @@ export const fi: TranslationLanguage = { on_this_page: 'Tällä sivulla', next_page: 'Seuraava', previous_page: 'Edellinen', + toc_back_to_space: 'Takaisin: ${1}', page_last_modified: 'Päivitetty viimeksi ${1}', was_this_helpful: 'Oliko tästä apua?', was_this_helpful_positive: 'Kyllä oli!', diff --git a/packages/gitbook/src/intl/translations/fr.ts b/packages/gitbook/src/intl/translations/fr.ts index 301e8270e..c5107c236 100644 --- a/packages/gitbook/src/intl/translations/fr.ts +++ b/packages/gitbook/src/intl/translations/fr.ts @@ -45,6 +45,7 @@ export const fr: TranslationLanguage = { on_this_page: 'Sur cette page', next_page: 'Suivant', previous_page: 'Précédent', + toc_back_to_space: 'Retour à ${1}', page_last_modified: 'Mis à jour ${1}', was_this_helpful: 'Ce contenu vous a-t-il été utile ?', was_this_helpful_positive: 'Oui, tout à fait !', diff --git a/packages/gitbook/src/intl/translations/he.ts b/packages/gitbook/src/intl/translations/he.ts index bafe59cb8..929187094 100644 --- a/packages/gitbook/src/intl/translations/he.ts +++ b/packages/gitbook/src/intl/translations/he.ts @@ -44,6 +44,7 @@ export const he: TranslationLanguage = { on_this_page: 'בדף הזה', next_page: 'הבא', previous_page: 'הקודם', + toc_back_to_space: 'חזרה אל ${1}', page_last_modified: 'עודכן לאחרונה ${1}', was_this_helpful: 'האם זה היה מועיל?', was_this_helpful_positive: 'כן, זה עזר!', diff --git a/packages/gitbook/src/intl/translations/hi.ts b/packages/gitbook/src/intl/translations/hi.ts index fa21019cd..b195edd58 100644 --- a/packages/gitbook/src/intl/translations/hi.ts +++ b/packages/gitbook/src/intl/translations/hi.ts @@ -44,6 +44,7 @@ export const hi: TranslationLanguage = { on_this_page: 'इस पृष्ठ पर', next_page: 'अगला', previous_page: 'पिछला', + toc_back_to_space: '${1} पर वापस जाएँ', page_last_modified: 'अंतिम अपडेट ${1}', was_this_helpful: 'क्या यह उपयोगी था?', was_this_helpful_positive: 'हां, था!', diff --git a/packages/gitbook/src/intl/translations/hr.ts b/packages/gitbook/src/intl/translations/hr.ts index 05c417d84..64a150bd8 100644 --- a/packages/gitbook/src/intl/translations/hr.ts +++ b/packages/gitbook/src/intl/translations/hr.ts @@ -44,6 +44,7 @@ export const hr: TranslationLanguage = { on_this_page: 'Na ovoj stranici', next_page: 'Sljedeća', previous_page: 'Prethodna', + toc_back_to_space: 'Natrag na ${1}', page_last_modified: 'Zadnje ažurirano ${1}', was_this_helpful: 'Je li ovo bilo korisno?', was_this_helpful_positive: 'Da, bilo je!', diff --git a/packages/gitbook/src/intl/translations/hu.ts b/packages/gitbook/src/intl/translations/hu.ts index 9c3d4586d..898b9bdf6 100644 --- a/packages/gitbook/src/intl/translations/hu.ts +++ b/packages/gitbook/src/intl/translations/hu.ts @@ -45,6 +45,7 @@ export const hu: TranslationLanguage = { on_this_page: 'Ezen az oldalon', next_page: 'Következő', previous_page: 'Előző', + toc_back_to_space: 'Vissza ide: ${1}', page_last_modified: 'Utoljára frissítve: ${1}', was_this_helpful: 'Hasznos volt ez?', was_this_helpful_positive: 'Igen, az volt!', diff --git a/packages/gitbook/src/intl/translations/id.ts b/packages/gitbook/src/intl/translations/id.ts index d87cf516e..342b706bf 100644 --- a/packages/gitbook/src/intl/translations/id.ts +++ b/packages/gitbook/src/intl/translations/id.ts @@ -44,6 +44,7 @@ export const id: TranslationLanguage = { on_this_page: 'Di halaman ini', next_page: 'Berikutnya', previous_page: 'Sebelumnya', + toc_back_to_space: 'Kembali ke ${1}', page_last_modified: 'Terakhir diperbarui ${1}', was_this_helpful: 'Apakah ini membantu?', was_this_helpful_positive: 'Ya, membantu!', diff --git a/packages/gitbook/src/intl/translations/it.ts b/packages/gitbook/src/intl/translations/it.ts index 30cecfe79..111b96159 100644 --- a/packages/gitbook/src/intl/translations/it.ts +++ b/packages/gitbook/src/intl/translations/it.ts @@ -46,6 +46,7 @@ export const it: TranslationLanguage = { on_this_page: 'In questa pagina', next_page: 'Successivo', previous_page: 'Precedente', + toc_back_to_space: 'Torna a ${1}', page_last_modified: 'Ultimo aggiornamento ${1}', was_this_helpful: 'È stato utile?', was_this_helpful_positive: 'Sì, lo è stato!', diff --git a/packages/gitbook/src/intl/translations/ja.ts b/packages/gitbook/src/intl/translations/ja.ts index a908bf42b..7f3533632 100644 --- a/packages/gitbook/src/intl/translations/ja.ts +++ b/packages/gitbook/src/intl/translations/ja.ts @@ -45,6 +45,7 @@ export const ja: TranslationLanguage = { on_this_page: 'このページ内', next_page: '次へ', previous_page: '前へ', + toc_back_to_space: '${1}に戻る', page_last_modified: '最終更新 ${1}', was_this_helpful: '役に立ちましたか?', was_this_helpful_positive: 'はい、役に立ちました!', diff --git a/packages/gitbook/src/intl/translations/ko.ts b/packages/gitbook/src/intl/translations/ko.ts index f5ed3d8e1..00bea3c40 100644 --- a/packages/gitbook/src/intl/translations/ko.ts +++ b/packages/gitbook/src/intl/translations/ko.ts @@ -45,6 +45,7 @@ export const ko: TranslationLanguage = { on_this_page: '이 페이지에서', next_page: '다음', previous_page: '이전', + toc_back_to_space: '${1}(으)로 돌아가기', page_last_modified: '마지막 업데이트 ${1}', was_this_helpful: '도움이 되었나요?', was_this_helpful_positive: '네, 도움이 되었어요!', diff --git a/packages/gitbook/src/intl/translations/lt.ts b/packages/gitbook/src/intl/translations/lt.ts index 9dcf78ccd..d759f250f 100644 --- a/packages/gitbook/src/intl/translations/lt.ts +++ b/packages/gitbook/src/intl/translations/lt.ts @@ -44,6 +44,7 @@ export const lt: TranslationLanguage = { on_this_page: 'Šiame puslapyje', next_page: 'Kitas', previous_page: 'Ankstesnis', + toc_back_to_space: 'Atgal į ${1}', page_last_modified: 'Paskutinį kartą atnaujinta ${1}', was_this_helpful: 'Ar tai buvo naudinga?', was_this_helpful_positive: 'Taip, buvo!', diff --git a/packages/gitbook/src/intl/translations/lv.ts b/packages/gitbook/src/intl/translations/lv.ts index 324f4ad22..2bf4fcf6d 100644 --- a/packages/gitbook/src/intl/translations/lv.ts +++ b/packages/gitbook/src/intl/translations/lv.ts @@ -44,6 +44,7 @@ export const lv: TranslationLanguage = { on_this_page: 'Šajā lapā', next_page: 'Nākamā', previous_page: 'Iepriekšējā', + toc_back_to_space: 'Atpakaļ uz ${1}', page_last_modified: 'Pēdoreiz atjaunināts ${1}', was_this_helpful: 'Vai tas bija noderīgi?', was_this_helpful_positive: 'Jā, bija!', diff --git a/packages/gitbook/src/intl/translations/ms.ts b/packages/gitbook/src/intl/translations/ms.ts index 84651b9b7..6b930599d 100644 --- a/packages/gitbook/src/intl/translations/ms.ts +++ b/packages/gitbook/src/intl/translations/ms.ts @@ -44,6 +44,7 @@ export const ms: TranslationLanguage = { on_this_page: 'Pada halaman ini', next_page: 'Seterusnya', previous_page: 'Sebelumnya', + toc_back_to_space: 'Kembali ke ${1}', page_last_modified: 'Terakhir dikemas kini ${1}', was_this_helpful: 'Adakah ini membantu?', was_this_helpful_positive: 'Ya, membantu!', diff --git a/packages/gitbook/src/intl/translations/nl.ts b/packages/gitbook/src/intl/translations/nl.ts index 6fe56a31e..8d9d32f41 100644 --- a/packages/gitbook/src/intl/translations/nl.ts +++ b/packages/gitbook/src/intl/translations/nl.ts @@ -46,6 +46,7 @@ export const nl: TranslationLanguage = { on_this_page: 'Op deze pagina', next_page: 'Volgende', previous_page: 'Vorige', + toc_back_to_space: 'Terug naar ${1}', page_last_modified: 'Laatst bijgewerkt ${1}', was_this_helpful: 'Was dit nuttig?', was_this_helpful_positive: 'Ja, het was nuttig!', diff --git a/packages/gitbook/src/intl/translations/no.ts b/packages/gitbook/src/intl/translations/no.ts index f9bc27f09..d73d097f2 100644 --- a/packages/gitbook/src/intl/translations/no.ts +++ b/packages/gitbook/src/intl/translations/no.ts @@ -45,6 +45,7 @@ export const no: TranslationLanguage = { on_this_page: 'På denne siden', next_page: 'Neste', previous_page: 'Forrige', + toc_back_to_space: 'Tilbake til ${1}', page_last_modified: 'Sist oppdatert ${1}', was_this_helpful: 'Var dette nyttig?', was_this_helpful_positive: 'Ja, det var det!', diff --git a/packages/gitbook/src/intl/translations/pl.ts b/packages/gitbook/src/intl/translations/pl.ts index a7a8fa41d..af3f3a3ab 100644 --- a/packages/gitbook/src/intl/translations/pl.ts +++ b/packages/gitbook/src/intl/translations/pl.ts @@ -44,6 +44,7 @@ export const pl: TranslationLanguage = { on_this_page: 'Na tej stronie', next_page: 'Dalej', previous_page: 'Wstecz', + toc_back_to_space: 'Powrót do ${1}', page_last_modified: 'Ostatnia aktualizacja ${1}', was_this_helpful: 'Czy to było pomocne?', was_this_helpful_positive: 'Tak!', diff --git a/packages/gitbook/src/intl/translations/pt-br.ts b/packages/gitbook/src/intl/translations/pt-br.ts index fe067c70a..e04cee319 100644 --- a/packages/gitbook/src/intl/translations/pt-br.ts +++ b/packages/gitbook/src/intl/translations/pt-br.ts @@ -46,6 +46,7 @@ export const pt_br: TranslationLanguage = { on_this_page: 'Nesta página', next_page: 'Próximo', previous_page: 'Anterior', + toc_back_to_space: 'Voltar para ${1}', page_last_modified: 'Atualizado ${1}', was_this_helpful: 'Isto foi útil?', was_this_helpful_positive: 'Sim, foi!', diff --git a/packages/gitbook/src/intl/translations/pt.ts b/packages/gitbook/src/intl/translations/pt.ts index a9fe630ea..0e0752984 100644 --- a/packages/gitbook/src/intl/translations/pt.ts +++ b/packages/gitbook/src/intl/translations/pt.ts @@ -45,6 +45,7 @@ export const pt: TranslationLanguage = { on_this_page: 'Nesta página', next_page: 'Seguinte', previous_page: 'Anterior', + toc_back_to_space: 'Voltar para ${1}', page_last_modified: 'Última atualização ${1}', was_this_helpful: 'Isto foi útil?', was_this_helpful_positive: 'Sim, foi!', diff --git a/packages/gitbook/src/intl/translations/ro.ts b/packages/gitbook/src/intl/translations/ro.ts index 379f2ff51..d784d5977 100644 --- a/packages/gitbook/src/intl/translations/ro.ts +++ b/packages/gitbook/src/intl/translations/ro.ts @@ -45,6 +45,7 @@ export const ro: TranslationLanguage = { on_this_page: 'Pe această pagină', next_page: 'Următoarea', previous_page: 'Anterioara', + toc_back_to_space: 'Înapoi la ${1}', page_last_modified: 'Ultima actualizare ${1}', was_this_helpful: 'A fost util?', was_this_helpful_positive: 'Da, a fost!', diff --git a/packages/gitbook/src/intl/translations/ru.ts b/packages/gitbook/src/intl/translations/ru.ts index faf8f1111..dad75aa4c 100644 --- a/packages/gitbook/src/intl/translations/ru.ts +++ b/packages/gitbook/src/intl/translations/ru.ts @@ -46,6 +46,7 @@ export const ru: TranslationLanguage = { on_this_page: 'На этой странице', next_page: 'Следующая', previous_page: 'Предыдущая', + toc_back_to_space: 'Назад к ${1}', page_last_modified: 'Последнее обновление ${1}', was_this_helpful: 'Это было полезно?', was_this_helpful_positive: 'Да, было!', diff --git a/packages/gitbook/src/intl/translations/sk.ts b/packages/gitbook/src/intl/translations/sk.ts index da48a3be2..b48507f6c 100644 --- a/packages/gitbook/src/intl/translations/sk.ts +++ b/packages/gitbook/src/intl/translations/sk.ts @@ -45,6 +45,7 @@ export const sk: TranslationLanguage = { on_this_page: 'Na tejto stránke', next_page: 'Ďalej', previous_page: 'Predchádzajúca', + toc_back_to_space: 'Späť na ${1}', page_last_modified: 'Naposledy aktualizované ${1}', was_this_helpful: 'Bolo to užitočné?', was_this_helpful_positive: 'Áno, bolo!', diff --git a/packages/gitbook/src/intl/translations/sl.ts b/packages/gitbook/src/intl/translations/sl.ts index b3ae2b620..5517de6d8 100644 --- a/packages/gitbook/src/intl/translations/sl.ts +++ b/packages/gitbook/src/intl/translations/sl.ts @@ -45,6 +45,7 @@ export const sl: TranslationLanguage = { on_this_page: 'Na tej strani', next_page: 'Naslednja', previous_page: 'Prejšnja', + toc_back_to_space: 'Nazaj na ${1}', page_last_modified: 'Nazadnje posodobljeno ${1}', was_this_helpful: 'Je bilo to koristno?', was_this_helpful_positive: 'Da, bilo je!', diff --git a/packages/gitbook/src/intl/translations/sv.ts b/packages/gitbook/src/intl/translations/sv.ts index 0c375c5da..33096bec8 100644 --- a/packages/gitbook/src/intl/translations/sv.ts +++ b/packages/gitbook/src/intl/translations/sv.ts @@ -44,6 +44,7 @@ export const sv: TranslationLanguage = { on_this_page: 'På den här sidan', next_page: 'Nästa', previous_page: 'Föregående', + toc_back_to_space: 'Tillbaka till ${1}', page_last_modified: 'Senast uppdaterad ${1}', was_this_helpful: 'Var detta till hjälp?', was_this_helpful_positive: 'Ja, det var det!', diff --git a/packages/gitbook/src/intl/translations/th.ts b/packages/gitbook/src/intl/translations/th.ts index 36cd3f058..b225d5dee 100644 --- a/packages/gitbook/src/intl/translations/th.ts +++ b/packages/gitbook/src/intl/translations/th.ts @@ -43,6 +43,7 @@ export const th: TranslationLanguage = { on_this_page: 'ในหน้านี้', next_page: 'ถัดไป', previous_page: 'ก่อนหน้า', + toc_back_to_space: 'กลับไปที่ ${1}', page_last_modified: 'อัปเดตล่าสุด ${1}', was_this_helpful: 'สิ่งนี้มีประโยชน์หรือไม่?', was_this_helpful_positive: 'ใช่ มีประโยชน์!', diff --git a/packages/gitbook/src/intl/translations/tr.ts b/packages/gitbook/src/intl/translations/tr.ts index f819970ad..49c6d7c82 100644 --- a/packages/gitbook/src/intl/translations/tr.ts +++ b/packages/gitbook/src/intl/translations/tr.ts @@ -44,6 +44,7 @@ export const tr: TranslationLanguage = { on_this_page: 'Bu sayfada', next_page: 'Sonraki', previous_page: 'Önceki', + toc_back_to_space: 'Şuraya dön: ${1}', page_last_modified: 'Son güncelleme ${1}', was_this_helpful: 'Bu yararlı oldu mu?', was_this_helpful_positive: 'Evet, oldu!', diff --git a/packages/gitbook/src/intl/translations/uk.ts b/packages/gitbook/src/intl/translations/uk.ts index 9bec9bad0..ded36466c 100644 --- a/packages/gitbook/src/intl/translations/uk.ts +++ b/packages/gitbook/src/intl/translations/uk.ts @@ -44,6 +44,7 @@ export const uk: TranslationLanguage = { on_this_page: 'На цій сторінці', next_page: 'Наступна', previous_page: 'Попередня', + toc_back_to_space: 'Назад до ${1}', page_last_modified: 'Останнє оновлення ${1}', was_this_helpful: 'Це було корисно?', was_this_helpful_positive: 'Так, було!', diff --git a/packages/gitbook/src/intl/translations/vi.ts b/packages/gitbook/src/intl/translations/vi.ts index 8d5f587ef..bf94065e1 100644 --- a/packages/gitbook/src/intl/translations/vi.ts +++ b/packages/gitbook/src/intl/translations/vi.ts @@ -44,6 +44,7 @@ export const vi: TranslationLanguage = { on_this_page: 'Trên trang này', next_page: 'Tiếp theo', previous_page: 'Trước', + toc_back_to_space: 'Quay lại ${1}', page_last_modified: 'Cập nhật lần cuối ${1}', was_this_helpful: 'Nội dung này có hữu ích không?', was_this_helpful_positive: 'Có, hữu ích!', diff --git a/packages/gitbook/src/intl/translations/yue.ts b/packages/gitbook/src/intl/translations/yue.ts index 436b4f94f..bdfa51a2c 100644 --- a/packages/gitbook/src/intl/translations/yue.ts +++ b/packages/gitbook/src/intl/translations/yue.ts @@ -43,6 +43,7 @@ export const yue: TranslationLanguage = { on_this_page: '本頁內容', next_page: '下一頁', previous_page: '上一頁', + toc_back_to_space: '返去${1}', page_last_modified: '最後更新 ${1}', was_this_helpful: '呢個有冇幫助?', was_this_helpful_positive: '有,幫到手!', diff --git a/packages/gitbook/src/intl/translations/zh-tw.ts b/packages/gitbook/src/intl/translations/zh-tw.ts index 830e519f7..033f8afcf 100644 --- a/packages/gitbook/src/intl/translations/zh-tw.ts +++ b/packages/gitbook/src/intl/translations/zh-tw.ts @@ -43,6 +43,7 @@ export const zh_tw: TranslationLanguage = { on_this_page: '本頁內容', next_page: '下一頁', previous_page: '上一頁', + toc_back_to_space: '返回${1}', page_last_modified: '最後更新於 ${1}', was_this_helpful: '這有幫助嗎?', was_this_helpful_positive: '有,很有幫助!', diff --git a/packages/gitbook/src/intl/translations/zh.ts b/packages/gitbook/src/intl/translations/zh.ts index ff64c3071..64467d78c 100644 --- a/packages/gitbook/src/intl/translations/zh.ts +++ b/packages/gitbook/src/intl/translations/zh.ts @@ -44,6 +44,7 @@ export const zh: TranslationLanguage = { on_this_page: '在本页', next_page: '下一页', previous_page: '上一页', + toc_back_to_space: '返回${1}', page_last_modified: '最后更新于 ${1}', was_this_helpful: '这有帮助吗?', was_this_helpful_positive: '是的,很有帮助!', diff --git a/packages/gitbook/src/lib/sites.ts b/packages/gitbook/src/lib/sites.ts index aae773614..e02dfd3af 100644 --- a/packages/gitbook/src/lib/sites.ts +++ b/packages/gitbook/src/lib/sites.ts @@ -259,6 +259,33 @@ export function getLocalizedTitle( return getLocalizedField(entity.localizedTitle, currentLanguage) ?? entity.title; } +/** + * A string concatenation of the site structure (sections and variants) titles for the + * current context, as shown in the site's navigation. Returns an empty string when the + * current space is the default section and variant (i.e. it has no distinguishing label). + */ +export function getSiteStructureTitle(context: GitBookSiteContext): string { + const { visibleSections: sections, siteSpace, visibleSiteSpaces: siteSpaces } = context; + const currentLanguage = context.locale; + + const title = []; + if ( + sections && + sections.current.default === false && // Only if the current section is not the default one + sections.list.filter((section) => section.object === 'site-section').length > 1 // Only if there are multiple sections + ) { + title.push(getLocalizedTitle(sections.current, currentLanguage)); + } + if ( + siteSpaces.length > 1 && // Only if there are multiple variants + siteSpace.default === false && // Only if the variant is not the default one + siteSpaces.filter((space) => space.space.language === siteSpace.space.language).length > 1 // Only if there are multiple variants *for the current language*. This filters out spaces that are "just" translations of each other, not versions. + ) { + title.push(getLocalizedTitle(siteSpace, siteSpace.space.language)); + } + return title.join(' '); +} + /** * Get the localized description for a site entity. */ From 37c2dd8819e228486dec5bb9a333913307cdd96d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= Date: Sun, 5 Jul 2026 22:06:20 +0200 Subject: [PATCH 07/46] Fix flaky customer screenshots caused by the cookie banner (#4363) Co-authored-by: Claude Opus 4.8 (1M context) --- .changeset/customer-goto-domcontentloaded.md | 5 + .changeset/flaky-cookie-banner-screenshots.md | 5 + .../hide-admin-toolbar-in-screenshots.md | 5 + .../reduced-motion-maxwidth-transition.md | 5 + .changeset/reset-cross-space-nav-e2e.md | 5 + bun.lock | 94 ++++++++++-- packages/gitbook/e2e/customers.spec.ts | 142 +++++++++--------- packages/gitbook/e2e/internal.spec.ts | 26 ++-- packages/gitbook/e2e/util.ts | 38 ++++- packages/gitbook/package.json | 4 +- .../src/components/AdminToolbar/Toolbar.tsx | 5 +- .../components/SpaceLayout/SpaceLayout.tsx | 2 +- packages/gitbook/tests/markdown.test.ts | 2 +- 13 files changed, 233 insertions(+), 105 deletions(-) create mode 100644 .changeset/customer-goto-domcontentloaded.md create mode 100644 .changeset/flaky-cookie-banner-screenshots.md create mode 100644 .changeset/hide-admin-toolbar-in-screenshots.md create mode 100644 .changeset/reduced-motion-maxwidth-transition.md create mode 100644 .changeset/reset-cross-space-nav-e2e.md diff --git a/.changeset/customer-goto-domcontentloaded.md b/.changeset/customer-goto-domcontentloaded.md new file mode 100644 index 000000000..c34b07ba3 --- /dev/null +++ b/.changeset/customer-goto-domcontentloaded.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Navigate customer visual tests with `domcontentloaded` to avoid `load`-event hangs on external sites. diff --git a/.changeset/flaky-cookie-banner-screenshots.md b/.changeset/flaky-cookie-banner-screenshots.md new file mode 100644 index 000000000..8f3b9601a --- /dev/null +++ b/.changeset/flaky-cookie-banner-screenshots.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Fix flaky customer e2e screenshots by waiting for the built-in cookie banner before capturing. diff --git a/.changeset/hide-admin-toolbar-in-screenshots.md b/.changeset/hide-admin-toolbar-in-screenshots.md new file mode 100644 index 000000000..76d60b905 --- /dev/null +++ b/.changeset/hide-admin-toolbar-in-screenshots.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Add a `data-testid` to the admin toolbar so e2e tests can assert its presence while hiding it from visual screenshots (it animates open, causing flaky diffs). diff --git a/.changeset/reduced-motion-maxwidth-transition.md b/.changeset/reduced-motion-maxwidth-transition.md new file mode 100644 index 000000000..682519b53 --- /dev/null +++ b/.changeset/reduced-motion-maxwidth-transition.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Disable the content max-width transition under reduced motion, matching the surrounding layout transitions. diff --git a/.changeset/reset-cross-space-nav-e2e.md b/.changeset/reset-cross-space-nav-e2e.md new file mode 100644 index 000000000..e343faac9 --- /dev/null +++ b/.changeset/reset-cross-space-nav-e2e.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Reset cross-space navigation state between e2e navigations so the "Back to " shortcut can't leak in and cause flaky screenshots. diff --git a/bun.lock b/bun.lock index 072d9c885..e6214a000 100644 --- a/bun.lock +++ b/bun.lock @@ -201,8 +201,8 @@ "zustand": "^5.0.3", }, "devDependencies": { - "@argos-ci/playwright": "^6.4.2", - "@playwright/test": "^1.58.2", + "@argos-ci/playwright": "^7.2.0", + "@playwright/test": "^1.61.1", "@scalar/api-client-react": "catalog:", "@tailwindcss/postcss": "^4.1.11", "@types/js-cookie": "^3.0.6", @@ -392,15 +392,15 @@ "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], - "@argos-ci/api-client": ["@argos-ci/api-client@0.16.0", "", { "dependencies": { "debug": "^4.4.3", "openapi-fetch": "^0.15.0" } }, "sha512-BG6g+AZABKN8W2syzfPWKhPxxrAB9Wjp2iNS11R4IskHDV6T41+qeQDpT88GOq6eUZ64Sjzoh/nXG+9EpNxtfw=="], + "@argos-ci/api-client": ["@argos-ci/api-client@0.24.0", "", { "dependencies": { "debug": "^4.4.3", "openapi-fetch": "^0.17.0", "p-retry": "^8.0.0" } }, "sha512-pWEkmqVyD2CbGGG9KsTQgufWIYRzMsZVWyLY4d3lI2pUbuTlv0W0pvvmJybmKkIk3Tcn1T5vMBjT2DYuUiZbqQ=="], - "@argos-ci/browser": ["@argos-ci/browser@5.1.2", "", {}, "sha512-eQqtM54Vh83++Dac5+7ml4YXKW/KnUHRQWjTZ+VGeXfOJdjnZa4JjfVL6fnFG6CKrXjCK5dd9gcZRmbVpbt8rA=="], + "@argos-ci/browser": ["@argos-ci/browser@6.3.0", "", {}, "sha512-quTww/Yhm2jv+Y6q6dD4xEpm9Bb/SrZNFJaXgncAnCvW8jNnCrmDE2+8zAuGUh41Wkq1JQNDYKlBCO/RpY6A8Q=="], - "@argos-ci/core": ["@argos-ci/core@5.1.1", "", { "dependencies": { "@argos-ci/api-client": "0.16.0", "@argos-ci/util": "3.2.0", "convict": "^6.2.4", "debug": "^4.4.3", "fast-glob": "^3.3.3", "mime-types": "^3.0.2", "sharp": "^0.34.5", "tmp": "^0.2.5" } }, "sha512-RI5pYb5wmWUoXzWefNCjKSqGA3BroUB2ac9awlYV3McJjSAW8nCGJUt2Eds90Shkp+i4S6KbMLnLG56slhTCsg=="], + "@argos-ci/core": ["@argos-ci/core@6.4.0", "", { "dependencies": { "@argos-ci/api-client": "0.24.0", "@argos-ci/util": "4.0.0", "convict": "^6.2.5", "debug": "^4.4.3", "fast-glob": "^3.3.3", "mime-types": "^3.0.2", "p-retry": "^8.0.0", "sharp": "^0.35.2", "tmp": "^0.2.7" } }, "sha512-YUySBUbjHpI8GZKe96n4VzgPPXl90hjw/GCHLQ3U6xbbIEZczCBCHeHB/MDjyXCHyk+9T7FUJSE0ddWnpDRI8Q=="], - "@argos-ci/playwright": ["@argos-ci/playwright@6.4.2", "", { "dependencies": { "@argos-ci/browser": "5.1.2", "@argos-ci/core": "5.1.1", "@argos-ci/util": "3.2.0", "chalk": "^5.6.2", "debug": "^4.4.3" } }, "sha512-0HE4rmgx2/znlMUi3z0Bp8OD7u7zNls9SlljL+qTVoWQRW22sDvpmRdZpjzb4pEjoe6onqPTDOqm5/f8QYHMwg=="], + "@argos-ci/playwright": ["@argos-ci/playwright@7.2.0", "", { "dependencies": { "@argos-ci/browser": "6.3.0", "@argos-ci/core": "6.4.0", "@argos-ci/util": "4.0.0", "chalk": "^5.6.2", "debug": "^4.4.3" } }, "sha512-YkZswuc+hZLC8sUUp6QPzZJSfZsxnbt+sfyZr5DWSHuz5g2XRxJ8oEsXJwFraDMYku5jwfAtqXvza1J3WUo6qA=="], - "@argos-ci/util": ["@argos-ci/util@3.2.0", "", {}, "sha512-/Bn0qCH8VsdPv5WB9TUEf3oTgsIqsTMUEjPVDopHLzKK+j7nQYGOF3MnN7VhBow82BXeStBfJCS3UiZ6cgxRlw=="], + "@argos-ci/util": ["@argos-ci/util@4.0.0", "", {}, "sha512-2yrJr81OVpa2TkvSjzGZlGv6SVqZcxF62AVk6M79aZ7nXkwWFg3tjA8awbFwn2mODI67gkP5FarAD29bzx3P3w=="], "@ast-grep/napi": ["@ast-grep/napi@0.40.5", "", { "optionalDependencies": { "@ast-grep/napi-darwin-arm64": "0.40.5", "@ast-grep/napi-darwin-x64": "0.40.5", "@ast-grep/napi-linux-arm64-gnu": "0.40.5", "@ast-grep/napi-linux-arm64-musl": "0.40.5", "@ast-grep/napi-linux-x64-gnu": "0.40.5", "@ast-grep/napi-linux-x64-musl": "0.40.5", "@ast-grep/napi-win32-arm64-msvc": "0.40.5", "@ast-grep/napi-win32-ia32-msvc": "0.40.5", "@ast-grep/napi-win32-x64-msvc": "0.40.5" } }, "sha512-hJA62OeBKUQT68DD2gDyhOqJxZxycqg8wLxbqjgqSzYttCMSDL9tiAQ9abgekBYNHudbJosm9sWOEbmCDfpX2A=="], @@ -812,6 +812,8 @@ "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + "@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.3", "", { "dependencies": { "@img/sharp-wasm32": "0.35.3" }, "os": "freebsd" }, "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg=="], + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], @@ -850,6 +852,8 @@ "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + "@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.3", "", { "dependencies": { "@img/sharp-wasm32": "0.35.3" }, "cpu": "none" }, "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q=="], + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], @@ -1026,7 +1030,7 @@ "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], - "@playwright/test": ["@playwright/test@1.58.2", "", { "dependencies": { "playwright": "1.58.2" }, "bin": { "playwright": "cli.js" } }, "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA=="], + "@playwright/test": ["@playwright/test@1.61.1", "", { "dependencies": { "playwright": "1.61.1" }, "bin": { "playwright": "cli.js" } }, "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig=="], "@poppinss/colors": ["@poppinss/colors@4.1.5", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-FvdDqtcRCtz6hThExcFOgW0cWX+xwSMWcRuQe5ZEb2m7cVQOAVZOIMt+/v9RxGiD9/OY16qJBXK4CVKWAPalBw=="], @@ -2024,7 +2028,7 @@ "convert-hrtime": ["convert-hrtime@3.0.0", "", {}, "sha512-7V+KqSvMiHp8yWDuwfww06XleMWVVB9b9tURBx+G7UTADuo5hYPuowKloz4OzOqbPezxgo+fdQ1522WzPG4OeA=="], - "convict": ["convict@6.2.4", "", { "dependencies": { "lodash.clonedeep": "^4.5.0", "yargs-parser": "^20.2.7" } }, "sha512-qN60BAwdMVdofckX7AlohVJ2x9UvjTNoKVXCL2LxFk1l7757EJqf1nySdMkPQer0bt8kQ5lQiyZ9/2NvrFBuwQ=="], + "convict": ["convict@6.2.5", "", { "dependencies": { "lodash.clonedeep": "^4.5.0", "yargs-parser": "^20.2.7" } }, "sha512-JtXpxqDqJ8P0UwEHwhxLzCIXQy97vlYBZR222Sbzb1q1Erex9ASrztJ29SyhWFQjod1AeFBaPzEEC8YvtZMIYg=="], "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], @@ -2908,11 +2912,11 @@ "oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="], - "openapi-fetch": ["openapi-fetch@0.15.2", "", { "dependencies": { "openapi-typescript-helpers": "^0.0.15" } }, "sha512-rdYTzUmSsJevmNqg7fwUVGuKc2Gfb9h6ph74EVPkPfIGJaZTfqdIbJahtbJ3qg1LKinln30hqZniLnKpH0RJBg=="], + "openapi-fetch": ["openapi-fetch@0.17.0", "", { "dependencies": { "openapi-typescript-helpers": "^0.1.0" } }, "sha512-PsbZR1wAPcG91eEthKhN+Zn92FMHxv+/faECIwjXdxfTODGSGegYv0sc1Olz+HYPvKOuoXfp+0pA2XVt2cI0Ig=="], "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], - "openapi-typescript-helpers": ["openapi-typescript-helpers@0.0.15", "", {}, "sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw=="], + "openapi-typescript-helpers": ["openapi-typescript-helpers@0.1.0", "", {}, "sha512-OKTGPthhivLw/fHz6c3OPtg72vi86qaMlqbJuVJ23qOvQ+53uw1n7HdmkJFibloF7QEjDrDkzJiOJuockM/ljw=="], "os-paths": ["os-paths@4.4.0", "", {}, "sha512-wrAwOeXp1RRMFfQY8Sy7VaGVmPocaLwSFOYCGKSyo8qmJ+/yaafCl5BCA1IQZWqFSRBrKDYFeR9d/VyQzfH/jg=="], @@ -3322,7 +3326,7 @@ "tldts-core": ["tldts-core@7.0.30", "", {}, "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q=="], - "tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="], + "tmp": ["tmp@0.2.7", "", {}, "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], @@ -3538,6 +3542,8 @@ "@argos-ci/core/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + "@argos-ci/core/sharp": ["sharp@0.35.3", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.3", "@img/sharp-darwin-x64": "0.35.3", "@img/sharp-freebsd-wasm32": "0.35.3", "@img/sharp-libvips-darwin-arm64": "1.3.2", "@img/sharp-libvips-darwin-x64": "1.3.2", "@img/sharp-libvips-linux-arm": "1.3.2", "@img/sharp-libvips-linux-arm64": "1.3.2", "@img/sharp-libvips-linux-ppc64": "1.3.2", "@img/sharp-libvips-linux-riscv64": "1.3.2", "@img/sharp-libvips-linux-s390x": "1.3.2", "@img/sharp-libvips-linux-x64": "1.3.2", "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", "@img/sharp-libvips-linuxmusl-x64": "1.3.2", "@img/sharp-linux-arm": "0.35.3", "@img/sharp-linux-arm64": "0.35.3", "@img/sharp-linux-ppc64": "0.35.3", "@img/sharp-linux-riscv64": "0.35.3", "@img/sharp-linux-s390x": "0.35.3", "@img/sharp-linux-x64": "0.35.3", "@img/sharp-linuxmusl-arm64": "0.35.3", "@img/sharp-linuxmusl-x64": "0.35.3", "@img/sharp-webcontainers-wasm32": "0.35.3", "@img/sharp-win32-arm64": "0.35.3", "@img/sharp-win32-ia32": "0.35.3", "@img/sharp-win32-x64": "0.35.3" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q=="], + "@aws-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.734.0", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-o11tSPTT70nAkGV1fN9wm/hAIiLPyWX6SuGf+9JyTp7S/rC2cFWhR26MvA69nplcjNaXVzB0f+QFrLXXjOqCrg=="], "@aws-crypto/crc32c/@aws-sdk/types": ["@aws-sdk/types@3.734.0", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-o11tSPTT70nAkGV1fN9wm/hAIiLPyWX6SuGf+9JyTp7S/rC2cFWhR26MvA69nplcjNaXVzB0f+QFrLXXjOqCrg=="], @@ -3630,6 +3636,10 @@ "@hyperjump/json-schema/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], + "@img/sharp-freebsd-wasm32/@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.3", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w=="], + + "@img/sharp-webcontainers-wasm32/@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.3", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w=="], + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], @@ -3682,7 +3692,7 @@ "@oxc-transform/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], - "@playwright/test/playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="], + "@playwright/test/playwright": ["playwright@1.61.1", "", { "dependencies": { "playwright-core": "1.61.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ=="], "@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], @@ -4288,6 +4298,58 @@ "@argos-ci/core/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + "@argos-ci/core/sharp/@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], + + "@argos-ci/core/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.2" }, "os": "darwin", "cpu": "arm64" }, "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg=="], + + "@argos-ci/core/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.2" }, "os": "darwin", "cpu": "x64" }, "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w=="], + + "@argos-ci/core/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg=="], + + "@argos-ci/core/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw=="], + + "@argos-ci/core/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.2", "", { "os": "linux", "cpu": "arm" }, "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ=="], + + "@argos-ci/core/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA=="], + + "@argos-ci/core/sharp/@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw=="], + + "@argos-ci/core/sharp/@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.2", "", { "os": "linux", "cpu": "none" }, "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w=="], + + "@argos-ci/core/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ=="], + + "@argos-ci/core/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w=="], + + "@argos-ci/core/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw=="], + + "@argos-ci/core/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ=="], + + "@argos-ci/core/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.2" }, "os": "linux", "cpu": "arm" }, "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA=="], + + "@argos-ci/core/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ=="], + + "@argos-ci/core/sharp/@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.2" }, "os": "linux", "cpu": "ppc64" }, "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA=="], + + "@argos-ci/core/sharp/@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.2" }, "os": "linux", "cpu": "none" }, "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ=="], + + "@argos-ci/core/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.2" }, "os": "linux", "cpu": "s390x" }, "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw=="], + + "@argos-ci/core/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA=="], + + "@argos-ci/core/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w=="], + + "@argos-ci/core/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg=="], + + "@argos-ci/core/sharp/@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w=="], + + "@argos-ci/core/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw=="], + + "@argos-ci/core/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.3", "", { "os": "win32", "cpu": "x64" }, "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA=="], + + "@argos-ci/core/sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "@argos-ci/core/sharp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "@aws-crypto/crc32/@aws-sdk/types/@smithy/types": ["@smithy/types@4.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-enhjdwp4D7CXmwLtD6zbcDMbo6/T6WtuuKCY49Xxc6OMOmUWlBEBDREsxxgV2LIdeQPW756+f97GzcgAwp3iLw=="], "@aws-crypto/crc32c/@aws-sdk/types/@smithy/types": ["@smithy/types@4.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-enhjdwp4D7CXmwLtD6zbcDMbo6/T6WtuuKCY49Xxc6OMOmUWlBEBDREsxxgV2LIdeQPW756+f97GzcgAwp3iLw=="], @@ -4332,6 +4394,10 @@ "@headlessui/react/@floating-ui/react/@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.7", "", { "dependencies": { "@floating-ui/dom": "^1.7.5" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg=="], + "@img/sharp-freebsd-wasm32/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], + + "@img/sharp-webcontainers-wasm32/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], @@ -4370,7 +4436,7 @@ "@playwright/test/playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], - "@playwright/test/playwright/playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="], + "@playwright/test/playwright/playwright-core": ["playwright-core@1.61.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg=="], "@radix-ui/react-arrow/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "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-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], diff --git a/packages/gitbook/e2e/customers.spec.ts b/packages/gitbook/e2e/customers.spec.ts index 422e99ee4..afd5410e0 100644 --- a/packages/gitbook/e2e/customers.spec.ts +++ b/packages/gitbook/e2e/customers.spec.ts @@ -65,11 +65,12 @@ const testCases: TestsCase[] = [ contentBaseURL: 'https://docs.gmgn.ai', tests: [{ name: 'Home', url: '/' }], }, - { - name: 'docs.portainer.io', - contentBaseURL: 'https://docs.portainer.io', - tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], - }, + // Removed, an AI widget, makes it flaky. + // { + // name: 'docs.portainer.io', + // contentBaseURL: 'https://docs.portainer.io', + // tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], + // }, { name: 'docs.chirptoken.io', contentBaseURL: 'https://docs.chirptoken.io', @@ -133,7 +134,7 @@ const testCases: TestsCase[] = [ { name: 'docs.tickettool.xyz', contentBaseURL: 'https://docs.tickettool.xyz', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'wiki.redmodding.org', @@ -167,11 +168,6 @@ const testCases: TestsCase[] = [ }, ], }, - { - name: 'sosovalue-white-paper.gitbook.io', - contentBaseURL: 'https://sosovalue-white-paper.gitbook.io', - tests: [{ name: 'Home', url: '/' }], - }, { name: 'chartschool.stockcharts.com', contentBaseURL: 'https://chartschool.stockcharts.com', @@ -180,18 +176,13 @@ const testCases: TestsCase[] = [ { name: 'docs.soniclabs.com', contentBaseURL: 'https://docs.soniclabs.com', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'docs.thousandeyes.com', contentBaseURL: 'https://docs.thousandeyes.com', tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, - { - name: 'docs.raydium.io', - contentBaseURL: 'https://docs.raydium.io', - tests: [{ name: 'Home', url: '/' }], - }, { name: 'docs.fluentbit.io', contentBaseURL: 'https://docs.fluentbit.io', @@ -214,17 +205,17 @@ const testCases: TestsCase[] = [ { name: 'unsloth.ai/docs', contentBaseURL: 'https://unsloth.ai', - tests: [{ name: 'Home', url: '/docs' }], + tests: [{ name: 'Home', url: '/docs', run: waitForCookiesDialog }], }, { name: 'mariadb.com/docs', contentBaseURL: 'https://mariadb.com', - tests: [{ name: 'Home', url: '/docs' }], + tests: [{ name: 'Home', url: '/docs', run: waitForCookiesDialog }], }, { name: 'docs.n8n.io', contentBaseURL: 'https://docs.n8n.io', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'docs.cherry-ai.com', @@ -234,17 +225,17 @@ const testCases: TestsCase[] = [ { name: 'library.zoom.com', contentBaseURL: 'https://library.zoom.com', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'help.verkada.com', contentBaseURL: 'https://help.verkada.com', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'docs.overleaf.com', contentBaseURL: 'https://docs.overleaf.com', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'wiki.tiltedphoques.com/tilted-online', @@ -259,27 +250,27 @@ const testCases: TestsCase[] = [ { name: 'kakaobusiness.gitbook.io/main', contentBaseURL: 'https://kakaobusiness.gitbook.io', - tests: [{ name: 'Home', url: '/main' }], + tests: [{ name: 'Home', url: '/main', run: waitForCookiesDialog }], }, { name: 'docs.maestro.dev', contentBaseURL: 'https://docs.maestro.dev', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'developers.oxylabs.io', contentBaseURL: 'https://developers.oxylabs.io', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'docs.parallels.com/landing', contentBaseURL: 'https://docs.parallels.com', - tests: [{ name: 'Home', url: '/landing' }], + tests: [{ name: 'Home', url: '/landing', run: waitForCookiesDialog }], }, { name: 'help.impact.com', contentBaseURL: 'https://help.impact.com', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'docs.9proxy.com', @@ -299,7 +290,7 @@ const testCases: TestsCase[] = [ { name: 'help.aikido.dev', contentBaseURL: 'https://help.aikido.dev', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'doc.demarche.numerique.gouv.fr', @@ -319,17 +310,17 @@ const testCases: TestsCase[] = [ { name: 'docs.triumpharcade.com', contentBaseURL: 'https://docs.triumpharcade.com', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'docs.nats.io', contentBaseURL: 'https://docs.nats.io', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'help.glpi-project.org', contentBaseURL: 'https://help.glpi-project.org', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'docs.waydro.id', @@ -349,38 +340,39 @@ const testCases: TestsCase[] = [ { name: 'docs.pinot.apache.org', contentBaseURL: 'https://docs.pinot.apache.org', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'docs.devolutions.net', contentBaseURL: 'https://docs.devolutions.net', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'guides.gresb.com', contentBaseURL: 'https://guides.gresb.com', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'docs.prestashop-project.org/welcome', contentBaseURL: 'https://docs.prestashop-project.org', - tests: [{ name: 'Home', url: '/welcome' }], + tests: [{ name: 'Home', url: '/welcome', run: waitForCookiesDialog }], }, { name: 'help.researchgate.net', contentBaseURL: 'https://help.researchgate.net', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'docs.verifone.com', contentBaseURL: 'https://docs.verifone.com', - tests: [{ name: 'Home', url: '/' }], - }, - { - name: 'docs.roboflow.com', - contentBaseURL: 'https://docs.roboflow.com', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, + // Deactivate it because of a custom Ask AI that causes flakiness. + // { + // name: 'docs.roboflow.com', + // contentBaseURL: 'https://docs.roboflow.com', + // tests: [{ name: 'Home', url: '/' }], + // }, { name: 'www.netexec.wiki', contentBaseURL: 'https://www.netexec.wiki', @@ -395,42 +387,43 @@ const testCases: TestsCase[] = [ name: 'gitbook.com/docs', contentBaseURL: 'https://gitbook.com', tests: [ - { name: 'Home', url: '/docs' }, + { name: 'Home', url: '/docs', run: waitForCookiesDialog }, { name: 'OpenAPI', url: '/docs/developers/gitbook-api/api-reference/docs-sites/site-ai-ask', + run: waitForCookiesDialog, }, ], }, { name: 'documentation.gravitee.io', contentBaseURL: 'https://documentation.gravitee.io', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'faq.wanttopay.net/wanttopay-app', contentBaseURL: 'https://faq.wanttopay.net', - tests: [{ name: 'Home', url: '/wanttopay-app' }], + tests: [{ name: 'Home', url: '/wanttopay-app', run: waitForCookiesDialog }], }, { name: 'guide.prismlive.com', contentBaseURL: 'https://guide.prismlive.com', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'docs.ionos.com/cloud', contentBaseURL: 'https://docs.ionos.com', - tests: [{ name: 'Home', url: '/cloud' }], + tests: [{ name: 'Home', url: '/cloud', run: waitForCookiesDialog }], }, { name: 'support.evite.com', contentBaseURL: 'https://support.evite.com', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'knowledge.illumina.com', contentBaseURL: 'https://knowledge.illumina.com', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'wiki.retrobat.org', @@ -440,7 +433,7 @@ const testCases: TestsCase[] = [ { name: 'wiki.polymaker.com', contentBaseURL: 'https://wiki.polymaker.com', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'docs.ducks-services.com', @@ -450,7 +443,7 @@ const testCases: TestsCase[] = [ { name: 'docs.hex-rays.com', contentBaseURL: 'https://docs.hex-rays.com', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'whitepaper.interlinklabs.ai', @@ -460,7 +453,7 @@ const testCases: TestsCase[] = [ { name: 'help.openloyalty.io', contentBaseURL: 'https://help.openloyalty.io', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'retrozia.gitbook.io/retrozia', @@ -470,17 +463,17 @@ const testCases: TestsCase[] = [ { name: 'helpcenter.channable.com', contentBaseURL: 'https://helpcenter.channable.com', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'developerdocs.instructure.com', contentBaseURL: 'https://developerdocs.instructure.com', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'legal.jagex.com', contentBaseURL: 'https://legal.jagex.com', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'manual.edgetx.org', @@ -490,17 +483,18 @@ const testCases: TestsCase[] = [ { name: 'docs.cortex.io', contentBaseURL: 'https://docs.cortex.io', - tests: [{ name: 'Home', url: '/' }], - }, - { - name: 'docs.mufy.ai', - contentBaseURL: 'https://docs.mufy.ai', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, + // Flaky because of the GIF + // { + // name: 'docs.mufy.ai', + // contentBaseURL: 'https://docs.mufy.ai', + // tests: [{ name: 'Home', url: '/' }], + // }, { name: 'docs.ndi.video/all', contentBaseURL: 'https://docs.ndi.video', - tests: [{ name: 'Home', url: '/all' }], + tests: [{ name: 'Home', url: '/all', run: waitForCookiesDialog }], }, { name: 'docs.sevenpens.com/drawtab', @@ -515,7 +509,7 @@ const testCases: TestsCase[] = [ { name: 'docs.holybro.com', contentBaseURL: 'https://docs.holybro.com', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'help.tokenpocket.pro/en', @@ -530,7 +524,7 @@ const testCases: TestsCase[] = [ { name: 'tools.osintnewsletter.com', contentBaseURL: 'https://tools.osintnewsletter.com', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'wiki.mmorealms.gg', @@ -540,7 +534,7 @@ const testCases: TestsCase[] = [ { name: 'docs.vectra.ai', contentBaseURL: 'https://docs.vectra.ai', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'docs.cipp.app', @@ -565,32 +559,32 @@ const testCases: TestsCase[] = [ { name: 'docs.patchmypc.com', contentBaseURL: 'https://docs.patchmypc.com', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'guide.cryosparc.com', contentBaseURL: 'https://guide.cryosparc.com', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'guides.stellaraio.com/stellar', contentBaseURL: 'https://guides.stellaraio.com', - tests: [{ name: 'Home', url: '/stellar' }], + tests: [{ name: 'Home', url: '/stellar', run: waitForCookiesDialog }], }, { name: 'docs.iyzico.com', contentBaseURL: 'https://docs.iyzico.com', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'help.wotnot.io', contentBaseURL: 'https://help.wotnot.io', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'docs.sportmonks.com/v3', contentBaseURL: 'https://docs.sportmonks.com', - tests: [{ name: 'Home', url: '/v3' }], + tests: [{ name: 'Home', url: '/v3', run: waitForCookiesDialog }], }, { name: 'docs.payments.thalescloud.io', @@ -600,12 +594,12 @@ const testCases: TestsCase[] = [ { name: 'doc.anytype.io/anytype-docs', contentBaseURL: 'https://doc.anytype.io', - tests: [{ name: 'Home', url: '/anytype-docs' }], + tests: [{ name: 'Home', url: '/anytype-docs', run: waitForCookiesDialog }], }, { name: 'help.blotato.com', contentBaseURL: 'https://help.blotato.com', - tests: [{ name: 'Home', url: '/' }], + tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, { name: 'docs.cartographer3d.com', diff --git a/packages/gitbook/e2e/internal.spec.ts b/packages/gitbook/e2e/internal.spec.ts index 77a86956f..77ee57d9d 100644 --- a/packages/gitbook/e2e/internal.spec.ts +++ b/packages/gitbook/e2e/internal.spec.ts @@ -32,6 +32,7 @@ import { runTestCases, setTimeToMorning, waitForAIChatResponse, + waitForAdminToolbar, waitForCookiesDialog, waitForCoverImages, waitForNotFound, @@ -612,21 +613,19 @@ const testCases: TestsCase[] = [ name: 'Site with sections and section groups', url: '', }, - { - name: 'Section group dropdown', - url: '', - run: async (page) => { - await page.getByRole('button', { name: 'Test Section Group 1' }).hover(); - await expect(page.getByRole('link', { name: /Section B/ })).toBeVisible(); - }, - }, { name: 'Section group link', url: '', screenshot: false, run: async (page) => { - const sectionGroupDropdown = await page.getByText('Test Section Group 1'); - await sectionGroupDropdown.hover(); + const trigger = page.getByRole('button', { name: 'Test Section Group 1' }); + // Radix NavigationMenu opens the dropdown on a `pointermove`. A single + // synthetic hover can land before hydration and be lost, so re-hover + // until the dropdown content actually appears. + await expect(async () => { + await trigger.hover(); + await expect(page.getByText('Section B')).toBeVisible({ timeout: 1000 }); + }).toPass({ timeout: 15000 }); await page.getByText('Section B').click(); await page.waitForURL((url) => url.pathname.includes('/sections/sections-4')); }, @@ -656,7 +655,12 @@ const testCases: TestsCase[] = [ { name: 'Revision', url: '~/revisions/S55pwsEr5UVoroaOiWnP/blocks/headings', - run: waitForCookiesDialog, + run: async (page) => { + await waitForCookiesDialog(page); + // Viewing a past revision shows the admin toolbar; assert it is + // present (it is hidden from the screenshot as it animates open). + await waitForAdminToolbar(page); + }, }, { name: 'Invalid revision', diff --git a/packages/gitbook/e2e/util.ts b/packages/gitbook/e2e/util.ts index 9b170e78c..402f4aede 100644 --- a/packages/gitbook/e2e/util.ts +++ b/packages/gitbook/e2e/util.ts @@ -172,6 +172,19 @@ export async function waitForCookiesDialog(page: Page) { }); } +/** + * Wait for the GitBook admin toolbar to be present. + * + * The toolbar only renders when signed in to GitBook. It is hidden from + * screenshots (see `argosCSS`) because it auto-expands with an animation, so + * use this to assert it is rendered without capturing its flaky visual state. + */ +export async function waitForAdminToolbar(page: Page) { + await expect(page.getByTestId('admin-toolbar')).toBeVisible({ + timeout: 10_000, + }); +} + export async function waitForNotFound(_page: Page, response: Response | null) { expect(response).not.toBeNull(); expect(response?.status()).toBe(404); @@ -248,6 +261,19 @@ export function runTestCases(testCases: TestsCase[]) { ); } + // Reset the cross-space navigation state on every document load so the + // "Back to " shortcut never leaks between navigations/tests. It is + // detected client-side from this sessionStorage, and a stale value (e.g. + // after a retry or a cross-space redirect) makes it appear or not + // non-deterministically, causing flaky screenshots. + await page.addInitScript(() => { + try { + sessionStorage.removeItem('gitbook-space-navigation:last'); + sessionStorage.removeItem('gitbook-space-navigation:back'); + sessionStorage.removeItem('gitbook-space-navigation:from-picker'); + } catch {} + }); + // 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) => { @@ -263,7 +289,11 @@ export function runTestCases(testCases: TestsCase[]) { } }); - const response = await page.goto(url); + // Wait only for `domcontentloaded` rather than the default `load`: these + // are real customer sites whose third-party subresources can hang and + // never fire `load`, aborting the navigation. Argos stabilization (run in + // `beforeScreenshot`) still waits for images/fonts before capturing. + const response = await page.goto(url, { waitUntil: 'domcontentloaded' }); if (testEntry.run) { await testEntry.run(page, response); } @@ -283,6 +313,12 @@ export function runTestCases(testCases: TestsCase[]) { /* Hide Intercom */ .intercom-lightweight-app { display: none !important; + } + /* Hide the GitBook admin toolbar: it auto-expands with an + animation, so its state at capture time is non-deterministic. + Its presence is asserted separately via waitForAdminToolbar. */ + [data-testid="admin-toolbar"] { + display: none !important; } `, threshold: screenshotOptions?.threshold ?? undefined, diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json index eb46bcf3e..37a97b527 100644 --- a/packages/gitbook/package.json +++ b/packages/gitbook/package.json @@ -93,8 +93,8 @@ "zustand": "^5.0.3" }, "devDependencies": { - "@argos-ci/playwright": "^6.4.2", - "@playwright/test": "^1.58.2", + "@argos-ci/playwright": "^7.2.0", + "@playwright/test": "^1.61.1", "@scalar/api-client-react": "catalog:", "@tailwindcss/postcss": "^4.1.11", "@types/js-cookie": "^3.0.6", diff --git a/packages/gitbook/src/components/AdminToolbar/Toolbar.tsx b/packages/gitbook/src/components/AdminToolbar/Toolbar.tsx index 8b62699a6..93f70d63a 100644 --- a/packages/gitbook/src/components/AdminToolbar/Toolbar.tsx +++ b/packages/gitbook/src/components/AdminToolbar/Toolbar.tsx @@ -105,7 +105,10 @@ export function Toolbar(props: ToolbarProps) { } return ( - + diff --git a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx index ae3e69456..a516cba1a 100644 --- a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx +++ b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx @@ -141,7 +141,7 @@ export function SpaceLayout(props: SpaceLayoutProps) { 'lg:flex-row', 'lg:justify-center', CONTAINER_STYLE, - 'transition-[max-width] duration-300', + 'transition-[max-width] duration-300 motion-reduce:transition-none', !withTopHeader || variants.generic.length > 1 ? 'has-sidebar' diff --git a/packages/gitbook/tests/markdown.test.ts b/packages/gitbook/tests/markdown.test.ts index 0360e675f..d2d55b676 100644 --- a/packages/gitbook/tests/markdown.test.ts +++ b/packages/gitbook/tests/markdown.test.ts @@ -191,7 +191,7 @@ describe('markdown ask responses', () => { expect(response.headers.get('x-robots-tag')).toBe('noindex'); expect(text).toContain(ASK_QUESTION_HEADING); }, - { timeout: 30_000 } + { timeout: 60_000 } ); it( From 139b897521bb2312c1b04ed17f8486594ac72e07 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein Date: Mon, 6 Jul 2026 11:03:39 +0200 Subject: [PATCH 08/46] Improve language selector on mobile (#4361) --- .../gitbook/src/components/Header/Header.tsx | 4 +- .../src/components/Header/SpacesDropdown.tsx | 41 ++++++++++--------- .../components/SpaceLayout/SpaceLayout.tsx | 3 +- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/packages/gitbook/src/components/Header/Header.tsx b/packages/gitbook/src/components/Header/Header.tsx index ae04b9390..b4c791047 100644 --- a/packages/gitbook/src/components/Header/Header.tsx +++ b/packages/gitbook/src/components/Header/Header.tsx @@ -196,7 +196,7 @@ export async function Header(props: { ) ?? siteSpace } siteSpaces={variants.translations} - className="flex! site-header:theme-bold:text-header-link hover:site-header:theme-bold:bg-header-link/3 focus-visible:site-header:theme-bold:bg-header-link/3 aria-expanded:site-header:theme-bold:bg-header-link/5" + className="flex! -mx-3 site-header:theme-bold:text-header-link hover:site-header:theme-bold:bg-header-link/3 focus-visible:site-header:theme-bold:bg-header-link/3 aria-expanded:site-header:theme-bold:bg-header-link/5" /> ) : null} @@ -217,7 +217,7 @@ export async function Header(props: { ) ?? siteSpace } siteSpaces={variants.translations} - className="my-1.5 ml-2 self-start" + className="-mx-3 my-1.5 ml-2 self-start" /> ) : null} diff --git a/packages/gitbook/src/components/Header/SpacesDropdown.tsx b/packages/gitbook/src/components/Header/SpacesDropdown.tsx index 477a87a6f..c9fa092bc 100644 --- a/packages/gitbook/src/components/Header/SpacesDropdown.tsx +++ b/packages/gitbook/src/components/Header/SpacesDropdown.tsx @@ -14,15 +14,15 @@ function startsWithEmoji(text: string): boolean { return EMOJI_REGEX.test(text); } -export function SpacesDropdown(props: { - context: GitBookSiteContext; - siteSpace: SiteSpace; - siteSpaces: SiteSpace[]; - className?: string; - variant?: ButtonProps['variant']; - icon?: IconName; -}) { - const { context, siteSpace, siteSpaces, className, variant = 'secondary', icon } = props; +export function SpacesDropdown( + props: { + context: GitBookSiteContext; + siteSpace: SiteSpace; + siteSpaces: SiteSpace[]; + className?: string; + } & ButtonProps +) { + const { context, siteSpace, siteSpaces, className, ...buttonProps } = props; const currentLanguage = context.locale; const dropdownClassName = tcls( @@ -41,8 +41,8 @@ export function SpacesDropdown(props: { return ( ); } diff --git a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx index a516cba1a..1f273a1d4 100644 --- a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx +++ b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx @@ -171,7 +171,7 @@ export function SpaceLayout(props: SpaceLayoutProps) { // On bold themes also color the TOC header so the logo looks correct. 'site-header:theme-bold:bg-header-background', 'site-header:theme-bold:m-[-1.5rem_-1px_-0.5rem_-2rem]', - 'site-header:theme-bold:p-[1rem_0_1rem_2rem]' + 'site-header:theme-bold:p-[1rem_1rem_1rem_2rem]' )} > @@ -185,6 +185,7 @@ export function SpaceLayout(props: SpaceLayoutProps) { } siteSpaces={variants.translations} className="[&_.button-leading-icon]:block! ml-auto py-2 [&_.button-content]:hidden" + variant="header" /> ) : null} From fdea8f1c47fd95e7c167d7874c23eacb5c24cfe1 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein Date: Mon, 6 Jul 2026 11:49:47 +0200 Subject: [PATCH 09/46] Show section, variant & section groups in page breadcrumbs (#4357) Co-authored-by: Claude Opus 4.8 --- .changeset/breadcrumbs-section-variant.md | 5 + .../Header/SpacesDropdownMenuItem.tsx | 2 +- .../PageBody/BreadcrumbItemDropdown.tsx | 102 ++++++ .../src/components/PageBody/PageHeader.tsx | 315 ++++++++++++++++-- .../SiteSections/encodeClientSiteSections.ts | 4 +- .../components/primitives/DropdownMenu.tsx | 71 +++- 6 files changed, 460 insertions(+), 39 deletions(-) create mode 100644 .changeset/breadcrumbs-section-variant.md create mode 100644 packages/gitbook/src/components/PageBody/BreadcrumbItemDropdown.tsx diff --git a/.changeset/breadcrumbs-section-variant.md b/.changeset/breadcrumbs-section-variant.md new file mode 100644 index 000000000..0a7116f6d --- /dev/null +++ b/.changeset/breadcrumbs-section-variant.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Show where a page lives in the site at the start of the page breadcrumbs: its section (including any enclosing section groups) and variant. Hovering any breadcrumb item reveals a dropdown to switch to its siblings at the same level — other section groups, sections, variants, page groups or pages — computed on the server from already-available data (no extra request). Language variants are left to the dedicated language picker rather than shown as a crumb. Also restyles the breadcrumbs to normal casing (matching search results). diff --git a/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx b/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx index 6ca5aa50a..98c134ee8 100644 --- a/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx +++ b/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx @@ -8,7 +8,7 @@ import { } from '../hooks'; import { DropdownMenuItem } from '../primitives/DropdownMenu'; -interface VariantSpace { +export interface VariantSpace { id: string; title: string; url: string; diff --git a/packages/gitbook/src/components/PageBody/BreadcrumbItemDropdown.tsx b/packages/gitbook/src/components/PageBody/BreadcrumbItemDropdown.tsx new file mode 100644 index 000000000..16f36d107 --- /dev/null +++ b/packages/gitbook/src/components/PageBody/BreadcrumbItemDropdown.tsx @@ -0,0 +1,102 @@ +'use client'; + +import { type ClassValue, tcls } from '@/lib/tailwind'; + +import { PageIcon } from '../PageIcon'; +import { StyledLink } from '../primitives'; +import { DropdownMenu, DropdownMenuItem } from '../primitives/DropdownMenu'; + +export type BreadcrumbSibling = { + id: string; + title: string; + href: string; + emoji?: string; + icon?: string; + /** Whether this sibling is the one currently represented by the breadcrumb item. */ + isActive: boolean; +}; + +/** + * A breadcrumb item. It navigates on click, and when there is more than one sibling at the same + * level (another section, variant, group or page) it reveals a dropdown of those siblings on hover + * so the reader can jump between them. + * + * This is a client component so the trigger/menu — which Radix clones through its `Slot` — is built + * from serializable props, mirroring `HeaderLinkDropdown`. + */ +export function BreadcrumbItemDropdown(props: { + /** Link for the crumb. Omitted for non-navigable items such as section groups. */ + href?: string; + label: string; + emoji?: string; + icon?: string; + /** Class names for the breadcrumb link itself. */ + linkClassName: ClassValue; + /** All items at the same level, including the current one (flagged `isActive`). */ + siblings: BreadcrumbSibling[]; + /** + * Custom dropdown content, rendered instead of the `siblings` list. Used by the variant crumb to + * reuse the header's variant switcher, whose entries resolve to the current page in the target + * variant (via per-page alternates) rather than just its landing page. When set, the dropdown is + * always shown. + */ + children?: React.ReactNode; +}) { + const { href, label, emoji, icon, linkClassName, siblings, children } = props; + + const content = ( + <> + {emoji || icon ? ( + + ) : null} + {label} + + ); + + // Navigable crumbs render a link; section groups (no href) render a plain label. An empty href + // is still a real link — it's the site's first page — so key on `undefined`, not falsiness, and + // normalize "" to "/" the way the table of contents does. + const trigger = + href !== undefined ? ( + + {content} + + ) : ( + + {content} + + ); + + // No dropdown when there is nowhere else to go at this level. + if (children == null && siblings.length <= 1) { + return trigger; + } + + return ( + + {children ?? + siblings.map((sibling) => ( + + ) : undefined + } + > + {sibling.title} + + ))} + + ); +} diff --git a/packages/gitbook/src/components/PageBody/PageHeader.tsx b/packages/gitbook/src/components/PageBody/PageHeader.tsx index c48d86697..bef42c772 100644 --- a/packages/gitbook/src/components/PageBody/PageHeader.tsx +++ b/packages/gitbook/src/components/PageBody/PageHeader.tsx @@ -1,14 +1,19 @@ import type { GitBookSiteContext } from '@/lib/context'; -import type { AncestorRevisionPage } from '@/lib/pages'; +import { type AncestorRevisionPage, resolveFirstDocument } from '@/lib/pages'; +import { getLocalizedTitle, getSiteSpaceURL } from '@/lib/sites'; import { tcls } from '@/lib/tailwind'; import { getPageRSSURL } from '@/routes/rss'; import { CustomizationPageActionType, + type RevisionPage, type RevisionPageDocument, + type SiteSection, + type SiteSectionGroup, SiteVisibility, } from '@gitbook/api'; import { Icon } from '@gitbook/icons'; import urlJoin from 'url-join'; +import { SpacesDropdownMenuItems, type VariantSpace } from '../Header/SpacesDropdownMenuItem'; import { getPDFURLSearchParams } from '../PDF'; import { PageActionsDropdown, @@ -16,8 +21,10 @@ import { } from '../PageActions/PageActionsDropdown'; import { PageAsideToggleButton } from '../PageAside/PageAsideButton'; import { PageIcon } from '../PageIcon'; +import { findBestTargetURL } from '../SiteSections/encodeClientSiteSections'; +import { categorizeVariants } from '../SpaceLayout/categorizeVariants'; import { CONTENT_STYLE, CONTENT_STYLE_REDUCED } from '../layout'; -import { StyledLink } from '../primitives'; +import { BreadcrumbItemDropdown, type BreadcrumbSibling } from './BreadcrumbItemDropdown'; import { PageTags } from './PageTags'; export async function PageHeader(props: { @@ -31,6 +38,86 @@ export async function PageHeader(props: { const hasAncestors = ancestors.length > 0; + // Surface where the page lives in the site at the start of the breadcrumbs: the section (with + // its enclosing section groups) and the variant. Each crumb also carries its siblings, so + // hovering it reveals a dropdown to switch to them. + // Only surface section crumbs when the site actually has more than one section to switch between + // (mirroring the section tabs). A single-space site has one section named after the site, which + // is not a meaningful breadcrumb. + const hasMultipleSections = Boolean( + context.visibleSections && context.visibleSections.list.length > 1 + ); + const currentSection = hasMultipleSections ? (context.sections?.current ?? null) : null; + // Variants to offer as a crumb: only the "generic" variants (versions, etc.). Language variants + // are excluded — they belong to the dedicated language picker — using the same split it uses. + // Only surface the variant crumb when there's more than one to switch between. + const variantSpaces = categorizeVariants(context).generic; + const currentSiteSpace = variantSpaces.length > 1 ? context.siteSpace : null; + const contextCrumbs: BreadcrumbContextCrumb[] = []; + if (currentSection) { + // Walk the full section tree so the current section (and its enclosing groups) always shows + // as a crumb, even when it's hidden in the site structure. Only *visible* sections/groups + // are offered as siblings to switch to, though. + const chain = context.sections + ? findSectionChain(context.sections.list, currentSection.id) + : []; + const visibleSectionIds = context.visibleSections + ? collectSectionNodeIds(context.visibleSections.list) + : null; + for (const { node, siblings } of chain) { + contextCrumbs.push({ + key: `section-${node.id}`, + // Section groups have no URL of their own; only sections are navigable. Resolve the + // section to the site space matching the current variant so switching sections keeps + // the reader on their current variant (same logic as the section tabs). + href: node.object === 'site-section' ? findBestTargetURL(context, node) : undefined, + label: getLocalizedTitle(node, context.locale), + icon: node.icon, + siblings: siblings + // Don't offer hidden sections/groups as switch targets. + .filter((sibling) => !visibleSectionIds || visibleSectionIds.has(sibling.id)) + .map((sibling) => { + const href = getSectionNodeURL(context, sibling); + // Keep siblings whose URL is "" (the site's first page); only drop the ones + // with no resolvable URL at all. + return href !== undefined + ? { + id: sibling.id, + title: getLocalizedTitle(sibling, context.locale), + href, + icon: sibling.icon, + isActive: sibling.id === node.id, + } + : null; + }) + .filter((sibling) => sibling !== null), + }); + } + } + if (currentSiteSpace) { + contextCrumbs.push({ + key: `variant-${currentSiteSpace.id}`, + href: getSiteSpaceURL(context, currentSiteSpace), + label: getLocalizedTitle(currentSiteSpace, context.locale), + siblings: [], + // Reuse the header's variant switcher rather than plain per-variant links, so each entry + // resolves to the current page in the target variant (via per-page alternates) instead + // of that variant's landing page. + variantSwitcher: { + slimSpaces: variantSpaces.map((siteSpace) => ({ + id: siteSpace.id, + title: getLocalizedTitle(siteSpace, context.locale), + url: getSiteSpaceURL(context, siteSpace), + isActive: siteSpace.id === currentSiteSpace.id, + spaceId: siteSpace.space.id, + })), + curPath: currentSiteSpace.path, + }, + }); + } + const hasContextCrumbs = contextCrumbs.length > 0; + const showBreadcrumbs = hasAncestors || hasContextCrumbs; + const pageActionsEnabled = page.layout.actions !== false; // Show page actions if *any* of the configured actions are enabled, or if the RSS feed is @@ -55,7 +142,7 @@ export async function PageHeader(props: {
{hasPageActions ? ( @@ -74,44 +161,43 @@ export async function PageHeader(props: {
- {hasAncestors && ( -