feat(templates): add Tera runtime template engine

Lay groundwork for user-editable templates by adding Tera as a
runtime template engine alongside the existing maud templates.

Changes:
- Add tera dependency
- Create TemplateEngine struct with render methods
- Add TemplateLoad/TemplateRender error variants
- Add section_type/template fields to Frontmatter
- Create templates/ directory with base, page, section, and content templates

Dead code warnings are expected; TemplateEngine will be wired
in to replace maud in subsequent commits.
This commit is contained in:
Timothy DeHerrera
2026-01-31 14:59:49 -07:00
parent 1bf265f14b
commit 3df7fda26a
14 changed files with 647 additions and 1 deletions

283
Cargo.lock generated
View File

@@ -41,6 +41,15 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.100" version = "1.0.100"
@@ -53,6 +62,12 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]] [[package]]
name = "base64-simd" name = "base64-simd"
version = "0.7.0" version = "0.7.0"
@@ -114,6 +129,16 @@ dependencies = [
"syn 2.0.114", "syn 2.0.114",
] ]
[[package]]
name = "bstr"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
dependencies = [
"memchr",
"serde",
]
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.19.1" version = "3.19.1"
@@ -164,6 +189,39 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chrono"
version = "0.4.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
dependencies = [
"iana-time-zone",
"num-traits",
"windows-link",
]
[[package]]
name = "chrono-tz"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb"
dependencies = [
"chrono",
"chrono-tz-build",
"phf 0.11.3",
]
[[package]]
name = "chrono-tz-build"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1"
dependencies = [
"parse-zoneinfo",
"phf 0.11.3",
"phf_codegen 0.11.3",
]
[[package]] [[package]]
name = "const-str" name = "const-str"
version = "0.3.2" version = "0.3.2"
@@ -193,6 +251,12 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.17" version = "0.2.17"
@@ -339,6 +403,12 @@ dependencies = [
"matches", "matches",
] ]
[[package]]
name = "deunicode"
version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04"
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@@ -455,6 +525,30 @@ dependencies = [
"wasip2", "wasip2",
] ]
[[package]]
name = "globset"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
dependencies = [
"aho-corasick",
"bstr",
"log",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "globwalk"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
dependencies = [
"bitflags",
"ignore",
"walkdir",
]
[[package]] [[package]]
name = "graphlib_rust" name = "graphlib_rust"
version = "0.0.2" version = "0.0.2"
@@ -516,12 +610,61 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "humansize"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
dependencies = [
"libm",
]
[[package]]
name = "iana-time-zone"
version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "ident_case" name = "ident_case"
version = "1.0.1" version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "ignore"
version = "0.4.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a"
dependencies = [
"crossbeam-deque",
"globset",
"log",
"memchr",
"regex-automata",
"same-file",
"walkdir",
"winapi-util",
]
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.13.0" version = "2.13.0"
@@ -600,6 +743,12 @@ version = "0.2.180"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
[[package]]
name = "libm"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]] [[package]]
name = "lightningcss" name = "lightningcss"
version = "1.0.0-alpha.70" version = "1.0.0-alpha.70"
@@ -724,6 +873,7 @@ dependencies = [
"pulldown-cmark", "pulldown-cmark",
"serde", "serde",
"tempfile", "tempfile",
"tera",
"thiserror 2.0.18", "thiserror 2.0.18",
"toml 0.8.23", "toml 0.8.23",
"tree-sitter", "tree-sitter",
@@ -743,6 +893,15 @@ dependencies = [
"walkdir", "walkdir",
] ]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.3" version = "1.21.3"
@@ -804,6 +963,15 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "parse-zoneinfo"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24"
dependencies = [
"regex",
]
[[package]] [[package]]
name = "pastey" name = "pastey"
version = "0.1.1" version = "0.1.1"
@@ -816,6 +984,12 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]]
name = "percent-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]] [[package]]
name = "pest" name = "pest"
version = "2.8.5" version = "2.8.5"
@@ -964,6 +1138,15 @@ dependencies = [
"siphasher", "siphasher",
] ]
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]] [[package]]
name = "precomputed-hash" name = "precomputed-hash"
version = "0.1.1" version = "0.1.1"
@@ -1078,6 +1261,18 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [ dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core", "rand_core",
] ]
@@ -1086,6 +1281,9 @@ name = "rand_core"
version = "0.6.4" version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.17",
]
[[package]] [[package]]
name = "rapidhash" name = "rapidhash"
@@ -1338,6 +1536,16 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "slug"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724"
dependencies = [
"deunicode",
"wasm-bindgen",
]
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.15.1" version = "1.15.1"
@@ -1418,6 +1626,28 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "tera"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8004bca281f2d32df3bacd59bc67b312cb4c70cea46cbd79dbe8ac5ed206722"
dependencies = [
"chrono",
"chrono-tz",
"globwalk",
"humansize",
"lazy_static",
"percent-encoding",
"pest",
"pest_derive",
"rand",
"regex",
"serde",
"serde_json",
"slug",
"unicode-segmentation",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.69" version = "1.0.69"
@@ -1821,12 +2051,65 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]] [[package]]
name = "windows-link" name = "windows-link"
version = "0.2.1" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.61.2" version = "0.61.2"

