Accessible breadcrumb navigation backed by GET /v1/seo/breadcrumb. Watches lng/lat and renders linked crumbs when base_url is set.
No breadcrumb for this point.
bunx shadcn@latest add https://mapbase.dev/r/seo-breadcrumb-nav.jsonThe shadcn CLI copies the file(s) below into your tree, installs npm dependencies, and recursively pulls registry dependencies (including from cross-registry URLs).
None.
"use client"
import * as React from "react"
import {
Mapbase,
MapbaseError,
type CountryCode,
type SeoBreadcrumbResponse,
} from "mapbase"
import { ChevronRight } from "lucide-react"
import { cn } from "@/lib/utils"
export type SeoBreadcrumbNavProps = {
apiKey: string
baseUrl?: string
lng: number
lat: number
country?: CountryCode
/** Site origin for absolute links (e.g. https://example.com). */
siteBaseUrl?: string
separator?: string
debounceMs?: number
disabled?: boolean
className?: string
}
/**
* Accessible breadcrumb navigation backed by `GET /v1/seo/breadcrumb`.
*/
export function SeoBreadcrumbNav({
apiKey,
baseUrl,
lng,
lat,
country,
siteBaseUrl,
debounceMs = 300,
disabled,
className,
}: SeoBreadcrumbNavProps) {
const [data, setData] = React.useState<SeoBreadcrumbResponse | null>(null)
const [loading, setLoading] = React.useState(false)
const [error, setError] = React.useState<MapbaseError | null>(null)
const client = React.useMemo(
() => new Mapbase(apiKey, baseUrl ? { baseUrl } : undefined),
[apiKey, baseUrl],
)
const countryKey = country ?? ""
React.useEffect(() => {
if (disabled) return
if (!Number.isFinite(lng) || !Number.isFinite(lat)) {
setData(null)
setError(null)
setLoading(false)
return
}
const controller = new AbortController()
const timer = window.setTimeout(async () => {
setLoading(true)
setError(null)
const response = await client.seo.breadcrumb(
{
lng,
lat,
...(country ? { country } : {}),
...(siteBaseUrl ? { base_url: siteBaseUrl } : {}),
},
{ signal: controller.signal },
)
if (controller.signal.aborted) return
if (response.error) {
setError(response.error)
setData(null)
} else {
setData(response.data)
}
setLoading(false)
}, debounceMs)
return () => {
controller.abort()
window.clearTimeout(timer)
}
}, [client, lng, lat, countryKey, siteBaseUrl, debounceMs, disabled])
if (loading) {
return (
<p className={cn("text-xs text-muted-foreground", className)}>
Loading breadcrumb…
</p>
)
}
if (error) {
return (
<p className={cn("text-xs text-destructive", className)} role="alert">
{error.message}
</p>
)
}
if (!data || data.items.length === 0) {
return (
<p className={cn("text-xs text-muted-foreground", className)}>
No breadcrumb for this point.
</p>
)
}
return (
<nav aria-label="Breadcrumb" className={cn("text-sm", className)}>
<ol className="flex flex-wrap items-center gap-1">
{data.items.map((item, index) => {
const isLast = index === data.items.length - 1
return (
<li key={item.id} className="inline-flex items-center gap-1">
{index > 0 ? (
<ChevronRight
className="size-3.5 shrink-0 text-muted-foreground"
aria-hidden
/>
) : null}
{item.href && !isLast ? (
<a
href={item.href}
className="text-muted-foreground hover:text-foreground"
>
{item.name}
</a>
) : (
<span
className={isLast ? "font-medium" : "text-muted-foreground"}
aria-current={isLast ? "page" : undefined}
>
{item.name}
</span>
)}
</li>
)
})}
</ol>
</nav>
)
}