mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-21 01:53:26 +00:00
Expose a ~gitbook/auth/login URL to handle redirection to upstream auth with current location passed (#4113)
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "gitbook",
|
||||
@@ -349,7 +348,7 @@
|
||||
"react-dom": "catalog:",
|
||||
},
|
||||
"catalog": {
|
||||
"@gitbook/api": "0.169.0",
|
||||
"@gitbook/api": "0.170.0",
|
||||
"@scalar/api-client-react": "^1.3.46",
|
||||
"@tsconfig/node20": "^20.1.6",
|
||||
"@tsconfig/strictest": "^2.0.6",
|
||||
@@ -746,7 +745,7 @@
|
||||
|
||||
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@7.1.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.1.0" } }, "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA=="],
|
||||
|
||||
"@gitbook/api": ["@gitbook/api@0.169.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-kzJT8P5HCnqeYG3kFmBfvKMjQhTuUVmNV5Wvyh87QnoOA3odwh2C3qONpTmBA6cNE3ymDnxs8SJT9FG/qquRZQ=="],
|
||||
"@gitbook/api": ["@gitbook/api@0.170.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-RCW9tipg8CJqXPHrZ65wj/EwN4/Rmvy4uOZ8tez5B9NcM7C4vYJOBcgsBeGD4bB5I0YmH/uyZMno5zgIM8sOKw=="],
|
||||
|
||||
"@gitbook/browser-types": ["@gitbook/browser-types@workspace:packages/browser-types"],
|
||||
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@
|
||||
"catalog": {
|
||||
"@tsconfig/strictest": "^2.0.6",
|
||||
"@tsconfig/node20": "^20.1.6",
|
||||
"@gitbook/api": "0.169.0",
|
||||
"@gitbook/api": "0.170.0",
|
||||
"@scalar/api-client-react": "^1.3.46",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { type RouteLayoutParams, getDynamicSiteContext } from '@/app/utils';
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
/**
|
||||
* Redirect to the upstream auth provider login URL of site, or to the site root when not configured.
|
||||
*/
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<RouteLayoutParams> }
|
||||
) {
|
||||
const { context } = await getDynamicSiteContext(await params);
|
||||
const noLoginFallbackURL = context.linker.toAbsoluteURL(context.linker.toPathInSite(''));
|
||||
|
||||
if (!context.site.urls.login) {
|
||||
return NextResponse.redirect(noLoginFallbackURL);
|
||||
}
|
||||
|
||||
try {
|
||||
const loginURL = new URL(context.site.urls.login);
|
||||
const location = request.nextUrl.searchParams.get('location');
|
||||
if (location) {
|
||||
loginURL.searchParams.set('location', location);
|
||||
}
|
||||
|
||||
return NextResponse.redirect(loginURL);
|
||||
} catch (_error) {
|
||||
return NextResponse.redirect(noLoginFallbackURL);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
import { isSiteAuthLoginHref } from '@/lib/auth-login-link';
|
||||
import { resolveContentRefFallback, resolveContentRefInDocument } from '@/lib/references';
|
||||
import * as api from '@gitbook/api';
|
||||
import type { IconName } from '@gitbook/icons';
|
||||
import type React from 'react';
|
||||
import { Button, type ButtonProps } from '../primitives';
|
||||
import { SiteAuthLoginButton } from '../primitives/SiteAuthLoginLink';
|
||||
import type { InlineProps } from './Inline';
|
||||
import { InlineActionButton } from './InlineActionButton';
|
||||
import { NotFoundRefHoverCard } from './NotFoundRefHoverCard';
|
||||
@@ -58,21 +61,27 @@ export async function InlineLinkButton(
|
||||
const href =
|
||||
resolved?.href ??
|
||||
(inline.data.ref ? resolveContentRefFallback(inline.data.ref)?.href : undefined);
|
||||
const sharedProps: React.ComponentProps<typeof Button> = {
|
||||
...buttonProps,
|
||||
insights: {
|
||||
type: 'link_click' as const,
|
||||
link: {
|
||||
target: inline.data.ref,
|
||||
position: api.SiteInsightsLinkPosition.Content,
|
||||
},
|
||||
},
|
||||
href,
|
||||
disabled: href === undefined,
|
||||
};
|
||||
|
||||
const button = (
|
||||
<Button
|
||||
{...buttonProps}
|
||||
insights={{
|
||||
type: 'link_click',
|
||||
link: {
|
||||
target: inline.data.ref,
|
||||
position: api.SiteInsightsLinkPosition.Content,
|
||||
},
|
||||
}}
|
||||
href={href}
|
||||
disabled={href === undefined}
|
||||
/>
|
||||
);
|
||||
const button =
|
||||
href &&
|
||||
context.contentContext &&
|
||||
isSiteAuthLoginHref(context.contentContext.linker, href) ? (
|
||||
<SiteAuthLoginButton {...sharedProps} />
|
||||
) : (
|
||||
<Button {...sharedProps} />
|
||||
);
|
||||
|
||||
if (inline.data.ref && !resolved) {
|
||||
return <NotFoundRefHoverCard context={context}>{button}</NotFoundRefHoverCard>;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isSiteAuthLoginHref } from '@/lib/auth-login-link';
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
import {
|
||||
type ContentRef,
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
SiteInsightsLinkPosition,
|
||||
} from '@gitbook/api';
|
||||
import assertNever from 'assert-never';
|
||||
import type React from 'react';
|
||||
|
||||
import { resolveContentRef } from '@/lib/references';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
@@ -17,6 +19,11 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuItem,
|
||||
} from '../primitives/DropdownMenu';
|
||||
import {
|
||||
SiteAuthLoginButton,
|
||||
SiteAuthLoginDropdownMenuItem,
|
||||
SiteAuthLoginLink,
|
||||
} from '../primitives/SiteAuthLoginLink';
|
||||
|
||||
export async function HeaderLink(props: {
|
||||
context: GitBookSiteContext;
|
||||
@@ -44,6 +51,7 @@ export async function HeaderLink(props: {
|
||||
title={link.title}
|
||||
isDropdown
|
||||
href={target?.href}
|
||||
isSiteAuthLoginHref={isSiteAuthLoginHref(context.linker, target.href)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -68,6 +76,7 @@ export async function HeaderLink(props: {
|
||||
title={link.title}
|
||||
isDropdown={false}
|
||||
href={target?.href}
|
||||
isSiteAuthLoginHref={target ? isSiteAuthLoginHref(context.linker, target.href) : false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -79,6 +88,7 @@ export type HeaderLinkNavItemProps = {
|
||||
title: string;
|
||||
href?: string;
|
||||
isDropdown: boolean;
|
||||
isSiteAuthLoginHref: boolean;
|
||||
} & DropdownButtonProps<HTMLElement>;
|
||||
|
||||
function HeaderLinkNavItem(props: HeaderLinkNavItemProps) {
|
||||
@@ -99,7 +109,16 @@ function HeaderItemButton(
|
||||
linkStyle: 'button-secondary' | 'button-primary';
|
||||
}
|
||||
) {
|
||||
const { linkTarget, linkStyle, headerPreset, title, href, isDropdown, ...rest } = props;
|
||||
const {
|
||||
linkTarget,
|
||||
linkStyle,
|
||||
headerPreset,
|
||||
title,
|
||||
href,
|
||||
isDropdown,
|
||||
isSiteAuthLoginHref,
|
||||
...rest
|
||||
} = props;
|
||||
const variant = (() => {
|
||||
switch (linkStyle) {
|
||||
case 'button-secondary':
|
||||
@@ -110,21 +129,25 @@ function HeaderItemButton(
|
||||
assertNever(linkStyle);
|
||||
}
|
||||
})();
|
||||
return (
|
||||
<Button
|
||||
href={href}
|
||||
variant={variant}
|
||||
size="medium"
|
||||
insights={{
|
||||
type: 'link_click',
|
||||
link: {
|
||||
target: linkTarget,
|
||||
position: SiteInsightsLinkPosition.Header,
|
||||
},
|
||||
}}
|
||||
label={title}
|
||||
{...rest}
|
||||
/>
|
||||
const sharedProps: React.ComponentProps<typeof Button> = {
|
||||
href,
|
||||
variant,
|
||||
size: 'medium' as const,
|
||||
insights: {
|
||||
type: 'link_click' as const,
|
||||
link: {
|
||||
target: linkTarget,
|
||||
position: SiteInsightsLinkPosition.Header,
|
||||
},
|
||||
},
|
||||
label: title,
|
||||
...rest,
|
||||
};
|
||||
|
||||
return isSiteAuthLoginHref ? (
|
||||
<SiteAuthLoginButton {...sharedProps} />
|
||||
) : (
|
||||
<Button {...sharedProps} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -154,20 +177,28 @@ function getHeaderLinkClassName(_props: { headerPreset: CustomizationHeaderPrese
|
||||
}
|
||||
|
||||
function HeaderItemLink(props: Omit<HeaderLinkNavItemProps, 'linkStyle'>) {
|
||||
const { linkTarget, headerPreset, title, isDropdown, href, ...rest } = props;
|
||||
return (
|
||||
<Link
|
||||
href={href ?? '#'}
|
||||
className={getHeaderLinkClassName({ headerPreset })}
|
||||
insights={{
|
||||
type: 'link_click',
|
||||
link: {
|
||||
target: linkTarget,
|
||||
position: SiteInsightsLinkPosition.Header,
|
||||
},
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
const { linkTarget, headerPreset, title, isDropdown, href, isSiteAuthLoginHref, ...rest } =
|
||||
props;
|
||||
const sharedProps = {
|
||||
href: href ?? '#',
|
||||
className: getHeaderLinkClassName({ headerPreset }),
|
||||
insights: {
|
||||
type: 'link_click' as const,
|
||||
link: {
|
||||
target: linkTarget,
|
||||
position: SiteInsightsLinkPosition.Header,
|
||||
},
|
||||
},
|
||||
...rest,
|
||||
};
|
||||
|
||||
return isSiteAuthLoginHref ? (
|
||||
<SiteAuthLoginLink {...sharedProps}>
|
||||
{title}
|
||||
{isDropdown ? <ToggleChevron /> : null}
|
||||
</SiteAuthLoginLink>
|
||||
) : (
|
||||
<Link {...sharedProps}>
|
||||
{title}
|
||||
{isDropdown ? <ToggleChevron /> : null}
|
||||
</Link>
|
||||
@@ -204,18 +235,20 @@ async function SubHeaderLink(props: {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
href={target.href}
|
||||
insights={{
|
||||
type: 'link_click',
|
||||
link: {
|
||||
target: link.to,
|
||||
position: SiteInsightsLinkPosition.Header,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{link.title}
|
||||
</DropdownMenuItem>
|
||||
const sharedProps = {
|
||||
href: target.href,
|
||||
insights: {
|
||||
type: 'link_click' as const,
|
||||
link: {
|
||||
target: link.to,
|
||||
position: SiteInsightsLinkPosition.Header,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return isSiteAuthLoginHref(context.linker, target.href) ? (
|
||||
<SiteAuthLoginDropdownMenuItem {...sharedProps}>{link.title}</SiteAuthLoginDropdownMenuItem>
|
||||
) : (
|
||||
<DropdownMenuItem {...sharedProps}>{link.title}</DropdownMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isSiteAuthLoginHref } from '@/lib/auth-login-link';
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
import {
|
||||
type CustomizationContentLink,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownSubMenu,
|
||||
} from '../primitives/DropdownMenu';
|
||||
import { SiteAuthLoginDropdownMenuItem } from '../primitives/SiteAuthLoginLink';
|
||||
import styles from './headerLinks.module.css';
|
||||
|
||||
/**
|
||||
@@ -84,6 +86,18 @@ async function MoreMenuLink(props: {
|
||||
const { context, link } = props;
|
||||
|
||||
const target = link.to ? await resolveContentRef(link.to, context) : null;
|
||||
const sharedProps = {
|
||||
href: target?.href,
|
||||
insights: link.to
|
||||
? {
|
||||
type: 'link_click' as const,
|
||||
link: {
|
||||
target: link.to,
|
||||
position: SiteInsightsLinkPosition.Header,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
return 'links' in link && link.links.length > 0 ? (
|
||||
<DropdownSubMenu label={link.title}>
|
||||
@@ -91,22 +105,11 @@ async function MoreMenuLink(props: {
|
||||
return <MoreMenuLink key={index} {...props} link={subLink} />;
|
||||
})}
|
||||
</DropdownSubMenu>
|
||||
) : (
|
||||
<DropdownMenuItem
|
||||
href={target?.href}
|
||||
insights={
|
||||
link.to
|
||||
? {
|
||||
type: 'link_click',
|
||||
link: {
|
||||
target: link.to,
|
||||
position: SiteInsightsLinkPosition.Header,
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
) : isSiteAuthLoginHref(context.linker, target?.href) && sharedProps.href ? (
|
||||
<SiteAuthLoginDropdownMenuItem {...sharedProps} href={sharedProps.href}>
|
||||
{link.title}
|
||||
</DropdownMenuItem>
|
||||
</SiteAuthLoginDropdownMenuItem>
|
||||
) : (
|
||||
<DropdownMenuItem {...sharedProps}>{link.title}</DropdownMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
'use client';
|
||||
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
import { useMemo } from 'react';
|
||||
import type React from 'react';
|
||||
|
||||
import { removeTrailingSlash } from '@/lib/paths';
|
||||
import { Button, type ButtonProps } from './Button';
|
||||
import { DropdownMenuItem } from './DropdownMenu';
|
||||
import { Link, type LinkInsightsProps, type LinkProps } from './Link';
|
||||
|
||||
/**
|
||||
* Enrich a site auth login link with the current location relative to the site URL.
|
||||
*/
|
||||
function useSiteAuthLoginHrefWithLocation(href: string) {
|
||||
const rawPathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const currentSearch = searchParams?.toString();
|
||||
const pathname = rawPathname ?? '/';
|
||||
|
||||
return useMemo(() => {
|
||||
const baseURL = typeof window !== 'undefined' ? window.location.origin : 'http://localhost';
|
||||
const resolved = URL.canParse(href) ? new URL(href) : new URL(href, baseURL);
|
||||
const siteBasePath = removeTrailingSlash(
|
||||
resolved.pathname.replace(/\/~gitbook\/auth\/login\/?$/, '')
|
||||
);
|
||||
const locationPath =
|
||||
siteBasePath && pathname.startsWith(`${siteBasePath}/`)
|
||||
? pathname.slice(siteBasePath.length)
|
||||
: pathname === siteBasePath
|
||||
? '/'
|
||||
: pathname;
|
||||
|
||||
resolved.searchParams.set(
|
||||
'location',
|
||||
`${locationPath}${currentSearch ? `?${currentSearch}` : ''}`
|
||||
);
|
||||
|
||||
return href.startsWith('http')
|
||||
? resolved.toString()
|
||||
: `${resolved.pathname}${resolved.search}`;
|
||||
}, [currentSearch, href, pathname]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Link component that preserves the current page location through the auth login flow.
|
||||
*/
|
||||
export function SiteAuthLoginLink(props: LinkProps) {
|
||||
const href = useSiteAuthLoginHrefWithLocation(props.href);
|
||||
return <Link {...props} href={href} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Button variant of SiteAuthLoginLink.
|
||||
*/
|
||||
export function SiteAuthLoginButton(props: ButtonProps) {
|
||||
const href = useSiteAuthLoginHrefWithLocation(props.href ?? '#');
|
||||
return <Button {...props} href={href} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dropdown menu item variant of SiteAuthLoginLink.
|
||||
*/
|
||||
export function SiteAuthLoginDropdownMenuItem(
|
||||
props: {
|
||||
href: string;
|
||||
target?: React.HTMLAttributeAnchorTarget;
|
||||
active?: boolean;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
leadingIcon?: React.ReactNode | string;
|
||||
} & LinkInsightsProps
|
||||
) {
|
||||
const href = useSiteAuthLoginHrefWithLocation(props.href);
|
||||
return <DropdownMenuItem {...props} href={href} />;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { isSiteAuthLoginHref } from './auth-login-link';
|
||||
import { createLinker, linkerForPublishedURL } from './links';
|
||||
|
||||
type SiteFixture = {
|
||||
label: string;
|
||||
publishedURL: string;
|
||||
siteBasePath: string;
|
||||
localhostLoginURL: string;
|
||||
previewLoginURL: string;
|
||||
publishedLoginURL: string;
|
||||
};
|
||||
|
||||
const previewHost = '41ba7c7e-gitbook-open-v2-preview.gitbook.workers.dev';
|
||||
const fixtures: SiteFixture[] = [
|
||||
{
|
||||
label: 'proxy sites',
|
||||
publishedURL: 'https://gitbook.com/docs',
|
||||
siteBasePath: '/url/gitbook.com/docs',
|
||||
localhostLoginURL: 'https://localhost:3000/url/gitbook.com/docs/~gitbook/auth/login',
|
||||
previewLoginURL: `https://${previewHost}/url/gitbook.com/docs/~gitbook/auth/login`,
|
||||
publishedLoginURL: 'https://gitbook.com/docs/~gitbook/auth/login',
|
||||
},
|
||||
{
|
||||
label: 'GitBook-hosted site with a path prefix',
|
||||
publishedURL: 'https://gitbook.gitbook.io/test/',
|
||||
siteBasePath: '/url/gitbook.gitbook.io/test',
|
||||
localhostLoginURL: 'https://localhost:3000/url/gitbook.gitbook.io/test/~gitbook/auth/login',
|
||||
previewLoginURL: `https://${previewHost}/url/gitbook.gitbook.io/test/~gitbook/auth/login`,
|
||||
publishedLoginURL: 'https://gitbook.gitbook.io/test/~gitbook/auth/login',
|
||||
},
|
||||
{
|
||||
label: 'custom domain at the root path',
|
||||
publishedURL: 'https://docs.acme.org/',
|
||||
siteBasePath: '/url/docs.acme.org',
|
||||
localhostLoginURL: 'https://localhost:3000/url/docs.acme.org/~gitbook/auth/login',
|
||||
previewLoginURL: `https://${previewHost}/url/docs.acme.org/~gitbook/auth/login`,
|
||||
publishedLoginURL: 'https://docs.acme.org/~gitbook/auth/login',
|
||||
},
|
||||
];
|
||||
|
||||
function createURLModeSiteLinker(currentHost: string, siteBasePath: string, publishedURL: string) {
|
||||
const linker = createLinker({
|
||||
protocol: 'https:',
|
||||
host: currentHost,
|
||||
siteBasePath,
|
||||
spaceBasePath: `${siteBasePath}/getting-started`,
|
||||
});
|
||||
|
||||
return linkerForPublishedURL(linker, publishedURL);
|
||||
}
|
||||
|
||||
describe('isSiteAuthLoginHref', () => {
|
||||
describe.each(fixtures)('$label', (fixture) => {
|
||||
it('matches the localhost absolute login URL for the current site', () => {
|
||||
const linker = createURLModeSiteLinker(
|
||||
'localhost:3000',
|
||||
fixture.siteBasePath,
|
||||
fixture.publishedURL
|
||||
);
|
||||
|
||||
expect(isSiteAuthLoginHref(linker, fixture.localhostLoginURL)).toBe(true);
|
||||
});
|
||||
|
||||
it('matches the workers preview absolute login URL for the current site', () => {
|
||||
const linker = createURLModeSiteLinker(
|
||||
previewHost,
|
||||
fixture.siteBasePath,
|
||||
fixture.publishedURL
|
||||
);
|
||||
|
||||
expect(isSiteAuthLoginHref(linker, fixture.previewLoginURL)).toBe(true);
|
||||
});
|
||||
|
||||
it('matches the published-site absolute login URL when served through preview', () => {
|
||||
const linker = createURLModeSiteLinker(
|
||||
previewHost,
|
||||
fixture.siteBasePath,
|
||||
fixture.publishedURL
|
||||
);
|
||||
|
||||
expect(isSiteAuthLoginHref(linker, fixture.publishedLoginURL)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { GitBookLinker } from './links';
|
||||
import { removeTrailingSlash } from './paths';
|
||||
|
||||
/**
|
||||
* Check if an href points to the current site's auth login route.
|
||||
*/
|
||||
export function isSiteAuthLoginHref(
|
||||
linker: Pick<GitBookLinker, 'toPathInSite' | 'toAbsoluteURL' | 'toLinkForContent'>,
|
||||
href?: string | null
|
||||
) {
|
||||
if (!href) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const relativeLoginHref = linker.toPathInSite('~gitbook/auth/login');
|
||||
const absoluteLoginHref = linker.toAbsoluteURL(relativeLoginHref);
|
||||
const siteRelativeHref = URL.canParse(href) ? linker.toLinkForContent(href) : href;
|
||||
|
||||
return (
|
||||
normalizeHref(href) === normalizeHref(relativeLoginHref) ||
|
||||
normalizeHref(href) === normalizeHref(absoluteLoginHref) ||
|
||||
normalizeHref(siteRelativeHref) === normalizeHref(relativeLoginHref)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize an href so login-route comparisons ignore trailing slashes.
|
||||
*/
|
||||
function normalizeHref(href: string) {
|
||||
if (URL.canParse(href)) {
|
||||
const url = new URL(href);
|
||||
return `${url.origin}${removeTrailingSlash(url.pathname)}`;
|
||||
}
|
||||
|
||||
return removeTrailingSlash(href);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
import type {
|
||||
LocalizedString,
|
||||
LocalizedTitle,
|
||||
SiteSection,
|
||||
SiteSectionGroup,
|
||||
SiteSpace,
|
||||
@@ -189,7 +189,7 @@ function findSiteSpaceByIdInSiteSpaces(
|
||||
* Get the localized title for a site entity (SiteSection, SiteSectionGroup, or SiteSpace).
|
||||
*/
|
||||
export function getLocalizedTitle(
|
||||
entity: { title: string; localizedTitle?: LocalizedString },
|
||||
entity: { title: string; localizedTitle?: LocalizedTitle },
|
||||
currentLanguage: TranslationLanguage | undefined
|
||||
): string {
|
||||
return getLocalizedField(entity.localizedTitle, currentLanguage) ?? entity.title;
|
||||
@@ -199,7 +199,7 @@ export function getLocalizedTitle(
|
||||
* Get the localized description for a site entity.
|
||||
*/
|
||||
export function getLocalizedDescription(
|
||||
entity: { description?: string; localizedDescription?: LocalizedString },
|
||||
entity: { description?: string; localizedDescription?: LocalizedTitle },
|
||||
currentLanguage: TranslationLanguage | undefined
|
||||
): string | undefined {
|
||||
return getLocalizedField(entity.localizedDescription, currentLanguage) ?? entity.description;
|
||||
@@ -209,7 +209,7 @@ export function getLocalizedDescription(
|
||||
* Get a localized field value for the given language.
|
||||
*/
|
||||
function getLocalizedField(
|
||||
localizedField: LocalizedString | undefined,
|
||||
localizedField: LocalizedTitle | undefined,
|
||||
currentLanguage: TranslationLanguage | undefined
|
||||
): string | undefined {
|
||||
if (localizedField && currentLanguage && localizedField[currentLanguage]) {
|
||||
|
||||
@@ -681,8 +681,9 @@ function encodePathInSiteContent(
|
||||
return { pathname, routeType: 'static' };
|
||||
case '~gitbook/pdf':
|
||||
case '~gitbook/search':
|
||||
case '~gitbook/auth/login':
|
||||
case '~scalar/proxy':
|
||||
// PDF and search routes are always dynamic as they depend on the request.
|
||||
// PDF, search and auth routes are always dynamic as they depend on the request.
|
||||
return { pathname, routeType: 'dynamic' };
|
||||
default: {
|
||||
// If the pathname is a markdown file or the request is accepting markdown,
|
||||
|
||||
Reference in New Issue
Block a user