feat(content): add nested children to NavItem for hierarchical nav

Extend NavItem struct with children field to support nested navigation.
discover_nav() now populates children for section items by collecting
section pages via Section::collect_items() and mapping them to child
NavItem entries. Children are sorted by weight then alphabetically.
This commit is contained in:
Timothy DeHerrera
2026-02-01 09:00:04 -07:00
parent 4cd0c86688
commit b3a540651a

View File

@@ -28,6 +28,9 @@ pub struct NavItem {
pub path: String, pub path: String,
/// Sort order (lower = first, default 50) /// Sort order (lower = first, default 50)
pub weight: i64, pub weight: i64,
/// Child navigation items (section pages)
#[serde(skip_serializing_if = "Vec::is_empty")]
pub children: Vec<NavItem>,
} }
/// Parsed frontmatter from a content file. /// Parsed frontmatter from a content file.
@@ -206,6 +209,7 @@ pub fn discover_nav(content_dir: &Path) -> Result<Vec<NavItem>> {
.unwrap_or(content.frontmatter.title), .unwrap_or(content.frontmatter.title),
path: format!("/{}.html", slug), path: format!("/{}.html", slug),
weight: content.frontmatter.weight.unwrap_or(50), weight: content.frontmatter.weight.unwrap_or(50),
children: Vec::new(),
}); });
} }
} }
@@ -218,6 +222,34 @@ pub fn discover_nav(content_dir: &Path) -> Result<Vec<NavItem>> {
.file_name() .file_name()
.and_then(|n| n.to_str()) .and_then(|n| n.to_str())
.unwrap_or("section"); .unwrap_or("section");
// Collect section items as child nav items
let section = Section {
index: content.clone(),
name: dir_name.to_string(),
section_type: content
.frontmatter
.section_type
.clone()
.unwrap_or_else(|| dir_name.to_string()),
path: path.clone(),
};
let mut children: Vec<NavItem> = section
.collect_items()?
.into_iter()
.map(|item| NavItem {
label: item.frontmatter.nav_label.unwrap_or(item.frontmatter.title),
path: format!("/{}/{}.html", dir_name, item.slug),
weight: item.frontmatter.weight.unwrap_or(50),
children: Vec::new(),
})
.collect();
// Sort children by weight, then alphabetically
children
.sort_by(|a, b| a.weight.cmp(&b.weight).then_with(|| a.label.cmp(&b.label)));
nav_items.push(NavItem { nav_items.push(NavItem {
label: content label: content
.frontmatter .frontmatter
@@ -225,6 +257,7 @@ pub fn discover_nav(content_dir: &Path) -> Result<Vec<NavItem>> {
.unwrap_or(content.frontmatter.title), .unwrap_or(content.frontmatter.title),
path: format!("/{}/index.html", dir_name), path: format!("/{}/index.html", dir_name),
weight: content.frontmatter.weight.unwrap_or(50), weight: content.frontmatter.weight.unwrap_or(50),
children,
}); });
} }
} }
@@ -507,6 +540,39 @@ mod tests {
assert_eq!(nav[0].label, "About"); // Uses nav_label, not title assert_eq!(nav[0].label, "About"); // Uses nav_label, not title
} }
#[test]
fn test_discover_nav_populates_section_children() {
let dir = create_test_dir();
let content_dir = dir.path();
// Create section directory with _index.md and child pages
let features_dir = content_dir.join("features");
fs::create_dir(&features_dir).expect("failed to create features dir");
write_frontmatter(&features_dir.join("_index.md"), "Features", Some(10), None);
write_frontmatter(
&features_dir.join("templates.md"),
"Templates",
Some(20),
None,
);
write_frontmatter(
&features_dir.join("highlights.md"),
"Syntax Highlighting",
Some(10),
None,
);
let nav = discover_nav(content_dir).expect("discover_nav failed");
assert_eq!(nav.len(), 1);
assert_eq!(nav[0].label, "Features");
assert_eq!(nav[0].children.len(), 2);
// Children should be sorted by weight
assert_eq!(nav[0].children[0].label, "Syntax Highlighting"); // weight 10
assert_eq!(nav[0].children[0].path, "/features/highlights.html");
assert_eq!(nav[0].children[1].label, "Templates"); // weight 20
assert_eq!(nav[0].children[1].path, "/features/templates.html");
}
// ========================================================================= // =========================================================================
// discover_sections tests // discover_sections tests
// ========================================================================= // =========================================================================