Appearance
Are you an LLM? You can read better optimized documentation at /features/breadcrumb_navigation.md for this page in Markdown format
Breadcrumb navigation
This document provides a detailed technical overview of the breadcrumb navigation system within the application. The breadcrumbs provide users with contextual awareness of their location within the site's hierarchy and are dynamically generated on each request.
Overview
The breadcrumb navigation system is implemented as a piece of custom ASP.NET Core middleware that intercepts incoming requests, analyzes the URL path, and constructs a hierarchical list of navigation links. This list is then made available to the view layer for rendering.
The system is designed to be automatic for most routes, deriving breadcrumb labels from the URL path segments themselves. For dynamic routes, such as pages whose titles are fetched from the Contentful CMS, a manager service is provided to allow for programmatic customization of the breadcrumb labels.
The key components of this system are:
BreadcrumbsMiddleware: The core logic for generating breadcrumbs from the request path.IBreadcrumbsManager: A service for modifying the generated breadcrumbs, typically used in controllers or Razor Pages to insert dynamic data.Breadcrumbs.cshtml: A shared partial view responsible for rendering the final HTML.
Breadcrumb Middleware
The heart of the system is the Breadcrumbs class, which functions as custom ASP.NET Core middleware. It is registered in the application's request pipeline in Startup.cs via the UseBreadcrumbs() extension method.
See: /configuration/middleware.md
csharp
// In Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// ... other middleware
app.UseBreadcrumbs();
app.UseDeeplinks();
app.UseMvc(routes =>
// ...
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
Execution Flow
On every HTTP request, the Invoke method of the Breadcrumbs middleware performs the following steps:
- Path Segmentation: The request path (e.g.,
/courses/hello-contentful/lessons) is split into its constituent parts ("courses","hello-contentful","lessons"). - Home Crumb: A root "Home" breadcrumb pointing to
/is always added first. The label "Home" is retrieved usingIViewLocalizerto ensure it is correctly translated for the current culture. - Iterative Generation: The middleware iterates through each path segment. For each segment:
- A cumulative path is built (e.g.,
/courses, then/courses/hello-contentful). - A label is generated. The middleware first transforms the segment (e.g., "hello-contentful" -> "hello contentful") and then attempts to find a corresponding localization key (e.g.,
coursesLabel). If a key is found, the localized value is used; otherwise, the transformed segment is used as the label.
- A cumulative path is built (e.g.,
- Context Storage: The final
List<Breadcrumb>is stored in theHttpContext.Itemscollection with the key"breadcrumbs". This makes the list available for the remainder of the request's lifetime, accessible by both downstream services and the view layer.
csharp
// TheExampleApp/Configuration/Breadcrumbs.cs
public class Breadcrumbs
{
private readonly RequestDelegate _next;
private readonly IViewLocalizer _localizer;
// ... constructor ...
public async Task Invoke(HttpContext context)
{
var path = context.Request.Path;
var parts = path.ToString().Split("/", StringSplitOptions.RemoveEmptyEntries);
var items = new List<Breadcrumb>();
items.Add(new Breadcrumb { Label = _localizer["homeLabel"].Value, Path = "/" });
var translations = _localizer.GetAllStrings(false);
foreach (var part in parts)
{
var label = part.Replace("-", " ");
if(translations.Any(c => c.Name == $"{LowerCaseFirstChar(label)}Label"))
{
label = _localizer[$"{LowerCaseFirstChar(label)}Label"].Value;
}
items.Add(new Breadcrumb { Label = label, Path = $"/{string.Join('/', parts.Take(Array.IndexOf(parts, part) + 1))}" });
}
context.Items["breadcrumbs"] = items;
await _next(context);
}
// ...
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Route-based Generation
The default behavior of the middleware is to generate breadcrumbs based on the URL structure. This works well for static and semi-static routes.
| Request Path | Generated Breadcrumbs (before customization) |
|---|---|
/ | Home |
/courses | Home > Courses |
/courses/some-course-slug | Home > Courses > some course slug |
/courses/some-course-slug/lessons | Home > Courses > some course slug > lessons |
Localization Logic
The system attempts to localize path segments by creating a key with a Label suffix, where the first character of the transformed label is lowercased. For example, the path segment courses (after hyphen replacement) is transformed into the localization key coursesLabel. This key is then looked up in the active locale's JSON resource file (e.g., en-US.json).
Gotcha: If a path segment does not have a corresponding localization key, it will be rendered directly after replacing hyphens with spaces. This is why dynamic segments like course slugs (some-course-slug) require customization.
Breadcrumbs Manager
For dynamic routes, the auto-generated labels (e.g., "hello-contentful") are not user-friendly. The IBreadcrumbsManager service provides a mechanism to modify the breadcrumb list after it has been generated by the middleware but before it is rendered by the view.
The service is registered for dependency injection in Startup.cs and can be injected into any Controller or Razor PageModel.
See: /architecture/dependency_injection.md
csharp
// In Startup.cs
services.AddTransient<IBreadcrumbsManager, BreadcrumbsManager>();1
2
2
The manager exposes a single method, ReplaceCrumbForSlug, which finds a breadcrumb by its initial label (the URL slug) and updates it with a new, more descriptive label.
csharp
// TheExampleApp/Configuration/Breadcrumbs.cs
public interface IBreadcrumbsManager
{
void ReplaceCrumbForSlug(string slug, string label);
}
public class BreadcrumbsManager : IBreadcrumbsManager
{
private readonly IHttpContextAccessor _accessor;
private List<Breadcrumb> _crumbs;
public BreadcrumbsManager(IHttpContextAccessor accessor)
{
_accessor = accessor;
_crumbs = accessor.HttpContext.Items["breadcrumbs"] as List<Breadcrumb>;
}
public void ReplaceCrumbForSlug(string slug, string label)
{
if(_crumbs.Any(c => c.Label == slug))
{
_crumbs.First(c => c.Label == slug).Label = label;
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Customization
To provide user-friendly labels for dynamic content (e.g., course titles from Contentful), you must use the IBreadcrumbsManager.
Example Usage
Imagine a Razor Page for displaying a course, located at /Courses/{slug}. The OnGetAsync handler would fetch the course from Contentful and then use the manager to update the breadcrumb.
csharp
// Hypothetical example for a PageModel
public class CourseModel : PageModel
{
private readonly IContentfulClient _client;
private readonly IBreadcrumbsManager _breadcrumbsManager;
public CourseModel(IContentfulClient client, IBreadcrumbsManager breadcrumbsManager)
{
_client = client;
_breadcrumbsManager = breadcrumbsManager;
}
public async Task<IActionResult> OnGetAsync(string slug)
{
// 1. Fetch the dynamic content from Contentful
var course = await _client.GetEntryBySlug<Course>(slug);
if (course == null) return NotFound();
// 2. Use the manager to replace the slug-based label with the fetched title
// The middleware would have created a label "hello contentful" from the slug "hello-contentful".
_breadcrumbsManager.ReplaceCrumbForSlug(slug.Replace("-", " "), course.Title);
// ... rest of the handler logic
return Page();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Breadcrumb Rendering
The final step is rendering the breadcrumbs in the UI. This is handled by the shared partial view Views/Shared/Breadcrumbs.cshtml. This view is intended to be included in the layout or specific pages where breadcrumbs are required.
See: /views/layout.md
The view retrieves the List<Breadcrumb> from Context.Items and renders it as an unordered list. If no breadcrumbs are found in the context, it renders nothing.
cshtml
@* TheExampleApp/Views/Shared/Breadcrumbs.cshtml *@
@{
var crumbs = Context.Items["breadcrumbs"] as List<TheExampleApp.Configuration.Breadcrumb>;
}
@if (crumbs != null)
{
<nav class="breadcrumb">
<ul>
@foreach (var crumb in crumbs)
{
<li><a href="@crumb.Path">@crumb.Label</a></li>
}
</ul>
</nav>
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16