Skip to content

Commit da2f59d

Browse files
brsonclaude
andcommitted
Fix breadcrumb navigation links on all item pages
Breadcrumbs on item pages (struct, enum, trait, function, etc.) linked to "#" instead of actual module URLs. Add shared build_breadcrumbs() that computes correct relative URLs for each ancestor module, use it from all 8 item renderers and the module renderer (removing duplicate Breadcrumb struct from module.rs), and update all item templates to use the new breadcrumbs variable. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f753af9 commit da2f59d

11 files changed

Lines changed: 115 additions & 50 deletions

File tree

crates/rustmax-rustdoc/src/render/item.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ pub fn render_struct(ctx: &RenderContext, item: &RenderableItem) -> AnyResult<St
6666
let impls = collect_impls(ctx, item.id, depth);
6767
tera_ctx.insert("impls", &impls);
6868
tera_ctx.insert("path_to_root", &path_to_root);
69+
tera_ctx.insert("breadcrumbs", &super::build_breadcrumbs(&item.path, depth));
6970

7071
// Sidebar HTML.
7172
let sidebar_html = super::sidebar::render_sidebar(ctx, &item.path, &path_to_root)?;
@@ -131,6 +132,7 @@ pub fn render_union(ctx: &RenderContext, item: &RenderableItem) -> AnyResult<Str
131132
let impls = collect_impls(ctx, item.id, depth);
132133
tera_ctx.insert("impls", &impls);
133134
tera_ctx.insert("path_to_root", &path_to_root);
135+
tera_ctx.insert("breadcrumbs", &super::build_breadcrumbs(&item.path, depth));
134136

135137
// Sidebar HTML.
136138
let sidebar_html = super::sidebar::render_sidebar(ctx, &item.path, &path_to_root)?;
@@ -172,6 +174,7 @@ pub fn render_function(ctx: &RenderContext, item: &RenderableItem) -> AnyResult<
172174
tera_ctx.insert("docs", &docs);
173175

174176
tera_ctx.insert("path_to_root", &path_to_root);
177+
tera_ctx.insert("breadcrumbs", &super::build_breadcrumbs(&item.path, depth));
175178

176179
// Sidebar HTML.
177180
let sidebar_html = super::sidebar::render_sidebar(ctx, &item.path, &path_to_root)?;
@@ -264,6 +267,7 @@ pub fn render_enum(ctx: &RenderContext, item: &RenderableItem) -> AnyResult<Stri
264267
let impls = collect_impls(ctx, item.id, depth);
265268
tera_ctx.insert("impls", &impls);
266269
tera_ctx.insert("path_to_root", &path_to_root);
270+
tera_ctx.insert("breadcrumbs", &super::build_breadcrumbs(&item.path, depth));
267271

268272
// Sidebar HTML.
269273
let sidebar_html = super::sidebar::render_sidebar(ctx, &item.path, &path_to_root)?;
@@ -366,6 +370,7 @@ pub fn render_trait(ctx: &RenderContext, item: &RenderableItem) -> AnyResult<Str
366370
tera_ctx.insert("implementors", &implementors);
367371

368372
tera_ctx.insert("path_to_root", &path_to_root);
373+
tera_ctx.insert("breadcrumbs", &super::build_breadcrumbs(&item.path, depth));
369374

370375
// Sidebar HTML.
371376
let sidebar_html = super::sidebar::render_sidebar(ctx, &item.path, &path_to_root)?;
@@ -407,6 +412,7 @@ pub fn render_type_alias(ctx: &RenderContext, item: &RenderableItem) -> AnyResul
407412
tera_ctx.insert("docs", &docs);
408413

409414
tera_ctx.insert("path_to_root", &path_to_root);
415+
tera_ctx.insert("breadcrumbs", &super::build_breadcrumbs(&item.path, depth));
410416

411417
// Sidebar HTML.
412418
let sidebar_html = super::sidebar::render_sidebar(ctx, &item.path, &path_to_root)?;
@@ -456,6 +462,7 @@ pub fn render_constant(ctx: &RenderContext, item: &RenderableItem) -> AnyResult<
456462
tera_ctx.insert("docs", &docs);
457463

458464
tera_ctx.insert("path_to_root", &path_to_root);
465+
tera_ctx.insert("breadcrumbs", &super::build_breadcrumbs(&item.path, depth));
459466

460467
// Sidebar HTML.
461468
let sidebar_html = super::sidebar::render_sidebar(ctx, &item.path, &path_to_root)?;
@@ -497,6 +504,7 @@ pub fn render_macro(ctx: &RenderContext, item: &RenderableItem) -> AnyResult<Str
497504
.unwrap_or_default();
498505
tera_ctx.insert("docs", &docs);
499506
tera_ctx.insert("path_to_root", &path_to_root);
507+
tera_ctx.insert("breadcrumbs", &super::build_breadcrumbs(&item.path, depth));
500508

