Skip to content

Commit 682ce9f

Browse files
rsbhclaude
andauthored
feat: support order in meta.json and folder redirect (#62)
* 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) <noreply@anthropic.com> * fix: restore isContentRoot with some() check Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: use contentConfig with explicit isContentRoot check Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1450e83 commit 682ce9f

3 files changed

Lines changed: 53 additions & 14 deletions

File tree

packages/chronicle/src/lib/source.ts

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -118,41 +118,60 @@ export function invalidate() {
118118
cachedNavMap = null;
119119
}
120120

121-
function getOrder(node: Node, orderMap: Map<string, number>): number | undefined {
122-
if (node.type === 'page') return orderMap.get(node.url);
123-
if (node.type === 'folder' && node.index) return orderMap.get(node.index.url);
121+
function getOrder(node: Node, pageOrderMap: Map<string, number>, folderOrderMap: Map<string, number>): number | undefined {
122+
if (node.type === 'page') return pageOrderMap.get(node.url);
123+
if (node.type === 'folder') {
124+
if (node.index) {
125+
const fromMeta = folderOrderMap.get(node.index.url);
126+
if (fromMeta !== undefined) return fromMeta;
127+
return pageOrderMap.get(node.index.url);
128+
}
129+
}
124130
return undefined;
125131
}
126132

127-
function sortNodes(nodes: Node[], orderMap: Map<string, number>): Node[] {
133+
function sortNodes(nodes: Node[], pageOrderMap: Map<string, number>, folderOrderMap: Map<string, number>): Node[] {
128134
return [...nodes]
129135
.map(n =>
130136
n.type === 'folder'
131-
? ({ ...n, children: sortNodes(n.children, orderMap) } as Folder)
137+
? ({ ...n, children: sortNodes(n.children, pageOrderMap, folderOrderMap) } as Folder)
132138
: n
133139
)
134140
.sort(
135141
(a, b) =>
136-
(getOrder(a, orderMap) ?? Number.MAX_SAFE_INTEGER) -
137-
(getOrder(b, orderMap) ?? Number.MAX_SAFE_INTEGER)
142+
(getOrder(a, pageOrderMap, folderOrderMap) ?? Number.MAX_SAFE_INTEGER) -
143+
(getOrder(b, pageOrderMap, folderOrderMap) ?? Number.MAX_SAFE_INTEGER)
138144
);
139145
}
140146

141-
function sortTreeByOrder(tree: Root, pages: { url: string; data: unknown }[]): Root {
142-
const orderMap = new Map<string, number>();
147+
function buildFolderOrderMap(metaFiles: { path: string; data: Record<string, unknown> }[]): Map<string, number> {
148+
const map = new Map<string, number>();
149+
for (const meta of metaFiles) {
150+
const order = meta.data.order as number | undefined;
151+
if (order === undefined) continue;
152+
const folderUrl = '/' + meta.path.replace(/\/meta\.json$/, '');
153+
map.set(folderUrl, order);
154+
}
155+
return map;
156+
}
157+
158+
function sortTreeByOrder(tree: Root, pages: { url: string; data: unknown }[], metaFiles: { path: string; data: Record<string, unknown> }[]): Root {
159+
const pageOrderMap = new Map<string, number>();
143160
for (const page of pages) {
144161
const d = page.data as Record<string, unknown>;
145162
const order = d.order as number | undefined;
146-
if (order !== undefined) orderMap.set(page.url, order);
147-
if (page.url === '/') orderMap.set('/', order ?? 0);
163+
if (order !== undefined) pageOrderMap.set(page.url, order);
164+
if (page.url === '/') pageOrderMap.set('/', order ?? 0);
148165
}
149-
return { ...tree, children: sortNodes(tree.children, orderMap) };
166+
const folderOrderMap = buildFolderOrderMap(metaFiles);
167+
return { ...tree, children: sortNodes(tree.children, pageOrderMap, folderOrderMap) };
150168
}
151169

152170
export async function getPageTree(): Promise<Root> {
153171
if (cachedTree) return cachedTree;
154172
const s = await getSource();
155-
cachedTree = sortTreeByOrder(s.pageTree as Root, s.getPages());
173+
const metaFiles = buildFiles().filter(f => f.type === 'meta') as { path: string; data: Record<string, unknown> }[];
174+
cachedTree = sortTreeByOrder(s.pageTree as Root, s.getPages(), metaFiles);
156175
return cachedTree;
157176
}
158177

packages/chronicle/src/pages/DocsPage.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ function getFirstPageUrl(nodes: Node[]): string | null {
1616
return null;
1717
}
1818

19+
function findFolderFirstPage(nodes: Node[], pathname: string): string | null {
20+
for (const node of nodes) {
21+
if (node.type === 'folder') {
22+
const folderUrl = node.index?.url;
23+
if (folderUrl === pathname) return getFirstPageUrl(node.children);
24+
const found = findFolderFirstPage(node.children, pathname);
25+
if (found) return found;
26+
}
27+
}
28+
return null;
29+
}
30+
1931
interface DocsPageProps {
2032
slug: string[];
2133
}
@@ -24,11 +36,18 @@ export function DocsPage({ slug }: DocsPageProps) {
2436
const { config, tree, page, isLoading, errorStatus } = usePageContext();
2537

2638
if (errorStatus === 404) {
27-
const isContentRoot = config.content?.some(c => slug.length === 1 && slug[0] === c.dir);
39+
const pathname = `/${slug.join('/')}`;
40+
const contentConfig = config.content?.find(c => c.dir === slug[0]);
41+
const isContentRoot = slug.length === 1 && slug[0] === contentConfig?.dir;
42+
if (contentConfig?.index_page) {
43+
return <Navigate to={`/${contentConfig.dir}/${contentConfig.index_page}`} replace />;
44+
}
2845
if (isContentRoot) {
2946
const firstUrl = getFirstPageUrl(tree.children);
3047
if (firstUrl) return <Navigate to={firstUrl} replace />;
3148
}
49+
const folderFirstUrl = findFolderFirstPage(tree.children, pathname);
50+
if (folderFirstUrl) return <Navigate to={folderFirstUrl} replace />;
3251
return <NotFound />;
3352
}
3453
if (errorStatus) return <NotFound />;

packages/chronicle/src/types/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ const contentEntrySchema = z.object({
8686
label: z.string().min(1),
8787
description: z.string().optional(),
8888
icon: z.string().optional(),
89+
index_page: z.string().optional(),
8990
})
9091

9192
// Variants map to Apsara Badge color prop.

0 commit comments

Comments
 (0)