diff --git a/.changeset/page-tag-title.md b/.changeset/page-tag-title.md
new file mode 100644
index 000000000..72cc8e4c7
--- /dev/null
+++ b/.changeset/page-tag-title.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Use the page's tag title, when set, for the HTML `
` of published pages.
diff --git a/bun.lock b/bun.lock
index 98b2a4052..aa8fd04d6 100644
--- a/bun.lock
+++ b/bun.lock
@@ -355,7 +355,7 @@
},
"catalog": {
"@base-ui/react": "^1.7.0",
- "@gitbook/api": "0.203.0",
+ "@gitbook/api": "0.204.0",
"@scalar/api-client-react": "^1.3.46",
"@tsconfig/node20": "^20.1.6",
"@tsconfig/strictest": "^2.0.6",
@@ -727,7 +727,7 @@
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@7.2.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.2.0" } }, "sha512-6639htZMjEkwskf3J+e6/iar+4cTNM9qhoWuRfj9F3eJD6r7iCzV1SWnQr2Mdv0QT0suuqU8BoJCZUyCtP9R4Q=="],
- "@gitbook/api": ["@gitbook/api@0.203.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-cSFUMM7cIMTHW9cdMpexGSkHHaXmc506fx+LQE2+KLqce8GlBDU/ElIP4RU0MGtN+SSYF31AUgvPD8ZerBVcMA=="],
+ "@gitbook/api": ["@gitbook/api@0.204.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-SJCe0Ipyt+V5HZX4EZpFO8AHTOWIMwj2ljQTbcqgU7rVOAyCNDzILp8mRk9stbboSi1pv09+rDnZK1ZguPvFTw=="],
"@gitbook/browser-types": ["@gitbook/browser-types@workspace:packages/browser-types"],
diff --git a/package.json b/package.json
index 194a469ea..26d42da57 100644
--- a/package.json
+++ b/package.json
@@ -48,7 +48,7 @@
"@tsconfig/strictest": "^2.0.6",
"@tsconfig/node20": "^20.1.6",
"@base-ui/react": "^1.7.0",
- "@gitbook/api": "0.203.0",
+ "@gitbook/api": "0.204.0",
"@scalar/api-client-react": "^1.3.46",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
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..3a4626646
--- /dev/null
+++ b/packages/gitbook/src/components/SitePage/title.ts
@@ -0,0 +1,23 @@
+import type { RevisionPageDocument } from '@gitbook/api';
+
+import type { GitBookSiteContext } from '@/lib/context';
+import { getSiteStructureTitle } from '@/lib/sites';
+
+/**
+ * Get the for a page.
+ */
+export function getPageFullTitle(context: GitBookSiteContext, page: RevisionPageDocument) {
+ const { site } = context;
+ const siteStructureTitle = getSiteStructureTitle(context);
+ const tagTitle = page.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(' | ');
+}