-
Notifications
You must be signed in to change notification settings - Fork 1
feat: support order in meta.json and folder redirect #62
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ac5f1eb
ffcdf0f
25828c9
82bf974
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
rohilsurana marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const found = findFolderFirstPage(node.children, pathname); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (found) return found; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+19
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Confirm fumadocs Folder type has no intrinsic url/path field independent of `index`.
fd -i 'page-tree' node_modules/fumadocs-core 2>/dev/null
rg -nP --type=ts -C5 '(interface|type)\s+Folder\b' node_modules/fumadocs-core 2>/dev/null | head -120Repository: raystack/chronicle Length of output: 44 🏁 Script executed: cat -n packages/chronicle/src/pages/DocsPage.tsx | head -60Repository: raystack/chronicle Length of output: 2553 🏁 Script executed: rg -n "type Node|interface Node|type Folder|interface Folder" packages/chronicle --type=ts --type=tsx -A 3Repository: raystack/chronicle Length of output: 90 🏁 Script executed: rg -n "findFolderFirstPage" packages/chronicle --type=ts --type=tsx -B 5 -A 5Repository: raystack/chronicle Length of output: 90 🏁 Script executed: rg -n "type Node|interface Node" packages/chronicle --type=ts -A 5Repository: raystack/chronicle Length of output: 44 🏁 Script executed: cat package.json | grep -A 5 -B 5 fumadocsRepository: raystack/chronicle Length of output: 44 🏁 Script executed: # Try to find Node definition in node_modules if available
find node_modules -name "page-tree*" -type f 2>/dev/null | head -20Repository: raystack/chronicle Length of output: 44 🏁 Script executed: cat packages/chronicle/package.json | grep -E "name|fumadocs|dependencies" -A 30Repository: raystack/chronicle Length of output: 2271 🏁 Script executed: cat package.json | grep -E "fumadocs" -B 2 -A 2Repository: raystack/chronicle Length of output: 44 🏁 Script executed: # Examine the logic: how pathname is constructed and used
rg -n "pathname|slug" packages/chronicle/src/pages/DocsPage.tsx -B 2 -A 2Repository: raystack/chronicle Length of output: 1493 🌐 Web query:
💡 Result: In fumadocs-core, the page-tree is a hierarchical structure for navigation, defined by the Node type which is a union: type Node = Item | Separator | Folder [1][2][3]. Folders represent collapsible sections containing child nodes. Folder interface [1][2]: interface Folder { $id?: string; $ref?: { metaFile?: string }; type: 'folder'; name: ReactNode; description?: ReactNode; root?: boolean; // Marks as root folder, affects navigation visibility [4][3] defaultOpen?: boolean; collapsible?: boolean; index?: Item; // Index page for the folder (not used for root folders) [2] icon?: ReactNode; children: Node[]; // Recursive child nodes } Item (page) interface, relevant for index and leaf pages [1]: interface Item { $id?: string; $ref?: { file: string }; type: 'page'; name: ReactNode; url: string; // URL/path of the page external?: boolean; description?: ReactNode; icon?: ReactNode; } The structure is built from content directories and meta.json files, where folders can define pages array for custom ordering/exclusions [4]. URLs derive from file paths/slugs [4][5]. Root is typically { type: 'root', children: Node[] } or array of root Folders [6]. Citations:
🏁 Script executed: # Search for other usages of Node/Folder in the codebase to understand structure
rg -n "node\." packages/chronicle/src/pages/DocsPage.tsx --type=ts -B 1 -A 1Repository: raystack/chronicle Length of output: 560 🏁 Script executed: # Look for page-tree usage elsewhere in the codebase
rg -n "page-tree|Node\[" packages/chronicle/src --type=ts -C 3Repository: raystack/chronicle Length of output: 8376
The match condition is The PR targets the case where visiting A path-aware traversal that constructs the folder's URL from accumulated path segments is needed to match and redirect folders without index pages. 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface DocsPageProps { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| slug: string[]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -24,11 +36,18 @@ 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 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) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return <Navigate to={`/${contentConfig.dir}/${contentConfig.index_page}`} replace />; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
rohilsurana marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isContentRoot) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const firstUrl = getFirstPageUrl(tree.children); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (firstUrl) return <Navigate to={firstUrl} replace />; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const folderFirstUrl = findFolderFirstPage(tree.children, pathname); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (folderFirstUrl) return <Navigate to={folderFirstUrl} replace />; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
38
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The There is also a redirect-loop risk: if 🐛 Proposed fix — gate `index_page` on `isContentRoot` 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) {
- return <Navigate to={`/${contentConfig.dir}/${contentConfig.index_page}`} replace />;
- }
if (isContentRoot) {
+ if (contentConfig?.index_page) {
+ return <Navigate to={`/${contentConfig.dir}/${contentConfig.index_page}`} replace />;
+ }
const firstUrl = getFirstPageUrl(tree.children);
if (firstUrl) return <Navigate to={firstUrl} replace />;
}
const folderFirstUrl = findFolderFirstPage(tree.children, pathname);
if (folderFirstUrl) return <Navigate to={folderFirstUrl} replace />;
return <NotFound />;
}Consider also validating that the 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return <NotFound />; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (errorStatus) return <NotFound />; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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(), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win Naming inconsistency: prefer The rest of ♻️ Proposed rename- index_page: z.string().optional(),
+ indexPage: z.string().optional(),Update the corresponding read site in 🤖 Prompt for AI Agents |
||
| }) | ||
|
|
||
| // Variants map to Apsara Badge color prop. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: raystack/chronicle
Length of output: 44
🏁 Script executed:
# First, check the full source.ts file to understand the context wc -l packages/chronicle/src/lib/source.tsRepository: raystack/chronicle
Length of output: 103
🏁 Script executed:
Repository: raystack/chronicle
Length of output: 1712
🏁 Script executed:
Repository: raystack/chronicle
Length of output: 44
🏁 Script executed:
Repository: raystack/chronicle
Length of output: 44
🏁 Script executed:
Repository: raystack/chronicle
Length of output: 1709
🏁 Script executed:
Repository: raystack/chronicle
Length of output: 44
🏁 Script executed:
Repository: raystack/chronicle
Length of output: 44
🏁 Script executed:
Repository: raystack/chronicle
Length of output: 44
🏁 Script executed:
Repository: raystack/chronicle
Length of output: 44
🏁 Script executed:
Repository: raystack/chronicle
Length of output: 44
🏁 Script executed:
Repository: raystack/chronicle
Length of output: 93
🏁 Script executed:
Repository: raystack/chronicle
Length of output: 44
🌐 Web query:
fumadocs-core Folder type definition page-tree💡 Result:
In fumadocs-core, the “folder” node in the Page Tree is defined as a TypeScript interface with the node type discriminator type: 'folder'. A Folder node contains name, children, and optional folder-specific metadata like index, description, and collapsible/root/defaultOpen/icon settings. [1][2] Example shape (from the “Page Tree API” docs): interface Folder { $id?: string; $ref?: { metaFile?: string }; type: 'folder'; name: ReactNode; description?: ReactNode; root?: boolean; defaultOpen?: boolean; collapsible?: boolean; index?: Item; icon?: ReactNode; children: Node[]; } [1] How this maps to folder behavior: - A page tree is described as navigation links that includes separators and folders. [2] - A Folder node is explicitly “a node containing multiple children nodes.” [2] - Folder-specific “meta.json” options (title/icon/root/defaultOpen/collapsible/description/pages) are part of the meta data used to control folder behavior in the page tree. [3][4] Related utility types/docs: - The “Page Tree Utils” docs show that fumadocs-core exports helpers operating on the page tree root type Root from fumadocs-core/page-tree. [5] Citations: [1] https://www.mintlify.com/fuma-nama/fumadocs/api/core/page-tree [2] https://www.fumadocs.dev/docs/headless/page-tree [5] https://www.fumadocs.dev/docs/headless/utils/page-tree [3] https://www.fumadocs.dev/docs/headless/page-conventions [4] https://www.mintlify.com/fuma-nama/fumadocs/api/core/source
Citations:
Folder
orderinmeta.jsonis silently ignored when the folder has no index page.getOrderonly resolves a folder's order vianode.index?.url, butbuildFolderOrderMapkeys entries by the folder's URL derived frommeta.path. For folders without anindex.mdx/readme.mdx,node.indexisundefined, so the meta-based order never gets consulted and the folder falls into the "no order" bucket (sorted last). The PR's stated goal of usingorderinmeta.jsonto control folder sorting therefore only holds for folders that happen to have an index page.The
Foldertype from fumadocs-core has no standalone path/url property—onlyindexandchildren. To fix this, the comparator needs the folder's own URL (built and passed down during tree traversal) rather than relying onnode.index.url.🤖 Prompt for AI Agents