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:
11
src/feed.rs
11
src/feed.rs
@@ -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
|
||||||
|
|||||||
58
src/main.rs
58
src/main.rs
@@ -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 §ions {
|
|
||||||
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(§ion.name).join("index.html");
|
let out_path = output_dir.join(§ion.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,
|
|
||||||
§ions,
|
|
||||||
&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
|
||||||
|
|||||||
@@ -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()),
|
||||||
|
|||||||
Reference in New Issue
Block a user