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.
This commit is contained in:
Timothy DeHerrera
2026-01-31 13:56:16 -07:00
parent b978edf4f2
commit a5c56c2b2f
2 changed files with 62 additions and 22 deletions

View File

@@ -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<Vec<Content>> {
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");

View File

@@ -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)