"use client" // Use the locale-aware Link + usePathname from next-intl. With the // plain `next/link` and `next/navigation` imports the hrefs were // emitted without a locale (404s) AND the active-page detection // failed because `pathname` carried the `/en/` prefix while sidebar // items don't, so findIndex always returned -1 → no Previous/Next // buttons. See app/[locale]/docs/layout.tsx for the wider context. import { Link, usePathname } from "@/i18n/navigation" import { ChevronLeft, ChevronRight } from "lucide-react" import { useTranslations } from "next-intl" import { sidebarItems } from "@/components/DocSidebar" interface DocNavigationProps { className?: string } interface SubMenuItem { title: string i18nKey?: string href: string submenu?: SubMenuItem[] } interface FlatPage { title: string i18nKey?: string href: string section?: string sectionI18nKey?: string } // Sidebar entries whose href contains a fragment (`#host`, `#lxc-net`, // …) are visual section headers that group submenu items inside an // existing physical page (currently only Storage Share Manager uses // this pattern — `Host storage integration` and `LXC network sharing` // are headers for groups of subpages, but their href is the parent // Overview page with an anchor). They aren't standalone docs the // reader can advance to, so including them in the Previous/Next walk // produces two regressions: on the page they anchor (`#host`) Next // circles back to the same URL, and on a regular page that happens to // sit next to one of them in the flat list (`lxc-mount-points`) Next // jumps to the section header instead of skipping to the next real // subpage. Skip them at walk time so both cases collapse to "the next // real page in reading order". const isAnchorOnlyHref = (href: string) => href.includes("#") function walkSubmenu( items: SubMenuItem[], section: string, sectionI18nKey: string | undefined, out: FlatPage[], ) { items.forEach((sub) => { if (!isAnchorOnlyHref(sub.href)) { out.push({ title: sub.title, i18nKey: sub.i18nKey, href: sub.href, section, sectionI18nKey, }) } if (sub.submenu && sub.submenu.length > 0) { walkSubmenu(sub.submenu, section, sectionI18nKey, out) } }) } export function DocNavigation({ className }: DocNavigationProps) { const pathname = usePathname() const tNav = useTranslations("docNav") const tSidebar = useTranslations("docSidebar") const tItem = (i18nKey: string | undefined, fallback: string) => { if (!i18nKey) return fallback try { return tSidebar(`items.${i18nKey}`) } catch { return fallback } } const flattenSidebarItems = (): FlatPage[] => { const flatItems: FlatPage[] = [] sidebarItems.forEach((item) => { if (item.href && !isAnchorOnlyHref(item.href)) { flatItems.push({ title: item.title, i18nKey: item.i18nKey, href: item.href }) } if (item.submenu) { walkSubmenu(item.submenu as SubMenuItem[], item.title, item.i18nKey, flatItems) } }) return flatItems } // Dedupe consecutive entries with the same href. Several sidebar // sections (Post-Install, GPUs, Create VM, Disk Manager, …) have a // parent whose href equals its first child's "Overview" href, so the // flat sequence contains the same page twice in a row. Without dedup, // Previous/Next on the parent would point to itself. const rawPages = flattenSidebarItems() const allPages: FlatPage[] = [] for (const p of rawPages) { if (allPages.length > 0 && allPages[allPages.length - 1].href === p.href) continue allPages.push(p) } // Normalize trailing slashes before comparing. Next.js is configured // with `trailingSlash: true` (so GitHub Pages serves `/foo/` as // `foo/index.html`), which means usePathname() returns // `/docs/.../page/` while sidebarItems declares hrefs as // `/docs/.../page` (no trailing slash). Without this normalization // findIndex always returned -1 → prevPage was null and nextPage was // allPages[0] (Introduction) on every page, so the bottom Previous/Next // bar showed "Next: Introduction" everywhere regardless of the route. const stripTrailingSlash = (s: string) => (s !== "/" ? s.replace(/\/+$/, "") : s) const normalizedPathname = stripTrailingSlash(pathname) // Anchored URLs (`/docs/storage-share/#host`) share their pathname // with the Overview page, so they collapse to that page's flat-list // index — Previous walks back into the previous section as usual and // Next advances to the first real subpage (`host-nfs`) instead of // looping back to the same anchor. const currentPageIndex = allPages.findIndex( (page) => stripTrailingSlash(page.href) === normalizedPathname, ) const prevPage = currentPageIndex > 0 ? allPages[currentPageIndex - 1] : null const nextPage = currentPageIndex < allPages.length - 1 ? allPages[currentPageIndex + 1] : null if (!prevPage && !nextPage) return null return (