501509
// Sidebar HTML.
502510
let sidebar_html = super::sidebar::render_sidebar(ctx, &item.path, &path_to_root)?;

crates/rustmax-rustdoc/src/render/mod.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,37 @@ impl<'a> RenderContext<'a> {
410410
}
411411
}
412412

413+
/// A breadcrumb navigation entry.
414+
#[derive(serde::Serialize)]
415+
pub struct Breadcrumb {
416+
pub name: String,
417+
pub url: Option<String>,
418+
}
419+
420+
/// Build breadcrumb entries for an item page.
421+
///
422+
/// For an item at path `["std", "thread", "spawn"]`, item pages live at
423+
/// `std/thread/fn.spawn.html` (depth = path.len() - 1 = 2). Each ancestor
424+
/// component links to its module's `index.html`.
425+
pub fn build_breadcrumbs(path: &[String], depth: usize) -> Vec<Breadcrumb> {
426+
path.iter().enumerate().map(|(i, name)| {
427+
let url = if i == path.len() - 1 {
428+
None
429+
} else {
430+
// Go up `depth` levels to root, then down to the ancestor module.
431+
let ancestor_path = &path[..=i];
432+
Some(format!(
433+
"{}{}index.html",
434+
"../".repeat(depth),
435+
ancestor_path.iter()
436+
.map(|p| format!("{}/", p))
437+
.collect::<String>(),
438+
))
439+
};
440+
Breadcrumb { name: name.clone(), url }
441+
}).collect()
442+
}
443+
413444
fn load_templates() -> AnyResult<Tera> {
414445
let mut tera = Tera::default();
415446

@@ -698,4 +729,46 @@ mod tests {
698729
Some(&"../../mycrate/thread/struct.JoinHandle.html".to_string()),
699730
);
700731
}
732+
733+
#[test]
734+
fn test_build_breadcrumbs_item_page() {
735+
// Item at std::thread::spawn, depth=2 (file at std/thread/fn.spawn.html).
736+
let path: Vec<String> = vec!["std", "thread", "spawn"]
737+
.into_iter().map(String::from).collect();
738+
let crumbs = build_breadcrumbs(&path, 2);
739+
740+
assert_eq!(crumbs.len(), 3);
741+
assert_eq!(crumbs[0].name, "std");
742+
assert_eq!(crumbs[0].url.as_deref(), Some("../../std/index.html"));
743+
assert_eq!(crumbs[1].name, "thread");
744+
assert_eq!(crumbs[1].url.as_deref(), Some("../../std/thread/index.html"));
745+
assert_eq!(crumbs[2].name, "spawn");
746+
assert_eq!(crumbs[2].url, None);
747+
}
748+
749+
#[test]
750+
fn test_build_breadcrumbs_module_page() {
751+
// Module at std::thread, depth=2 (file at std/thread/index.html).
752+
let path: Vec<String> = vec!["std", "thread"]
753+
.into_iter().map(String::from).collect();
754+
let crumbs = build_breadcrumbs(&path, 2);
755+
756+
assert_eq!(crumbs.len(), 2);
757+
assert_eq!(crumbs[0].name, "std");
758+
assert_eq!(crumbs[0].url.as_deref(), Some("../../std/index.html"));
759+
assert_eq!(crumbs[1].name, "thread");
760+
assert_eq!(crumbs[1].url, None);
761+
}
762+
763+
#[test]
764+
fn test_build_breadcrumbs_crate_root() {
765+
// Crate root module, depth=1 (file at std/index.html).
766+
let path: Vec<String> = vec!["std"]
767+
.into_iter().map(String::from).collect();
768+
let crumbs = build_breadcrumbs(&path, 1);
769+
770+
assert_eq!(crumbs.len(), 1);
771+
assert_eq!(crumbs[0].name, "std");
772+
assert_eq!(crumbs[0].url, None);
773+
}
701774
}

crates/rustmax-rustdoc/src/render/module.rs

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,8 @@ pub fn render_module(ctx: &RenderContext, tree: &ModuleTree) -> AnyResult<String
2020
.unwrap_or_default();
2121

2222
// Build breadcrumbs with URLs.
23-
let breadcrumbs: Vec<Breadcrumb> = path.iter().enumerate().map(|(i, name)| {
24-
let url = if i == path.len() - 1 {
25-
// Current page, no link.
26-
None
27-
} else {
28-
// Link to ancestor module: go up (path.len() - 1 - i) levels.
29-
let ups = path.len() - 1 - i;
30-
Some(format!("{}index.html", "../".repeat(ups)))
31-
};
32-
Breadcrumb { name: name.clone(), url }
33-
}).collect();
34-
tera_ctx.insert("breadcrumbs", &breadcrumbs);
23+
// For modules, depth = path.len() (file is at <path>/index.html).
24+
tera_ctx.insert("breadcrumbs", &super::build_breadcrumbs(&path, path.len()));
3525

