Skip to content

Commit 270e5ef

Browse files
authored
Improve linking to other sections/variants in development (#2883)
1 parent 1f47333 commit 270e5ef

File tree

12 files changed

+133
-59
lines changed

12 files changed

+133
-59
lines changed

packages/gitbook-v2/src/app/utils.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,19 @@ function createLinkerFromParams(params: RouteLayoutParams) {
9494
}
9595

9696
const gitbookURL = new URL(GITBOOK_URL);
97-
return createLinker({
97+
const linker = createLinker({
9898
protocol: gitbookURL.protocol,
9999
host: gitbookURL.host,
100100
pathname: `/url/${url.host}`,
101101
});
102+
103+
// Create link in the same format for links to other sites/sections.
104+
linker.toLinkForContent = (rawURL: string) => {
105+
const urlObject = new URL(rawURL);
106+
return `/url/${urlObject.host}${urlObject.pathname}`;
107+
};
108+
109+
return linker;
102110
}
103111

104112
function getSiteURLFromParams(params: RouteLayoutParams) {

packages/gitbook-v2/src/lib/links.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ export interface GitBookSpaceLinker {
2323
* Generate an absolute URL for a given path.
2424
*/
2525
toAbsoluteURL(absolutePath: string): string;
26+
27+
/**
28+
* Generate a link (URL or path) for a GitBook content URL (url of another site)
29+
*/
30+
toLinkForContent(url: string): string;
2631
}
2732

2833
/**
@@ -52,6 +57,10 @@ export function createLinker(
5257
toPathForPage({ pages, page, anchor }) {
5358
return linker.toPathInSpace(getPagePath(pages, page)) + (anchor ? `#${anchor}` : '');
5459
},
60+
61+
toLinkForContent(url: string): string {
62+
return url;
63+
},
5564
};
5665

5766
return linker;
@@ -79,6 +88,10 @@ export function appendPrefixToLinker(
7988
(anchor ? `#${anchor}` : '')
8089
);
8190
},
91+
92+
toLinkForContent(url: string): string {
93+
return linker.toLinkForContent(url);
94+
},
8295
};
8396

8497
return linkerWithPrefix;

packages/gitbook/src/app/middleware/(space)/~gitbook/pdf/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ import { tcls } from '@/lib/tailwind';
2727
import { type PDFSearchParams, getPDFSearchParams } from '@/lib/urls';
2828
import { defaultCustomizationForSpace } from '@/lib/utils';
2929

30-
import './pdf.css';
3130
import { PageControlButtons } from './PageControlButtons';
3231
import { PrintButton } from './PrintButton';
32+
import './pdf.css';
3333
import { getV1ContextForPDF } from './pointer';
3434

3535
const DEFAULT_LIMIT = 100;
@@ -99,7 +99,7 @@ export default async function PDFHTMLOutput(props: {
9999
<div className={tcls('fixed', 'left-12', 'top-12', 'print:hidden', 'z-50')}>
100100
<a
101101
title={tString(language, 'pdf_goback')}
102-
href={pdfParams.back ?? context.linker.toAbsoluteURL('')}
102+
href={pdfParams.back ?? linker.toAbsoluteURL(linker.toPathInSpace(''))}
103103
className={tcls(
104104
'flex',
105105
'flex-row',

packages/gitbook/src/components/Header/Header.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { getSpaceLanguage, t } from '@/intl/server';
66
import { tcls } from '@/lib/tailwind';
77

88
import { SearchButton } from '../Search';
9-
import { SiteSectionTabs } from '../SiteSections';
9+
import { SiteSectionTabs, encodeClientSiteSections } from '../SiteSections';
1010
import { HeaderLink } from './HeaderLink';
1111
import { HeaderLinkMore } from './HeaderLinkMore';
1212
import { HeaderLinks } from './HeaderLinks';
@@ -95,6 +95,7 @@ export function Header(props: { context: GitBookSiteContext; withTopHeader?: boo
9595
{isMultiVariants && (
9696
<div className="mr-auto page-no-toc:flex hidden">
9797
<SpacesDropdown
98+
context={context}
9899
siteSpace={siteSpace}
99100
siteSpaces={siteSpaces}
100101
className={
@@ -168,7 +169,9 @@ export function Header(props: { context: GitBookSiteContext; withTopHeader?: boo
168169
</div>
169170
</div>
170171
</div>
171-
{sections ? <SiteSectionTabs sections={sections} /> : null}
172+
{sections ? (
173+
<SiteSectionTabs sections={encodeClientSiteSections(context, sections)} />
174+
) : null}
172175
</header>
173176
);
174177
}

packages/gitbook/src/components/Header/SpacesDropdown.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@ import type { SiteSpace } from '@gitbook/api';
22

33
import { tcls } from '@/lib/tailwind';
44

5+
import type { GitBookSiteContext } from '@v2/lib/context';
56
import { Dropdown, DropdownChevron, DropdownMenu } from './Dropdown';
67
import { SpacesDropdownMenuItem } from './SpacesDropdownMenuItem';
78

89
export function SpacesDropdown(props: {
10+
context: GitBookSiteContext;
911
siteSpace: SiteSpace;
1012
siteSpaces: SiteSpace[];
1113
className?: string;
1214
}) {
13-
const { siteSpace, siteSpaces, className } = props;
15+
const { context, siteSpace, siteSpaces, className } = props;
16+
const { linker } = context;
1417

1518
return (
1619
<Dropdown
@@ -72,7 +75,9 @@ export function SpacesDropdown(props: {
7275
variantSpace={{
7376
id: otherSiteSpace.id,
7477
title: otherSiteSpace.title,
75-
url: otherSiteSpace.urls.published ?? otherSiteSpace.space.urls.app,
78+
url: otherSiteSpace.urls.published
79+
? linker.toLinkForContent(otherSiteSpace.urls.published)
80+
: otherSiteSpace.space.urls.app,
7681
}}
7782
active={otherSiteSpace.id === siteSpace.id}
7883
/>

packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
'use client';
22

33
import type { Space } from '@gitbook/api';
4-
import { useSelectedLayoutSegment } from 'next/navigation';
54

5+
import { joinPath } from '@/lib/paths';
6+
import { useCurrentPagePath } from '../hooks';
67
import { DropdownMenuItem } from './Dropdown';
78

89
function useVariantSpaceHref(variantSpaceUrl: string) {
9-
const currentPathname = useSelectedLayoutSegment() ?? '';
10-
const targetUrl = new URL(variantSpaceUrl);
11-
targetUrl.pathname += `/${currentPathname}`;
12-
targetUrl.pathname = targetUrl.pathname.replace(/\/{2,}/g, '/').replace(/\/$/, '');
13-
targetUrl.searchParams.set('fallback', 'true');
10+
const currentPathname = useCurrentPagePath();
1411

15-
return targetUrl.toString();
12+
if (URL.canParse(variantSpaceUrl)) {
13+
const targetUrl = new URL(variantSpaceUrl);
14+
targetUrl.pathname = joinPath(targetUrl.pathname, currentPathname);
15+
targetUrl.searchParams.set('fallback', 'true');
16+
17+
return targetUrl.toString();
18+
}
19+
20+
// Fallback when the URL path is a relative path (in development mode)
21+
return `${joinPath(variantSpaceUrl, currentPathname)}?fallback=true`;
1622
}
1723

1824
export function SpacesDropdownMenuItem(props: {

packages/gitbook/src/components/SiteSections/SiteSectionList.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
11
'use client';
22

3-
import type { SiteSection, SiteSectionGroup } from '@gitbook/api';
43
import { Icon, type IconName } from '@gitbook/icons';
54
import { motion } from 'framer-motion';
65
import React from 'react';
76

8-
import type { SectionsList } from '@/lib/api';
97
import { type ClassValue, tcls } from '@/lib/tailwind';
108

119
import { TOCScrollContainer, useScrollToActiveTOCItem } from '../TableOfContents/TOCScroller';
1210
import { useIsMounted, useToggleAnimation } from '../hooks';
1311
import { Link } from '../primitives';
1412
import { SectionIcon } from './SectionIcon';
13+
import type {
14+
ClientSiteSection,
15+
ClientSiteSectionGroup,
16+
ClientSiteSections,
17+
} from './encodeClientSiteSections';
1518

1619
const MAX_ITEMS = 5; // If there are more sections than this, they'll be shown below the fold in a scrollview.
1720

1821
/**
1922
* A list of items representing site sections for multi-section sites
2023
*/
21-
export function SiteSectionList(props: { sections: SectionsList; className: ClassValue }) {
24+
export function SiteSectionList(props: { sections: ClientSiteSections; className: ClassValue }) {
2225
const {
2326
sections: { list: sectionsAndGroups, current: currentSection },
2427
className,
@@ -63,7 +66,7 @@ export function SiteSectionList(props: { sections: SectionsList; className: Clas
6366
}
6467

6568
export function SiteSectionListItem(props: {
66-
section: SiteSection;
69+
section: ClientSiteSection;
6770
isActive: boolean;
6871
className?: string;
6972
}) {
@@ -77,7 +80,7 @@ export function SiteSectionListItem(props: {
7780

7881
return (
7982
<Link
80-
href={section.urls.published ?? ''}
83+
href={section.url}
8184
ref={linkRef}
8285
aria-current={isActive && 'page'}
8386
className={tcls(
@@ -111,8 +114,8 @@ export function SiteSectionListItem(props: {
111114
}
112115

113116
export function SiteSectionGroupItem(props: {
114-
group: SiteSectionGroup;
115-
currentSection: SiteSection;
117+
group: ClientSiteSectionGroup;
118+
currentSection: ClientSiteSection;
116119
}) {
117120
const { group, currentSection } = props;
118121

packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx

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

3-
import type { SiteSection } from '@gitbook/api';
43
import { Icon, type IconName } from '@gitbook/icons';
54
import * as NavigationMenu from '@radix-ui/react-navigation-menu';
65
import React from 'react';
76

87
import { Link } from '@/components/primitives';
9-
import type { SectionsList } from '@/lib/api';
108
import { tcls } from '@/lib/tailwind';
119

1210
import { SectionIcon } from './SectionIcon';
11+
import type { ClientSiteSection, ClientSiteSections } from './encodeClientSiteSections';
1312

1413
const VIEWPORT_ITEM_WIDTH = 240; /* width of the tile (w-60) */
1514
const MIN_ITEMS_FOR_COLS = 4; /* number of items to switch to 2 columns */
1615
/**
1716
* A set of navigational links representing site sections for multi-section sites
1817
*/
19-
export function SiteSectionTabs(props: { sections: SectionsList }) {
18+
export function SiteSectionTabs(props: { sections: ClientSiteSections }) {
2019
const {
2120
sections: { list: sectionsAndGroups, current: currentSection },
2221
} = props;
@@ -91,9 +90,7 @@ export function SiteSectionTabs(props: { sections: SectionsList }) {
9190
</NavigationMenu.Trigger>
9291
<NavigationMenu.Content className="absolute top-0 left-0 z-20 w-full data-[motion=from-end]:motion-safe:animate-enterFromRight data-[motion=from-start]:motion-safe:animate-enterFromLeft data-[motion=to-end]:motion-safe:animate-exitToRight data-[motion=to-start]:motion-safe:animate-exitToLeft md:w-max">
9392
<SectionGroupTileList
94-
sections={
95-
sectionOrGroup.sections as SiteSection[]
96-
}
93+
sections={sectionOrGroup.sections}
9794
currentSection={currentSection}
9895
/>
9996
</NavigationMenu.Content>
@@ -102,7 +99,7 @@ export function SiteSectionTabs(props: { sections: SectionsList }) {
10299
) : (
103100
<NavigationMenu.Link asChild>
104101
<SectionTab
105-
url={sectionOrGroup.urls.published ?? ''}
102+
url={sectionOrGroup.url}
106103
isActive={isActive}
107104
title={title}
108105
icon={icon ? (icon as IconName) : undefined}
@@ -213,7 +210,10 @@ function ActiveTabIndicator() {
213210
/**
214211
* A list of section tiles grouped in the dropdown for a section group
215212
*/
216-
function SectionGroupTileList(props: { sections: SiteSection[]; currentSection: SiteSection }) {
213+
function SectionGroupTileList(props: {
214+
sections: ClientSiteSection[];
215+
currentSection: ClientSiteSection;
216+
}) {
217217
const { sections, currentSection } = props;
218218
return (
219219
<ul
@@ -236,13 +236,13 @@ function SectionGroupTileList(props: { sections: SiteSection[]; currentSection:
236236
/**
237237
* A section tile shown in the dropdown for a section group
238238
*/
239-
function SectionGroupTile(props: { section: SiteSection; isActive: boolean }) {
239+
function SectionGroupTile(props: { section: ClientSiteSection; isActive: boolean }) {
240240
const { section, isActive } = props;
241-
const { urls, icon, title } = section;
241+
const { url, icon, title } = section;
242242
return (
243243
<li className="flex w-full md:w-60">
244244
<Link
245-
href={urls.published ?? ''}
245+
href={url}
246246
className={tcls(
247247
'flex min-h-12 w-full select-none flex-col gap-2 rounded p-3 transition-colors hover:bg-tint-hover',
248248
isActive
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import type { SiteSection, SiteSectionGroup } from '@gitbook/api';
2+
import type { GitBookSiteContext, SiteSections } from '@v2/lib/context';
3+
4+
export type ClientSiteSections = {
5+
list: (ClientSiteSection | ClientSiteSectionGroup)[];
6+
current: ClientSiteSection;
7+
};
8+
9+
export type ClientSiteSection = Pick<
10+
SiteSection,
11+
'id' | 'title' | 'description' | 'icon' | 'object'
12+
> & {
13+
url: string;
14+
};
15+
16+
export type ClientSiteSectionGroup = Pick<SiteSectionGroup, 'id' | 'title' | 'icon' | 'object'> & {
17+
sections: ClientSiteSection[];
18+
};
19+
20+
/**
21+
* Encode the list of site sections into the data to be rendered in the client.
22+
*/
23+
export function encodeClientSiteSections(context: GitBookSiteContext, sections: SiteSections) {
24+
const { list, current } = sections;
25+
26+
const clientSections: (ClientSiteSection | ClientSiteSectionGroup)[] = [];
27+
28+
for (const item of list) {
29+
if (item.object === 'site-section-group') {
30+
clientSections.push({
31+
id: item.id,
32+
title: item.title,
33+
icon: item.icon,
34+
object: item.object,
35+
sections: item.sections.map((section) => encodeSection(context, section)),
36+
});
37+
} else {
38+
clientSections.push(encodeSection(context, item));
39+
}
40+
}
41+
42+
return {
43+
list: clientSections,
44+
current: encodeSection(context, current),
45+
};
46+
}
47+
48+
function encodeSection(context: GitBookSiteContext, section: SiteSection) {
49+
const { linker } = context;
50+
return {
51+
id: section.id,
52+
title: section.title,
53+
description: section.description,
54+
icon: section.icon,
55+
object: section.object,
56+
url: section.urls.published ? linker.toLinkForContent(section.urls.published) : '',
57+
};
58+
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1-
export * from './SiteSectionTabs';
1+
export * from './encodeClientSiteSections';
22
export * from './SiteSectionList';
3+
export * from './SiteSectionTabs';

0 commit comments

Comments
 (0)