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:
58
src/main.rs
58
src/main.rs
@@ -13,7 +13,7 @@ mod mermaid;
|
|||||||
mod render;
|
mod render;
|
||||||
mod templates;
|
mod templates;
|
||||||
|
|
||||||
use crate::content::{Content, ContentKind};
|
use crate::content::{discover_nav, Content, ContentKind, NavItem};
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -38,21 +38,24 @@ fn run() -> Result<()> {
|
|||||||
// Load site configuration
|
// Load site configuration
|
||||||
let config = config::SiteConfig::load(config_path)?;
|
let config = config::SiteConfig::load(config_path)?;
|
||||||
|
|
||||||
|
// Discover navigation from filesystem
|
||||||
|
let nav = discover_nav(content_dir)?;
|
||||||
|
|
||||||
// 0. Copy static assets
|
// 0. Copy static assets
|
||||||
copy_static_assets(static_dir, output_dir)?;
|
copy_static_assets(static_dir, output_dir)?;
|
||||||
|
|
||||||
// 1. Process blog posts
|
// 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)
|
// 2. Generate blog index (sorted by date, newest first)
|
||||||
posts.sort_by(|a, b| b.frontmatter.date.cmp(&a.frontmatter.date));
|
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
|
// 2b. Generate Atom feed
|
||||||
generate_feed(output_dir, &posts, &config)?;
|
generate_feed(output_dir, &posts, &config)?;
|
||||||
|
|
||||||
// 3. Process standalone pages (about, collab)
|
// 3. Process standalone pages (discovered dynamically)
|
||||||
process_pages(content_dir, output_dir, &config)?;
|
process_pages(content_dir, output_dir, &config, &nav)?;
|
||||||
|
|
||||||
// 4. Process projects and generate project index
|
// 4. Process projects and generate project index
|
||||||
let mut projects = process_projects(content_dir)?;
|
let mut projects = process_projects(content_dir)?;
|
||||||
@@ -62,10 +65,10 @@ fn run() -> Result<()> {
|
|||||||
.unwrap_or(99)
|
.unwrap_or(99)
|
||||||
.cmp(&b.frontmatter.weight.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
|
// 5. Generate homepage
|
||||||
generate_homepage(content_dir, output_dir, &config)?;
|
generate_homepage(content_dir, output_dir, &config, &nav)?;
|
||||||
|
|
||||||
eprintln!("done!");
|
eprintln!("done!");
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -76,6 +79,7 @@ fn process_blog_posts(
|
|||||||
content_dir: &Path,
|
content_dir: &Path,
|
||||||
output_dir: &Path,
|
output_dir: &Path,
|
||||||
config: &config::SiteConfig,
|
config: &config::SiteConfig,
|
||||||
|
nav: &[NavItem],
|
||||||
) -> Result<Vec<Content>> {
|
) -> Result<Vec<Content>> {
|
||||||
let blog_dir = content_dir.join("blog");
|
let blog_dir = content_dir.join("blog");
|
||||||
let mut posts = Vec::new();
|
let mut posts = Vec::new();
|
||||||
@@ -94,7 +98,8 @@ fn process_blog_posts(
|
|||||||
let content = Content::from_path(path, ContentKind::Post)?;
|
let content = Content::from_path(path, ContentKind::Post)?;
|
||||||
let html_body = render::markdown_to_html(&content.body);
|
let html_body = render::markdown_to_html(&content.body);
|
||||||
let page_path = format!("/{}", content.output_path(content_dir).display());
|
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())?;
|
write_output(output_dir, content_dir, &content, page.into_string())?;
|
||||||
posts.push(content);
|
posts.push(content);
|
||||||
@@ -108,11 +113,12 @@ fn generate_blog_index(
|
|||||||
output_dir: &Path,
|
output_dir: &Path,
|
||||||
posts: &[Content],
|
posts: &[Content],
|
||||||
config: &config::SiteConfig,
|
config: &config::SiteConfig,
|
||||||
|
nav: &[NavItem],
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let out_path = output_dir.join("blog/index.html");
|
let out_path = output_dir.join("blog/index.html");
|
||||||
eprintln!("generating: {}", out_path.display());
|
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 {
|
fs::create_dir_all(out_path.parent().unwrap()).map_err(|e| Error::CreateDir {
|
||||||
path: out_path.parent().unwrap().to_path_buf(),
|
path: out_path.parent().unwrap().to_path_buf(),
|
||||||
@@ -144,17 +150,32 @@ fn generate_feed(output_dir: &Path, posts: &[Content], config: &config::SiteConf
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process standalone pages in content/ (about.md, collab.md)
|
/// Process standalone pages in content/ (top-level .md files excluding _index.md)
|
||||||
fn process_pages(content_dir: &Path, output_dir: &Path, config: &config::SiteConfig) -> Result<()> {
|
fn process_pages(
|
||||||
for name in ["about.md", "collab.md"] {
|
content_dir: &Path,
|
||||||
let path = content_dir.join(name);
|
output_dir: &Path,
|
||||||
if path.exists() {
|
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());
|
eprintln!("processing: {}", path.display());
|
||||||
|
|
||||||
let content = Content::from_path(&path, ContentKind::Page)?;
|
let content = Content::from_path(&path, ContentKind::Page)?;
|
||||||
let html_body = render::markdown_to_html(&content.body);
|
let html_body = render::markdown_to_html(&content.body);
|
||||||
let page_path = format!("/{}", content.output_path(content_dir).display());
|
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())?;
|
write_output(output_dir, content_dir, &content, page.into_string())?;
|
||||||
}
|
}
|
||||||
@@ -187,12 +208,13 @@ fn generate_projects_index(
|
|||||||
output_dir: &Path,
|
output_dir: &Path,
|
||||||
projects: &[Content],
|
projects: &[Content],
|
||||||
config: &config::SiteConfig,
|
config: &config::SiteConfig,
|
||||||
|
nav: &[NavItem],
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let out_path = output_dir.join("projects/index.html");
|
let out_path = output_dir.join("projects/index.html");
|
||||||
eprintln!("generating: {}", out_path.display());
|
eprintln!("generating: {}", out_path.display());
|
||||||
|
|
||||||
let page =
|
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 {
|
fs::create_dir_all(out_path.parent().unwrap()).map_err(|e| Error::CreateDir {
|
||||||
path: out_path.parent().unwrap().to_path_buf(),
|
path: out_path.parent().unwrap().to_path_buf(),
|
||||||
@@ -213,13 +235,15 @@ fn generate_homepage(
|
|||||||
content_dir: &Path,
|
content_dir: &Path,
|
||||||
output_dir: &Path,
|
output_dir: &Path,
|
||||||
config: &config::SiteConfig,
|
config: &config::SiteConfig,
|
||||||
|
nav: &[NavItem],
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let index_path = content_dir.join("_index.md");
|
let index_path = content_dir.join("_index.md");
|
||||||
eprintln!("generating: homepage");
|
eprintln!("generating: homepage");
|
||||||
|
|
||||||
let content = Content::from_path(&index_path, ContentKind::Section)?;
|
let content = Content::from_path(&index_path, ContentKind::Section)?;
|
||||||
let html_body = render::markdown_to_html(&content.body);
|
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");
|
let out_path = output_dir.join("index.html");
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
//! HTML templates using maud.
|
//! HTML templates using maud.
|
||||||
|
|
||||||
use crate::config::SiteConfig;
|
use crate::config::SiteConfig;
|
||||||
use crate::content::{Content, Frontmatter};
|
use crate::content::{Content, Frontmatter, NavItem};
|
||||||
use maud::{html, Markup, PreEscaped, DOCTYPE};
|
use maud::{html, Markup, PreEscaped, DOCTYPE};
|
||||||
|
|
||||||
/// Compute relative path prefix based on page depth.
|
/// Compute relative path prefix based on page depth.
|
||||||
@@ -33,6 +33,7 @@ pub fn render_post(
|
|||||||
content_html: &str,
|
content_html: &str,
|
||||||
page_path: &str,
|
page_path: &str,
|
||||||
config: &SiteConfig,
|
config: &SiteConfig,
|
||||||
|
nav: &[NavItem],
|
||||||
) -> Markup {
|
) -> Markup {
|
||||||
let depth = path_depth(page_path);
|
let depth = path_depth(page_path);
|
||||||
let prefix = relative_prefix(depth);
|
let prefix = relative_prefix(depth);
|
||||||
@@ -63,6 +64,7 @@ pub fn render_post(
|
|||||||
},
|
},
|
||||||
page_path,
|
page_path,
|
||||||
config,
|
config,
|
||||||
|
nav,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +74,7 @@ pub fn render_page(
|
|||||||
content_html: &str,
|
content_html: &str,
|
||||||
page_path: &str,
|
page_path: &str,
|
||||||
config: &SiteConfig,
|
config: &SiteConfig,
|
||||||
|
nav: &[NavItem],
|
||||||
) -> Markup {
|
) -> Markup {
|
||||||
base_layout(
|
base_layout(
|
||||||
&frontmatter.title,
|
&frontmatter.title,
|
||||||
@@ -85,6 +88,7 @@ pub fn render_page(
|
|||||||
},
|
},
|
||||||
page_path,
|
page_path,
|
||||||
config,
|
config,
|
||||||
|
nav,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +98,7 @@ pub fn render_homepage(
|
|||||||
content_html: &str,
|
content_html: &str,
|
||||||
page_path: &str,
|
page_path: &str,
|
||||||
config: &SiteConfig,
|
config: &SiteConfig,
|
||||||
|
nav: &[NavItem],
|
||||||
) -> Markup {
|
) -> Markup {
|
||||||
base_layout(
|
base_layout(
|
||||||
&frontmatter.title,
|
&frontmatter.title,
|
||||||
@@ -110,6 +115,7 @@ pub fn render_homepage(
|
|||||||
},
|
},
|
||||||
page_path,
|
page_path,
|
||||||
config,
|
config,
|
||||||
|
nav,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +125,7 @@ pub fn render_blog_index(
|
|||||||
posts: &[Content],
|
posts: &[Content],
|
||||||
page_path: &str,
|
page_path: &str,
|
||||||
config: &SiteConfig,
|
config: &SiteConfig,
|
||||||
|
nav: &[NavItem],
|
||||||
) -> Markup {
|
) -> Markup {
|
||||||
base_layout(
|
base_layout(
|
||||||
title,
|
title,
|
||||||
@@ -143,6 +150,7 @@ pub fn render_blog_index(
|
|||||||
},
|
},
|
||||||
page_path,
|
page_path,
|
||||||
config,
|
config,
|
||||||
|
nav,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +160,7 @@ pub fn render_projects_index(
|
|||||||
projects: &[Content],
|
projects: &[Content],
|
||||||
page_path: &str,
|
page_path: &str,
|
||||||
config: &SiteConfig,
|
config: &SiteConfig,
|
||||||
|
nav: &[NavItem],
|
||||||
) -> Markup {
|
) -> Markup {
|
||||||
base_layout(
|
base_layout(
|
||||||
title,
|
title,
|
||||||
@@ -179,11 +188,18 @@ pub fn render_projects_index(
|
|||||||
},
|
},
|
||||||
page_path,
|
page_path,
|
||||||
config,
|
config,
|
||||||
|
nav,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Base HTML layout wrapper.
|
/// 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 depth = path_depth(page_path);
|
||||||
let prefix = relative_prefix(depth);
|
let prefix = relative_prefix(depth);
|
||||||
let canonical_url = format!("{}{}", config.base_url.trim_end_matches('/'), page_path);
|
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 {
|
body {
|
||||||
nav {
|
nav {
|
||||||
a href=(format!("{}/index.html", prefix)) { (config.title) }
|
a href=(format!("{}/index.html", prefix)) { (config.title) }
|
||||||
a href=(format!("{}/blog/index.html", prefix)) { "blog" }
|
@for item in nav {
|
||||||
a href=(format!("{}/projects/index.html", prefix)) { "projects" }
|
a href=(format!("{}{}", prefix, item.path)) { (item.label) }
|
||||||
a href=(format!("{}/about.html", prefix)) { "about" }
|
}
|
||||||
}
|
}
|
||||||
main {
|
main {
|
||||||
(content)
|
(content)
|
||||||
|
|||||||
Reference in New Issue
Block a user