mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-30 14:08:33 +00:00
Respect page description visibility settings in HTML and full-page Markdown output
This commit is contained in:
@@ -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}
|
||||
</h1>
|
||||
) : null}
|
||||
{page.description && page.layout.description ? (
|
||||
{description ? (
|
||||
<p
|
||||
data-cover-aware-text
|
||||
data-over-cover
|
||||
@@ -277,7 +278,7 @@ export async function PageHeader(props: {
|
||||
'clear-both'
|
||||
)}
|
||||
>
|
||||
{page.description}
|
||||
{description}
|
||||
</p>
|
||||
) : null}
|
||||
</>
|
||||
|
||||
@@ -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<Partial<RevisionPageDocument>, 'layout'> & {
|
||||
layout?: Partial<RevisionPageDocument['layout']>;
|
||||
}
|
||||
) {
|
||||
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 })],
|
||||
|
||||
@@ -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<string> {
|
||||
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',
|
||||
|
||||
@@ -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/';
|
||||
|
||||
|
||||
@@ -17,6 +17,15 @@ export type ResolvedPagePath<Page extends RevisionPageDocument | RevisionPageGro
|
||||
ancestors: AncestorRevisionPage[];
|
||||
};
|
||||
|
||||
/** Return the page description when it is configured to be visible. */
|
||||
export function getPageDescription(
|
||||
page: Pick<RevisionPageDocument, 'description'> & {
|
||||
layout: Pick<RevisionPageDocument['layout'], 'description'>;
|
||||
}
|
||||
): string | undefined {
|
||||
return page.description && page.layout.description ? page.description : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a page path to a page document.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user