feat: implement relative paths for IPFS/decentralization

- src/templates.rs: Add depth parameter to all template functions.
  Add relative_prefix() helper that generates "./", "../", "../..", etc.
  All hrefs now use relative paths instead of absolute.
  Unit test for relative_prefix() added.

- src/main.rs: Pass depth to templates based on output location.
  Root=0, blog index/pages=1, blog posts=2.

Navigation works correctly when viewed via file:// protocol.
Site is now IPFS-compatible.
This commit is contained in:
Timothy DeHerrera
2026-01-24 21:22:03 -07:00
parent 7fa60d9b07
commit b9be21156d
2 changed files with 51 additions and 18 deletions

View File

@@ -78,7 +78,7 @@ fn process_blog_posts(content_dir: &Path, output_dir: &Path) -> Result<Vec<Conte
let content = Content::from_path(path, ContentKind::Post)?; let content = Content::from_path(path, ContentKind::Post)?;
let html_body = render::markdown_to_html(&content.body); let html_body = render::markdown_to_html(&content.body);
let page = templates::render_post(&content.frontmatter, &html_body); let page = templates::render_post(&content.frontmatter, &html_body, 2);
write_output(output_dir, content_dir, &content, page.into_string())?; write_output(output_dir, content_dir, &content, page.into_string())?;
posts.push(content); posts.push(content);
@@ -92,7 +92,7 @@ fn generate_blog_index(output_dir: &Path, posts: &[Content]) -> Result<()> {
let out_path = output_dir.join("blog/index.html"); let out_path = output_dir.join("blog/index.html");
eprintln!("generating: {}", out_path.display()); eprintln!("generating: {}", out_path.display());
let page = templates::render_blog_index("Blog", posts); let page = templates::render_blog_index("Blog", posts, 1);
fs::create_dir_all(out_path.parent().unwrap()).map_err(|e| Error::CreateDir { fs::create_dir_all(out_path.parent().unwrap()).map_err(|e| Error::CreateDir {
path: out_path.parent().unwrap().to_path_buf(), path: out_path.parent().unwrap().to_path_buf(),
@@ -117,7 +117,7 @@ fn process_pages(content_dir: &Path, output_dir: &Path) -> Result<()> {
let content = Content::from_path(&path, ContentKind::Page)?; let content = Content::from_path(&path, ContentKind::Page)?;
let html_body = render::markdown_to_html(&content.body); let html_body = render::markdown_to_html(&content.body);
let page = templates::render_page(&content.frontmatter, &html_body); let page = templates::render_page(&content.frontmatter, &html_body, 1);
write_output(output_dir, content_dir, &content, page.into_string())?; write_output(output_dir, content_dir, &content, page.into_string())?;
} }
@@ -150,7 +150,7 @@ fn generate_projects_index(output_dir: &Path, projects: &[Content]) -> Result<()
let out_path = output_dir.join("projects/index.html"); let out_path = output_dir.join("projects/index.html");
eprintln!("generating: {}", out_path.display()); eprintln!("generating: {}", out_path.display());
let page = templates::render_projects_index("Projects", projects); let page = templates::render_projects_index("Projects", projects, 1);
fs::create_dir_all(out_path.parent().unwrap()).map_err(|e| Error::CreateDir { fs::create_dir_all(out_path.parent().unwrap()).map_err(|e| Error::CreateDir {
path: out_path.parent().unwrap().to_path_buf(), path: out_path.parent().unwrap().to_path_buf(),
@@ -173,7 +173,7 @@ fn generate_homepage(content_dir: &Path, output_dir: &Path) -> Result<()> {
let content = Content::from_path(&index_path, ContentKind::Section)?; let content = Content::from_path(&index_path, ContentKind::Section)?;
let html_body = render::markdown_to_html(&content.body); let html_body = render::markdown_to_html(&content.body);
let page = templates::render_homepage(&content.frontmatter, &html_body); let page = templates::render_homepage(&content.frontmatter, &html_body, 0);
let out_path = output_dir.join("index.html"); let out_path = output_dir.join("index.html");

View File

@@ -3,8 +3,21 @@
use crate::content::{Content, Frontmatter}; use crate::content::{Content, Frontmatter};
use maud::{html, Markup, PreEscaped, DOCTYPE}; 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::<Vec<_>>().join("/")
}
}
/// Render a blog post with the base layout. /// Render a blog post with the base layout.
pub fn render_post(frontmatter: &Frontmatter, content_html: &str) -> Markup { pub fn render_post(frontmatter: &Frontmatter, content_html: &str, depth: usize) -> Markup {
let prefix = relative_prefix(depth);
base_layout( base_layout(
&frontmatter.title, &frontmatter.title,
html! { html! {
@@ -20,7 +33,7 @@ pub fn render_post(frontmatter: &Frontmatter, content_html: &str) -> Markup {
@if !frontmatter.tags.is_empty() { @if !frontmatter.tags.is_empty() {
ul.tags { ul.tags {
@for tag in &frontmatter.tags { @for tag in &frontmatter.tags {
li { a href=(format!("/tags/{}/", tag)) { (tag) } } li { a href=(format!("{}/tags/{}/", prefix, tag)) { (tag) } }
} }
} }
} }
@@ -30,11 +43,12 @@ pub fn render_post(frontmatter: &Frontmatter, content_html: &str) -> Markup {
} }
} }
}, },
depth,
) )
} }
/// Render a standalone page (about, collab, etc.) /// Render a standalone page (about, collab, etc.)
pub fn render_page(frontmatter: &Frontmatter, content_html: &str) -> Markup { pub fn render_page(frontmatter: &Frontmatter, content_html: &str, depth: usize) -> Markup {
base_layout( base_layout(
&frontmatter.title, &frontmatter.title,
html! { html! {
@@ -45,11 +59,12 @@ pub fn render_page(frontmatter: &Frontmatter, content_html: &str) -> Markup {
} }
} }
}, },
depth,
) )
} }
/// Render the homepage. /// Render the homepage.
pub fn render_homepage(frontmatter: &Frontmatter, content_html: &str) -> Markup { pub fn render_homepage(frontmatter: &Frontmatter, content_html: &str, depth: usize) -> Markup {
base_layout( base_layout(
&frontmatter.title, &frontmatter.title,
html! { html! {
@@ -63,11 +78,12 @@ pub fn render_homepage(frontmatter: &Frontmatter, content_html: &str) -> Markup
(PreEscaped(content_html)) (PreEscaped(content_html))
} }
}, },
depth,
) )
} }
/// Render the blog listing page. /// Render the blog listing page.
pub fn render_blog_index(title: &str, posts: &[Content]) -> Markup { pub fn render_blog_index(title: &str, posts: &[Content], depth: usize) -> Markup {
base_layout( base_layout(
title, title,
html! { html! {
@@ -75,7 +91,8 @@ pub fn render_blog_index(title: &str, posts: &[Content]) -> Markup {
ul.post-list { ul.post-list {
@for post in posts { @for post in posts {
li { li {
a href=(format!("/blog/{}/", post.slug)) { // Posts are siblings in the same directory
a href=(format!("./{}/", post.slug)) {
span.title { (post.frontmatter.title) } span.title { (post.frontmatter.title) }
@if let Some(ref date) = post.frontmatter.date { @if let Some(ref date) = post.frontmatter.date {
time.date { (date) } time.date { (date) }
@@ -88,11 +105,12 @@ pub fn render_blog_index(title: &str, posts: &[Content]) -> Markup {
} }
} }
}, },
depth,
) )
} }
/// Render the projects page with cards. /// Render the projects page with cards.
pub fn render_projects_index(title: &str, projects: &[Content]) -> Markup { pub fn render_projects_index(title: &str, projects: &[Content], depth: usize) -> Markup {
base_layout( base_layout(
title, title,
html! { html! {
@@ -117,11 +135,13 @@ pub fn render_projects_index(title: &str, projects: &[Content]) -> Markup {
} }
} }
}, },
depth,
) )
} }
/// Base HTML layout wrapper. /// Base HTML layout wrapper.
fn base_layout(title: &str, content: Markup) -> Markup { fn base_layout(title: &str, content: Markup, depth: usize) -> Markup {
let prefix = relative_prefix(depth);
html! { html! {
(DOCTYPE) (DOCTYPE)
html lang="en" { html lang="en" {
@@ -129,14 +149,14 @@ fn base_layout(title: &str, content: Markup) -> Markup {
meta charset="utf-8"; meta charset="utf-8";
meta name="viewport" content="width=device-width, initial-scale=1"; meta name="viewport" content="width=device-width, initial-scale=1";
title { (title) " | nrd.sh" } title { (title) " | nrd.sh" }
link rel="stylesheet" href="/style.css"; link rel="stylesheet" href=(format!("{}/style.css", prefix));
} }
body { body {
nav { nav {
a href="/" { "nrd.sh" } a href=(format!("{}/", prefix)) { "nrd.sh" }
a href="/blog/" { "blog" } a href=(format!("{}/blog/", prefix)) { "blog" }
a href="/projects/" { "projects" } a href=(format!("{}/projects/", prefix)) { "projects" }
a href="/about/" { "about" } a href=(format!("{}/about/", prefix)) { "about" }
} }
main { main {
(content) (content)
@@ -148,3 +168,16 @@ fn base_layout(title: &str, content: Markup) -> Markup {
} }
} }
} }
#[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), "../../..");
}
}