feat(nav): add anchor TOC in sidebar for toc-enabled pages

- Pass extracted anchors through template context
- Render anchor links under active page when page.toc is true
- Hierarchical indentation by heading level (h2-h6)
- CSS styling: smaller font, muted color, border-left indicator
- Add toc: true to templates.md as example
This commit is contained in:
Timothy DeHerrera
2026-02-01 09:37:01 -07:00
parent a59b8ff2ab
commit dcc98dccef
5 changed files with 76 additions and 6 deletions

View File

@@ -142,10 +142,16 @@ fn run(config_path: &Path) -> Result<()> {
// Render individual content pages for all sections
for item in &items {
eprintln!(" processing: {}", item.slug);
let (html_body, _anchors) = render::markdown_to_html(&item.body);
let (html_body, anchors) = render::markdown_to_html(&item.body);
let page_path = format!("/{}", item.output_path(&content_dir).display());
let html =
engine.render_content(item, &html_body, &page_path, &config, &manifest.nav)?;
let html = engine.render_content(
item,
&html_body,
&page_path,
&config,
&manifest.nav,
&anchors,
)?;
write_output(&output_dir, &content_dir, item, html)?;
}
@@ -259,9 +265,10 @@ fn process_pages(
eprintln!("processing: {}", path.display());
let content = Content::from_path(&path, ContentKind::Page)?;
let (html_body, _anchors) = render::markdown_to_html(&content.body);
let (html_body, anchors) = render::markdown_to_html(&content.body);
let page_path = format!("/{}", content.output_path(content_dir).display());
let html = engine.render_page(&content, &html_body, &page_path, config, nav)?;
let html =
engine.render_page(&content, &html_body, &page_path, config, nav, &anchors)?;
write_output(output_dir, content_dir, &content, html)?;
}
@@ -278,13 +285,14 @@ fn generate_homepage(
) -> Result<()> {
eprintln!("generating: homepage");
let (html_body, _anchors) = render::markdown_to_html(&manifest.homepage.body);
let (html_body, anchors) = render::markdown_to_html(&manifest.homepage.body);
let html = engine.render_page(
&manifest.homepage,
&html_body,
"/index.html",
config,
&manifest.nav,
&anchors,
)?;
let out_path = output_dir.join("index.html");

View File

@@ -8,6 +8,7 @@ use tera::{Context, Tera};
use crate::config::SiteConfig;
use crate::content::{Content, NavItem};
use crate::error::{Error, Result};
use crate::render::Anchor;
/// Runtime template engine wrapping Tera.
pub struct TemplateEngine {
@@ -40,11 +41,13 @@ impl TemplateEngine {
page_path: &str,
config: &SiteConfig,
nav: &[NavItem],
anchors: &[Anchor],
) -> Result<String> {
let mut ctx = self.base_context(page_path, config, nav);
ctx.insert("title", &content.frontmatter.title);
ctx.insert("page", &FrontmatterContext::from(&content.frontmatter));
ctx.insert("content", html_body);
ctx.insert("anchors", anchors);
self.render("page.html", &ctx)
}
@@ -56,6 +59,7 @@ impl TemplateEngine {
page_path: &str,
config: &SiteConfig,
nav: &[NavItem],
anchors: &[Anchor],
) -> Result<String> {
let template = content
.frontmatter
@@ -66,6 +70,7 @@ impl TemplateEngine {
ctx.insert("title", &content.frontmatter.title);
ctx.insert("page", &FrontmatterContext::from(&content.frontmatter));
ctx.insert("content", html_body);
ctx.insert("anchors", anchors);
self.render(template, &ctx)
}
@@ -145,6 +150,8 @@ pub struct FrontmatterContext {
pub tags: Vec<String>,
pub weight: Option<i64>,
pub link_to: Option<String>,
/// Enable table of contents (anchor nav in sidebar)
pub toc: bool,
}
impl From<&crate::content::Frontmatter> for FrontmatterContext {
@@ -156,6 +163,7 @@ impl From<&crate::content::Frontmatter> for FrontmatterContext {
tags: fm.tags.clone(),
weight: fm.weight,
link_to: fm.link_to.clone(),
toc: fm.toc.unwrap_or(false),
}
}
}