From ac5f1eb01d5ffe377f9af3a8aa06b73c059030d5 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Wed, 13 May 2026 11:22:15 +0530 Subject: [PATCH 1/4] feat: add index_page config for content directories Set index_page in chronicle.yaml content entry to specify which page to redirect to when visiting a content root (e.g., /docs). Falls back to first page in tree when not set. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/chronicle/src/pages/DocsPage.tsx | 6 +++++- packages/chronicle/src/types/config.ts | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/chronicle/src/pages/DocsPage.tsx b/packages/chronicle/src/pages/DocsPage.tsx index 0492324a..414b6d08 100644 --- a/packages/chronicle/src/pages/DocsPage.tsx +++ b/packages/chronicle/src/pages/DocsPage.tsx @@ -24,7 +24,11 @@ export function DocsPage({ slug }: DocsPageProps) { const { config, tree, page, isLoading, errorStatus } = usePageContext(); if (errorStatus === 404) { - const isContentRoot = config.content?.some(c => slug.length === 1 && slug[0] === c.dir); + const contentEntry = config.content?.find(c => slug.length === 1 && slug[0] === c.dir); + const isContentRoot = !!contentEntry; + if (contentEntry?.index_page) { + return ; + } if (isContentRoot) { const firstUrl = getFirstPageUrl(tree.children); if (firstUrl) return ; diff --git a/packages/chronicle/src/types/config.ts b/packages/chronicle/src/types/config.ts index b99628e7..23272462 100644 --- a/packages/chronicle/src/types/config.ts +++ b/packages/chronicle/src/types/config.ts @@ -86,6 +86,7 @@ const contentEntrySchema = z.object({ label: z.string().min(1), description: z.string().optional(), icon: z.string().optional(), + index_page: z.string().optional(), }) // Variants map to Apsara Badge color prop. From ffcdf0f9bfd31b3056ba73549f7ed2606c07d642 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Wed, 13 May 2026 11:35:31 +0530 Subject: [PATCH 2/4] fix: restore isContentRoot with some() check Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/chronicle/src/pages/DocsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/chronicle/src/pages/DocsPage.tsx b/packages/chronicle/src/pages/DocsPage.tsx index 414b6d08..864bc341 100644 --- a/packages/chronicle/src/pages/DocsPage.tsx +++ b/packages/chronicle/src/pages/DocsPage.tsx @@ -24,8 +24,8 @@ export function DocsPage({ slug }: DocsPageProps) { const { config, tree, page, isLoading, errorStatus } = usePageContext(); if (errorStatus === 404) { + const isContentRoot = config.content?.some(c => slug.length === 1 && slug[0] === c.dir); const contentEntry = config.content?.find(c => slug.length === 1 && slug[0] === c.dir); - const isContentRoot = !!contentEntry; if (contentEntry?.index_page) { return ; } From 25828c9937860684856abcda7cb67dcc520bd05f Mon Sep 17 00:00:00 2001 From: Rishabh Date: Wed, 13 May 2026 11:38:45 +0530 Subject: [PATCH 3/4] refactor: use contentConfig with explicit isContentRoot check Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/chronicle/src/pages/DocsPage.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/chronicle/src/pages/DocsPage.tsx b/packages/chronicle/src/pages/DocsPage.tsx index 864bc341..56b88a3f 100644 --- a/packages/chronicle/src/pages/DocsPage.tsx +++ b/packages/chronicle/src/pages/DocsPage.tsx @@ -24,10 +24,10 @@ export function DocsPage({ slug }: DocsPageProps) { const { config, tree, page, isLoading, errorStatus } = usePageContext(); if (errorStatus === 404) { - const isContentRoot = config.content?.some(c => slug.length === 1 && slug[0] === c.dir); - const contentEntry = config.content?.find(c => slug.length === 1 && slug[0] === c.dir); - if (contentEntry?.index_page) { - return ; + const contentConfig = config.content?.find(c => c.dir === slug[0]); + const isContentRoot = slug.length === 1 && slug[0] === contentConfig?.dir; + if (contentConfig?.index_page) { + return ; } if (isContentRoot) { const firstUrl = getFirstPageUrl(tree.children); From 82bf974c7f72107f4e0aa4f15ab3c4c10edb6f26 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Wed, 13 May 2026 12:28:51 +0530 Subject: [PATCH 4/4] feat: support order in meta.json and folder redirect to first child - meta.json order field sorts folders in sidebar - Visiting a folder URL redirects to its first sorted child page - Folder order from meta.json takes priority over index page frontmatter Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/chronicle/src/lib/source.ts | 45 ++++++++++++++++------- packages/chronicle/src/pages/DocsPage.tsx | 15 ++++++++ 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/packages/chronicle/src/lib/source.ts b/packages/chronicle/src/lib/source.ts index 581e3e44..5078a2c9 100644 --- a/packages/chronicle/src/lib/source.ts +++ b/packages/chronicle/src/lib/source.ts @@ -118,41 +118,60 @@ export function invalidate() { cachedNavMap = null; } -function getOrder(node: Node, orderMap: Map): number | undefined { - if (node.type === 'page') return orderMap.get(node.url); - if (node.type === 'folder' && node.index) return orderMap.get(node.index.url); +function getOrder(node: Node, pageOrderMap: Map, folderOrderMap: Map): number | undefined { + if (node.type === 'page') return pageOrderMap.get(node.url); + if (node.type === 'folder') { + if (node.index) { + const fromMeta = folderOrderMap.get(node.index.url); + if (fromMeta !== undefined) return fromMeta; + return pageOrderMap.get(node.index.url); + } + } return undefined; } -function sortNodes(nodes: Node[], orderMap: Map): Node[] { +function sortNodes(nodes: Node[], pageOrderMap: Map, folderOrderMap: Map): Node[] { return [...nodes] .map(n => n.type === 'folder' - ? ({ ...n, children: sortNodes(n.children, orderMap) } as Folder) + ? ({ ...n, children: sortNodes(n.children, pageOrderMap, folderOrderMap) } as Folder) : n ) .sort( (a, b) => - (getOrder(a, orderMap) ?? Number.MAX_SAFE_INTEGER) - - (getOrder(b, orderMap) ?? Number.MAX_SAFE_INTEGER) + (getOrder(a, pageOrderMap, folderOrderMap) ?? Number.MAX_SAFE_INTEGER) - + (getOrder(b, pageOrderMap, folderOrderMap) ?? Number.MAX_SAFE_INTEGER) ); } -function sortTreeByOrder(tree: Root, pages: { url: string; data: unknown }[]): Root { - const orderMap = new Map(); +function buildFolderOrderMap(metaFiles: { path: string; data: Record }[]): Map { + const map = new Map(); + for (const meta of metaFiles) { + const order = meta.data.order as number | undefined; + if (order === undefined) continue; + const folderUrl = '/' + meta.path.replace(/\/meta\.json$/, ''); + map.set(folderUrl, order); + } + return map; +} + +function sortTreeByOrder(tree: Root, pages: { url: string; data: unknown }[], metaFiles: { path: string; data: Record }[]): Root { + const pageOrderMap = new Map(); for (const page of pages) { const d = page.data as Record; const order = d.order as number | undefined; - if (order !== undefined) orderMap.set(page.url, order); - if (page.url === '/') orderMap.set('/', order ?? 0); + if (order !== undefined) pageOrderMap.set(page.url, order); + if (page.url === '/') pageOrderMap.set('/', order ?? 0); } - return { ...tree, children: sortNodes(tree.children, orderMap) }; + const folderOrderMap = buildFolderOrderMap(metaFiles); + return { ...tree, children: sortNodes(tree.children, pageOrderMap, folderOrderMap) }; } export async function getPageTree(): Promise { if (cachedTree) return cachedTree; const s = await getSource(); - cachedTree = sortTreeByOrder(s.pageTree as Root, s.getPages()); + const metaFiles = buildFiles().filter(f => f.type === 'meta') as { path: string; data: Record }[]; + cachedTree = sortTreeByOrder(s.pageTree as Root, s.getPages(), metaFiles); return cachedTree; } diff --git a/packages/chronicle/src/pages/DocsPage.tsx b/packages/chronicle/src/pages/DocsPage.tsx index 56b88a3f..768e3e0d 100644 --- a/packages/chronicle/src/pages/DocsPage.tsx +++ b/packages/chronicle/src/pages/DocsPage.tsx @@ -16,6 +16,18 @@ function getFirstPageUrl(nodes: Node[]): string | null { return null; } +function findFolderFirstPage(nodes: Node[], pathname: string): string | null { + for (const node of nodes) { + if (node.type === 'folder') { + const folderUrl = node.index?.url; + if (folderUrl === pathname) return getFirstPageUrl(node.children); + const found = findFolderFirstPage(node.children, pathname); + if (found) return found; + } + } + return null; +} + interface DocsPageProps { slug: string[]; } @@ -24,6 +36,7 @@ export function DocsPage({ slug }: DocsPageProps) { const { config, tree, page, isLoading, errorStatus } = usePageContext(); if (errorStatus === 404) { + const pathname = `/${slug.join('/')}`; const contentConfig = config.content?.find(c => c.dir === slug[0]); const isContentRoot = slug.length === 1 && slug[0] === contentConfig?.dir; if (contentConfig?.index_page) { @@ -33,6 +46,8 @@ export function DocsPage({ slug }: DocsPageProps) { const firstUrl = getFirstPageUrl(tree.children); if (firstUrl) return ; } + const folderFirstUrl = findFolderFirstPage(tree.children, pathname); + if (folderFirstUrl) return ; return ; } if (errorStatus) return ;