View File

@@ -34,6 +34,7 @@ lightningcss = "1.0.0-alpha.70"
# Config parsing # Config parsing
katex-rs = "0.2.3" katex-rs = "0.2.3"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
tera = "1"
toml = "0.8" toml = "0.8"
# Diagram rendering # Diagram rendering

View File

@@ -2,6 +2,7 @@
use crate::error::{Error, Result}; use crate::error::{Error, Result};
use gray_matter::{engine::YAML, Matter}; use gray_matter::{engine::YAML, Matter};
use serde::Serialize;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -19,7 +20,7 @@ pub enum ContentKind {
} }
/// A navigation menu item discovered from the filesystem. /// A navigation menu item discovered from the filesystem.
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize)]
pub struct NavItem { pub struct NavItem {
/// Display label (from nav_label or title) /// Display label (from nav_label or title)
pub label: String, pub label: String,
@@ -42,6 +43,10 @@ pub struct Frontmatter {
pub link_to: Option<String>, pub link_to: Option<String>,
/// Custom navigation label (defaults to title) /// Custom navigation label (defaults to title)
pub nav_label: Option<String>, pub nav_label: Option<String>,
/// Section type for template dispatch (e.g., "blog", "projects")
pub section_type: Option<String>,
/// Override template for this content item
pub template: Option<String>,
} }
/// A content item ready for rendering. /// A content item ready for rendering.
@@ -134,6 +139,8 @@ fn parse_frontmatter(path: &Path, parsed: &gray_matter::ParsedEntity) -> Result<
let weight = pod.get("weight").and_then(|v| v.as_i64().ok()); let weight = pod.get("weight").and_then(|v| v.as_i64().ok());
let link_to = pod.get("link_to").and_then(|v| v.as_string().ok()); let link_to = pod.get("link_to").and_then(|v| v.as_string().ok());
let nav_label = pod.get("nav_label").and_then(|v| v.as_string().ok()); let nav_label = pod.get("nav_label").and_then(|v| v.as_string().ok());
let section_type = pod.get("section_type").and_then(|v| v.as_string().ok());
let template = pod.get("template").and_then(|v| v.as_string().ok());
// Handle nested taxonomies.tags structure // Handle nested taxonomies.tags structure
let tags = if let Some(taxonomies) = pod.get("taxonomies") { let tags = if let Some(taxonomies) = pod.get("taxonomies") {
@@ -162,6 +169,8 @@ fn parse_frontmatter(path: &Path, parsed: &gray_matter::ParsedEntity) -> Result<
weight, weight,
link_to, link_to,
nav_label, nav_label,
section_type,
template,
}) })
} }

View File

@@ -40,6 +40,14 @@ pub enum Error {
/// Failed to parse configuration file. /// Failed to parse configuration file.
#[error("invalid config in {path}: {message}")] #[error("invalid config in {path}: {message}")]
Config { path: PathBuf, message: String }, Config { path: PathBuf, message: String },
/// Failed to load templates.
#[error("failed to load templates: {message}")]
TemplateLoad { message: String },
/// Failed to render template.
#[error("failed to render template '{template}': {message}")]
TemplateRender { template: String, message: String },
} }
/// Result type alias for compiler operations. /// Result type alias for compiler operations.

View File

@@ -11,6 +11,7 @@ mod highlight;
mod math; mod math;
mod mermaid; mod mermaid;
mod render; mod render;
mod template_engine;
mod templates; mod templates;
use crate::content::{discover_nav, Content, ContentKind, NavItem}; use crate::content::{discover_nav, Content, ContentKind, NavItem};

199
src/template_engine.rs Normal file
View File

