refactor(main): integrate SiteManifest for unified content discovery

Replace separate discover_nav + discover_sections calls with single
SiteManifest::discover() in run() pipeline:

- Update generate_feed() to accept &SiteManifest
- Update generate_sitemap_file() to accept &SiteManifest
- Simplify process_pages() to return Result<()>
- Remove redundant all_posts collection (now in manifest.posts)
- Remove unused discover_nav and discover_sections imports

All 49 tests pass, site builds identically.
This commit is contained in:
Timothy DeHerrera
2026-01-31 22:13:09 -07:00
parent 96aa60d9e4
commit 759838e7f5
3 changed files with 35 additions and 45 deletions

View File

@@ -1,11 +1,16 @@
//! Atom feed generation. //! Atom feed generation.
use crate::config::SiteConfig; use crate::config::SiteConfig;
use crate::content::Content; use crate::content::SiteManifest;
use std::path::Path; use std::path::Path;
/// Generate an Atom 1.0 feed from blog posts. /// Generate an Atom 1.0 feed from blog posts in the manifest.
pub fn generate_atom_feed(posts: &[Content], config: &SiteConfig, content_root: &Path) -> String { pub fn generate_atom_feed(
manifest: &SiteManifest,
config: &SiteConfig,
content_root: &Path,
) -> String {
let posts = &manifest.posts;
let base_url = config.base_url.trim_end_matches('/'); let base_url = config.base_url.trim_end_matches('/');
// Use the most recent post date as feed updated time, or fallback // Use the most recent post date as feed updated time, or fallback

View File

@@ -14,7 +14,7 @@ mod render;
mod sitemap; mod sitemap;
mod template_engine; mod template_engine;
use crate::content::{discover_nav, discover_sections, Content, ContentKind, NavItem}; use crate::content::{Content, ContentKind, NavItem};
use crate::error::{Error, Result}; use crate::error::{Error, Result};
use crate::template_engine::{ContentContext, TemplateEngine}; use crate::template_engine::{ContentContext, TemplateEngine};
use std::fs; use std::fs;
@@ -99,17 +99,14 @@ fn run(config_path: &Path) -> Result<()> {
// Load Tera templates // Load Tera templates
let engine = TemplateEngine::new(&template_dir)?; let engine = TemplateEngine::new(&template_dir)?;
// Discover navigation from filesystem // Discover all site content in a single pass
let nav = discover_nav(&content_dir)?; let manifest = content::SiteManifest::discover(&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. Discover and process all sections // 1. Process all sections
let sections = discover_sections(&content_dir)?; for section in &manifest.sections {
let mut all_posts = Vec::new(); // For feed generation
for section in &sections {
eprintln!("processing section: {}", section.name); eprintln!("processing section: {}", section.name);
// Collect and sort items in this section // Collect and sort items in this section
@@ -120,7 +117,6 @@ fn run(config_path: &Path) -> Result<()> {
"blog" => { "blog" => {
// Blog: sort by date, newest first // Blog: sort by date, newest first
items.sort_by(|a, b| b.frontmatter.date.cmp(&a.frontmatter.date)); items.sort_by(|a, b| b.frontmatter.date.cmp(&a.frontmatter.date));
all_posts.extend(items.iter().cloned());
} }
"projects" => { "projects" => {
// Projects: sort by weight // Projects: sort by weight
@@ -148,7 +144,8 @@ fn run(config_path: &Path) -> Result<()> {
eprintln!(" processing: {}", item.slug); eprintln!(" processing: {}", item.slug);
let html_body = render::markdown_to_html(&item.body); let html_body = render::markdown_to_html(&item.body);
let page_path = format!("/{}", item.output_path(&content_dir).display()); let page_path = format!("/{}", item.output_path(&content_dir).display());
let html = engine.render_content(item, &html_body, &page_path, &config, &nav)?; let html =
engine.render_content(item, &html_body, &page_path, &config, &manifest.nav)?;
write_output(&output_dir, &content_dir, item, html)?; write_output(&output_dir, &content_dir, item, html)?;
} }
@@ -164,15 +161,15 @@ fn run(config_path: &Path) -> Result<()> {
&item_contexts, &item_contexts,
&page_path, &page_path,
&config, &config,
&nav, &manifest.nav,
)?; );
let out_path = output_dir.join(&section.name).join("index.html"); let out_path = output_dir.join(&section.name).join("index.html");
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(),
source: e, source: e,
})?; })?;
fs::write(&out_path, html).map_err(|e| Error::WriteFile { fs::write(&out_path, html?).map_err(|e| Error::WriteFile {
path: out_path.clone(), path: out_path.clone(),
source: e, source: e,
})?; })?;
@@ -180,24 +177,18 @@ fn run(config_path: &Path) -> Result<()> {
} }
// 2. Generate Atom feed (blog posts only) // 2. Generate Atom feed (blog posts only)
if !all_posts.is_empty() { if !manifest.posts.is_empty() {
generate_feed(&output_dir, &all_posts, &config, &content_dir)?; generate_feed(&output_dir, &manifest, &config, &content_dir)?;
} }
// 3. Process standalone pages (discovered dynamically) // 3. Process standalone pages
let standalone_pages = process_pages(&content_dir, &output_dir, &config, &nav, &engine)?; process_pages(&content_dir, &output_dir, &config, &manifest.nav, &engine)?;
// 4. Generate homepage // 4. Generate homepage
generate_homepage(&content_dir, &output_dir, &config, &nav, &engine)?; generate_homepage(&content_dir, &output_dir, &config, &manifest.nav, &engine)?;
// 5. Generate sitemap // 5. Generate sitemap
generate_sitemap_file( generate_sitemap_file(&output_dir, &manifest, &config, &content_dir)?;
&output_dir,
&sections,
&standalone_pages,
&config,
&content_dir,
)?;
eprintln!("done!"); eprintln!("done!");
Ok(()) Ok(())
@@ -206,14 +197,14 @@ fn run(config_path: &Path) -> Result<()> {
/// Generate the Atom feed /// Generate the Atom feed
fn generate_feed( fn generate_feed(
output_dir: &Path, output_dir: &Path,
posts: &[Content], manifest: &content::SiteManifest,
config: &config::SiteConfig, config: &config::SiteConfig,
content_dir: &Path, content_dir: &Path,
) -> Result<()> { ) -> Result<()> {
let out_path = output_dir.join("feed.xml"); let out_path = output_dir.join("feed.xml");
eprintln!("generating: {}", out_path.display()); eprintln!("generating: {}", out_path.display());
let feed_xml = feed::generate_atom_feed(posts, config, content_dir); let feed_xml = feed::generate_atom_feed(manifest, config, content_dir);
fs::write(&out_path, feed_xml).map_err(|e| Error::WriteFile { fs::write(&out_path, feed_xml).map_err(|e| Error::WriteFile {
path: out_path.clone(), path: out_path.clone(),
@@ -227,15 +218,14 @@ fn generate_feed(
/// Generate the XML sitemap /// Generate the XML sitemap
fn generate_sitemap_file( fn generate_sitemap_file(
output_dir: &Path, output_dir: &Path,
sections: &[content::Section], manifest: &content::SiteManifest,
pages: &[Content],
config: &config::SiteConfig, config: &config::SiteConfig,
content_dir: &Path, content_dir: &Path,
) -> Result<()> { ) -> Result<()> {
let out_path = output_dir.join("sitemap.xml"); let out_path = output_dir.join("sitemap.xml");
eprintln!("generating: {}", out_path.display()); eprintln!("generating: {}", out_path.display());
let sitemap_xml = sitemap::generate_sitemap(sections, pages, config, content_dir); let sitemap_xml = sitemap::generate_sitemap(manifest, config, content_dir);
fs::write(&out_path, sitemap_xml).map_err(|e| Error::WriteFile { fs::write(&out_path, sitemap_xml).map_err(|e| Error::WriteFile {
path: out_path.clone(), path: out_path.clone(),
@@ -247,16 +237,13 @@ fn generate_sitemap_file(
} }
/// Process standalone pages in content/ (top-level .md files excluding _index.md) /// Process standalone pages in content/ (top-level .md files excluding _index.md)
/// Returns the discovered pages for use by sitemap generation.
fn process_pages( fn process_pages(
content_dir: &Path, content_dir: &Path,
output_dir: &Path, output_dir: &Path,
config: &config::SiteConfig, config: &config::SiteConfig,
nav: &[NavItem], nav: &[NavItem],
engine: &TemplateEngine, engine: &TemplateEngine,
) -> Result<Vec<Content>> { ) -> Result<()> {
let mut pages = Vec::new();
// Dynamically discover top-level .md files (except _index.md) // Dynamically discover top-level .md files (except _index.md)
let entries = fs::read_dir(content_dir).map_err(|e| Error::ReadFile { let entries = fs::read_dir(content_dir).map_err(|e| Error::ReadFile {
path: content_dir.to_path_buf(), path: content_dir.to_path_buf(),
@@ -277,10 +264,9 @@ fn process_pages(
let html = engine.render_page(&content, &html_body, &page_path, config, nav)?; let html = engine.render_page(&content, &html_body, &page_path, config, nav)?;
write_output(output_dir, content_dir, &content, html)?; write_output(output_dir, content_dir, &content, html)?;
pages.push(content);
} }
} }
Ok(pages) Ok(())
} }
/// Generate the homepage from content/_index.md /// Generate the homepage from content/_index.md

View File

@@ -1,7 +1,7 @@
//! XML sitemap generation for SEO. //! XML sitemap generation for SEO.
use crate::config::SiteConfig; use crate::config::SiteConfig;
use crate::content::{Content, Section}; use crate::content::SiteManifest;
use std::path::Path; use std::path::Path;
/// A URL entry for the sitemap. /// A URL entry for the sitemap.
@@ -12,7 +12,7 @@ pub struct SitemapEntry {
pub lastmod: Option<String>, pub lastmod: Option<String>,
} }
/// Generate an XML sitemap from discovered content. /// Generate an XML sitemap from the site manifest.
/// ///
/// Includes: /// Includes:
/// - Homepage /// - Homepage
@@ -20,8 +20,7 @@ pub struct SitemapEntry {
/// - Section items (posts, projects, etc.) /// - Section items (posts, projects, etc.)
/// - Standalone pages /// - Standalone pages
pub fn generate_sitemap( pub fn generate_sitemap(
sections: &[Section], manifest: &SiteManifest,
pages: &[Content],
config: &SiteConfig, config: &SiteConfig,
content_root: &Path, content_root: &Path,
) -> String { ) -> String {
@@ -35,7 +34,7 @@ pub fn generate_sitemap(
}); });
// Sections and their items // Sections and their items
for section in sections { for section in &manifest.sections {
// Section index // Section index
entries.push(SitemapEntry { entries.push(SitemapEntry {
loc: format!("{}/{}/index.html", base_url, section.name), loc: format!("{}/{}/index.html", base_url, section.name),
@@ -55,7 +54,7 @@ pub fn generate_sitemap(
} }
// Standalone pages // Standalone pages
for page in pages { for page in &manifest.pages {
let relative_path = page.output_path(content_root); let relative_path = page.output_path(content_root);
entries.push(SitemapEntry { entries.push(SitemapEntry {
loc: format!("{}/{}", base_url, relative_path.display()), loc: format!("{}/{}", base_url, relative_path.display()),