//! HTML templates using maud.
use crate::content::{Content, Frontmatter};
use maud::{html, Markup, PreEscaped, DOCTYPE};
/// Compute relative path prefix based on page depth.
/// depth=0 (root) → "."
/// depth=1 (one level deep) → ".."
/// depth=2 → "../.."
fn relative_prefix(depth: usize) -> String {
if depth == 0 {
".".to_string()
} else {
(0..depth).map(|_| "..").collect::>().join("/")
}
}
/// Render a blog post with the base layout.
pub fn render_post(frontmatter: &Frontmatter, content_html: &str, depth: usize) -> Markup {
let prefix = relative_prefix(depth);
base_layout(
&frontmatter.title,
html! {
article.post {
header {
h1 { (frontmatter.title) }
@if let Some(ref date) = frontmatter.date {
time.date { (date) }
}
@if let Some(ref desc) = frontmatter.description {
p.description { (desc) }
}
@if !frontmatter.tags.is_empty() {
ul.tags {
@for tag in &frontmatter.tags {
li { a href=(format!("{}/tags/{}/", prefix, tag)) { (tag) } }
}
}
}
}
section.content {
(PreEscaped(content_html))
}
}
},
depth,
)
}
/// Render a standalone page (about, collab, etc.)
pub fn render_page(frontmatter: &Frontmatter, content_html: &str, depth: usize) -> Markup {
base_layout(
&frontmatter.title,
html! {
article.page {
h1 { (frontmatter.title) }
section.content {
(PreEscaped(content_html))
}
}
},
depth,
)
}
/// Render the homepage.
pub fn render_homepage(frontmatter: &Frontmatter, content_html: &str, depth: usize) -> Markup {
base_layout(
&frontmatter.title,
html! {
section.hero {
h1 { (frontmatter.title) }
@if let Some(ref desc) = frontmatter.description {
p.tagline { (desc) }
}
}
section.content {
(PreEscaped(content_html))
}
},
depth,
)
}
/// Render the blog listing page.
pub fn render_blog_index(title: &str, posts: &[Content], depth: usize) -> Markup {
base_layout(
title,
html! {
h1 { (title) }
ul.post-list {
@for post in posts {
li {
// Posts are siblings in the same directory
a href=(format!("./{}/", post.slug)) {
span.title { (post.frontmatter.title) }
@if let Some(ref date) = post.frontmatter.date {
time.date { (date) }
}
}
@if let Some(ref desc) = post.frontmatter.description {
p.description { (desc) }
}
}
}
}
},
depth,
)
}
/// Render the projects page with cards.
pub fn render_projects_index(title: &str, projects: &[Content], depth: usize) -> Markup {
base_layout(
title,
html! {
h1 { (title) }
ul.project-cards {
@for project in projects {
li.card {
@if let Some(ref link) = project.frontmatter.link_to {
a href=(link) target="_blank" rel="noopener" {
h2 { (project.frontmatter.title) }
@if let Some(ref desc) = project.frontmatter.description {
p { (desc) }
}
}
} @else {
h2 { (project.frontmatter.title) }
@if let Some(ref desc) = project.frontmatter.description {
p { (desc) }
}
}
}
}
}
},
depth,
)
}
/// Base HTML layout wrapper.
fn base_layout(title: &str, content: Markup, depth: usize) -> Markup {
let prefix = relative_prefix(depth);
html! {
(DOCTYPE)
html lang="en" {
head {
meta charset="utf-8";
meta name="viewport" content="width=device-width, initial-scale=1";
title { (title) " | nrd.sh" }
link rel="stylesheet" href=(format!("{}/style.css", prefix));
}
body {
nav {
a href=(format!("{}/", prefix)) { "nrd.sh" }
a href=(format!("{}/blog/", prefix)) { "blog" }
a href=(format!("{}/projects/", prefix)) { "projects" }
a href=(format!("{}/about/", prefix)) { "about" }
}
main {
(content)
}
footer {
p { "© nrdxp" }
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_relative_prefix() {
assert_eq!(relative_prefix(0), ".");
assert_eq!(relative_prefix(1), "..");
assert_eq!(relative_prefix(2), "../..");
assert_eq!(relative_prefix(3), "../../..");
}
}