Skip to content

Commit aa8bbad

Browse files
authored
Fallback redirect to root when switching variants (#2376)
1 parent b8c136e commit aa8bbad

File tree

4 files changed

+87
-11
lines changed

4 files changed

+87
-11
lines changed

e2e/pages.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ const testCases: TestsCase[] = [
129129

130130
// It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant
131131
await page.waitForURL(
132-
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions/v/2.0/reference/api-reference/pets',
132+
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions/v/2.0/reference/api-reference/pets?fallback=true',
133133
);
134134
},
135135
},
@@ -152,7 +152,7 @@ const testCases: TestsCase[] = [
152152

153153
// It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant
154154
await page.waitForURL(
155-
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-share-links/bRfQbzwsK8rbN1GRxx7K/v/2.0/reference/api-reference/pets',
155+
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-share-links/bRfQbzwsK8rbN1GRxx7K/v/2.0/reference/api-reference/pets?fallback=true',
156156
);
157157
},
158158
},
@@ -187,7 +187,7 @@ const testCases: TestsCase[] = [
187187

188188
// It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant
189189
await page.waitForURL(
190-
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-va/v/2.0/reference/api-reference/pets',
190+
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-va/v/2.0/reference/api-reference/pets?fallback=true',
191191
);
192192
},
193193
},

src/app/(space)/(content)/[[...pathname]]/PageClientLayout.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
'use client';
22

3+
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
4+
import React from 'react';
5+
36
import { useScrollToHash } from '@/components/hooks';
47

58
/**
@@ -9,5 +12,29 @@ export function PageClientLayout(props: {}) {
912
// We use this hook in the page layout to ensure the elements for the blocks
1013
// are rendered before we scroll to the hash.
1114
useScrollToHash();
15+
16+
useStripFallbackQueryParam();
1217
return null;
1318
}
19+
20+
/**
21+
* Strip the fallback query parameter from current URL.
22+
*
23+
* When the user switches variants using the space dropdown, we pass a fallback=true parameter.
24+
* This parameter indicates that we should redirect to the root page if the path from the
25+
* previous variant doesn't exist in the new variant. If the path does exist, no redirect occurs,
26+
* so we need to remove the fallback parameter.
27+
*/
28+
function useStripFallbackQueryParam() {
29+
const router = useRouter();
30+
const pathname = usePathname();
31+
const searchParams = useSearchParams();
32+
33+
React.useEffect(() => {
34+
if (searchParams.has('fallback')) {
35+
const params = new URLSearchParams(searchParams.toString());
36+
params.delete('fallback');
37+
router.push(`${pathname}?${params.toString()}${window.location.hash ?? ''}`);
38+
}
39+
}, [router, pathname, searchParams]);
40+
}

src/app/(space)/(content)/[[...pathname]]/page.tsx

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import React from 'react';
66
import { PageAside } from '@/components/PageAside';
77
import { PageBody, PageCover } from '@/components/PageBody';
88
import { PageHrefContext, absoluteHref, pageHref } from '@/lib/links';
9-
import { getPagePath } from '@/lib/pages';
9+
import { getPagePath, resolveFirstDocument } from '@/lib/pages';
1010
import { ContentRefContext } from '@/lib/references';
1111
import { tcls } from '@/lib/tailwind';
1212
import { getContentTitle } from '@/lib/utils';
@@ -19,10 +19,12 @@ export const runtime = 'edge';
1919
/**
2020
* Fetch and render a page.
2121
*/
22-
export default async function Page(props: { params: PagePathParams }) {
23-
const { params } = props;
22+
export default async function Page(props: {
23+
params: PagePathParams;
24+
searchParams: { fallback?: string };
25+
}) {
26+
const { params, searchParams } = props;
2427

25-
const rawPathname = getPathnameParam(params);
2628
const {
2729
content: contentPointer,
2830
contentTarget,
@@ -32,9 +34,14 @@ export default async function Page(props: { params: PagePathParams }) {
3234
pages,
3335
page,
3436
document,
35-
} = await fetchPageData(params);
36-
const linksContext: PageHrefContext = {};
37+
} = await getPageDataWithFallback({
38+
pagePathParams: params,
39+
searchParams,
40+
redirectOnFallback: true,
41+
});
3742

43+
const linksContext: PageHrefContext = {};
44+
const rawPathname = getPathnameParam(params);
3845
if (!page) {
3946
const pathname = normalizePathname(rawPathname);
4047
if (pathname !== rawPathname) {
@@ -114,8 +121,18 @@ export async function generateViewport({ params }: { params: PagePathParams }):
114121
};
115122
}
116123

117-
export async function generateMetadata({ params }: { params: PagePathParams }): Promise<Metadata> {
118-
const { space, pages, page, customization, parent } = await fetchPageData(params);
124+
export async function generateMetadata({
125+
params,
126+
searchParams,
127+
}: {
128+
params: PagePathParams;
129+
searchParams: { fallback?: string };
130+
}): Promise<Metadata> {
131+
const { space, pages, page, customization, parent } = await getPageDataWithFallback({
132+
pagePathParams: params,
133+
searchParams,
134+
});
135+
119136
if (!page) {
120137
notFound();
121138
}
@@ -136,3 +153,34 @@ export async function generateMetadata({ params }: { params: PagePathParams }):
136153
},
137154
};
138155
}
156+
157+
/**
158+
* Fetches the page data matching the requested pathname and fallback to root page when page is not found.
159+
*/
160+
async function getPageDataWithFallback(args: {
161+
pagePathParams: PagePathParams;
162+
searchParams: { fallback?: string };
163+
redirectOnFallback?: boolean;
164+
}) {
165+
const { pagePathParams, searchParams, redirectOnFallback = false } = args;
166+
167+
const { pages, page: targetPage, ...otherPageData } = await fetchPageData(pagePathParams);
168+
169+
let page = targetPage;
170+
const canFallback = !!searchParams.fallback;
171+
if (!page && canFallback) {
172+
const rootPage = resolveFirstDocument(pages, []);
173+
174+
if (redirectOnFallback && rootPage?.page) {
175+
redirect(pageHref(pages, rootPage?.page));
176+
}
177+
178+
page = rootPage?.page;
179+
}
180+
181+
return {
182+
...otherPageData,
183+
pages,
184+
page,
185+
};
186+
}

src/components/Header/SpacesDropdownMenuItem.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ function useVariantSpaceHref(variantSpaceUrl: string) {
1010
const targetUrl = new URL(variantSpaceUrl);
1111
targetUrl.pathname += `/${currentPathname}`;
1212
targetUrl.pathname = targetUrl.pathname.replace(/\/{2,}/g, '/').replace(/\/$/, '');
13+
targetUrl.searchParams.set('fallback', 'true');
1314

1415
return targetUrl.toString();
1516
}

0 commit comments

Comments
 (0)