feat!: replace YAML frontmatter with TOML
Replace hand-rolled YAML parser (70 lines) with serde-backed toml::from_str<Frontmatter> (6 lines). Frontmatter delimiter changes from --- to +++ (Hugo TOML convention). New Frontmatter fields: - draft: bool (#[serde(default)]) for draft filtering - aliases: Vec<String> (#[serde(default)]) for URL redirects Date field upgraded from Option<String> to Option<NaiveDate> with custom deserializer for TOML native dates. Parse, don't validate: invalid dates now fail at deserialization time. Add chrono dependency with serde feature. Update cascade in sitemap.rs (NaiveDate→String at boundary), template_engine.rs (FrontmatterContext gains draft/aliases), and all 14 tests. BREAKING CHANGE: Content files must use +++ TOML frontmatter instead of --- YAML frontmatter.
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -200,6 +200,7 @@ checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"serde",
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1558,6 +1559,7 @@ dependencies = [
|
|||||||
name = "sukr"
|
name = "sukr"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
"katex-rs",
|
"katex-rs",
|
||||||
"lightningcss",
|
"lightningcss",
|
||||||
"mermaid-rs-renderer",
|
"mermaid-rs-renderer",
|
||||||
|
|||||||
@@ -64,9 +64,10 @@ lightningcss = "1.0.0-alpha.70"
|
|||||||
# tera: Jinja2-style templates, runtime-loaded (no recompilation needed).
|
# tera: Jinja2-style templates, runtime-loaded (no recompilation needed).
|
||||||
# serde/toml: Configuration parsing. Standard choices, no lighter alternatives.
|
# serde/toml: Configuration parsing. Standard choices, no lighter alternatives.
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
serde = { version = "1", features = ["derive"] }
|
chrono = { version = "0.4", default-features = false, features = ["serde"] }
|
||||||
tera = "1"
|
serde = { version = "1", features = ["derive"] }
|
||||||
toml = "0.8"
|
tera = "1"
|
||||||
|
toml = "0.8"
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
# Patches
|
# Patches
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ Implement the pre-1.0 API changes required to stabilize sukr's public contract:
|
|||||||
| Frontmatter format | **TOML** (replacing hand-rolled YAML) | `toml` crate + serde already in deps. Eliminates the fragile hand-rolled parser. Every future field is just a struct field with `#[derive(Deserialize)]`. |
|
| Frontmatter format | **TOML** (replacing hand-rolled YAML) | `toml` crate + serde already in deps. Eliminates the fragile hand-rolled parser. Every future field is just a struct field with `#[derive(Deserialize)]`. |
|
||||||
| Frontmatter delimiter | `+++` | Hugo convention for TOML frontmatter. Unambiguous — no risk of confusion with YAML `---` or Markdown horizontal rules. |
|
| Frontmatter delimiter | `+++` | Hugo convention for TOML frontmatter. Unambiguous — no risk of confusion with YAML `---` or Markdown horizontal rules. |
|
||||||
| Template naming | Mirror site.toml structure (`config.nav.nested` not `config.nested_nav`) | Consistency between config and templates; pre-1.0 is only window for this break |
|
| Template naming | Mirror site.toml structure (`config.nav.nested` not `config.nested_nav`) | Consistency between config and templates; pre-1.0 is only window for this break |
|
||||||
| Date type | Validate as YYYY-MM-DD string via `chrono::NaiveDate`, keep as `String` in template context | Validate at parse time; reject invalid formats. Feed generation already assumes this format. |
|
| Date type | `Option<chrono::NaiveDate>` with custom `deserialize_date` fn for TOML native dates | Parse, don't validate. Custom serde deserializer accepts `toml::Datetime`, extracts date, constructs `NaiveDate`. Invalid dates fail at deser. |
|
||||||
| Draft filtering | `draft: bool` (`#[serde(default)]`) in `Frontmatter`, filter in `collect_items()` and `discover()` | Filter early so drafts don't appear in nav, listings, sitemap, or feed. |
|
| Draft filtering | `draft: bool` (`#[serde(default)]`) in `Frontmatter`, filter in `collect_items()` and `discover()` | Filter early so drafts don't appear in nav, listings, sitemap, or feed. |
|
||||||
| Feed/sitemap config | `[feed]` and `[sitemap]` tables with `enabled` boolean in `SiteConfig` | Users need opt-out. Default `true` preserves backward compat. |
|
| Feed/sitemap config | `[feed]` and `[sitemap]` tables with `enabled` boolean in `SiteConfig` | Users need opt-out. Default `true` preserves backward compat. |
|
||||||
| Tag listing pages | Generate `/tags/<tag>.html` using a new `tags/default.html` template | Minimal approach — one template, one generation loop. No pagination. |
|
| Tag listing pages | Generate `/tags/<tag>.html` using a new `tags/default.html` template | Minimal approach — one template, one generation loop. No pagination. |
|
||||||
@@ -94,12 +94,12 @@ Items validated by codebase investigation:
|
|||||||
## Phases
|
## Phases
|
||||||
|
|
||||||
1. **Phase 1: TOML Frontmatter & Config Normalization** — replace the parser, migrate content, fix naming
|
1. **Phase 1: TOML Frontmatter & Config Normalization** — replace the parser, migrate content, fix naming
|
||||||
- [ ] Replace `Frontmatter` struct with `#[derive(Deserialize)]`
|
- [x] Replace `Frontmatter` struct with `#[derive(Deserialize)]`
|
||||||
- [ ] Add new fields: `draft: bool` (`#[serde(default)]`), `aliases: Vec<String>` (`#[serde(default)]`), keep all existing fields
|
- [x] Add new fields: `draft: bool` (`#[serde(default)]`), `aliases: Vec<String>` (`#[serde(default)]`), keep all existing fields
|
||||||
- [ ] Replace `parse_frontmatter()` with `toml::from_str::<Frontmatter>()`
|
- [x] Replace `parse_frontmatter()` with `toml::from_str::<Frontmatter>()`
|
||||||
- [ ] Update `extract_frontmatter()` to detect `+++` delimiters instead of `---`
|
- [x] Update `extract_frontmatter()` to detect `+++` delimiters instead of `---`
|
||||||
- [ ] Add date validation: custom deserializer or post-parse check for YYYY-MM-DD via `chrono::NaiveDate`
|
- [x] Add date validation: custom `deserialize_date` fn for TOML native dates → `chrono::NaiveDate`
|
||||||
- [ ] Change `tags` from `taxonomies.tags` nesting to flat `tags = ["..."]` (direct TOML array)
|
- [x] Change `tags` from `taxonomies.tags` nesting to flat `tags = ["..."]` (direct TOML array)
|
||||||
- [ ] Migrate all 17 content files from YAML (`---`) to TOML (`+++`) frontmatter
|
- [ ] Migrate all 17 content files from YAML (`---`) to TOML (`+++`) frontmatter
|
||||||
- [ ] Update embedded frontmatter examples in documentation pages (7 files)
|
- [ ] Update embedded frontmatter examples in documentation pages (7 files)
|
||||||
- [ ] Add `FeedConfig` and `SitemapConfig` structs to `config.rs` with `enabled: bool` (default `true`)
|
- [ ] Add `FeedConfig` and `SitemapConfig` structs to `config.rs` with `enabled: bool` (default `true`)
|
||||||
|
|||||||
155
src/content.rs
155
src/content.rs
@@ -1,8 +1,8 @@
|
|||||||
//! Content discovery and frontmatter parsing.
|
//! Content discovery and frontmatter parsing.
|
||||||
|
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use serde::Serialize;
|
use chrono::NaiveDate;
|
||||||
use std::collections::HashMap;
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
@@ -40,24 +40,39 @@ pub struct NavItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parsed frontmatter from a content file.
|
/// Parsed frontmatter from a content file.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct Frontmatter {
|
pub struct Frontmatter {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
#[serde(default)]
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub date: Option<String>,
|
#[serde(default, deserialize_with = "deserialize_date")]
|
||||||
|
pub date: Option<NaiveDate>,
|
||||||
|
#[serde(default)]
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
/// Sort order for nav and listings
|
/// Sort order for nav and listings
|
||||||
|
#[serde(default)]
|
||||||
pub weight: Option<i64>,
|
pub weight: Option<i64>,
|
||||||
/// For project cards: external link
|
/// For project cards: external link
|
||||||
|
#[serde(default)]
|
||||||
pub link_to: Option<String>,
|
pub link_to: Option<String>,
|
||||||
/// Custom navigation label (defaults to title)
|
/// Custom navigation label (defaults to title)
|
||||||
|
#[serde(default)]
|
||||||
pub nav_label: Option<String>,
|
pub nav_label: Option<String>,
|
||||||
/// Section type for template dispatch (e.g., "blog", "projects")
|
/// Section type for template dispatch (e.g., "blog", "projects")
|
||||||
|
#[serde(default)]
|
||||||
pub section_type: Option<String>,
|
pub section_type: Option<String>,
|
||||||
/// Override template for this content item
|
/// Override template for this content item
|
||||||
|
#[serde(default)]
|
||||||
pub template: Option<String>,
|
pub template: Option<String>,
|
||||||
/// Enable table of contents (anchor nav in sidebar)
|
/// Enable table of contents (anchor nav in sidebar)
|
||||||
|
#[serde(default)]
|
||||||
pub toc: Option<bool>,
|
pub toc: Option<bool>,
|
||||||
|
/// Whether this content is a draft (excluded from output)
|
||||||
|
#[serde(default)]
|
||||||
|
pub draft: bool,
|
||||||
|
/// Alternative URL paths that redirect to this content
|
||||||
|
#[serde(default)]
|
||||||
|
pub aliases: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A content item ready for rendering.
|
/// A content item ready for rendering.
|
||||||
@@ -71,7 +86,7 @@ pub struct Content {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Content {
|
impl Content {
|
||||||
/// Load and parse a markdown file with YAML frontmatter.
|
/// Load and parse a markdown file with TOML frontmatter.
|
||||||
pub fn from_path(path: impl AsRef<Path>, kind: ContentKind) -> Result<Self> {
|
pub fn from_path(path: impl AsRef<Path>, kind: ContentKind) -> Result<Self> {
|
||||||
Self::from_path_inner(path.as_ref(), kind)
|
Self::from_path_inner(path.as_ref(), kind)
|
||||||
}
|
}
|
||||||
@@ -82,8 +97,8 @@ impl Content {
|
|||||||
source: e,
|
source: e,
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let (yaml_block, body) = extract_frontmatter(&raw, path)?;
|
let (toml_block, body) = extract_frontmatter(&raw, path)?;
|
||||||
let frontmatter = parse_frontmatter(path, &yaml_block)?;
|
let frontmatter = parse_frontmatter(path, &toml_block)?;
|
||||||
|
|
||||||
// Derive slug from filename (without extension)
|
// Derive slug from filename (without extension)
|
||||||
let slug = path
|
let slug = path
|
||||||
@@ -124,105 +139,59 @@ impl Content {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract YAML frontmatter block and body from raw content.
|
/// Extract TOML frontmatter block and body from raw content.
|
||||||
/// Frontmatter must be delimited by `---` at start and end.
|
/// Frontmatter must be delimited by `+++` at start and end.
|
||||||
fn extract_frontmatter(raw: &str, path: &Path) -> Result<(String, String)> {
|
fn extract_frontmatter(raw: &str, path: &Path) -> Result<(String, String)> {
|
||||||
let trimmed = raw.trim_start();
|
let trimmed = raw.trim_start();
|
||||||
|
|
||||||
if !trimmed.starts_with("---") {
|
if !trimmed.starts_with("+++") {
|
||||||
return Err(Error::Frontmatter {
|
return Err(Error::Frontmatter {
|
||||||
path: path.to_path_buf(),
|
path: path.to_path_buf(),
|
||||||
message: "missing frontmatter delimiter".to_string(),
|
message: "missing frontmatter delimiter".to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the closing ---
|
// Find the closing +++
|
||||||
let after_first = &trimmed[3..].trim_start_matches(['\r', '\n']);
|
let after_first = &trimmed[3..].trim_start_matches(['\r', '\n']);
|
||||||
let end_idx = after_first
|
let end_idx = after_first
|
||||||
.find("\n---")
|
.find("\n+++")
|
||||||
.ok_or_else(|| Error::Frontmatter {
|
.ok_or_else(|| Error::Frontmatter {
|
||||||
path: path.to_path_buf(),
|
path: path.to_path_buf(),
|
||||||
message: "missing closing frontmatter delimiter".to_string(),
|
message: "missing closing frontmatter delimiter".to_string(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let yaml_block = after_first[..end_idx].to_string();
|
let toml_block = after_first[..end_idx].to_string();
|
||||||
let body = after_first[end_idx + 4..].trim_start().to_string();
|
let body = after_first[end_idx + 4..].trim_start().to_string();
|
||||||
|
|
||||||
Ok((yaml_block, body))
|
Ok((toml_block, body))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse simple YAML frontmatter into structured fields.
|
/// Parse TOML frontmatter into structured fields.
|
||||||
/// Supports: key: value, key: "quoted value", and nested taxonomies.tags
|
fn parse_frontmatter(path: &Path, toml_str: &str) -> Result<Frontmatter> {
|
||||||
fn parse_frontmatter(path: &Path, yaml: &str) -> Result<Frontmatter> {
|
toml::from_str(toml_str).map_err(|e| Error::Frontmatter {
|
||||||
let mut map: HashMap<String, String> = HashMap::new();
|
path: path.to_path_buf(),
|
||||||
let mut tags: Vec<String> = Vec::new();
|
message: e.to_string(),
|
||||||
let mut in_taxonomies = false;
|
|
||||||
let mut in_tags = false;
|
|
||||||
|
|
||||||
for line in yaml.lines() {
|
|
||||||
let trimmed = line.trim();
|
|
||||||
|
|
||||||
// Skip empty lines and comments
|
|
||||||
if trimmed.is_empty() || trimmed.starts_with('#') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle nested structure for taxonomies.tags
|
|
||||||
if trimmed == "taxonomies:" {
|
|
||||||
in_taxonomies = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if in_taxonomies && trimmed == "tags:" {
|
|
||||||
in_tags = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect tag list items
|
|
||||||
if in_tags && trimmed.starts_with("- ") {
|
|
||||||
let tag = trimmed[2..].trim().trim_matches('"').to_string();
|
|
||||||
tags.push(tag);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exit nested context on non-indented line
|
|
||||||
if !line.starts_with(' ') && !line.starts_with('\t') {
|
|
||||||
in_taxonomies = false;
|
|
||||||
in_tags = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse key: value
|
|
||||||
if let Some((key, value)) = trimmed.split_once(':') {
|
|
||||||
let key = key.trim().to_string();
|
|
||||||
let value = value.trim().trim_matches('"').to_string();
|
|
||||||
if !value.is_empty() {
|
|
||||||
map.insert(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let title = map
|
|
||||||
.get("title")
|
|
||||||
.cloned()
|
|
||||||
.ok_or_else(|| Error::Frontmatter {
|
|
||||||
path: path.to_path_buf(),
|
|
||||||
message: "missing required 'title' field".to_string(),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(Frontmatter {
|
|
||||||
title,
|
|
||||||
description: map.get("description").cloned(),
|
|
||||||
date: map.get("date").cloned(),
|
|
||||||
tags,
|
|
||||||
weight: map.get("weight").and_then(|v| v.parse().ok()),
|
|
||||||
link_to: map.get("link_to").cloned(),
|
|
||||||
nav_label: map.get("nav_label").cloned(),
|
|
||||||
section_type: map.get("section_type").cloned(),
|
|
||||||
template: map.get("template").cloned(),
|
|
||||||
toc: map.get("toc").and_then(|v| v.parse().ok()),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deserialize a TOML native date into `Option<NaiveDate>`.
|
||||||
|
///
|
||||||
|
/// TOML native dates (`date = 2026-01-15`) are parsed by the `toml` crate as
|
||||||
|
/// `toml::value::Datetime`. This deserializer accepts that type, extracts the
|
||||||
|
/// date component, and constructs a validated `chrono::NaiveDate`.
|
||||||
|
fn deserialize_date<'de, D>(deserializer: D) -> std::result::Result<Option<NaiveDate>, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let dt = toml::value::Datetime::deserialize(deserializer)?;
|
||||||
|
let date = dt
|
||||||
|
.date
|
||||||
|
.ok_or_else(|| serde::de::Error::custom("datetime must include a date component"))?;
|
||||||
|
NaiveDate::from_ymd_opt(date.year.into(), date.month.into(), date.day.into())
|
||||||
|
.ok_or_else(|| serde::de::Error::custom("invalid calendar date"))
|
||||||
|
.map(Some)
|
||||||
|
}
|
||||||
|
|
||||||
/// Discover navigation items from the content directory structure.
|
/// Discover navigation items from the content directory structure.
|
||||||
///
|
///
|
||||||
/// Rules:
|
/// Rules:
|
||||||
@@ -498,14 +467,14 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn write_frontmatter(path: &Path, title: &str, weight: Option<i64>, nav_label: Option<&str>) {
|
fn write_frontmatter(path: &Path, title: &str, weight: Option<i64>, nav_label: Option<&str>) {
|
||||||
let mut content = format!("---\ntitle: \"{}\"\n", title);
|
let mut content = format!("+++\ntitle = \"{}\"\n", title);
|
||||||
if let Some(w) = weight {
|
if let Some(w) = weight {
|
||||||
content.push_str(&format!("weight: {}\n", w));
|
content.push_str(&format!("weight = {}\n", w));
|
||||||
}
|
}
|
||||||
if let Some(label) = nav_label {
|
if let Some(label) = nav_label {
|
||||||
content.push_str(&format!("nav_label: \"{}\"\n", label));
|
content.push_str(&format!("nav_label = \"{}\"\n", label));
|
||||||
}
|
}
|
||||||
content.push_str("---\n\nBody content.");
|
content.push_str("+++\n\nBody content.");
|
||||||
fs::write(path, content).expect("failed to write test file");
|
fs::write(path, content).expect("failed to write test file");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -629,14 +598,14 @@ mod tests {
|
|||||||
section_type: Option<&str>,
|
section_type: Option<&str>,
|
||||||
weight: Option<i64>,
|
weight: Option<i64>,
|
||||||
) {
|
) {
|
||||||
let mut content = format!("---\ntitle: \"{}\"\n", title);
|
let mut content = format!("+++\ntitle = \"{}\"\n", title);
|
||||||
if let Some(st) = section_type {
|
if let Some(st) = section_type {
|
||||||
content.push_str(&format!("section_type: \"{}\"\n", st));
|
content.push_str(&format!("section_type = \"{}\"\n", st));
|
||||||
}
|
}
|
||||||
if let Some(w) = weight {
|
if let Some(w) = weight {
|
||||||
content.push_str(&format!("weight: {}\n", w));
|
content.push_str(&format!("weight = {}\n", w));
|
||||||
}
|
}
|
||||||
content.push_str("---\nSection content.\n");
|
content.push_str("+++\nSection content.\n");
|
||||||
fs::write(path, content).expect("failed to write section index");
|
fs::write(path, content).expect("failed to write section index");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -820,8 +789,8 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Create blog posts with dates
|
// Create blog posts with dates
|
||||||
let post1 = "---\ntitle: \"Post 1\"\ndate: \"2026-01-15\"\n---\nContent.".to_string();
|
let post1 = "+++\ntitle = \"Post 1\"\ndate = 2026-01-15\n+++\nContent.".to_string();
|
||||||
let post2 = "---\ntitle: \"Post 2\"\ndate: \"2026-01-20\"\n---\nContent.".to_string();
|
let post2 = "+++\ntitle = \"Post 2\"\ndate = 2026-01-20\n+++\nContent.".to_string();
|
||||||
fs::write(content_dir.join("blog/post1.md"), &post1).unwrap();
|
fs::write(content_dir.join("blog/post1.md"), &post1).unwrap();
|
||||||
fs::write(content_dir.join("blog/post2.md"), &post2).unwrap();
|
fs::write(content_dir.join("blog/post2.md"), &post2).unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ pub fn generate_sitemap(
|
|||||||
// 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),
|
||||||
lastmod: section.index.frontmatter.date.clone(),
|
lastmod: section.index.frontmatter.date.map(|d| d.to_string()),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Section items
|
// Section items
|
||||||
@@ -48,7 +48,7 @@ pub fn generate_sitemap(
|
|||||||
let relative_path = item.output_path(content_root);
|
let relative_path = item.output_path(content_root);
|
||||||
entries.push(SitemapEntry {
|
entries.push(SitemapEntry {
|
||||||
loc: format!("{}/{}", base_url, relative_path.display()),
|
loc: format!("{}/{}", base_url, relative_path.display()),
|
||||||
lastmod: item.frontmatter.date.clone(),
|
lastmod: item.frontmatter.date.map(|d| d.to_string()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,7 +59,7 @@ pub fn generate_sitemap(
|
|||||||
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()),
|
||||||
lastmod: page.frontmatter.date.clone(),
|
lastmod: page.frontmatter.date.map(|d| d.to_string()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -166,6 +166,10 @@ pub struct FrontmatterContext {
|
|||||||
pub link_to: Option<String>,
|
pub link_to: Option<String>,
|
||||||
/// Enable table of contents (anchor nav in sidebar)
|
/// Enable table of contents (anchor nav in sidebar)
|
||||||
pub toc: bool,
|
pub toc: bool,
|
||||||
|
/// Whether this content is a draft
|
||||||
|
pub draft: bool,
|
||||||
|
/// Alternative URL paths
|
||||||
|
pub aliases: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FrontmatterContext {
|
impl FrontmatterContext {
|
||||||
@@ -174,11 +178,13 @@ impl FrontmatterContext {
|
|||||||
Self {
|
Self {
|
||||||
title: fm.title.clone(),
|
title: fm.title.clone(),
|
||||||
description: fm.description.clone(),
|
description: fm.description.clone(),
|
||||||
date: fm.date.clone(),
|
date: fm.date.map(|d| d.to_string()),
|
||||||
tags: fm.tags.clone(),
|
tags: fm.tags.clone(),
|
||||||
weight: fm.weight,
|
weight: fm.weight,
|
||||||
link_to: fm.link_to.clone(),
|
link_to: fm.link_to.clone(),
|
||||||
toc: fm.toc.unwrap_or(config.nav.toc),
|
toc: fm.toc.unwrap_or(config.nav.toc),
|
||||||
|
draft: fm.draft,
|
||||||
|
aliases: fm.aliases.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,6 +284,8 @@ mod tests {
|
|||||||
section_type: None,
|
section_type: None,
|
||||||
template: None,
|
template: None,
|
||||||
toc: Some(true),
|
toc: Some(true),
|
||||||
|
draft: false,
|
||||||
|
aliases: vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Frontmatter with explicit toc: false
|
// Frontmatter with explicit toc: false
|
||||||
@@ -292,6 +300,8 @@ mod tests {
|
|||||||
section_type: None,
|
section_type: None,
|
||||||
template: None,
|
template: None,
|
||||||
toc: Some(false),
|
toc: Some(false),
|
||||||
|
draft: false,
|
||||||
|
aliases: vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Frontmatter with no toc specified (None)
|
// Frontmatter with no toc specified (None)
|
||||||
@@ -306,6 +316,8 @@ mod tests {
|
|||||||
section_type: None,
|
section_type: None,
|
||||||
template: None,
|
template: None,
|
||||||
toc: None,
|
toc: None,
|
||||||
|
draft: false,
|
||||||
|
aliases: vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Explicit true overrides config false
|
// Explicit true overrides config false
|
||||||
|
|||||||
Reference in New Issue
Block a user