3626
// Determine if this is the crate root (path has just the crate name).
3727
let is_crate_root = path.len() == 1;
@@ -272,12 +262,6 @@ struct ItemSummary {
272262
short_doc: String,
273263
}
274264

275-
#[derive(serde::Serialize)]
276-
struct Breadcrumb {
277-
name: String,
278-
url: Option<String>,
279-
}
280-
281265
/// Build HTML path to an item in its original crate.
282266
fn build_original_crate_path(crate_name: &str, item_name: &str, inner: &ItemEnum) -> String {
283267
let prefix = match inner {

crates/rustmax-rustdoc/src/templates/constant.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@
3737
<main>
3838
<header>
3939
<nav class="breadcrumbs">
40-
{% for part in item_path %}
41-
{% if loop.last %}
42-
<span>{{ part }}</span>
40+
{% for crumb in breadcrumbs %}
41+
{% if crumb.url %}
42+
<a href="{{ crumb.url }}">{{ crumb.name }}</a> ::
4343
{% else %}
44-
<a href="#">{{ part }}</a> ::
44+
<span>{{ crumb.name }}</span>
4545
{% endif %}
4646
{% endfor %}
4747
</nav>

crates/rustmax-rustdoc/src/templates/enum.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@
3737
<main>
3838
<header>
3939
<nav class="breadcrumbs">
40-
{% for part in item_path %}
41-
{% if loop.last %}
42-
<span>{{ part }}</span>
40+
{% for crumb in breadcrumbs %}
41+
{% if crumb.url %}
42+
<a href="{{ crumb.url }}">{{ crumb.name }}</a> ::
4343
{% else %}
44-
<a href="#">{{ part }}</a> ::
44+
<span>{{ crumb.name }}</span>
4545
{% endif %}
4646
{% endfor %}
4747
</nav>

crates/rustmax-rustdoc/src/templates/function.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@
3737
<main>
3838
<header>
3939
<nav class="breadcrumbs">
40-
{% for part in item_path %}
41-
{% if loop.last %}
42-
<span>{{ part }}</span>
40+
{% for crumb in breadcrumbs %}
41+
{% if crumb.url %}
42+
<a href="{{ crumb.url }}">{{ crumb.name }}</a> ::
4343
{% else %}
44-
<a href="#">{{ part }}</a> ::
44+
<span>{{ crumb.name }}</span>
4545
{% endif %}
4646
{% endfor %}
4747
</nav>

crates/rustmax-rustdoc/src/templates/macro.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@
3737
<main>
3838
<header>
3939
<nav class="breadcrumbs">
40-
{% for part in item_path %}
41-
{% if loop.last %}
42-
<span>{{ part }}</span>
40+
{% for crumb in breadcrumbs %}
41+
{% if crumb.url %}
42+
<a href="{{ crumb.url }}">{{ crumb.name }}</a> ::
4343
{% else %}
44-
<a href="#">{{ part }}</a> ::
44+
<span>{{ crumb.name }}</span>
4545
{% endif %}
4646
{% endfor %}
4747
</nav>

crates/rustmax-rustdoc/src/templates/struct.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@
3737
<main>
3838
<header>
3939
<nav class="breadcrumbs">
40-
{% for part in item_path %}
41-
{% if loop.last %}
42-
<span>{{ part }}</span>
40+
{% for crumb in breadcrumbs %}
41+
{% if crumb.url %}
42+
<a href="{{ crumb.url }}">{{ crumb.name }}</a> ::
4343
{% else %}
44-
<a href="#">{{ part }}</a> ::
44+
<span>{{ crumb.name }}</span>
4545
{% endif %}
4646
{% endfor %}
4747
</nav>

crates/rustmax-rustdoc/src/templates/trait.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@
3737
<main>
3838
<header>
3939
<nav class="breadcrumbs">
40-
{% for part in item_path %}
41-
{% if loop.last %}
42-
<span>{{ part }}</span>
40+
{% for crumb in breadcrumbs %}
41+
{% if crumb.url %}
42+
<a href="{{ crumb.url }}">{{ crumb.name }}</a> ::
4343
{% else %}
44-
<a href="#">{{ part }}</a> ::
44+
<span>{{ crumb.name }}</span>
4545
{% endif %}
4646
{% endfor %}
4747
</nav>

crates/rustmax-rustdoc/src/templates/type_alias.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@
3737
<main>
3838
<header>
3939
<nav class="breadcrumbs">
40-
{% for part in item_path %}
41-
{% if loop.last %}
42-
<span>{{ part }}</span>
40+
{% for crumb in breadcrumbs %}
41+
{% if crumb.url %}
42+
<a href="{{ crumb.url }}">{{ crumb.name }}</a> ::
4343
{% else %}
44-
<a href="#">{{ part }}</a> ::
44+
<span>{{ crumb.name }}</span>
4545
{% endif %}
4646
{% endfor %}
4747
</nav>

0 commit comments

Comments
 (0)