2424
2525_REGISTERED_DOC_ROUTES : set [str ] = set ()
2626
27+ # Title-cased breadcrumb labels that should be displayed as acronyms.
28+ _BREADCRUMB_LABEL_OVERRIDES : dict [str , str ] = {
29+ "Ai" : "AI" ,
30+ "Api" : "API" ,
31+ "Sdk" : "SDK" ,
32+ "Cli" : "CLI" ,
33+ "Css" : "CSS" ,
34+ }
35+
2736
2837def _normalize_doc_route (path : str ) -> str :
2938 """Normalize a docs route to use leading and trailing slashes."""
@@ -38,8 +47,24 @@ def _register_doc_route(path: str) -> None:
3847
3948def _resolve_breadcrumb_href (
4049 href : str , registered_routes : Collection [str ] | None = None
41- ) -> str :
42- """Resolve a generated breadcrumb href to a registered docs route."""
50+ ) -> str | None :
51+ """Resolve a generated breadcrumb href to a registered docs route.
52+
53+ Breadcrumbs are built from path segments, but intermediate segments (e.g.
54+ ``/ai`` or ``/hosting``) are often just categories with no page of their
55+ own. This returns the matching route, preferring an ``overview`` child when
56+ the bare path is not itself a page, or ``None`` when no registered route
57+ exists so the caller can render the segment as non-clickable text instead of
58+ a broken link.
59+
60+ Args:
61+ href: The generated, app-relative breadcrumb href (no ``/docs`` prefix).
62+ registered_routes: Routes to match against. Defaults to the routes
63+ registered through the docpage template.
64+
65+ Returns:
66+ The resolved route, or ``None`` if no registered route matches.
67+ """
4368 routes = _REGISTERED_DOC_ROUTES if registered_routes is None else registered_routes
4469 route = _normalize_doc_route (href )
4570 if route in routes :
@@ -49,7 +74,7 @@ def _resolve_breadcrumb_href(
4974 if overview_route in routes :
5075 return overview_route
5176
52- return href
77+ return None
5378
5479
5580class FeedbackState (rx .State ):
@@ -678,17 +703,31 @@ def breadcrumb(path: str, nav_sidebar: rx.Component, doc_content: str | None = N
678703 for i , segment in enumerate (segments ):
679704 current_path += f"/{ segment } "
680705
681- # Add the breadcrumb item to the list
682- breadcrumbs .append (
683- rx .el .a (
684- to_title_case (to_snake_case (segment ), sep = " " ),
685- class_name = "min-h-8 flex items-center text-sm font-[525] text-m-slate-12 dark:text-m-slate-3 last:text-m-slate-7 dark:last:text-m-slate-6 hover:text-primary-10 dark:hover:text-primary-9"
686- + (" truncate" if i == len (segments ) - 1 else "" ),
687- underline = "none" ,
688- href = _resolve_breadcrumb_href (current_path ),
689- )
706+ label = to_title_case (to_snake_case (segment ), sep = " " )
707+ label = _BREADCRUMB_LABEL_OVERRIDES .get (label , label )
708+ base_class = ui .cn (
709+ "min-h-8 flex items-center text-sm font-[525] text-m-slate-12 dark:text-m-slate-3 last:text-m-slate-7 dark:last:text-m-slate-6" ,
710+ "truncate" if i == len (segments ) - 1 else "" ,
690711 )
691712
713+ # Category segments (e.g. /ai, /hosting) often have no page of their own.
714+ # Render those as plain text so the breadcrumb doesn't link to a 404.
715+ href = _resolve_breadcrumb_href (current_path )
716+ if href is None :
717+ breadcrumbs .append (rx .el .span (label , class_name = base_class ))
718+ else :
719+ breadcrumbs .append (
720+ rx .el .a (
721+ label ,
722+ class_name = ui .cn (
723+ base_class ,
724+ "hover:text-primary-10 dark:hover:text-primary-9" ,
725+ ),
726+ underline = "none" ,
727+ href = href ,
728+ )
729+ )
730+
692731 # If it's not the last segment, add a separator
693732 if i < len (segments ) - 1 :
694733 breadcrumbs .append (
0 commit comments