@@ -0,0 +1,199 @@
//! Tera-based template engine for runtime HTML generation.
use std::path::Path;
use serde::Serialize;
use tera::{Context, Tera};
use crate::config::SiteConfig;
use crate::content::{Content, NavItem};
use crate::error::{Error, Result};
/// Runtime template engine wrapping Tera.
pub struct TemplateEngine {
tera: Tera,
}
impl TemplateEngine {
/// Load templates from a directory (glob pattern: `templates/**/*`).
pub fn new(template_dir: &Path) -> Result<Self> {
let pattern = template_dir.join("**/*").display().to_string();
let tera = Tera::new(&pattern).map_err(|e| Error::TemplateLoad {
message: e.to_string(),
})?;
Ok(Self { tera })
}
/// Render a template by name with the given context.
pub fn render(&self, template_name: &str, context: &Context) -> Result<String> {
self.tera
.render(template_name, context)
.map_err(|e| Error::TemplateRender {
template: template_name.to_string(),
message: e.to_string(),
})
}
/// Render a standalone page (about, collab, etc.).
pub fn render_page(
&self,
content: &Content,
html_body: &str,
page_path: &str,
config: &SiteConfig,
nav: &[NavItem],
) -> Result<String> {
let mut ctx = self.base_context(page_path, config, nav);
ctx.insert("page", &FrontmatterContext::from(&content.frontmatter));
ctx.insert("content", html_body);
self.render("page.html", &ctx)
}
/// Render a content item (blog post, project, etc.).
pub fn render_content(
&self,
content: &Content,
html_body: &str,
page_path: &str,
config: &SiteConfig,
nav: &[NavItem],
) -> Result<String> {
let template = content
.frontmatter
.template
.as_deref()
.unwrap_or("content/default.html");
let mut ctx = self.base_context(page_path, config, nav);
ctx.insert("page", &FrontmatterContext::from(&content.frontmatter));
ctx.insert("content", html_body);
self.render(template, &ctx)
}
/// Render a section index page (blog index, projects index).
pub fn render_section(
&self,
section: &Content,
items: &[ContentContext],
page_path: &str,
config: &SiteConfig,
nav: &[NavItem],
) -> Result<String> {
let section_type = section
.frontmatter
.section_type
.as_deref()
.unwrap_or("default");
let template = format!("section/{}.html", section_type);
let mut ctx = self.base_context(page_path, config, nav);
ctx.insert("section", &FrontmatterContext::from(&section.frontmatter));
ctx.insert("items", items);
self.render(&template, &ctx)
}
/// Build base context with common variables.
fn base_context(&self, page_path: &str, config: &SiteConfig, nav: &[NavItem]) -> Context {
let mut ctx = Context::new();
ctx.insert("config", &ConfigContext::from(config));
ctx.insert("nav", nav);
ctx.insert("page_path", page_path);
ctx.insert("prefix", &relative_prefix(page_path));
ctx
}
}
/// Compute relative path prefix based on page depth.
fn relative_prefix(page_path: &str) -> String {
let depth = page_path.matches('/').count().saturating_sub(1);
if depth == 0 {
".".to_string()
} else {
(0..depth).map(|_| "..").collect::<Vec<_>>().join("/")
}
}
// ============================================================================
// Context structs for Tera serialization
// ============================================================================
/// Site config context for templates.
#[derive(Serialize)]
pub struct ConfigContext {
pub title: String,
pub author: String,
pub base_url: String,
}
impl From<&SiteConfig> for ConfigContext {
fn from(config: &SiteConfig) -> Self {
Self {
title: config.title.clone(),
author: config.author.clone(),
base_url: config.base_url.clone(),
}
}
}
/// Frontmatter context for templates.
#[derive(Serialize)]
pub struct FrontmatterContext {
pub title: String,
pub description: Option<String>,
pub date: Option<String>,
pub tags: Vec<String>,
pub weight: Option<i64>,
pub link_to: Option<String>,
}
impl From<&crate::content::Frontmatter> for FrontmatterContext {
fn from(fm: &crate::content::Frontmatter) -> Self {
Self {
title: fm.title.clone(),
description: fm.description.clone(),
date: fm.date.clone(),
tags: fm.tags.clone(),
weight: fm.weight,
link_to: fm.link_to.clone(),
}
}
}
/// Content item context for section listings.
#[derive(Serialize)]
pub struct ContentContext {
pub frontmatter: FrontmatterContext,
pub body: String,
pub slug: String,
pub path: String,
}
impl ContentContext {
pub fn from_content(content: &Content, content_dir: &Path) -> Self {
Self {
frontmatter: FrontmatterContext::from(&content.frontmatter),
body: content.body.clone(),
slug: content.slug.clone(),
path: format!("/{}", content.output_path(content_dir).display()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_relative_prefix_root() {
assert_eq!(relative_prefix("/index.html"), ".");
}
#[test]
fn test_relative_prefix_depth_1() {
assert_eq!(relative_prefix("/blog/index.html"), "..");
}
#[test]
fn test_relative_prefix_depth_2() {
assert_eq!(relative_prefix("/blog/posts/foo.html"), "../..");
}
}

25
templates/base.html Normal file
View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ title }} | {{ config.title }}</title>
<link rel="canonical" href="{{ config.base_url | trim_end_matches(pat='/') }}{{ page_path }}">
<link rel="alternate" type="application/atom+xml" title="Atom Feed" href="{{ config.base_url | trim_end_matches(pat='/') }}/feed.xml">
<link rel="stylesheet" href="{{ prefix }}/style.css">
</head>
<body>
<nav>
<a href="{{ prefix }}/index.html">{{ config.title }}</a>
{% for item in nav %}
<a href="{{ prefix }}{{ item.path }}">{{ item.label }}</a>
{% endfor %}
</nav>
<main>
{% block content %}{% endblock content %}
</main>
<footer>
<p>© {{ config.author }}</p>
</footer>
</body>
</html>

View File

@@ -0,0 +1,13 @@
{% extends "base.html" %}
{% block content %}
<article>
<h1>{{ page.title }}</h1>
{% if page.description %}
<p class="description">{{ page.description }}</p>
{% endif %}
<section class="content">
{{ content | safe }}
</section>
</article>
{% endblock content %}

View File

@@ -0,0 +1,25 @@
{% extends "base.html" %}
{% block content %}
<article class="post">
<header>
<h1>{{ page.title }}</h1>
{% if page.date %}
<time class="date">{{ page.date }}</time>
{% endif %}
{% if page.description %}
<p class="description">{{ page.description }}</p>
{% endif %}
{% if page.tags %}
<ul class="tags">
{% for tag in page.tags %}
<li><a href="{{ prefix }}/tags/{{ tag }}.html">{{ tag }}</a></li>
{% endfor %}
</ul>
{% endif %}
</header>
<section class="content">
{{ content | safe }}
</section>
</article>
{% endblock content %}

13
templates/homepage.html Normal file
View File

@@ -0,0 +1,13 @@
{% extends "base.html" %}
{% block content %}
<section class="hero">
<h1>{{ page.title }}</h1>
{% if page.description %}
<p class="tagline">{{ page.description }}</p>
{% endif %}
</section>
<section class="content">
{{ content | safe }}
</section>
{% endblock content %}

10
templates/page.html Normal file
View File

@@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block content %}
<article class="page">
<h1>{{ page.title }}</h1>
<section class="content">
{{ content | safe }}
</section>
</article>
{% endblock content %}

