The Art of the URI: Crafting a Perfect Breadcrumb Component in Quarkus
Move beyond static links. This guide shows you how to build a smart, zero-configuration UI component that automatically generates perfect breadcrumbs from the page URL.
Modern web applications often rely on breadcrumbs to improve navigation and user experience. In a Quarkus application using Qute for templating, you can implement dynamic, URI-based breadcrumbs in a clean and reusable way without hardcoding anything. In this tutorial, you'll build a breadcrumb component that parses the current URI and renders the navigation trail automatically using Qute Template Extensions.
Let’s walk through each step of building it.
Create Your Quarkus Project and add the Required Extensions
Before writing any code, make sure you have Qute and REST support in your project. Create a new Quarkus project:
quarkus create app com.example:breadcrumb-navigation \
--extension=quarkus-qute,quarkus-rest
cd breadcrumb-navigation
And if you just want to try out the running example, go straight to my Github repository, leave a star, and try it out directly from there.
Create the Breadcrumb Generator Logic
The core logic lives in a Qute Template Extension. These are static methods that can be invoked from within templates.
File: src/main/java/com/example/qute/BreadcrumbExtensions.java
package com.example;
import java.util.ArrayList;
import java.util.List;
import io.quarkus.qute.TemplateExtension;
import jakarta.ws.rs.core.UriInfo;
@TemplateExtension
public class BreadcrumbExtensions {
public record Crumb(String label, String url, boolean isLast) {
}
public static List<Crumb> breadcrumbs(UriInfo uriInfo) {
List<Crumb> crumbs = new ArrayList<>();
String path = uriInfo.getPath();
String[] segments = path.replaceAll("^/|/$", "").split("/");
if (segments.length == 1 && segments[0].isEmpty()) {
crumbs.add(new Crumb("Home", "/", true));
return crumbs;
}
crumbs.add(new Crumb("Home", "/", false));
// Check if this is a product details page
boolean isProductDetailsPage = segments.length >= 3 &&
"products".equals(segments[0]) &&
"details".equals(segments[segments.length - 1]);
if (!isProductDetailsPage) {
// Fallback to original behavior for non-product pages
StringBuilder pathBuilder = new StringBuilder();
for (int i = 0; i < segments.length; i++) {
String segment = segments[i];
if (segment.isEmpty())
continue;
pathBuilder.append("/").append(segment);
boolean isLast = (i == segments.length - 1);
String label = capitalize(segment);
crumbs.add(new Crumb(label, pathBuilder.toString(), isLast));
}
} else {
// Special handling for product details pages
StringBuilder pathBuilder = new StringBuilder();
// Process all segments except "details"
for (int i = 0; i < segments.length - 1; i++) {
String segment = segments[i];
if (segment.isEmpty())
continue;
pathBuilder.append("/").append(segment);
boolean isLast = (i == segments.length - 2); // -2 because we're skipping "details"
String label = capitalize(segment);
String url;
if (i == 0 && "products".equals(segment)) {
// Products root - make it non-clickable by setting url to null
url = null;
} else if (i >= 1) {
// For product and category segments, create valid detail links
url = pathBuilder.toString() + "/details";
} else {
url = pathBuilder.toString();
}
crumbs.add(new Crumb(label, url, isLast));
}
}
return crumbs;
}
private static String capitalize(String segment) {
String[] words = segment.replace('-', ' ').split("\\s+");
StringBuilder result = new StringBuilder();
for (String word : words) {
if (!word.isEmpty()) {
result.append(Character.toUpperCase(word.charAt(0)))
.append(word.substring(1))
.append(" ");
}
}
return result.toString().trim();
}
}
Core Purpose
Qute Template Extension - Automatically generates breadcrumb navigation from URL paths
Dynamic breadcrumbs - No manual configuration needed, works with any URL structure
Key Components
Crumb record - Holds breadcrumb data: label, url, isLast flag
breadcrumbs() method - Main logic that converts URL paths into breadcrumb chains
capitalize() helper - Formats segment names (e.g., "some-product" → "Some Product")
How It Works
Splits URL path - /products/laptop/gaming/details → ["products", "laptop", "gaming", "details"]
Always starts with "Home" - Root breadcrumb pointing to /
Builds progressive paths - Each breadcrumb represents one level deeper in the hierarchy
Special Logic for Product Pages
Detects product detail URLs - Pages ending with /details under /products/
Excludes "details" from labels - Users don't see "Details" in breadcrumb text
Fixes intermediate links - Adds /details suffix so links like /products/laptop become /products/laptop/details
Makes "Products" non-clickable - Since /products endpoint doesn't exist
Template Integration
Used in templates via - {#for crumb in uriInfo.breadcrumbs}
Generates Bootstrap breadcrumbs - Properly styled navigation component
Handles clickable vs non-clickable - Links vs plain text based on URL availability
Create the Reusable Breadcrumb Component
Instead of duplicating the HTML in every template, we’ll define a reusable Qute tag in src/main/resources/templates/tags/breadcrumb.html
.
{@jakarta.ws.rs.core.UriInfo uriInfo}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
{#for crumb in uriInfo.breadcrumbs}
<li class="breadcrumb-item {#if crumb.isLast}active{/if}" {#if crumb.isLast}aria-current="page"{/if}>
{#if crumb.isLast}
{crumb.label}
{#else}
{#if crumb.url}
<a href="{crumb.url}">{crumb.label}</a>
{#else}
<span class="text-muted">{crumb.label}</span>
{/if}
{/if}
</li>
{/for}
</ol>
</nav>
This tag loops over the generated list and uses Bootstrap classes for nice styling. You can customize this output or replace Bootstrap with your own styles.
Integrate into a Base Layout
Now let’s include the breadcrumb component into a base layout so it automatically appears on every page.
File: src/main/resources/templates/base.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{#insert title}Untitled{/insert}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
{#include tags/breadcrumb /}
<hr/>
<main>
{#insert body}
Content goes here.
{/insert}
</main>
</div>
</body>
</html>
This layout inserts the breadcrumb tag before the page content, wrapped inside a Bootstrap container.
Use the Layout in a Page
Let’s now use this base layout in a sample page template.
File: src/main/resources/templates/detail.html
{#include base}
{#title}Product Details{/title}
{#body}
<h1>Dynamic Breadcrumb Navigation</h1>
<p>This page demonstrates dynamic breadcrumbs using Qute extensions.</p>
<div class="card mt-4">
<div class="card-header">
<h5>Current URL Information</h5>
</div>
<div class="card-body">
<p><strong>Full Path:</strong> <code>{uriInfo.path}</code></p>
<p><strong>Base URI:</strong> <code>{uriInfo.baseUri}</code></p>
<p><strong>Request URI:</strong> <code>{uriInfo.requestUri}</code></p>
{#if product}
<p><strong>Product:</strong> <span class="badge bg-primary">{product}</span></p>
{/if}
{#if categories}
<p><strong>Raw Categories:</strong> <code>{categories}</code></p>
{/if}
{#if categorySegments}
<p><strong>Category Segments:</strong>
{#for segment in categorySegments}
<span class="badge bg-light text-dark me-1">{segment}</span>
{/for}
</p>
{/if}
{#if additional}
<p><strong>Additional Segment:</strong> <span class="badge bg-secondary">{additional}</span></p>
{/if}
{#if cat1}
<p><strong>Category 1:</strong> <span class="badge bg-success">{cat1}</span></p>
{/if}
{#if cat2}
<p><strong>Category 2:</strong> <span class="badge bg-info">{cat2}</span></p>
{/if}
{#if cat3}
<p><strong>Category 3+:</strong> <span class="badge bg-warning">{cat3}</span></p>
{/if}
</div>
</div>
<div class="card mt-4">
<div class="card-header">
<h5>Test URLs</h5>
</div>
<div class="card-body">
<p>Try these URLs to test the dynamic breadcrumb navigation:</p>
<ul>
<li><a href="/products/laptop/details">/products/laptop/details</a> (1 segment)</li>
<li><a href="/products/smartphone/electronics/details">/products/smartphone/electronics/details</a> (2 segments)</li>
<li><a href="/products/gaming-chair/furniture/office/details">/products/gaming-chair/furniture/office/details</a> (3 segments)</li>
<li><a href="/products/monitor/electronics/displays/ultrawide/details">/products/monitor/electronics/displays/ultrawide/details</a> (4+ segments)</li>
<li><a href="/products/keyboard/mechanical/gaming/rgb/wireless/details">/products/keyboard/mechanical/gaming/rgb/wireless/details</a> (5+ segments)</li>
</ul>
</div>
</div>
{/body}
{/include}
Add an Endpoint to Test
To test this breadcrumb behavior, let’s wire a REST controller.
Rename the GreetingResource.java to: src/main/java/com/example/MyPageResource.java
package com.example;
import io.quarkus.qute.Template;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.UriInfo;
@Path("/products")
public class MyPageResource {
@Inject
Template detail;
// Handle the simple case with no categories: /products/{product}/details
@GET
@Path("/{product}/details")
@Produces(MediaType.TEXT_HTML)
public String showSimpleProductDetails(@PathParam("product") String product, @Context UriInfo uriInfo) {
return detail.data("uriInfo", uriInfo)
.data("product", product)
.data("additional", null)
.data("cat1", null)
.data("cat2", null)
.data("cat3", null)
.data("categories", "")
.data("categorySegments", new String[0])
.render();
}
// Single wildcard pattern to handle all URL variations with categories
@GET
@Path("/{product}/{categories: .+}/details")
@Produces(MediaType.TEXT_HTML)
public String showProductDetails(
@PathParam("product") String product,
@PathParam("categories") String categories,
@Context UriInfo uriInfo) {
// Parse the categories path into individual segments
String[] categorySegments = categories.split("/");
// Extract individual category variables (null if not present)
String additional = categorySegments.length > 0 ? categorySegments[0] : null;
String cat1 = categorySegments.length > 1 ? categorySegments[1] : null;
String cat2 = categorySegments.length > 2 ? categorySegments[2] : null;
String cat3 = categorySegments.length > 3 ? String.join("/",
java.util.Arrays.copyOfRange(categorySegments, 3, categorySegments.length)) : null;
return detail.data("uriInfo", uriInfo)
.data("product", product)
.data("additional", additional)
.data("cat1", cat1)
.data("cat2", cat2)
.data("cat3", cat3)
.data("categories", categories)
.data("categorySegments", categorySegments)
.render();
}
}
This injects the current URI into the Qute template, which your breadcrumb.html
component consumes.
JAX-RS REST endpoint - Handles HTTP requests for product detail pages
URL pattern matching - Routes /products/*/details requests to appropriate methods
Template rendering - Uses Qute templates to generate HTML responses
Simple Product Pages
Pattern: /products/{product}/details (e.g., /products/laptop/details)
Handles: Basic product pages with no categories
Passes: Product name + null values for all category variables
Complex Product Pages
Pattern: /products/{product}/{categories: .+}/details
Handles: Product pages with any number of category levels
Examples: /products/laptop/gaming/details, /products/phone/electronics/mobile/details
Dynamic URL Processing
Wildcard capture - Uses .+ regex to capture variable category segments
Path parsing - Splits category string on / to extract individual segments
Variable mapping - Maps segments to template variables (additional, cat1, cat2, cat3)
Overflow handling - Groups extra segments into cat3 when there are 4+ categories
Template Data Binding
Consistent variables - Always passes same set of variables to template (some may be null)
UriInfo injection - Provides current request context for breadcrumb generation
Debug data - Includes raw categories string and categorySegments array for debugging
Try It Out
Start the app:
quarkus dev
Then open http://localhost:8080/products/some-product/details.
Automatically generated from the URI.
Next Steps
Localization: Use
MessageBundles
to translate breadcrumb labels.Icons: Add optional icons or tooltips to the
Crumb
record.Authorization-aware Crumbs: Extend the
breadcrumbs()
method to filter based on user roles.Breadcrumb Metadata: Use annotations on JAX-RS resources to override segment labels with meaningful names.
This is a simple but powerful way to bring smarter navigation into your Quarkus web applications.