diff --git a/packages/gitbook/src/components/PageBody/PageHeader.tsx b/packages/gitbook/src/components/PageBody/PageHeader.tsx
index 4b7d822e6..dc1138639 100644
--- a/packages/gitbook/src/components/PageBody/PageHeader.tsx
+++ b/packages/gitbook/src/components/PageBody/PageHeader.tsx
@@ -25,7 +25,7 @@ import { categorizeVariants } from '../SpaceLayout/categorizeVariants';
import { BreadcrumbItemDropdown, type BreadcrumbSibling } from './BreadcrumbItemDropdown';
import { PageTags } from './PageTags';
import type { GitBookSiteContext, SiteStructureNode } from '@/lib/context';
-import { type AncestorRevisionPage, resolveFirstDocument } from '@/lib/pages';
+import { getPageDescription, type AncestorRevisionPage, resolveFirstDocument } from '@/lib/pages';
import { getLocalizedTitle, getSiteSpaceURL } from '@/lib/sites';
import { tcls } from '@/lib/tailwind';
import { getPageRSSURL } from '@/routes/rss';
@@ -132,6 +132,7 @@ export async function PageHeader(props: {
}
const hasContextCrumbs = contextCrumbs.length > 0;
const showBreadcrumbs = hasAncestors || hasContextCrumbs;
+ const description = getPageDescription(page);
const pageActionsEnabled = page.layout.actions !== false;
@@ -148,7 +149,7 @@ export async function PageHeader(props: {
].some((type) => isPageActionEnabled(context.customization, type));
const hasPageActions = pageActionsEnabled && (hasConfiguredPageActions || withRSSFeed);
- if (!page.layout.title && !page.layout.description && !hasPageActions) {
+ if (!page.layout.title && !description && !hasPageActions) {
return null;
}
@@ -265,7 +266,7 @@ export async function PageHeader(props: {
{page.title}
) : null}
- {page.description && page.layout.description ? (
+ {description ? (
- {page.description}
+ {description}
) : null}
>
diff --git a/packages/gitbook/src/lib/markdownPage.test.ts b/packages/gitbook/src/lib/markdownPage.test.ts
index f7eb9d955..c0a5e8253 100644
--- a/packages/gitbook/src/lib/markdownPage.test.ts
+++ b/packages/gitbook/src/lib/markdownPage.test.ts
@@ -1,8 +1,10 @@
import { describe, expect, it } from 'bun:test';
-import type { GitBookAnyContext } from './context';
+import type { RevisionPageDocument } from '@gitbook/api';
+
+import type { GitBookAnyContext, GitBookSiteContext } from './context';
import { createLinker, linkerWithDirectPagePaths, linkerWithMarkdownPages } from './links';
-import { fromPageMarkdown, toPageMarkdown } from './markdownPage';
+import { fromPageMarkdown, getMarkdownForPage, toPageMarkdown } from './markdownPage';
const page = {
id: 'designer',
@@ -32,6 +34,84 @@ async function rewrite(markdown: string) {
return toPageMarkdown(await fromPageMarkdown(context, { markdown, pagePath: 'workflows' }));
}
+async function renderPage(
+ markdown: string,
+ pageOverrides: Omit, 'layout'> & {
+ layout?: Partial;
+ }
+) {
+ const page = {
+ ...pageBase,
+ ...pageOverrides,
+ layout: {
+ ...pageBase.layout,
+ ...pageOverrides.layout,
+ },
+ } as RevisionPageDocument;
+
+ return getMarkdownForPage(
+ {
+ ...context,
+ revisionId: 'revision',
+ dataFetcher: {
+ getRevisionPageMarkdown: async () => ({ data: markdown }),
+ },
+ } as unknown as GitBookSiteContext,
+ { page, ancestors: [] }
+ );
+}
+
+const pageBase = {
+ ...page,
+ layout: {
+ description: true,
+ },
+} as RevisionPageDocument;
+
+describe('page descriptions in markdown', () => {
+ it('renders visible descriptions immediately after the H1', async () => {
+ expect(
+ await renderPage('# About the Workflow Designer\n\nSome content.', {
+ description: 'Design workflows visually.',
+ })
+ ).toBe('# About the Workflow Designer\n\nDesign workflows visually.\n\nSome content.\n');
+ });
+
+ it('omits hidden and missing descriptions', async () => {
+ const pageMarkdown = '# About the Workflow Designer\n\nSome content.';
+ const markdown = await renderPage(pageMarkdown, {
+ description: 'Design workflows visually.',
+ layout: { description: false },
+ });
+
+ expect(markdown).toBe('# About the Workflow Designer\n\nSome content.\n');
+ expect(await renderPage(pageMarkdown, {})).toBe(
+ '# About the Workflow Designer\n\nSome content.\n'
+ );
+ });
+
+ it('keeps child links for an otherwise empty parent page', async () => {
+ const markdown = await renderPage('# Workflows\n', {
+ title: 'Workflows',
+ description: 'Build and manage workflows.',
+ pages: [
+ {
+ ...pageBase,
+ id: 'child',
+ title: 'First workflow',
+ path: 'first-workflow',
+ slug: 'first-workflow',
+ pages: [],
+ },
+ ],
+ });
+
+ expect(markdown).toContain('# Workflows');
+ expect(markdown).toContain('Build and manage workflows.');
+ expect(markdown).toContain('First workflow');
+ });
+});
+
describe('HTML links in page markdown', () => {
it.each([
['/pages/designer', linker.toPathForPagePath({ path: page.path })],
diff --git a/packages/gitbook/src/lib/markdownPage.ts b/packages/gitbook/src/lib/markdownPage.ts
index 75590995e..92929cb7a 100644
--- a/packages/gitbook/src/lib/markdownPage.ts
+++ b/packages/gitbook/src/lib/markdownPage.ts
@@ -26,6 +26,7 @@ import {
} from '@/lib/context';
import { DataFetcherError, throwIfDataError } from '@/lib/data';
import type { ResolvedPagePath } from '@/lib/pages';
+import { getPageDescription } from '@/lib/pages';
import { getIndexablePages } from '@/lib/sitemap';
import { getMarkdownForPagesTree } from '@/routes/llms';
@@ -70,13 +71,14 @@ export async function getMarkdownForPage(
markdown: rawMarkdown,
pagePath: page.path,
});
- insertDescriptionAfterHeading(tree, page.description);
// Handle empty document pages which have children
if (isEmptyMarkdownPage(tree) && page.pages.length > 0) {
return servePageGroup(context, page);
}
+ insertDescriptionAfterHeading(tree, getPageDescription(page));
+
return toPageMarkdown(tree);
}
@@ -107,13 +109,14 @@ export async function getMarkdownForPageInSpace(
markdown: rawMarkdown,
pagePath: page.path,
});
- insertDescriptionAfterHeading(tree, page.description);
// Handle empty document pages which have children (same as getMarkdownForPage)
if (isEmptyMarkdownPage(tree) && page.pages.length > 0) {
return renderGroupPageMarkdown({ linker: siteSpaceContext.linker, page });
}
+ insertDescriptionAfterHeading(tree, getPageDescription(page));
+
return toPageMarkdown(tree);
}
@@ -225,7 +228,8 @@ async function renderGroupPageMarkdown(args: {
}): Promise {
const { linker, page } = args;
const indexablePages = getIndexablePages(page.pages);
- const description = page.type === RevisionPageType.Document ? page.description : undefined;
+ const description =
+ page.type === RevisionPageType.Document ? getPageDescription(page) : undefined;
const markdownTree: Root = {
type: 'root',
diff --git a/packages/gitbook/src/lib/pages.test.ts b/packages/gitbook/src/lib/pages.test.ts
index f74da9131..d4cdfba7c 100644
--- a/packages/gitbook/src/lib/pages.test.ts
+++ b/packages/gitbook/src/lib/pages.test.ts
@@ -9,12 +9,29 @@ import {
import {
extractPagePath,
+ getPageDescription,
getSimilarPages,
resolveFirstDocument,
resolvePagePath,
resolvePagePathDocumentOrGroup,
} from './pages';
+describe('getPageDescription', () => {
+ const page = {
+ description: 'A useful description',
+ layout: { description: true },
+ };
+
+ it('returns the description when it is visible', () => {
+ expect(getPageDescription(page)).toBe(page.description);
+ });
+
+ it('omits hidden and missing descriptions', () => {
+ expect(getPageDescription({ ...page, layout: { description: false } })).toBeUndefined();
+ expect(getPageDescription({ layout: { description: true } })).toBeUndefined();
+ });
+});
+
describe('extractPagePath', () => {
const baseURL = 'https://docs.example.com/api/';
diff --git a/packages/gitbook/src/lib/pages.ts b/packages/gitbook/src/lib/pages.ts
index 8bcfc60bc..277c68fcf 100644
--- a/packages/gitbook/src/lib/pages.ts
+++ b/packages/gitbook/src/lib/pages.ts
@@ -17,6 +17,15 @@ export type ResolvedPagePath & {
+ layout: Pick;
+ }
+): string | undefined {
+ return page.description && page.layout.description ? page.description : undefined;
+}
+
/**
* Resolve a page path to a page document.
*/
diff --git a/packages/gitbook/src/routes/llms-full.ts b/packages/gitbook/src/routes/llms-full.ts
index 64fbfbf08..5b6e17de9 100644
--- a/packages/gitbook/src/routes/llms-full.ts
+++ b/packages/gitbook/src/routes/llms-full.ts
@@ -12,6 +12,7 @@ import {
import { throwIfDataError } from '@/lib/data';
import { getMarkdownContentType } from '@/lib/markdown-content-type';
import { fromPageMarkdown, toPageMarkdown } from '@/lib/markdownPage';
+import { getPageDescription } from '@/lib/pages';
import { getIndexablePages } from '@/lib/sitemap';
import { filterSiteSpacesByLocale, getSiteStructureSections } from '@/lib/sites';
@@ -199,12 +200,13 @@ async function getMarkdownForPage(
pagePath: page.path,
});
- if (page.description) {
+ const description = getPageDescription(page);
+ if (description) {
// The first node is the page title as a H1, we insert the description as a paragraph
// after it.
const descriptionNode: Paragraph = {
type: 'paragraph',
- children: [{ type: 'text', value: page.description }],
+ children: [{ type: 'text', value: description }],
};
tree.children.splice(1, 0, descriptionNode);
}