Add support for rendering Tags (#4006)

This commit is contained in:
Viktor Renkema
2026-02-20 10:03:49 +01:00
committed by GitHub
parent d9ff25d28e
commit 9dfa9c2db0
7 changed files with 133 additions and 10 deletions
@@ -1,7 +1,9 @@
import { formatDateFull, formatDateShort, formatNumericDate } from '@/components/utils/dates';
import { getRevisionTags, resolveBlockTags } from '@/lib/tags';
import { tcls } from '@/lib/tailwind';
import type { DocumentBlockUpdate, DocumentBlockUpdates } from '@gitbook/api';
import { assert } from 'ts-essentials';
import { Tag } from '../Tag';
import type { BlockProps } from './Block';
import { Blocks } from './Blocks';
@@ -35,13 +37,12 @@ export function Update(props: BlockProps<DocumentBlockUpdate>) {
short: formatDateShort(parsedDate),
}[dateFormat];
// Resolve tags from the block data using revision-level tag definitions
const revisionTags = getRevisionTags(contextProps.context.contentContext?.revision);
const resolvedTags = resolveBlockTags(block.data.tags, revisionTags);
return (
<div
className={tcls(
'relative flex flex-col gap-2 md:flex-row md:gap-4 lg:gap-12 xl:gap-20',
style
)}
>
<div className={tcls('relative flex flex-col gap-2 md:flex-row md:gap-4 lg:gap-8', style)}>
<div
className={tcls(
// Date is only sticky on larger screens when we use flex-row layout, with 0px fallback to prevent flicker and flaky tests before JS sets the variable
@@ -55,6 +56,13 @@ export function Update(props: BlockProps<DocumentBlockUpdate>) {
>
{displayDate}
</time>
{resolvedTags.length > 0 ? (
<div className="mt-2 flex flex-wrap gap-1.5">
{resolvedTags.map((tag) => (
<Tag key={tag.slug} label={tag.label} />
))}
</div>
) : null}
</div>
<Blocks
{...contextProps}
@@ -12,6 +12,7 @@ import {
} from '../PageActions/PageActionsDropdown';
import { PageIcon } from '../PageIcon';
import { StyledLink } from '../primitives';
import { PageTags } from './PageTags';
export async function PageHeader(props: {
context: GitBookSiteContext;
@@ -119,6 +120,7 @@ export async function PageHeader(props: {
</ol>
</nav>
)}
<PageTags page={page} revision={revision} />
{page.layout.title ? (
<h1
className={tcls(
@@ -0,0 +1,29 @@
import { getRevisionTags, resolveTag } from '@/lib/tags';
import type { Revision, RevisionPageDocument } from '@gitbook/api';
import { Tag } from '../Tag';
/**
* Render the tags that were added to a page.
*/
export function PageTags(props: { page: RevisionPageDocument; revision: Revision }) {
const { page, revision } = props;
const pageTags = page.tags; // TODO: simplify once new API lands as `tags` will always be defined.
if (!pageTags || pageTags.length === 0) {
return null;
}
const revisionTags = getRevisionTags(revision);
return (
<div className="flex flex-wrap gap-1.5">
{pageTags.map((tagRef) => {
const resolved = resolveTag(tagRef.tag.tag, revisionTags);
if (!resolved) {
return null;
}
return <Tag key={resolved.slug} label={resolved.label} />;
})}
</div>
);
}
@@ -1,12 +1,12 @@
'use client';
import { tcls } from '@/lib/tailwind';
import type { ClientTOCPageDocument } from './encodeClientTableOfContents';
import { SiteInsightsLinkPosition } from '@gitbook/api';
import { Tag } from '../Tag';
import { PagesList } from './PagesList';
import { TOCPageIcon } from './TOCPageIcon';
import { ToggleableLinkItem } from './ToggleableLinkItem';
import type { ClientTOCPageDocument } from './encodeClientTableOfContents';
export function PageDocumentItem(props: { page: ClientTOCPageDocument }) {
const { page } = props;
@@ -40,7 +40,10 @@ export function PageDocumentItem(props: { page: ClientTOCPageDocument }) {
}
>
<TOCPageIcon page={page} />
{page.title}
<span className="flex items-center gap-2">
{page.title}
{page.primaryTag ? <Tag label={page.primaryTag.label} /> : null}
</span>
</ToggleableLinkItem>
</li>
);
@@ -1,8 +1,9 @@
import type { GitBookSiteContext } from '@/lib/context';
import { getPagePaths, hasPageVisibleDescendant } from '@/lib/pages';
import { resolveContentRef } from '@/lib/references';
import { getRevisionTags, resolveTag } from '@/lib/tags';
import { removeUndefined } from '@/lib/typescript';
import type { ContentRef, RevisionPage } from '@gitbook/api';
import type { ContentRef, RevisionPage, RevisionPageTag, RevisionTag } from '@gitbook/api';
import assertNever from 'assert-never';
export type ClientTOCPageLink = {
@@ -24,6 +25,7 @@ export type ClientTOCPageDocument = {
icon?: string;
pathnames: string[];
descendants?: ClientTOCPage[];
primaryTag?: RevisionTag;
};
export type ClientTOCPageGroup = {
@@ -69,6 +71,8 @@ export async function encodeClientTableOfContents(
? await encodeClientTableOfContents(context, rootPages, page.pages)
: undefined;
const primaryTag = resolvePrimaryTag(page.tags, context);
result.push(
removeUndefined({
id: page.id,
@@ -78,6 +82,7 @@ export async function encodeClientTableOfContents(
icon: page.icon,
pathnames: getPagePaths(rootPages, page),
descendants,
primaryTag,
type: 'document',
})
);
@@ -122,3 +127,25 @@ export async function encodeClientTableOfContents(
return result;
}
/**
* Tags attached to a page can also get a `primary` prop which, when set, is the tag that gets shown in the TOC.
* - only 1 tag can be primary for available-space reasons.
* */
function resolvePrimaryTag(
pageTags: RevisionPageTag[] | undefined,
context: GitBookSiteContext
): RevisionTag | undefined {
// TODO: simplify once new API lands as `tags` will always be defined.
if (!pageTags || pageTags.length === 0) {
return undefined;
}
const primary = pageTags.find((t) => t.primary);
if (!primary) {
return undefined;
}
const revisionTags = getRevisionTags(context.revision);
return resolveTag(primary.tag.tag, revisionTags);
}
+22
View File
@@ -0,0 +1,22 @@
import { tcls } from '@/lib/tailwind';
import type { RevisionTag } from '@gitbook/api';
/**
* Renders a tag, used to tag content like pages and blocks.
*/
export function Tag(props: { label: RevisionTag['label']; className?: string }) {
const { label, className } = props;
return (
<span
data-tag=""
title={label}
className={tcls(
'inline-flex items-center rounded-full bg-tint-5 px-2 py-1 font-medium text-tint-strong text-xs leading-normal contrast-more:ring-1 contrast-more:ring-tint',
className
)}
>
<span className="truncate">{label}</span>
</span>
);
}
+32
View File
@@ -0,0 +1,32 @@
import type { ContentRefTag, Revision, RevisionTag } from '@gitbook/api';
/**
* Get the tags defined on a revision.
*/
export function getRevisionTags(revision: Revision | undefined): RevisionTag[] {
// TODO: simplify once new API lands as `tags` will always be defined.
return revision?.tags ?? [];
}
/**
* Resolve a tag slug to its full RevisionTag.
*/
export function resolveTag(slug: string, revisionTags: RevisionTag[]): RevisionTag | undefined {
return revisionTags.find((t) => t.slug === slug);
}
/**
* Resolve an array of block-level tag references to their full RevisionTags.
*/
export function resolveBlockTags(
tags: ContentRefTag[] | undefined,
revisionTags: RevisionTag[]
): RevisionTag[] {
// TODO: simplify once new API lands as `tags` will always be defined.
if (!tags || tags.length === 0) {
return [];
}
return tags
.map((tag) => resolveTag(tag.tag, revisionTags))
.filter((tag): tag is RevisionTag => tag !== undefined);
}