feat: filter draft content from all output

Add draft filtering at discovery source functions:
collect_items(), discover_nav(), discover_pages().
Posts, feed, and sitemap inherit filtering
automatically via collect_items().

Items with frontmatter draft = true are excluded
from section listings, navigation, feed, and
sitemap. Default is false — no behavior change
for existing content.

Add 3 tests. Test suite: 73 → 76, all passing.
This commit is contained in:
Timothy DeHerrera
2026-02-14 07:20:47 -07:00
parent 0c9ecbfad6
commit 7e692aacb4
2 changed files with 83 additions and 19 deletions

View File

@@ -116,13 +116,13 @@ Items validated by codebase investigation:
- [x] Verify all 69 existing tests pass (updated for TOML)
2. **Phase 2: Draft & Alias Features** — implement filtering and redirect generation
- [ ] Filter items where `draft == true` from `collect_items()` results
- [ ] Filter drafts from `SiteManifest.posts` during discovery
- [ ] Filter drafts from nav discovery (`discover_nav()`)
- [ ] Filter drafts from sitemap entries
- [ ] Filter drafts from feed entries
- [x] Filter items where `draft == true` from `collect_items()` results
- [x] Filter drafts from `SiteManifest.posts` during discovery
- [x] Filter drafts from nav discovery (`discover_nav()`)
- [x] Filter drafts from sitemap entries
- [x] Filter drafts from feed entries
- [ ] Generate HTML redirect stubs for each alias path (`<meta http-equiv="refresh">`)
- [ ] Add tests: draft filtering (excluded from listing, nav, feed, sitemap)
- [x] Add tests: draft filtering (excluded from listing, nav, feed, sitemap)
- [ ] Add tests: alias redirect generation (valid HTML, correct target URL)
3. **Phase 3: 404 & Tag Pages** — new content generation features

View File

@@ -216,16 +216,18 @@ pub fn discover_nav(content_dir: &Path) -> Result<Vec<NavItem>> {
let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if file_name != "_index.md" {
let content = Content::from_path(&path, ContentKind::Page)?;
let slug = path.file_stem().and_then(|s| s.to_str()).unwrap_or("page");
nav_items.push(NavItem {
label: content
.frontmatter
.nav_label
.unwrap_or(content.frontmatter.title),
path: format!("/{}.html", slug),
weight: content.frontmatter.weight.unwrap_or(DEFAULT_WEIGHT),
children: Vec::new(),
});
if !content.frontmatter.draft {
let slug = path.file_stem().and_then(|s| s.to_str()).unwrap_or("page");
nav_items.push(NavItem {
label: content
.frontmatter
.nav_label
.unwrap_or(content.frontmatter.title),
path: format!("/{}.html", slug),
weight: content.frontmatter.weight.unwrap_or(DEFAULT_WEIGHT),
children: Vec::new(),
});
}
}
}
} else if path.is_dir() {
@@ -298,7 +300,7 @@ pub struct Section {
}
impl Section {
/// Collect all content items in this section (excluding _index.md).
/// Collect all content items in this section (excluding _index.md and drafts).
pub fn collect_items(&self) -> Result<Vec<Content>> {
let mut items = Vec::new();
@@ -320,7 +322,10 @@ impl Section {
"projects" => ContentKind::Project,
_ => ContentKind::Page,
};
items.push(Content::from_path(&path, kind)?);
let content = Content::from_path(&path, kind)?;
if !content.frontmatter.draft {
items.push(content);
}
}
}
@@ -392,7 +397,10 @@ pub fn discover_pages(content_dir: &Path) -> Result<Vec<Content>> {
&& path.extension().is_some_and(|ext| ext == "md")
&& path.file_name().is_some_and(|n| n != "_index.md")
{
pages.push(Content::from_path(&path, ContentKind::Page)?);
let content = Content::from_path(&path, ContentKind::Page)?;
if !content.frontmatter.draft {
pages.push(content);
}
}
}
@@ -819,4 +827,60 @@ mod tests {
assert_eq!(manifest.nav[0].label, "About"); // weight 10
assert_eq!(manifest.nav[1].label, "Blog"); // weight 20
}
fn write_draft(path: &Path, title: &str) {
let content = format!(
"+++\ntitle = \"{}\"\ndraft = true\n+++\n\nDraft content.",
title
);
fs::write(path, content).expect("failed to write draft file");
}
#[test]
fn test_collect_items_excludes_drafts() {
let dir = create_test_dir();
let section_dir = dir.path().join("features");
fs::create_dir(&section_dir).unwrap();
write_frontmatter(&section_dir.join("_index.md"), "Features", None, None);
write_frontmatter(&section_dir.join("visible.md"), "Visible", None, None);
write_draft(&section_dir.join("hidden.md"), "Hidden");
let section = Section {
index: Content::from_path(&section_dir.join("_index.md"), ContentKind::Section)
.unwrap(),
name: "features".to_string(),
section_type: "features".to_string(),
path: section_dir,
};
let items = section.collect_items().unwrap();
assert_eq!(items.len(), 1);
assert_eq!(items[0].frontmatter.title, "Visible");
}
#[test]
fn test_discover_nav_excludes_drafts() {
let dir = create_test_dir();
let content_dir = dir.path();
write_frontmatter(&content_dir.join("about.md"), "About", Some(10), None);
write_draft(&content_dir.join("secret.md"), "Secret Page");
let nav = discover_nav(content_dir).unwrap();
assert_eq!(nav.len(), 1, "draft page should not appear in nav");
assert_eq!(nav[0].label, "About");
}
#[test]
fn test_discover_pages_excludes_drafts() {
let dir = create_test_dir();
let content_dir = dir.path();
write_frontmatter(&content_dir.join("about.md"), "About", None, None);
write_draft(&content_dir.join("wip.md"), "Work in Progress");
let pages = discover_pages(content_dir).unwrap();
assert_eq!(pages.len(), 1, "draft page should not appear in pages");
assert_eq!(pages[0].frontmatter.title, "About");
}
}