From a5c56c2b2fd70974d68c39cefcc4d27dd6c380d4 Mon Sep 17 00:00:00 2001 From: Timothy DeHerrera Date: Sat, 31 Jan 2026 13:56:16 -0700 Subject: [PATCH] feat(templates,main): wire dynamic nav through pipeline Update all template functions to accept nav parameter and iterate over discovered NavItem slice instead of hardcoded links. Refactor main.rs: - Call discover_nav() early in run() - Thread nav to all template renders - Replace hardcoded page list with dynamic discovery Navigation is now fully driven by filesystem structure. --- src/main.rs | 58 ++++++++++++++++++++++++++++++++++-------------- src/templates.rs | 26 +++++++++++++++++----- 2 files changed, 62 insertions(+), 22 deletions(-) diff --git a/src/main.rs b/src/main.rs index f0c283f..9444837 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,7 @@ mod mermaid; mod render; mod templates; -use crate::content::{Content, ContentKind}; +use crate::content::{discover_nav, Content, ContentKind, NavItem}; use crate::error::{Error, Result}; use std::fs; use std::path::Path; @@ -38,21 +38,24 @@ fn run() -> Result<()> { // Load site configuration let config = config::SiteConfig::load(config_path)?; + // Discover navigation from filesystem + let nav = discover_nav(content_dir)?; + // 0. Copy static assets copy_static_assets(static_dir, output_dir)?; // 1. Process blog posts - let mut posts = process_blog_posts(content_dir, output_dir, &config)?; + let mut posts = process_blog_posts(content_dir, output_dir, &config, &nav)?; // 2. Generate blog index (sorted by date, newest first) posts.sort_by(|a, b| b.frontmatter.date.cmp(&a.frontmatter.date)); - generate_blog_index(output_dir, &posts, &config)?; + generate_blog_index(output_dir, &posts, &config, &nav)?; // 2b. Generate Atom feed generate_feed(output_dir, &posts, &config)?; - // 3. Process standalone pages (about, collab) - process_pages(content_dir, output_dir, &config)?; + // 3. Process standalone pages (discovered dynamically) + process_pages(content_dir, output_dir, &config, &nav)?; // 4. Process projects and generate project index let mut projects = process_projects(content_dir)?; @@ -62,10 +65,10 @@ fn run() -> Result<()> { .unwrap_or(99) .cmp(&b.frontmatter.weight.unwrap_or(99)) }); - generate_projects_index(output_dir, &projects, &config)?; + generate_projects_index(output_dir, &projects, &config, &nav)?; // 5. Generate homepage - generate_homepage(content_dir, output_dir, &config)?; + generate_homepage(content_dir, output_dir, &config, &nav)?; eprintln!("done!"); Ok(()) @@ -76,6 +79,7 @@ fn process_blog_posts( content_dir: &Path, output_dir: &Path, config: &config::SiteConfig, + nav: &[NavItem], ) -> Result> { let blog_dir = content_dir.join("blog"); let mut posts = Vec::new(); @@ -94,7 +98,8 @@ fn process_blog_posts( let content = Content::from_path(path, ContentKind::Post)?; let html_body = render::markdown_to_html(&content.body); let page_path = format!("/{}", content.output_path(content_dir).display()); - let page = templates::render_post(&content.frontmatter, &html_body, &page_path, config); + let page = + templates::render_post(&content.frontmatter, &html_body, &page_path, config, nav); write_output(output_dir, content_dir, &content, page.into_string())?; posts.push(content); @@ -108,11 +113,12 @@ fn generate_blog_index( output_dir: &Path, posts: &[Content], config: &config::SiteConfig, + nav: &[NavItem], ) -> Result<()> { let out_path = output_dir.join("blog/index.html"); eprintln!("generating: {}", out_path.display()); - let page = templates::render_blog_index("Blog", posts, "/blog/index.html", config); + let page = templates::render_blog_index("Blog", posts, "/blog/index.html", config, nav); fs::create_dir_all(out_path.parent().unwrap()).map_err(|e| Error::CreateDir { path: out_path.parent().unwrap().to_path_buf(), @@ -144,17 +150,32 @@ fn generate_feed(output_dir: &Path, posts: &[Content], config: &config::SiteConf Ok(()) } -/// Process standalone pages in content/ (about.md, collab.md) -fn process_pages(content_dir: &Path, output_dir: &Path, config: &config::SiteConfig) -> Result<()> { - for name in ["about.md", "collab.md"] { - let path = content_dir.join(name); - if path.exists() { +/// Process standalone pages in content/ (top-level .md files excluding _index.md) +fn process_pages( + content_dir: &Path, + output_dir: &Path, + config: &config::SiteConfig, + nav: &[NavItem], +) -> Result<()> { + // Dynamically discover top-level .md files (except _index.md) + let entries = fs::read_dir(content_dir).map_err(|e| Error::ReadFile { + path: content_dir.to_path_buf(), + source: e, + })?; + + for entry in entries.filter_map(|e| e.ok()) { + let path = entry.path(); + if path.is_file() + && path.extension().is_some_and(|ext| ext == "md") + && path.file_name().is_some_and(|n| n != "_index.md") + { eprintln!("processing: {}", path.display()); let content = Content::from_path(&path, ContentKind::Page)?; let html_body = render::markdown_to_html(&content.body); let page_path = format!("/{}", content.output_path(content_dir).display()); - let page = templates::render_page(&content.frontmatter, &html_body, &page_path, config); + let page = + templates::render_page(&content.frontmatter, &html_body, &page_path, config, nav); write_output(output_dir, content_dir, &content, page.into_string())?; } @@ -187,12 +208,13 @@ fn generate_projects_index( output_dir: &Path, projects: &[Content], config: &config::SiteConfig, + nav: &[NavItem], ) -> Result<()> { let out_path = output_dir.join("projects/index.html"); eprintln!("generating: {}", out_path.display()); let page = - templates::render_projects_index("Projects", projects, "/projects/index.html", config); + templates::render_projects_index("Projects", projects, "/projects/index.html", config, nav); fs::create_dir_all(out_path.parent().unwrap()).map_err(|e| Error::CreateDir { path: out_path.parent().unwrap().to_path_buf(), @@ -213,13 +235,15 @@ fn generate_homepage( content_dir: &Path, output_dir: &Path, config: &config::SiteConfig, + nav: &[NavItem], ) -> Result<()> { let index_path = content_dir.join("_index.md"); eprintln!("generating: homepage"); let content = Content::from_path(&index_path, ContentKind::Section)?; let html_body = render::markdown_to_html(&content.body); - let page = templates::render_homepage(&content.frontmatter, &html_body, "/index.html", config); + let page = + templates::render_homepage(&content.frontmatter, &html_body, "/index.html", config, nav); let out_path = output_dir.join("index.html"); diff --git a/src/templates.rs b/src/templates.rs index 5030041..77e3206 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -1,7 +1,7 @@ //! HTML templates using maud. use crate::config::SiteConfig; -use crate::content::{Content, Frontmatter}; +use crate::content::{Content, Frontmatter, NavItem}; use maud::{html, Markup, PreEscaped, DOCTYPE}; /// Compute relative path prefix based on page depth. @@ -33,6 +33,7 @@ pub fn render_post( content_html: &str, page_path: &str, config: &SiteConfig, + nav: &[NavItem], ) -> Markup { let depth = path_depth(page_path); let prefix = relative_prefix(depth); @@ -63,6 +64,7 @@ pub fn render_post( }, page_path, config, + nav, ) } @@ -72,6 +74,7 @@ pub fn render_page( content_html: &str, page_path: &str, config: &SiteConfig, + nav: &[NavItem], ) -> Markup { base_layout( &frontmatter.title, @@ -85,6 +88,7 @@ pub fn render_page( }, page_path, config, + nav, ) } @@ -94,6 +98,7 @@ pub fn render_homepage( content_html: &str, page_path: &str, config: &SiteConfig, + nav: &[NavItem], ) -> Markup { base_layout( &frontmatter.title, @@ -110,6 +115,7 @@ pub fn render_homepage( }, page_path, config, + nav, ) } @@ -119,6 +125,7 @@ pub fn render_blog_index( posts: &[Content], page_path: &str, config: &SiteConfig, + nav: &[NavItem], ) -> Markup { base_layout( title, @@ -143,6 +150,7 @@ pub fn render_blog_index( }, page_path, config, + nav, ) } @@ -152,6 +160,7 @@ pub fn render_projects_index( projects: &[Content], page_path: &str, config: &SiteConfig, + nav: &[NavItem], ) -> Markup { base_layout( title, @@ -179,11 +188,18 @@ pub fn render_projects_index( }, page_path, config, + nav, ) } /// Base HTML layout wrapper. -fn base_layout(title: &str, content: Markup, page_path: &str, config: &SiteConfig) -> Markup { +fn base_layout( + title: &str, + content: Markup, + page_path: &str, + config: &SiteConfig, + nav: &[NavItem], +) -> Markup { let depth = path_depth(page_path); let prefix = relative_prefix(depth); let canonical_url = format!("{}{}", config.base_url.trim_end_matches('/'), page_path); @@ -202,9 +218,9 @@ fn base_layout(title: &str, content: Markup, page_path: &str, config: &SiteConfi body { nav { a href=(format!("{}/index.html", prefix)) { (config.title) } - a href=(format!("{}/blog/index.html", prefix)) { "blog" } - a href=(format!("{}/projects/index.html", prefix)) { "projects" } - a href=(format!("{}/about.html", prefix)) { "about" } + @for item in nav { + a href=(format!("{}{}", prefix, item.path)) { (item.label) } + } } main { (content)