diff --git a/packages/gitbook/src/components/SitePage/SitePage.test.ts b/packages/gitbook/src/components/SitePage/SitePage.test.ts new file mode 100644 index 000000000..9ceec6580 --- /dev/null +++ b/packages/gitbook/src/components/SitePage/SitePage.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from 'bun:test'; + +import type { RevisionPageDocument } from '@gitbook/api'; + +import { getPageFullTitle } from './title'; +import type { GitBookSiteContext } from '@/lib/context'; + +function makeContext(siteTitle: string, sectionTitle = ''): GitBookSiteContext { + return { + site: { title: siteTitle }, + visibleSections: sectionTitle + ? { + current: { title: sectionTitle, default: false }, + list: [{ object: 'site-section' }, { object: 'site-section' }], + } + : undefined, + visibleSiteSpaces: [], + } as unknown as GitBookSiteContext; +} + +function makePage(title: string, tagTitle?: string): RevisionPageDocument { + return { title, tagTitle } as unknown as RevisionPageDocument; +} + +describe('getPageFullTitle', () => { + it('uses tagTitle as the first segment', () => { + expect(getPageFullTitle(makeContext('GitBook'), makePage('Page title', 'SEO title'))).toBe( + 'SEO title | GitBook' + ); + }); + + it('falls back to the page title when tagTitle is absent', () => { + expect(getPageFullTitle(makeContext('GitBook'), makePage('Page title'))).toBe( + 'Page title | GitBook' + ); + }); + + it('deduplicates the section title against tagTitle', () => { + expect( + getPageFullTitle(makeContext('GitBook', 'Section'), makePage('Page title', 'Section')) + ).toBe('Section | GitBook'); + }); + + it('deduplicates the site title against tagTitle', () => { + expect( + getPageFullTitle(makeContext('SEO title'), makePage('Page title', 'SEO title')) + ).toBe('SEO title'); + }); +}); diff --git a/packages/gitbook/src/components/SitePage/SitePage.tsx b/packages/gitbook/src/components/SitePage/SitePage.tsx index 6ab1b65b7..49f313eec 100644 --- a/packages/gitbook/src/components/SitePage/SitePage.tsx +++ b/packages/gitbook/src/components/SitePage/SitePage.tsx @@ -4,7 +4,6 @@ import { notFound, redirect } from 'next/navigation'; import { CustomizationDefaultThemeMode, CustomizationHeaderPreset, - type RevisionPageDocument, SiteInsightsDisplayContext, type TranslationLanguage, } from '@gitbook/api'; @@ -18,6 +17,7 @@ import { getPathnameParam, } from './fetch'; import { PageClientLayout } from './PageClientLayout'; +import { getPageFullTitle } from './title'; import { UpdatesFilterProvider } from '@/components/DocumentView/UpdatesFilter'; import { UpdatesFilterScript } from '@/components/DocumentView/UpdatesFilterScript'; import { PageAside } from '@/components/PageAside'; @@ -33,11 +33,7 @@ import { getResizedImageURL } from '@/lib/images'; import { getPagePath } from '@/lib/pages'; import { resolveContentRef } from '@/lib/references'; import { isPageIndexable, isSiteIndexable } from '@/lib/seo'; -import { - getSiteSpacePagePaths, - getSiteStructureTitle, - resolveSiteSpaceCustomHomePage, -} from '@/lib/sites'; +import { getSiteSpacePagePaths, resolveSiteSpaceCustomHomePage } from '@/lib/sites'; import { tcls } from '@/lib/tailwind'; import { generateUpdatesFilterCSS, @@ -45,6 +41,8 @@ import { updatesFilterStyleHref, } from '@/lib/updates'; +export { getPageFullTitle } from './title'; + export type SitePageProps = { context: GitBookSiteContext; pageParams: PagePathParams; @@ -421,20 +419,3 @@ async function resolvePageMetaLinks( alternates: [], }; } - -/** - * Get the for a page. - */ -export function getPageFullTitle(context: GitBookSiteContext, page: RevisionPageDocument) { - const { site } = context; - const siteStructureTitle = getSiteStructureTitle(context); - - return [ - page.title, - // Prevent duplicate titles by comparing against the page title. - page.title !== siteStructureTitle ? siteStructureTitle : null, // The first page of a section is often the same as the section title, so we don't need to show it. - page.title !== site.title ? site.title : null, // The site title can also be the same as the site title on the site's landing page. - ] - .filter(Boolean) - .join(' | '); -} diff --git a/packages/gitbook/src/components/SitePage/title.ts b/packages/gitbook/src/components/SitePage/title.ts new file mode 100644 index 000000000..496801fe9 --- /dev/null +++ b/packages/gitbook/src/components/SitePage/title.ts @@ -0,0 +1,26 @@ +import type { RevisionPageDocument } from '@gitbook/api'; + +import type { GitBookSiteContext } from '@/lib/context'; +import { getSiteStructureTitle } from '@/lib/sites'; + +// TODO(RND-11994): drop once @gitbook/api ships RevisionPageDocument.tagTitle +type PageWithTagTitle = RevisionPageDocument & { tagTitle?: string }; + +/** + * Get the <title> for a page. + */ +export function getPageFullTitle(context: GitBookSiteContext, page: RevisionPageDocument) { + const { site } = context; + const siteStructureTitle = getSiteStructureTitle(context); + const tagTitle = (page as PageWithTagTitle).tagTitle || page.title; + + return [ + tagTitle, + // The first page of a section is often the same as the section title, so we don't need to show it. + tagTitle !== siteStructureTitle ? siteStructureTitle : null, + // The site title can also be the same as the page title on the site's landing page. + tagTitle !== site.title ? site.title : null, + ] + .filter(Boolean) + .join(' | '); +}