feat: add TOML site config for metadata
- Cargo.toml: Add toml and serde dependencies - site.toml: New config with title, author, base_url - src/config.rs: SiteConfig struct with load() function - src/error.rs: Add Error::Config for parse errors - src/main.rs: Load site.toml, thread config and page paths through generators - src/templates.rs: Use config.title in nav/titles, config.author in footer, config.base_url for canonical URLs. All three config fields verified in generated HTML output.
This commit is contained in:
63
Cargo.lock
generated
63
Cargo.lock
generated
@@ -326,7 +326,7 @@ checksum = "8666976c40b8633f918783969b6681a3ddb205f29150348617de425d85a3e3bd"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"toml",
|
||||
"toml 0.5.11",
|
||||
"yaml-rust2",
|
||||
]
|
||||
|
||||
@@ -510,7 +510,9 @@ dependencies = [
|
||||
"lightningcss",
|
||||
"maud",
|
||||
"pulldown-cmark",
|
||||
"serde",
|
||||
"thiserror 2.0.18",
|
||||
"toml 0.8.23",
|
||||
"tree-sitter-bash",
|
||||
"tree-sitter-highlight",
|
||||
"tree-sitter-json",
|
||||
@@ -931,6 +933,15 @@ dependencies = [
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
@@ -1062,6 +1073,47 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_write",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_write"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter"
|
||||
version = "0.24.7"
|
||||
@@ -1264,6 +1316,15 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
|
||||
@@ -20,3 +20,7 @@ tree-sitter-rust = "0.23"
|
||||
|
||||
# CSS processing
|
||||
lightningcss = "1.0.0-alpha.70"
|
||||
|
||||
# Config parsing
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
toml = "0.8"
|
||||
|
||||
5
site.toml
Normal file
5
site.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
# Site configuration for nrd.sh static site compiler
|
||||
|
||||
author = "nrdxp"
|
||||
base_url = "https://nrd.sh/"
|
||||
title = "nrd.sh"
|
||||
51
src/config.rs
Normal file
51
src/config.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
//! Site configuration loading.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use serde::Deserialize;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
/// Site-wide configuration loaded from site.toml.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SiteConfig {
|
||||
/// Site title (used in page titles and nav).
|
||||
pub title: String,
|
||||
/// Site author name.
|
||||
pub author: String,
|
||||
/// Base URL for the site (used for feeds, canonical links).
|
||||
pub base_url: String,
|
||||
}
|
||||
|
||||
impl SiteConfig {
|
||||
/// Load configuration from a TOML file.
|
||||
pub fn load(path: &Path) -> Result<Self> {
|
||||
let content = fs::read_to_string(path).map_err(|e| Error::ReadFile {
|
||||
path: path.to_path_buf(),
|
||||
source: e,
|
||||
})?;
|
||||
|
||||
toml::from_str(&content).map_err(|e| Error::Config {
|
||||
path: path.to_path_buf(),
|
||||
message: e.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_config() {
|
||||
let toml = r#"
|
||||
title = "Test Site"
|
||||
author = "Test Author"
|
||||
base_url = "https://example.com/"
|
||||
"#;
|
||||
|
||||
let config: SiteConfig = toml::from_str(toml).unwrap();
|
||||
assert_eq!(config.title, "Test Site");
|
||||
assert_eq!(config.author, "Test Author");
|
||||
assert_eq!(config.base_url, "https://example.com/");
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,10 @@ pub enum Error {
|
||||
/// Content directory not found.
|
||||
#[error("content directory not found: {0}")]
|
||||
ContentDirNotFound(PathBuf),
|
||||
|
||||
/// Failed to parse configuration file.
|
||||
#[error("invalid config in {path}: {message}")]
|
||||
Config { path: PathBuf, message: String },
|
||||
}
|
||||
|
||||
/// Result type alias for compiler operations.
|
||||
|
||||
54
src/main.rs
54
src/main.rs
@@ -2,6 +2,7 @@
|
||||
//!
|
||||
//! Transforms markdown content into a minimal static site.
|
||||
|
||||
mod config;
|
||||
mod content;
|
||||
mod css;
|
||||
mod error;
|
||||
@@ -25,23 +26,27 @@ fn run() -> Result<()> {
|
||||
let content_dir = Path::new("content");
|
||||
let output_dir = Path::new("public");
|
||||
let static_dir = Path::new("static");
|
||||
let config_path = Path::new("site.toml");
|
||||
|
||||
if !content_dir.exists() {
|
||||
return Err(Error::ContentDirNotFound(content_dir.to_path_buf()));
|
||||
}
|
||||
|
||||
// Load site configuration
|
||||
let config = config::SiteConfig::load(config_path)?;
|
||||
|
||||
// 0. Copy static assets
|
||||
copy_static_assets(static_dir, output_dir)?;
|
||||
|
||||
// 1. Process blog posts
|
||||
let mut posts = process_blog_posts(content_dir, output_dir)?;
|
||||
let mut posts = process_blog_posts(content_dir, output_dir, &config)?;
|
||||
|
||||
// 2. Generate blog index (sorted by date, newest first)
|
||||
posts.sort_by(|a, b| b.frontmatter.date.cmp(&a.frontmatter.date));
|
||||
generate_blog_index(output_dir, &posts)?;
|
||||
generate_blog_index(output_dir, &posts, &config)?;
|
||||
|
||||
// 3. Process standalone pages (about, collab)
|
||||
process_pages(content_dir, output_dir)?;
|
||||
process_pages(content_dir, output_dir, &config)?;
|
||||
|
||||
// 4. Process projects and generate project index
|
||||
let mut projects = process_projects(content_dir)?;
|
||||
@@ -51,17 +56,21 @@ fn run() -> Result<()> {
|
||||
.unwrap_or(99)
|
||||
.cmp(&b.frontmatter.weight.unwrap_or(99))
|
||||
});
|
||||
generate_projects_index(output_dir, &projects)?;
|
||||
generate_projects_index(output_dir, &projects, &config)?;
|
||||
|
||||
// 5. Generate homepage
|
||||
generate_homepage(content_dir, output_dir)?;
|
||||
generate_homepage(content_dir, output_dir, &config)?;
|
||||
|
||||
eprintln!("done!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Process all blog posts in content/blog/
|
||||
fn process_blog_posts(content_dir: &Path, output_dir: &Path) -> Result<Vec<Content>> {
|
||||
fn process_blog_posts(
|
||||
content_dir: &Path,
|
||||
output_dir: &Path,
|
||||
config: &config::SiteConfig,
|
||||
) -> Result<Vec<Content>> {
|
||||
let blog_dir = content_dir.join("blog");
|
||||
let mut posts = Vec::new();
|
||||
|
||||
@@ -78,7 +87,8 @@ fn process_blog_posts(content_dir: &Path, output_dir: &Path) -> Result<Vec<Conte
|
||||
|
||||
let content = Content::from_path(path, ContentKind::Post)?;
|
||||
let html_body = render::markdown_to_html(&content.body);
|
||||
let page = templates::render_post(&content.frontmatter, &html_body, 1);
|
||||
let page_path = format!("/{}", content.output_path(content_dir).display());
|
||||
let page = templates::render_post(&content.frontmatter, &html_body, &page_path, config);
|
||||
|
||||
write_output(output_dir, content_dir, &content, page.into_string())?;
|
||||
posts.push(content);
|
||||
@@ -88,11 +98,15 @@ fn process_blog_posts(content_dir: &Path, output_dir: &Path) -> Result<Vec<Conte
|
||||
}
|
||||
|
||||
/// Generate the blog listing page
|
||||
fn generate_blog_index(output_dir: &Path, posts: &[Content]) -> Result<()> {
|
||||
fn generate_blog_index(
|
||||
output_dir: &Path,
|
||||
posts: &[Content],
|
||||
config: &config::SiteConfig,
|
||||
) -> Result<()> {
|
||||
let out_path = output_dir.join("blog/index.html");
|
||||
eprintln!("generating: {}", out_path.display());
|
||||
|
||||
let page = templates::render_blog_index("Blog", posts, 1);
|
||||
let page = templates::render_blog_index("Blog", posts, "/blog/index.html", config);
|
||||
|
||||
fs::create_dir_all(out_path.parent().unwrap()).map_err(|e| Error::CreateDir {
|
||||
path: out_path.parent().unwrap().to_path_buf(),
|
||||
@@ -109,7 +123,7 @@ fn generate_blog_index(output_dir: &Path, posts: &[Content]) -> Result<()> {
|
||||
}
|
||||
|
||||
/// Process standalone pages in content/ (about.md, collab.md)
|
||||
fn process_pages(content_dir: &Path, output_dir: &Path) -> Result<()> {
|
||||
fn process_pages(content_dir: &Path, output_dir: &Path, config: &config::SiteConfig) -> Result<()> {
|
||||
for name in ["about.md", "collab.md"] {
|
||||
let path = content_dir.join(name);
|
||||
if path.exists() {
|
||||
@@ -117,7 +131,8 @@ fn process_pages(content_dir: &Path, output_dir: &Path) -> Result<()> {
|
||||
|
||||
let content = Content::from_path(&path, ContentKind::Page)?;
|
||||
let html_body = render::markdown_to_html(&content.body);
|
||||
let page = templates::render_page(&content.frontmatter, &html_body, 0);
|
||||
let page_path = format!("/{}", content.output_path(content_dir).display());
|
||||
let page = templates::render_page(&content.frontmatter, &html_body, &page_path, config);
|
||||
|
||||
write_output(output_dir, content_dir, &content, page.into_string())?;
|
||||
}
|
||||
@@ -146,11 +161,16 @@ fn process_projects(content_dir: &Path) -> Result<Vec<Content>> {
|
||||
}
|
||||
|
||||
/// Generate the projects listing page
|
||||
fn generate_projects_index(output_dir: &Path, projects: &[Content]) -> Result<()> {
|
||||
fn generate_projects_index(
|
||||
output_dir: &Path,
|
||||
projects: &[Content],
|
||||
config: &config::SiteConfig,
|
||||
) -> Result<()> {
|
||||
let out_path = output_dir.join("projects/index.html");
|
||||
eprintln!("generating: {}", out_path.display());
|
||||
|
||||
let page = templates::render_projects_index("Projects", projects, 1);
|
||||
let page =
|
||||
templates::render_projects_index("Projects", projects, "/projects/index.html", config);
|
||||
|
||||
fs::create_dir_all(out_path.parent().unwrap()).map_err(|e| Error::CreateDir {
|
||||
path: out_path.parent().unwrap().to_path_buf(),
|
||||
@@ -167,13 +187,17 @@ fn generate_projects_index(output_dir: &Path, projects: &[Content]) -> Result<()
|
||||
}
|
||||
|
||||
/// Generate the homepage from content/_index.md
|
||||
fn generate_homepage(content_dir: &Path, output_dir: &Path) -> Result<()> {
|
||||
fn generate_homepage(
|
||||
content_dir: &Path,
|
||||
output_dir: &Path,
|
||||
config: &config::SiteConfig,
|
||||
) -> Result<()> {
|
||||
let index_path = content_dir.join("_index.md");
|
||||
eprintln!("generating: homepage");
|
||||
|
||||
let content = Content::from_path(&index_path, ContentKind::Section)?;
|
||||
let html_body = render::markdown_to_html(&content.body);
|
||||
let page = templates::render_homepage(&content.frontmatter, &html_body, 0);
|
||||
let page = templates::render_homepage(&content.frontmatter, &html_body, "/index.html", config);
|
||||
|
||||
let out_path = output_dir.join("index.html");
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! HTML templates using maud.
|
||||
|
||||
use crate::config::SiteConfig;
|
||||
use crate::content::{Content, Frontmatter};
|
||||
use maud::{html, Markup, PreEscaped, DOCTYPE};
|
||||
|
||||
@@ -16,7 +17,13 @@ fn relative_prefix(depth: usize) -> String {
|
||||
}
|
||||
|
||||
/// Render a blog post with the base layout.
|
||||
pub fn render_post(frontmatter: &Frontmatter, content_html: &str, depth: usize) -> Markup {
|
||||
pub fn render_post(
|
||||
frontmatter: &Frontmatter,
|
||||
content_html: &str,
|
||||
page_path: &str,
|
||||
config: &SiteConfig,
|
||||
) -> Markup {
|
||||
let depth = page_path.matches('/').count();
|
||||
let prefix = relative_prefix(depth);
|
||||
base_layout(
|
||||
&frontmatter.title,
|
||||
@@ -43,12 +50,18 @@ pub fn render_post(frontmatter: &Frontmatter, content_html: &str, depth: usize)
|
||||
}
|
||||
}
|
||||
},
|
||||
depth,
|
||||
page_path,
|
||||
config,
|
||||
)
|
||||
}
|
||||
|
||||
/// Render a standalone page (about, collab, etc.)
|
||||
pub fn render_page(frontmatter: &Frontmatter, content_html: &str, depth: usize) -> Markup {
|
||||
pub fn render_page(
|
||||
frontmatter: &Frontmatter,
|
||||
content_html: &str,
|
||||
page_path: &str,
|
||||
config: &SiteConfig,
|
||||
) -> Markup {
|
||||
base_layout(
|
||||
&frontmatter.title,
|
||||
html! {
|
||||
@@ -59,12 +72,18 @@ pub fn render_page(frontmatter: &Frontmatter, content_html: &str, depth: usize)
|
||||
}
|
||||
}
|
||||
},
|
||||
depth,
|
||||
page_path,
|
||||
config,
|
||||
)
|
||||
}
|
||||
|
||||
/// Render the homepage.
|
||||
pub fn render_homepage(frontmatter: &Frontmatter, content_html: &str, depth: usize) -> Markup {
|
||||
pub fn render_homepage(
|
||||
frontmatter: &Frontmatter,
|
||||
content_html: &str,
|
||||
page_path: &str,
|
||||
config: &SiteConfig,
|
||||
) -> Markup {
|
||||
base_layout(
|
||||
&frontmatter.title,
|
||||
html! {
|
||||
@@ -78,12 +97,18 @@ pub fn render_homepage(frontmatter: &Frontmatter, content_html: &str, depth: usi
|
||||
(PreEscaped(content_html))
|
||||
}
|
||||
},
|
||||
depth,
|
||||
page_path,
|
||||
config,
|
||||
)
|
||||
}
|
||||
|
||||
/// Render the blog listing page.
|
||||
pub fn render_blog_index(title: &str, posts: &[Content], depth: usize) -> Markup {
|
||||
pub fn render_blog_index(
|
||||
title: &str,
|
||||
posts: &[Content],
|
||||
page_path: &str,
|
||||
config: &SiteConfig,
|
||||
) -> Markup {
|
||||
base_layout(
|
||||
title,
|
||||
html! {
|
||||
@@ -105,12 +130,18 @@ pub fn render_blog_index(title: &str, posts: &[Content], depth: usize) -> Markup
|
||||
}
|
||||
}
|
||||
},
|
||||
depth,
|
||||
page_path,
|
||||
config,
|
||||
)
|
||||
}
|
||||
|
||||
/// Render the projects page with cards.
|
||||
pub fn render_projects_index(title: &str, projects: &[Content], depth: usize) -> Markup {
|
||||
pub fn render_projects_index(
|
||||
title: &str,
|
||||
projects: &[Content],
|
||||
page_path: &str,
|
||||
config: &SiteConfig,
|
||||
) -> Markup {
|
||||
base_layout(
|
||||
title,
|
||||
html! {
|
||||
@@ -135,25 +166,30 @@ pub fn render_projects_index(title: &str, projects: &[Content], depth: usize) ->
|
||||
}
|
||||
}
|
||||
},
|
||||
depth,
|
||||
page_path,
|
||||
config,
|
||||
)
|
||||
}
|
||||
|
||||
/// Base HTML layout wrapper.
|
||||
fn base_layout(title: &str, content: Markup, depth: usize) -> Markup {
|
||||
fn base_layout(title: &str, content: Markup, page_path: &str, config: &SiteConfig) -> Markup {
|
||||
let depth = page_path.matches('/').count();
|
||||
let prefix = relative_prefix(depth);
|
||||
let canonical_url = format!("{}{}", config.base_url.trim_end_matches('/'), page_path);
|
||||
|
||||
html! {
|
||||
(DOCTYPE)
|
||||
html lang="en" {
|
||||
head {
|
||||
meta charset="utf-8";
|
||||
meta name="viewport" content="width=device-width, initial-scale=1";
|
||||
title { (title) " | nrd.sh" }
|
||||
title { (title) " | " (config.title) }
|
||||
link rel="canonical" href=(canonical_url);
|
||||
link rel="stylesheet" href=(format!("{}/style.css", prefix));
|
||||
}
|
||||
body {
|
||||
nav {
|
||||
a href=(format!("{}/index.html", prefix)) { "nrd.sh" }
|
||||
a href=(format!("{}/index.html", prefix)) { (config.title) }
|
||||
a href=(format!("{}/blog/index.html", prefix)) { "blog" }
|
||||
a href=(format!("{}/projects/index.html", prefix)) { "projects" }
|
||||
a href=(format!("{}/about.html", prefix)) { "about" }
|
||||
@@ -162,7 +198,7 @@ fn base_layout(title: &str, content: Markup, depth: usize) -> Markup {
|
||||
(content)
|
||||
}
|
||||
footer {
|
||||
p { "© nrdxp" }
|
||||
p { "© " (config.author) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user