From a7222beb9c15486be863ccdde4910a4e7e52ff89 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Mon, 18 May 2026 14:25:40 +0530 Subject: [PATCH 1/4] feat: add section label to search results Derive section from URL's first segment matched against content/API config entries. Returns label (e.g. "Docs", "Petstore") per result. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/chronicle/src/server/api/search.ts | 24 +++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/chronicle/src/server/api/search.ts b/packages/chronicle/src/server/api/search.ts index d790bec..b4e7288 100644 --- a/packages/chronicle/src/server/api/search.ts +++ b/packages/chronicle/src/server/api/search.ts @@ -182,6 +182,9 @@ export default defineHandler(async event => { const db = useDatabase(); const key = versionKey(ctx); + const config = loadConfig(); + const sectionMap = buildSectionMap(config, ctx); + if (!query) { const result = await db.sql`SELECT id, url, title, type FROM search_docs WHERE version = ${key} AND type = 'page' @@ -191,6 +194,7 @@ export default defineHandler(async event => { url: r.url, type: r.type, content: r.title, + section: getSectionLabel(r.url as string, sectionMap), }))); } @@ -214,6 +218,26 @@ export default defineHandler(async event => { content: r.title, match, snippet, + section: getSectionLabel(r.url as string, sectionMap), }; })); }); + +function buildSectionMap(config: ReturnType, ctx: VersionContext): Map { + const map = new Map(); + for (const entry of config.content ?? []) { + map.set(entry.dir, entry.label ?? entry.dir); + } + for (const api of config.api ?? []) { + const basePath = (api.basePath ?? '/apis').replace(/^\//, ''); + map.set(basePath, api.name); + } + return map; +} + +function getSectionLabel(url: string, sectionMap: Map): string | null { + const segments = url.replace(/^\//, '').split('/'); + const first = segments[0]; + if (!first) return null; + return sectionMap.get(first) ?? null; +} From c982574e9ba1bdddc6356a776e01570241d163f4 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Mon, 18 May 2026 14:26:22 +0530 Subject: [PATCH 2/4] feat: show section badge in search results Display content folder label (e.g. "Docs", "Petstore") as a badge on the right side of each search result. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/chronicle/src/components/ui/search.module.css | 6 ++++++ packages/chronicle/src/components/ui/search.tsx | 5 ++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/chronicle/src/components/ui/search.module.css b/packages/chronicle/src/components/ui/search.module.css index c82985d..66e2d4d 100644 --- a/packages/chronicle/src/components/ui/search.module.css +++ b/packages/chronicle/src/components/ui/search.module.css @@ -41,6 +41,12 @@ display: flex; align-items: center; gap: 12px; + flex: 1; +} + +.sectionBadge { + margin-left: auto; + flex-shrink: 0; } .resultText { diff --git a/packages/chronicle/src/components/ui/search.tsx b/packages/chronicle/src/components/ui/search.tsx index 4eee2fc..4c75730 100644 --- a/packages/chronicle/src/components/ui/search.tsx +++ b/packages/chronicle/src/components/ui/search.tsx @@ -3,7 +3,7 @@ import { HashtagIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline'; -import { Command, IconButton, Text } from '@raystack/apsara'; +import { Badge, Command, IconButton, Text } from '@raystack/apsara'; import { debounce } from 'lodash-es'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router'; @@ -18,6 +18,7 @@ interface SearchResult { content: string; match?: 'title' | 'heading' | 'body'; snippet?: string; + section?: string; } interface SearchProps { @@ -157,6 +158,7 @@ export function Search({ classNames }: SearchProps) { html={stripMethod(result.content)} /> + {result.section && {result.section}} ))} @@ -187,6 +189,7 @@ export function Search({ classNames }: SearchProps) { )} + {result.section && {result.section}} ))} From f8ff37d0de2d4f220131af6fc2d3230c95e054c6 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Mon, 18 May 2026 14:37:57 +0530 Subject: [PATCH 3/4] refactor: store section in DB at index time instead of per-query Compute section label once during indexing and store in search_docs table. Query handler reads it directly from DB. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/chronicle/src/server/api/search.ts | 26 +++++++++++---------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/chronicle/src/server/api/search.ts b/packages/chronicle/src/server/api/search.ts index b4e7288..ea340e8 100644 --- a/packages/chronicle/src/server/api/search.ts +++ b/packages/chronicle/src/server/api/search.ts @@ -14,6 +14,7 @@ interface SearchDocument { headings: string; body: string; type: 'page' | 'api'; + section: string; } import fs from 'node:fs/promises'; @@ -61,7 +62,8 @@ async function buildIndex(ctx: VersionContext, key: string) { headings TEXT NOT NULL, body TEXT NOT NULL, type TEXT NOT NULL, - version TEXT NOT NULL + version TEXT NOT NULL, + section TEXT NOT NULL DEFAULT '' )`); await db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS search_fts USING fts5( @@ -74,8 +76,8 @@ async function buildIndex(ctx: VersionContext, key: string) { const docs = await buildDocs(ctx); for (const doc of docs) { - await db.sql`INSERT INTO search_docs (id, url, title, headings, body, type, version) - VALUES (${doc.id}, ${doc.url}, ${doc.title}, ${doc.headings}, ${doc.body}, ${doc.type}, ${key})`; + await db.sql`INSERT INTO search_docs (id, url, title, headings, body, type, version, section) + VALUES (${doc.id}, ${doc.url}, ${doc.title}, ${doc.headings}, ${doc.body}, ${doc.type}, ${key}, ${doc.section})`; } await db.sql`INSERT INTO search_fts (rowid, title, headings, body) @@ -86,6 +88,8 @@ async function buildIndex(ctx: VersionContext, key: string) { async function buildDocs(ctx: VersionContext): Promise { const docs: SearchDocument[] = []; + const config = loadConfig(); + const sectionMap = buildSectionMap(config); const pages = await getPagesForVersion(ctx); for (const p of pages) { @@ -98,10 +102,10 @@ async function buildDocs(ctx: VersionContext): Promise { headings, body: [fm.description ?? '', body].join(' '), type: 'page', + section: getSectionLabel(p.url, sectionMap) ?? '', }); } - const config = loadConfig(); const apiConfigs = getApiConfigsForVersion(config, ctx.dir); if (apiConfigs.length) { const specs = await loadApiSpecs(apiConfigs); @@ -122,6 +126,7 @@ async function buildDocs(ctx: VersionContext): Promise { headings: op.summary ?? opId, body: [op.description ?? '', pathStr, method.toUpperCase()].join(' '), type: 'api', + section: spec.name, }); } } @@ -182,11 +187,8 @@ export default defineHandler(async event => { const db = useDatabase(); const key = versionKey(ctx); - const config = loadConfig(); - const sectionMap = buildSectionMap(config, ctx); - if (!query) { - const result = await db.sql`SELECT id, url, title, type FROM search_docs + const result = await db.sql`SELECT id, url, title, type, section FROM search_docs WHERE version = ${key} AND type = 'page' LIMIT 8`; return Response.json((result.rows ?? []).map(r => ({ @@ -194,12 +196,12 @@ export default defineHandler(async event => { url: r.url, type: r.type, content: r.title, - section: getSectionLabel(r.url as string, sectionMap), + section: r.section || null, }))); } const searchTerm = query.split(/\s+/).map(t => `"${t}"*`).join(' '); - const result = await db.sql`SELECT s.id, s.url, s.title, s.headings, s.body, s.type, + const result = await db.sql`SELECT s.id, s.url, s.title, s.headings, s.body, s.type, s.section, bm25(search_fts, 10.0, 5.0, 1.0) AS score FROM search_fts f JOIN search_docs s ON s.rowid = f.rowid @@ -218,12 +220,12 @@ export default defineHandler(async event => { content: r.title, match, snippet, - section: getSectionLabel(r.url as string, sectionMap), + section: r.section || null, }; })); }); -function buildSectionMap(config: ReturnType, ctx: VersionContext): Map { +function buildSectionMap(config: ReturnType): Map { const map = new Map(); for (const entry of config.content ?? []) { map.set(entry.dir, entry.label ?? entry.dir); From 5e3a65f7602805b5c1746fd3d9a114c66cb01e9b Mon Sep 17 00:00:00 2001 From: Rishabh Date: Mon, 18 May 2026 14:39:35 +0530 Subject: [PATCH 4/4] refactor: remove buildSectionMap/getSectionLabel, derive section inline Section for docs pages derived from URL + config.content inline. Section for API pages uses spec.name directly. No helper functions needed. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/chronicle/src/server/api/search.ts | 24 ++++----------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/packages/chronicle/src/server/api/search.ts b/packages/chronicle/src/server/api/search.ts index ea340e8..ac07a74 100644 --- a/packages/chronicle/src/server/api/search.ts +++ b/packages/chronicle/src/server/api/search.ts @@ -89,12 +89,14 @@ async function buildIndex(ctx: VersionContext, key: string) { async function buildDocs(ctx: VersionContext): Promise { const docs: SearchDocument[] = []; const config = loadConfig(); - const sectionMap = buildSectionMap(config); + const contentEntries = config.content ?? []; const pages = await getPagesForVersion(ctx); for (const p of pages) { const fm = extractFrontmatter(p); const { headings, body } = await getPageSearchContent(p); + const dir = p.url.replace(/^\//, '').split('/')[0]; + const entry = contentEntries.find(c => c.dir === dir); docs.push({ id: p.url, url: p.url, @@ -102,7 +104,7 @@ async function buildDocs(ctx: VersionContext): Promise { headings, body: [fm.description ?? '', body].join(' '), type: 'page', - section: getSectionLabel(p.url, sectionMap) ?? '', + section: entry?.label ?? dir ?? '', }); } @@ -225,21 +227,3 @@ export default defineHandler(async event => { })); }); -function buildSectionMap(config: ReturnType): Map { - const map = new Map(); - for (const entry of config.content ?? []) { - map.set(entry.dir, entry.label ?? entry.dir); - } - for (const api of config.api ?? []) { - const basePath = (api.basePath ?? '/apis').replace(/^\//, ''); - map.set(basePath, api.name); - } - return map; -} - -function getSectionLabel(url: string, sectionMap: Map): string | null { - const segments = url.replace(/^\//, '').split('/'); - const first = segments[0]; - if (!first) return null; - return sectionMap.get(first) ?? null; -}