From aa8bbaddc46aa90ef30c970a057b329cd7a694ec Mon Sep 17 00:00:00 2001 From: spastorelli Date: Tue, 16 Jul 2024 09:24:31 +0200 Subject: [PATCH] Fallback redirect to root when switching variants (#2376) --- e2e/pages.spec.ts | 6 +- .../[[...pathname]]/PageClientLayout.tsx | 27 ++++++++ .../(content)/[[...pathname]]/page.tsx | 64 ++++++++++++++++--- .../Header/SpacesDropdownMenuItem.tsx | 1 + 4 files changed, 87 insertions(+), 11 deletions(-) diff --git a/e2e/pages.spec.ts b/e2e/pages.spec.ts index b74c30bab1..5a0eed7426 100644 --- a/e2e/pages.spec.ts +++ b/e2e/pages.spec.ts @@ -129,7 +129,7 @@ const testCases: TestsCase[] = [ // It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant await page.waitForURL( - 'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions/v/2.0/reference/api-reference/pets', + 'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions/v/2.0/reference/api-reference/pets?fallback=true', ); }, }, @@ -152,7 +152,7 @@ const testCases: TestsCase[] = [ // It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant await page.waitForURL( - 'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-share-links/bRfQbzwsK8rbN1GRxx7K/v/2.0/reference/api-reference/pets', + 'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-share-links/bRfQbzwsK8rbN1GRxx7K/v/2.0/reference/api-reference/pets?fallback=true', ); }, }, @@ -187,7 +187,7 @@ const testCases: TestsCase[] = [ // It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant await page.waitForURL( - 'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-va/v/2.0/reference/api-reference/pets', + 'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-va/v/2.0/reference/api-reference/pets?fallback=true', ); }, }, diff --git a/src/app/(space)/(content)/[[...pathname]]/PageClientLayout.tsx b/src/app/(space)/(content)/[[...pathname]]/PageClientLayout.tsx index 25049cf7e4..2c248bec21 100644 --- a/src/app/(space)/(content)/[[...pathname]]/PageClientLayout.tsx +++ b/src/app/(space)/(content)/[[...pathname]]/PageClientLayout.tsx @@ -1,5 +1,8 @@ 'use client'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; +import React from 'react'; + import { useScrollToHash } from '@/components/hooks'; /** @@ -9,5 +12,29 @@ export function PageClientLayout(props: {}) { // We use this hook in the page layout to ensure the elements for the blocks // are rendered before we scroll to the hash. useScrollToHash(); + + useStripFallbackQueryParam(); return null; } + +/** + * Strip the fallback query parameter from current URL. + * + * When the user switches variants using the space dropdown, we pass a fallback=true parameter. + * This parameter indicates that we should redirect to the root page if the path from the + * previous variant doesn't exist in the new variant. If the path does exist, no redirect occurs, + * so we need to remove the fallback parameter. + */ +function useStripFallbackQueryParam() { + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + + React.useEffect(() => { + if (searchParams.has('fallback')) { + const params = new URLSearchParams(searchParams.toString()); + params.delete('fallback'); + router.push(`${pathname}?${params.toString()}${window.location.hash ?? ''}`); + } + }, [router, pathname, searchParams]); +} diff --git a/src/app/(space)/(content)/[[...pathname]]/page.tsx b/src/app/(space)/(content)/[[...pathname]]/page.tsx index 8385ca5569..d77f39eece 100644 --- a/src/app/(space)/(content)/[[...pathname]]/page.tsx +++ b/src/app/(space)/(content)/[[...pathname]]/page.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { PageAside } from '@/components/PageAside'; import { PageBody, PageCover } from '@/components/PageBody'; import { PageHrefContext, absoluteHref, pageHref } from '@/lib/links'; -import { getPagePath } from '@/lib/pages'; +import { getPagePath, resolveFirstDocument } from '@/lib/pages'; import { ContentRefContext } from '@/lib/references'; import { tcls } from '@/lib/tailwind'; import { getContentTitle } from '@/lib/utils'; @@ -19,10 +19,12 @@ export const runtime = 'edge'; /** * Fetch and render a page. */ -export default async function Page(props: { params: PagePathParams }) { - const { params } = props; +export default async function Page(props: { + params: PagePathParams; + searchParams: { fallback?: string }; +}) { + const { params, searchParams } = props; - const rawPathname = getPathnameParam(params); const { content: contentPointer, contentTarget, @@ -32,9 +34,14 @@ export default async function Page(props: { params: PagePathParams }) { pages, page, document, - } = await fetchPageData(params); - const linksContext: PageHrefContext = {}; + } = await getPageDataWithFallback({ + pagePathParams: params, + searchParams, + redirectOnFallback: true, + }); + const linksContext: PageHrefContext = {}; + const rawPathname = getPathnameParam(params); if (!page) { const pathname = normalizePathname(rawPathname); if (pathname !== rawPathname) { @@ -114,8 +121,18 @@ export async function generateViewport({ params }: { params: PagePathParams }): }; } -export async function generateMetadata({ params }: { params: PagePathParams }): Promise { - const { space, pages, page, customization, parent } = await fetchPageData(params); +export async function generateMetadata({ + params, + searchParams, +}: { + params: PagePathParams; + searchParams: { fallback?: string }; +}): Promise { + const { space, pages, page, customization, parent } = await getPageDataWithFallback({ + pagePathParams: params, + searchParams, + }); + if (!page) { notFound(); } @@ -136,3 +153,34 @@ export async function generateMetadata({ params }: { params: PagePathParams }): }, }; } + +/** + * Fetches the page data matching the requested pathname and fallback to root page when page is not found. + */ +async function getPageDataWithFallback(args: { + pagePathParams: PagePathParams; + searchParams: { fallback?: string }; + redirectOnFallback?: boolean; +}) { + const { pagePathParams, searchParams, redirectOnFallback = false } = args; + + const { pages, page: targetPage, ...otherPageData } = await fetchPageData(pagePathParams); + + let page = targetPage; + const canFallback = !!searchParams.fallback; + if (!page && canFallback) { + const rootPage = resolveFirstDocument(pages, []); + + if (redirectOnFallback && rootPage?.page) { + redirect(pageHref(pages, rootPage?.page)); + } + + page = rootPage?.page; + } + + return { + ...otherPageData, + pages, + page, + }; +} diff --git a/src/components/Header/SpacesDropdownMenuItem.tsx b/src/components/Header/SpacesDropdownMenuItem.tsx index ccbc04c1cc..df045d98e3 100644 --- a/src/components/Header/SpacesDropdownMenuItem.tsx +++ b/src/components/Header/SpacesDropdownMenuItem.tsx @@ -10,6 +10,7 @@ function useVariantSpaceHref(variantSpaceUrl: string) { const targetUrl = new URL(variantSpaceUrl); targetUrl.pathname += `/${currentPathname}`; targetUrl.pathname = targetUrl.pathname.replace(/\/{2,}/g, '/').replace(/\/$/, ''); + targetUrl.searchParams.set('fallback', 'true'); return targetUrl.toString(); }