mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
Core & UI: Support breadcrumbs for Search API
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'fumadocs-core': patch
|
||||
'fumadocs-ui': patch
|
||||
---
|
||||
|
||||
Support breadcrumbs for Search API
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { source } from '@/lib/source';
|
||||
import type { OramaDocument } from 'fumadocs-core/search/orama-cloud';
|
||||
import { getBreadcrumbItems } from 'fumadocs-core/breadcrumb';
|
||||
|
||||
export const revalidate = false;
|
||||
|
||||
@@ -10,6 +10,10 @@ export async function GET(): Promise<Response> {
|
||||
.filter((page) => page.slugs[0] !== 'openapi')
|
||||
.map((page) => {
|
||||
const { structuredData } = page.data;
|
||||
const items = getBreadcrumbItems(page.url, source.pageTree, {
|
||||
includePage: false,
|
||||
includeRoot: true,
|
||||
});
|
||||
|
||||
return {
|
||||
id: page.url,
|
||||
@@ -18,8 +22,11 @@ export async function GET(): Promise<Response> {
|
||||
url: page.url,
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
breadcrumbs: items.flatMap<string>((item) =>
|
||||
typeof item.name === 'string' ? item.name : [],
|
||||
),
|
||||
} satisfies OramaDocument;
|
||||
});
|
||||
|
||||
return NextResponse.json(results);
|
||||
return Response.json(results);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type ReactNode, useMemo } from 'react';
|
||||
import type * as PageTree from '@/source/page-tree/definitions';
|
||||
import { normalizeUrl } from '@/utils/normalize-url';
|
||||
import { findPath } from '@/utils/page-tree';
|
||||
|
||||
export interface BreadcrumbItem {
|
||||
name: ReactNode;
|
||||
@@ -118,42 +119,10 @@ export function searchPath(
|
||||
nodes: PageTree.Node[],
|
||||
url: string,
|
||||
): PageTree.Node[] | null {
|
||||
const items: PageTree.Node[] = [];
|
||||
url = normalizeUrl(url);
|
||||
const normalizedUrl = normalizeUrl(url);
|
||||
|
||||
function run(nodes: PageTree.Node[]): boolean {
|
||||
let separator: PageTree.Separator | undefined;
|
||||
|
||||
for (const node of nodes) {
|
||||
if (node.type === 'separator') separator = node;
|
||||
|
||||
if (node.type === 'folder') {
|
||||
if (node.index?.url === url) {
|
||||
if (separator) items.push(separator);
|
||||
items.push(node, node.index);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (run(node.children)) {
|
||||
items.unshift(node);
|
||||
if (separator) items.unshift(separator);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.type === 'page' && node.url === url) {
|
||||
if (separator) items.push(separator);
|
||||
items.push(node);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (run(nodes)) return items;
|
||||
return null;
|
||||
return findPath(
|
||||
nodes,
|
||||
(node) => node.type === 'page' && node.url === normalizedUrl,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface DocumentRecord {
|
||||
|
||||
title: string;
|
||||
description?: string;
|
||||
breadcrumbs?: string[];
|
||||
|
||||
/**
|
||||
* URL to the page
|
||||
@@ -75,6 +76,7 @@ export async function setIndexSettings(
|
||||
'content',
|
||||
'url',
|
||||
'section_id',
|
||||
'breadcrumbs',
|
||||
],
|
||||
searchableAttributes: ['title', 'section', 'content'],
|
||||
attributesToSnippet: [],
|
||||
@@ -95,6 +97,7 @@ function toIndex(page: DocumentRecord): BaseIndex[] {
|
||||
): BaseIndex {
|
||||
return {
|
||||
objectID: `${page._id}-${(id++).toString()}`,
|
||||
breadcrumbs: page.breadcrumbs,
|
||||
title: page.title,
|
||||
url: page.url,
|
||||
page_id: page._id,
|
||||
@@ -161,5 +164,7 @@ export interface BaseIndex {
|
||||
*/
|
||||
section_id?: string;
|
||||
|
||||
breadcrumbs?: string[];
|
||||
|
||||
content: string;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ export function groupResults(hits: Hit<BaseIndex>[]): SortedResult[] {
|
||||
grouped.push({
|
||||
id: hit.url,
|
||||
type: 'page',
|
||||
breadcrumbs: hit.breadcrumbs,
|
||||
url: hit.url,
|
||||
content: hit.title,
|
||||
});
|
||||
|
||||
@@ -108,6 +108,7 @@ export async function searchDocs(
|
||||
id: doc.page_id,
|
||||
type: 'page',
|
||||
content: doc.title,
|
||||
breadcrumbs: doc.breadcrumbs,
|
||||
contentWithHighlights: highlighter.highlight(doc.title),
|
||||
url: doc.url,
|
||||
});
|
||||
|
||||
@@ -55,6 +55,7 @@ export interface OramaDocument {
|
||||
* Data to be added to each section index
|
||||
*/
|
||||
extra_data?: object;
|
||||
breadcrumbs?: string[];
|
||||
}
|
||||
|
||||
export interface OramaIndex {
|
||||
@@ -75,6 +76,8 @@ export interface OramaIndex {
|
||||
*/
|
||||
section?: string;
|
||||
|
||||
breadcrumbs?: string[];
|
||||
|
||||
/**
|
||||
* Heading (anchor) id
|
||||
*/
|
||||
@@ -129,6 +132,7 @@ function toIndex(page: OramaDocument): OramaIndex[] {
|
||||
section,
|
||||
section_id: sectionId,
|
||||
content,
|
||||
breadcrumbs: page.breadcrumbs,
|
||||
...page.extra_data,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,11 +7,22 @@ import {
|
||||
} from '@orama/orama';
|
||||
import { type AdvancedOptions, type SimpleOptions } from '@/search/server';
|
||||
|
||||
export type SimpleDocument = TypedDocument<Orama<typeof simpleSchema>>;
|
||||
export const simpleSchema = {
|
||||
url: 'string',
|
||||
title: 'string',
|
||||
breadcrumbs: 'string[]',
|
||||
description: 'string',
|
||||
content: 'string',
|
||||
keywords: 'string',
|
||||
} as const;
|
||||
|
||||
export type AdvancedDocument = TypedDocument<Orama<typeof advancedSchema>>;
|
||||
export const advancedSchema = {
|
||||
content: 'string',
|
||||
page_id: 'string',
|
||||
type: 'string',
|
||||
breadcrumbs: 'string[]',
|
||||
tags: 'enum[]',
|
||||
url: 'string',
|
||||
embeddings: 'vector[512]',
|
||||
@@ -36,11 +47,8 @@ export async function createDB({
|
||||
|
||||
const mapTo: PartialSchemaDeep<AdvancedDocument>[] = [];
|
||||
items.forEach((page) => {
|
||||
const tags = Array.isArray(page.tag)
|
||||
? page.tag
|
||||
: page.tag
|
||||
? [page.tag]
|
||||
: [];
|
||||
const pageTag = page.tag ?? [];
|
||||
const tags = Array.isArray(pageTag) ? pageTag : [pageTag];
|
||||
const data = page.structuredData;
|
||||
let id = 0;
|
||||
|
||||
@@ -49,13 +57,16 @@ export async function createDB({
|
||||
page_id: page.id,
|
||||
type: 'page',
|
||||
content: page.title,
|
||||
breadcrumbs: page.breadcrumbs,
|
||||
tags,
|
||||
url: page.url,
|
||||
});
|
||||
|
||||
const nextId = () => `${page.id}-${id++}`;
|
||||
|
||||
if (page.description) {
|
||||
mapTo.push({
|
||||
id: `${page.id}-${(id++).toString()}`,
|
||||
id: nextId(),
|
||||
page_id: page.id,
|
||||
tags,
|
||||
type: 'text',
|
||||
@@ -66,7 +77,7 @@ export async function createDB({
|
||||
|
||||
for (const heading of data.headings) {
|
||||
mapTo.push({
|
||||
id: `${page.id}-${(id++).toString()}`,
|
||||
id: nextId(),
|
||||
page_id: page.id,
|
||||
type: 'heading',
|
||||
tags,
|
||||
@@ -77,7 +88,7 @@ export async function createDB({
|
||||
|
||||
for (const content of data.contents) {
|
||||
mapTo.push({
|
||||
id: `${page.id}-${(id++).toString()}`,
|
||||
id: nextId(),
|
||||
page_id: page.id,
|
||||
tags,
|
||||
type: 'text',
|
||||
@@ -91,15 +102,6 @@ export async function createDB({
|
||||
return db;
|
||||
}
|
||||
|
||||
export type SimpleDocument = TypedDocument<Orama<typeof simpleSchema>>;
|
||||
export const simpleSchema = {
|
||||
url: 'string',
|
||||
title: 'string',
|
||||
description: 'string',
|
||||
content: 'string',
|
||||
keywords: 'string',
|
||||
} as const;
|
||||
|
||||
export async function createDBSimple({
|
||||
indexes,
|
||||
tokenizer,
|
||||
@@ -120,6 +122,7 @@ export async function createDBSimple({
|
||||
items.map((page) => ({
|
||||
title: page.title,
|
||||
description: page.description,
|
||||
breadcrumbs: page.breadcrumbs,
|
||||
url: page.url,
|
||||
content: page.content,
|
||||
keywords: page.keywords,
|
||||
|
||||
@@ -15,32 +15,58 @@ import { type StructuredData } from '@/mdx-plugins';
|
||||
import { basename, extname } from '@/source/path';
|
||||
import type { I18nConfig } from '@/i18n';
|
||||
import type { Language } from '@orama/orama';
|
||||
import { findPath } from '@/utils/page-tree';
|
||||
|
||||
type Awaitable<T> = T | Promise<T>;
|
||||
|
||||
async function pageToIndex(page: Page): Promise<AdvancedIndex> {
|
||||
let structuredData: StructuredData | undefined;
|
||||
|
||||
if ('structuredData' in page.data) {
|
||||
structuredData = page.data.structuredData as StructuredData;
|
||||
} else if ('load' in page.data && typeof page.data.load === 'function') {
|
||||
structuredData = (await page.data.load()).structuredData;
|
||||
function defaultBuildIndex(source: LoaderOutput<LoaderConfig>) {
|
||||
function isBreadcrumbItem(item: unknown): item is string {
|
||||
return typeof item === 'string' && item.length > 0;
|
||||
}
|
||||
|
||||
if (!structuredData)
|
||||
throw new Error(
|
||||
'Cannot find structured data from page, please define the page to index function.',
|
||||
);
|
||||
return async (page: Page): Promise<AdvancedIndex> => {
|
||||
let breadcrumbs: string[] | undefined;
|
||||
let structuredData: StructuredData | undefined;
|
||||
|
||||
return {
|
||||
title: page.data.title ?? basename(page.path, extname(page.path)),
|
||||
description:
|
||||
'description' in page.data
|
||||
? (page.data.description as string)
|
||||
: undefined,
|
||||
url: page.url,
|
||||
id: page.url,
|
||||
structuredData,
|
||||
if ('structuredData' in page.data) {
|
||||
structuredData = page.data.structuredData as StructuredData;
|
||||
} else if ('load' in page.data && typeof page.data.load === 'function') {
|
||||
structuredData = (await page.data.load()).structuredData;
|
||||
}
|
||||
|
||||
if (!structuredData)
|
||||
throw new Error(
|
||||
'Cannot find structured data from page, please define the page to index function.',
|
||||
);
|
||||
|
||||
const pageTree = source.getPageTree(page.locale);
|
||||
const path = findPath(
|
||||
pageTree.children,
|
||||
(node) => node.type === 'page' && node.url === page.url,
|
||||
);
|
||||
if (path) {
|
||||
breadcrumbs = [];
|
||||
path.pop();
|
||||
|
||||
if (isBreadcrumbItem(pageTree.name)) {
|
||||
breadcrumbs.push(pageTree.name);
|
||||
}
|
||||
|
||||
for (const segment of path) {
|
||||
if (!isBreadcrumbItem(segment.name)) continue;
|
||||
|
||||
breadcrumbs.push(segment.name);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title: page.data.title ?? basename(page.path, extname(page.path)),
|
||||
breadcrumbs,
|
||||
description: page.data.description,
|
||||
url: page.url,
|
||||
id: page.url,
|
||||
structuredData,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -72,12 +98,12 @@ export function createFromSource<S extends LoaderOutput<LoaderConfig>>(
|
||||
|
||||
export function createFromSource<S extends LoaderOutput<LoaderConfig>>(
|
||||
source: S,
|
||||
_buildIndexOrOptions:
|
||||
_buildIndexOrOptions?:
|
||||
| ((page: InferPageType<S>) => Awaitable<AdvancedIndex>)
|
||||
| Options<S> = pageToIndex,
|
||||
| Options<S>,
|
||||
_options?: Omit<Options<S>, 'buildIndex'>,
|
||||
): SearchAPI {
|
||||
const { buildIndex = pageToIndex, ...options }: Options<S> = {
|
||||
const { buildIndex = defaultBuildIndex(source), ...options }: Options<S> = {
|
||||
...(typeof _buildIndexOrOptions === 'function'
|
||||
? {
|
||||
buildIndex: _buildIndexOrOptions,
|
||||
@@ -85,49 +111,32 @@ export function createFromSource<S extends LoaderOutput<LoaderConfig>>(
|
||||
: _buildIndexOrOptions),
|
||||
..._options,
|
||||
};
|
||||
const i18n = source._i18n;
|
||||
let server: Promise<SearchAPI>;
|
||||
|
||||
if (i18n) {
|
||||
const indexes = source.getLanguages().flatMap((entry) => {
|
||||
return entry.pages.map(async (page) => ({
|
||||
...(await buildIndex(page as InferPageType<S>)),
|
||||
locale: entry.language,
|
||||
}));
|
||||
if (source._i18n) {
|
||||
return createI18nSearchAPI('advanced', {
|
||||
...options,
|
||||
i18n: source._i18n,
|
||||
indexes: async () => {
|
||||
const indexes = source.getLanguages().flatMap((entry) => {
|
||||
return entry.pages.map(async (page) => ({
|
||||
...(await buildIndex(page as InferPageType<S>)),
|
||||
locale: entry.language,
|
||||
}));
|
||||
});
|
||||
|
||||
return Promise.all(indexes);
|
||||
},
|
||||
});
|
||||
|
||||
server = Promise.all(indexes).then((loaded) =>
|
||||
createI18nSearchAPI('advanced', {
|
||||
...options,
|
||||
i18n,
|
||||
indexes: loaded,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
const indexes = source.getPages().map(async (page) => {
|
||||
return buildIndex(page as InferPageType<S>);
|
||||
});
|
||||
|
||||
server = Promise.all(indexes).then((loaded) =>
|
||||
createSearchAPI('advanced', {
|
||||
...options,
|
||||
indexes: loaded,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
async export(...args) {
|
||||
return (await server).export(...args);
|
||||
return createSearchAPI('advanced', {
|
||||
...options,
|
||||
indexes: async () => {
|
||||
const indexes = source
|
||||
.getPages()
|
||||
.map((page) => buildIndex(page as InferPageType<S>));
|
||||
|
||||
return Promise.all(indexes);
|
||||
},
|
||||
async GET(...args) {
|
||||
return (await server).GET(...args);
|
||||
},
|
||||
async search(...args) {
|
||||
return (await server).search(...args);
|
||||
},
|
||||
async staticGET(...args) {
|
||||
return (await server).staticGET(...args);
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ export async function searchAdvanced(
|
||||
id: pageId,
|
||||
type: 'page',
|
||||
content: page.content,
|
||||
breadcrumbs: page.breadcrumbs,
|
||||
contentWithHighlights: highlighter.highlight(page.content),
|
||||
url: page.url,
|
||||
});
|
||||
@@ -67,6 +68,7 @@ export async function searchAdvanced(
|
||||
list.push({
|
||||
id: hit.document.id.toString(),
|
||||
content: hit.document.content,
|
||||
breadcrumbs: hit.document.breadcrumbs,
|
||||
contentWithHighlights: highlighter.highlight(hit.document.content),
|
||||
type: hit.document.type as SortedResult['type'],
|
||||
url: hit.document.url,
|
||||
|
||||
@@ -26,6 +26,7 @@ export async function searchSimple(
|
||||
return result.hits.map<SortedResult>((hit) => ({
|
||||
type: 'page',
|
||||
content: hit.document.title,
|
||||
breadcrumbs: hit.document.breadcrumbs,
|
||||
contentWithHighlights: highlighter.highlight(hit.document.title),
|
||||
id: hit.document.url,
|
||||
url: hit.document.url,
|
||||
|
||||
@@ -101,6 +101,7 @@ export function createSearchAPI<T extends SearchType>(
|
||||
export interface Index {
|
||||
title: string;
|
||||
description?: string;
|
||||
breadcrumbs?: string[];
|
||||
content: string;
|
||||
url: string;
|
||||
keywords?: string;
|
||||
@@ -134,6 +135,8 @@ export interface AdvancedIndex {
|
||||
*/
|
||||
keywords?: string;
|
||||
|
||||
breadcrumbs?: string[];
|
||||
|
||||
/**
|
||||
* Required if tag filter is enabled
|
||||
*/
|
||||
|
||||
@@ -333,7 +333,7 @@ function build(id: string, ctx: PageTreeBuilderContext): PageTree.Root {
|
||||
const folder = buildFolderNode('', true, ctx)!;
|
||||
let root: PageTree.Root = {
|
||||
$id: id,
|
||||
name: folder.name,
|
||||
name: folder.name || 'Docs',
|
||||
children: folder.children,
|
||||
};
|
||||
|
||||
|
||||
@@ -119,3 +119,51 @@ function findParentFromTree(
|
||||
return findParentFromTree(node.fallback, url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search the path of a node in the tree matched by the matcher.
|
||||
*
|
||||
* @returns The path to the target node (from starting root), or null if the page doesn't exist
|
||||
*/
|
||||
export function findPath(
|
||||
nodes: PageTree.Node[],
|
||||
matcher: (node: PageTree.Node) => boolean,
|
||||
options: {
|
||||
includeSeparator?: boolean;
|
||||
} = {},
|
||||
): PageTree.Node[] | null {
|
||||
const { includeSeparator = true } = options;
|
||||
|
||||
function run(nodes: PageTree.Node[]): PageTree.Node[] | undefined {
|
||||
let separator: PageTree.Separator | undefined;
|
||||
|
||||
for (const node of nodes) {
|
||||
if (matcher(node)) {
|
||||
const items: PageTree.Node[] = [];
|
||||
if (separator) items.push(separator);
|
||||
items.push(node);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
if (node.type === 'separator' && includeSeparator) {
|
||||
separator = node;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node.type === 'folder') {
|
||||
const items =
|
||||
node.index && matcher(node.index) ? [node.index] : run(node.children);
|
||||
|
||||
if (items) {
|
||||
items.unshift(node);
|
||||
if (separator) items.unshift(separator);
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return run(nodes) ?? null;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { FileText, Hash, Search as SearchIcon } from 'lucide-react';
|
||||
import { ChevronRight, Hash, Search as SearchIcon } from 'lucide-react';
|
||||
import {
|
||||
type ComponentProps,
|
||||
createContext,
|
||||
@@ -184,7 +184,7 @@ export function SearchDialogContent({
|
||||
aria-describedby={undefined}
|
||||
{...props}
|
||||
className={cn(
|
||||
'fixed left-1/2 top-4 md:top-[calc(50%-250px)] z-50 w-[calc(100%-1rem)] max-w-screen-sm -translate-x-1/2 rounded-2xl border bg-fd-popover/80 backdrop-blur-xl text-fd-popover-foreground shadow-2xl shadow-black/50 overflow-hidden data-[state=closed]:animate-fd-dialog-out data-[state=open]:animate-fd-dialog-in',
|
||||
'fixed left-1/2 top-4 md:top-[calc(50%-250px)] z-50 w-[calc(100%-1rem)] max-w-screen-sm -translate-x-1/2 rounded-xl border bg-fd-popover/80 backdrop-blur-xl text-fd-popover-foreground shadow-2xl shadow-black/50 overflow-hidden data-[state=closed]:animate-fd-dialog-out data-[state=open]:animate-fd-dialog-in',
|
||||
'*:border-b *:has-[+:last-child[data-empty=true]]:border-b-0 *:data-[empty=true]:border-b-0 *:last:border-b-0',
|
||||
props.className,
|
||||
)}
|
||||
@@ -316,12 +316,8 @@ export function SearchDialogList({
|
||||
);
|
||||
}
|
||||
|
||||
const icons = {
|
||||
text: null,
|
||||
heading: <Hash className="size-4 shrink-0 text-fd-muted-foreground" />,
|
||||
page: (
|
||||
<FileText className="size-6 text-fd-muted-foreground bg-fd-muted border p-0.5 rounded-sm shadow-sm shrink-0" />
|
||||
),
|
||||
const icons: Record<string, ReactNode> = {
|
||||
heading: <Hash className="inline me-1 size-4 text-fd-muted-foreground" />,
|
||||
};
|
||||
|
||||
export function SearchDialogListItem({
|
||||
@@ -354,11 +350,7 @@ export function SearchDialogListItem({
|
||||
)}
|
||||
aria-selected={active}
|
||||
className={cn(
|
||||
'relative flex select-none flex-row items-center gap-2 p-2 text-start text-sm rounded-lg',
|
||||
item.type !== 'page' && 'ps-8',
|
||||
item.type === 'page' || item.type === 'heading'
|
||||
? 'font-medium'
|
||||
: 'text-fd-popover-foreground/80',
|
||||
'relative select-none px-2.5 py-1.5 text-start text-sm rounded-lg',
|
||||
active && 'bg-fd-accent text-fd-accent-foreground',
|
||||
className,
|
||||
)}
|
||||
@@ -367,14 +359,31 @@ export function SearchDialogListItem({
|
||||
>
|
||||
{children ?? (
|
||||
<>
|
||||
<div className="inline-flex items-center text-fd-muted-foreground text-xs empty:hidden">
|
||||
{item.breadcrumbs?.map((item, i) => (
|
||||
<Fragment key={i}>
|
||||
{i > 0 && <ChevronRight className="size-4" />}
|
||||
{item}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{item.type !== 'page' && (
|
||||
<div
|
||||
role="none"
|
||||
className="absolute start-4.5 inset-y-0 w-px bg-fd-border"
|
||||
className="absolute start-3 inset-y-0 w-px bg-fd-border"
|
||||
/>
|
||||
)}
|
||||
{icons[item.type]}
|
||||
<p className="min-w-0 truncate">
|
||||
<p
|
||||
className={cn(
|
||||
'min-w-0 truncate',
|
||||
item.type !== 'page' && 'ps-4',
|
||||
item.type === 'page' || item.type === 'heading'
|
||||
? 'font-medium'
|
||||
: 'text-fd-popover-foreground/80',
|
||||
)}
|
||||
>
|
||||
{icons[item.type]}
|
||||
{item.contentWithHighlights
|
||||
? render(item.contentWithHighlights)
|
||||
: item.content}
|
||||
@@ -474,7 +483,7 @@ function renderHighlights(highlights: HighlightedText<ReactNode>[]): ReactNode {
|
||||
return highlights.map((node, i) => {
|
||||
if (node.styles?.highlight) {
|
||||
return (
|
||||
<span key={i} className="text-fd-primary bg-fd-primary/10">
|
||||
<span key={i} className="text-fd-primary underline">
|
||||
{node.content}
|
||||
</span>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user