Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 63 additions & 5 deletions packages/gitbook/src/components/SitePage/SitePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getPageDocument } from '@/lib/data';
import {
CustomizationHeaderPreset,
CustomizationThemeMode,
type RevisionPageDocument,
SiteInsightsDisplayContext,
} from '@gitbook/api';
import type { Metadata, Viewport } from 'next';
Expand All @@ -14,7 +15,9 @@ import { getPagePath } from '@/lib/pages';
import { isPageIndexable, isSiteIndexable } from '@/lib/seo';

import { getResizedImageURL } from '@/lib/images';
import { removeTrailingSlash } from '@/lib/paths';
import { tcls } from '@/lib/tailwind';
import { assert } from 'ts-essentials';
import { PageContextProvider } from '../PageContext';
import { PageClientLayout } from './PageClientLayout';
import { type PagePathParams, fetchPageData, getPathnameParam } from './fetch';
Expand Down Expand Up @@ -106,16 +109,15 @@ export async function generateSitePageMetadata(props: SitePageProps): Promise<Me
}

const { page, ancestors } = pageTarget;
const { site, customization, revision, linker, imageResizer } = context;
const { site, customization, linker, imageResizer } = context;
const { canonical, languages } = getCanonicalAndLanguages(context, page);

return {
title: [page.title, site.title].filter(Boolean).join(' | '),
description: page.description ?? '',
alternates: {
// Trim trailing slashes in canonical URL to match the redirect behavior
canonical: linker
.toAbsoluteURL(linker.toPathForPage({ pages: revision.pages, page }))
.replace(/\/+$/, ''),
canonical,
languages,
types: {
'text/markdown': `${linker.toAbsoluteURL(linker.toPathInSpace(page.path))}.md`,
},
Expand All @@ -137,6 +139,62 @@ export async function generateSitePageMetadata(props: SitePageProps): Promise<Me
};
}

/**
* Generates the canonical URL and alternate language URLs for the given page.
*/
function getCanonicalAndLanguages(context: GitBookSiteContext, page: RevisionPageDocument) {
const { siteSpaces, siteSpace, revision } = context;

const linker =
siteSpace.default || typeof siteSpace.space.language !== 'undefined'
? context.linker
: context.linker.fork({
spaceBasePath: removeTrailingSlash(
context.linker.spaceBasePath.split('/').filter(Boolean).slice(0, -1).join('/')
),
});

// Trim trailing slashes in canonical URL to match the redirect behavior
const canonicalURL = new URL(
linker
.toAbsoluteURL(linker.toPathForPage({ pages: revision.pages, page }))
.replace(/\/+$/, '')
);

// For non-default site spaces, add a fallback query param so a page not found can still redirect to the home page
if (!siteSpace.default) {
canonicalURL.searchParams.set('fallback', 'true');
}

const canonical = canonicalURL.toString();
// Get other language versions
const languages: NonNullable<Metadata['alternates']>['languages'] = {};
siteSpaces
.filter(
(sp) => sp.section === siteSpace.section && typeof sp.space.language !== 'undefined'
)
.forEach((langSiteSpace) => {
const publishedURL = langSiteSpace.urls.published;
const language = langSiteSpace.space.language;
assert(publishedURL, `Published URL must be defined for space in ${langSiteSpace.id}`);
assert(language, `Language must be defined for space in ${langSiteSpace.id}`);

const langSiteSpaceURL = linker.toAbsoluteURL(
linker.toLinkForContent(new URL(publishedURL).toString())
);

// @ts-expect-error - Metadata.languages does not have all the language types that GitBook supports
languages[language] = new URL(
getPagePath(revision.pages, page),
langSiteSpaceURL.endsWith('/') ? langSiteSpaceURL : `${langSiteSpaceURL}/`
)
.toString()
.replace(/\/+$/, '');
});

return { canonical, languages };
}

/**
* Fetches all the data required to render the site page.
*/
Expand Down
Loading