View File

@@ -0,0 +1,20 @@
{% extends "base.html" %}
{% block content %}
<h1>{{ section.title }}</h1>
<ul class="post-list">
{% for item in items %}
<li>
<a href="./{{ item.slug }}.html">
<span class="title">{{ item.frontmatter.title }}</span>
{% if item.frontmatter.date %}
<time class="date">{{ item.frontmatter.date }}</time>
{% endif %}
</a>
{% if item.frontmatter.description %}
<p class="description">{{ item.frontmatter.description }}</p>
{% endif %}
</li>
{% endfor %}
</ul>
{% endblock content %}

View File

@@ -0,0 +1,15 @@
{% extends "base.html" %}
{% block content %}
<h1>{{ section.title }}</h1>
<ul class="item-list">
{% for item in items %}
<li>
<a href="./{{ item.slug }}.html">{{ item.frontmatter.title }}</a>
{% if item.frontmatter.description %}
<p>{{ item.frontmatter.description }}</p>
{% endif %}
</li>
{% endfor %}
</ul>
{% endblock content %}

View File

@@ -0,0 +1,24 @@
{% extends "base.html" %}
{% block content %}
<h1>{{ section.title }}</h1>
<ul class="project-cards">
{% for item in items %}
<li class="card">
{% if item.frontmatter.link_to %}
<a href="{{ item.frontmatter.link_to }}" target="_blank" rel="noopener">
<h2>{{ item.frontmatter.title }}</h2>
{% if item.frontmatter.description %}
<p>{{ item.frontmatter.description }}</p>
{% endif %}
</a>
{% else %}
<h2>{{ item.frontmatter.title }}</h2>
{% if item.frontmatter.description %}
<p>{{ item.frontmatter.description }}</p>
{% endif %}
{% endif %}
</li>
{% endfor %}
</ul>
{% endblock content %}