Skip to content

Commit 866f738

Browse files
rr404jdv
andauthored
new breadcrumb wip (#1063)
* New second bar with better breadcrumb * version selector moved to right side of secondary bar * hiding old breadcrumb --------- Co-authored-by: jdv <julien@crowdsec.net>
1 parent bb4a199 commit 866f738

11 files changed

Lines changed: 1212 additions & 48 deletions

File tree

CURRENT_HIERARCHY.json

Lines changed: 810 additions & 0 deletions
Large diffs are not rendered by default.

crowdsec-docs/docusaurus.config.ts

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { Config } from "@docusaurus/types";
44
import { themes } from "prism-react-renderer";
55

66
import tailwindPlugin from "./plugins/tailwind-config";
7-
import { ctiApiSidebar, guidesSideBar, remediationSideBar, trackerApiSidebar } from "./sidebarsUnversioned";
7+
import sidebarsUnversioned from "./sidebarsUnversioned";
88

99
const extractPreprocessor = require("./plugins/extract-preprocessor");
1010

@@ -31,6 +31,7 @@ function handleSidebarItems(items) {
3131
return arr;
3232
}
3333

34+
// This function generates redirects for all items in the unversioned sidebars, so that if we move a doc from versioned to unversioned, we don't break existing links. It handles both string items (doc ids) and nested objects (categories with their own items).
3435
const backportRedirect = (s) => {
3536
const arr = [];
3637
if (typeof s === "string") {
@@ -56,13 +57,9 @@ const ACADEMY_URL = `https://academy.crowdsec.net/courses?${
5657
process.env.NODE_ENV === "production" ? "utm_source=docs&utm_medium=menu&utm_campaign=top-menu&utm_id=academydocs" : ""
5758
}`;
5859

60+
/** IF you make significant changes to the nav bar or side bars
61+
* make sure to have proper mapping in crowdsec-docs/src/sectionMap.ts */
5962
const NAVBAR_ITEMS: NavbarItem[] = [
60-
{
61-
type: "docsVersionDropdown",
62-
docsPluginId: "default",
63-
position: "left",
64-
dropdownActiveClassDisabled: true,
65-
},
6663
{
6764
label: "Security Stack",
6865
position: "left",
@@ -178,12 +175,7 @@ const FOOTER_LINKS = [
178175
];
179176

180177
const redirects = [
181-
...[
182-
...(Array.isArray(remediationSideBar) ? remediationSideBar : [remediationSideBar]),
183-
...(Array.isArray(ctiApiSidebar) ? ctiApiSidebar : [ctiApiSidebar]),
184-
...(Array.isArray(trackerApiSidebar) ? trackerApiSidebar : [trackerApiSidebar]),
185-
...(Array.isArray(guidesSideBar) ? guidesSideBar : [guidesSideBar]),
186-
].flatMap(backportRedirect),
178+
...Object.values(sidebarsUnversioned).flatMap(backportRedirect),
187179
{ from: "/docs/troubleshooting", to: "/u/troubleshooting/intro" },
188180
{ from: "/docs/next/troubleshooting", to: "/u/troubleshooting/intro" },
189181
{ from: "/docs/faq", to: "/u/troubleshooting/intro" },
@@ -338,7 +330,6 @@ const config: Config = {
338330

339331
["./plugins/gtag/index.ts", { trackingID: "G-0TFBMNTDFQ" }],
340332
"./plugins/leadfeeder/index.js",
341-
["@docusaurus/plugin-client-redirects", { redirects }],
342333
[
343334
"@signalwire/docusaurus-plugin-llms-txt",
344335
{
@@ -386,6 +377,7 @@ const config: Config = {
386377
onRouteError: "warn",
387378
},
388379
],
380+
["@docusaurus/plugin-client-redirects", { redirects }],
389381
tailwindPlugin,
390382
],
391383
};

crowdsec-docs/sidebars.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import type { SidebarsConfig } from "@docusaurus/plugin-content-docs";
22

33
type SidebarConfig = SidebarsConfig[string];
44

5+
/** IF you make significant changes to the nav bar or side bars
6+
* make sure to have proper mapping in crowdsec-docs/src/sectionMap.ts */
57
const sidebarsConfig: SidebarConfig = {
68
// By default, Docusaurus generates a sidebar from the docs folder structure
79
//tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],

crowdsec-docs/sidebarsUnversioned.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import type { SidebarsConfig } from "@docusaurus/plugin-content-docs";
22

33
type SidebarConfig = SidebarsConfig[string];
44

5+
/** IF you make significant changes to the nav bar or side bars
6+
* make sure to have proper mapping in crowdsec-docs/src/sectionMap.ts */
57
const sidebarsUnversionedConfig: SidebarConfig = {
68
ctiApiSidebar: [
79
{
@@ -942,14 +944,3 @@ const sidebarsUnversionedConfig: SidebarConfig = {
942944
};
943945

944946
export default sidebarsUnversionedConfig;
945-
946-
export const {
947-
ctiApiSidebar,
948-
trackerApiSidebar,
949-
consoleSidebar,
950-
remediationSideBar,
951-
blocklistsSideBar,
952-
troubleshootingSideBar,
953-
guidesSideBar,
954-
gettingStarted,
955-
} = sidebarsUnversionedConfig;

crowdsec-docs/src/css/navbar.css

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crowdsec-docs/src/sectionMap.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* Maps each Docusaurus sidebar name (the key in sidebars.ts / sidebarsUnversioned.ts)
3+
* to a human-readable label and the path to that section's intro page.
4+
*
5+
* The label appears as the first breadcrumb item after the home icon.
6+
* The introPath is where clicking that breadcrumb item navigates to.
7+
*
8+
* Optional `parent` adds an intermediate crumb before the section label,
9+
* e.g. for sub-sections of Security Engine.
10+
*
11+
* When you add or rename a sidebar key in sidebars.ts or sidebarsUnversioned.ts,
12+
* update this map accordingly. Versioned sidebar files (versioned_sidebars/) reuse
13+
* the same sidebar names so no extra entries are needed for them.
14+
*/
15+
export type SectionInfo = {
16+
label: string;
17+
introPath: string;
18+
parent?: {
19+
label: string;
20+
introPath: string;
21+
};
22+
};
23+
24+
export const SECTION_MAP: Record<string, SectionInfo> = {
25+
// ── Versioned (sidebars.ts) ───────────────────────────────────
26+
tutorialSidebar: {
27+
label: "Security Engine",
28+
introPath: "/docs/intro",
29+
},
30+
appSecSideBar: {
31+
label: "Web Application Firewall",
32+
introPath: "/docs/next/appsec/intro",
33+
parent: { label: "Security Engine", introPath: "/docs/intro" },
34+
},
35+
cscliSidebar: {
36+
label: "Cscli",
37+
introPath: "/docs/cscli/cscli",
38+
parent: { label: "Security Engine", introPath: "/docs/intro" },
39+
},
40+
sdkSideBar: {
41+
label: "SDK",
42+
introPath: "/docs/getting_started/sdk_intro",
43+
parent: { label: "Security Engine", introPath: "/docs/intro" },
44+
},
45+
46+
// ── Unversioned (sidebarsUnversioned.ts) ─────────────────────
47+
consoleSidebar: {
48+
label: "Console",
49+
introPath: "/u/console/intro",
50+
},
51+
ctiApiSidebar: {
52+
label: "CTI",
53+
introPath: "/u/cti_api/intro",
54+
},
55+
remediationSideBar: {
56+
label: "Remediation Components",
57+
introPath: "/u/bouncers/intro",
58+
},
59+
blocklistsSideBar: {
60+
label: "Blocklists",
61+
introPath: "/u/blocklists/getting_started",
62+
},
63+
trackerApiSidebar: {
64+
label: "Tracker API",
65+
introPath: "/u/tracker_api/intro",
66+
},
67+
troubleshootingSideBar: {
68+
label: "Troubleshooting",
69+
introPath: "/u/troubleshooting/intro",
70+
},
71+
guidesSideBar: {
72+
label: "Guides",
73+
introPath: "/u/user_guides/intro",
74+
},
75+
gettingStarted: {
76+
label: "Getting Started",
77+
introPath: "/u/getting_started/intro",
78+
parent: { label: "Security Engine", introPath: "/docs/intro" },
79+
},
80+
};
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/**
2+
* CrowdSec Made Swizzled override of @docusaurus/theme-classic's DocRoot/Layout component.
3+
*
4+
* The original only renders the sidebar + main content area.
5+
* This override injects a custom SecondaryNavbar above that content, which adds:
6+
* - A breadcrumb trail (Home > Section > [current page path])
7+
* - A version dropdown (when multiple doc versions exist)
8+
*
9+
* Docusaurus picks this file automatically because it lives at
10+
* src/theme/DocRoot/Layout/index.tsx, shadowing the original in node_modules.
11+
*/
12+
13+
import Link from "@docusaurus/Link";
14+
import type { PropSidebarBreadcrumbsItem } from "@docusaurus/plugin-content-docs";
15+
import {
16+
useActiveDocContext,
17+
useActivePlugin,
18+
useDocsPreferredVersion,
19+
useDocsSidebar,
20+
useSidebarBreadcrumbs,
21+
useVersions,
22+
} from "@docusaurus/plugin-content-docs/client";
23+
import { useHistorySelector } from "@docusaurus/theme-common";
24+
import { SECTION_MAP } from "@site/src/sectionMap";
25+
import BackToTopButton from "@theme/BackToTopButton";
26+
import type { Props } from "@theme/DocRoot/Layout";
27+
import DocRootLayoutMain from "@theme/DocRoot/Layout/Main";
28+
import DocRootLayoutSidebar from "@theme/DocRoot/Layout/Sidebar";
29+
import clsx from "clsx";
30+
import { ChevronRight, House } from "lucide-react";
31+
import React, { type ReactNode, useState } from "react";
32+
33+
import styles from "./styles.module.css";
34+
35+
function VersionDropdown({ pluginId }: { pluginId: string }): ReactNode {
36+
const versions = useVersions(pluginId);
37+
const activeDocContext = useActiveDocContext(pluginId);
38+
const { savePreferredVersionName } = useDocsPreferredVersion(pluginId);
39+
const search = useHistorySelector((h) => h.location.search);
40+
const hash = useHistorySelector((h) => h.location.hash);
41+
42+
const activeVersion = activeDocContext.activeVersion;
43+
if (!activeVersion || versions.length <= 1) return null;
44+
45+
return (
46+
<div className={styles.versionDropdown}>
47+
<span className={styles.versionLabel}>Security Engine version:</span>
48+
<select
49+
value={activeVersion.name}
50+
onChange={(e) => {
51+
const selected = versions.find((v) => v.name === e.target.value);
52+
if (!selected) return;
53+
savePreferredVersionName(selected.name);
54+
const alternateDoc = activeDocContext.alternateDocVersions[selected.name];
55+
const target = alternateDoc?.path ?? selected.path;
56+
window.location.href = `${target}${search}${hash}`;
57+
}}
58+
aria-label="Select documentation version"
59+
>
60+
{versions.map((v) => (
61+
<option key={v.name} value={v.name}>
62+
{v.label}
63+
</option>
64+
))}
65+
</select>
66+
</div>
67+
);
68+
}
69+
70+
function SecondaryNavbar(): ReactNode {
71+
const sidebar = useDocsSidebar();
72+
const activePlugin = useActivePlugin();
73+
const breadcrumbs = useSidebarBreadcrumbs();
74+
75+
const pluginId = activePlugin?.pluginId ?? "default";
76+
const sidebarName = sidebar?.name ?? "";
77+
const section = SECTION_MAP[sidebarName];
78+
79+
return (
80+
<div className={styles.secondaryNavbar}>
81+
<div className={styles.secondaryNavbarInner}>
82+
<nav className={styles.breadcrumb} aria-label="Breadcrumb">
83+
{/* Home icon */}
84+
<Link to="/" className={styles.breadcrumbHome} aria-label="Home">
85+
<House size={14} aria-hidden />
86+
</Link>
87+
88+
{/* Optional parent section (e.g. "Security Engine" for AppSec) */}
89+
{section?.parent && (
90+
<>
91+
<ChevronRight size={13} className={styles.breadcrumbSeparator} aria-hidden />
92+
<Link to={section.parent.introPath} className={styles.breadcrumbItem}>
93+
{section.parent.label}
94+
</Link>
95+
</>
96+
)}
97+
98+
{/* Section label (e.g. "Remediation Components") */}
99+
{section && (
100+
<>
101+
<ChevronRight size={13} className={styles.breadcrumbSeparator} aria-hidden />
102+
<Link to={section.introPath} className={styles.breadcrumbItem}>
103+
{section.label}
104+
</Link>
105+
</>
106+
)}
107+
108+
{/* Sidebar breadcrumb trail */}
109+
{breadcrumbs?.map((item: PropSidebarBreadcrumbsItem, idx: number) => {
110+
const isLast = idx === breadcrumbs.length - 1;
111+
const href = item.type === "category" && (item as { linkUnlisted?: boolean }).linkUnlisted ? undefined : item.href;
112+
113+
return (
114+
<React.Fragment key={`${item.label}-${idx}`}>
115+
<ChevronRight size={13} className={styles.breadcrumbSeparator} aria-hidden />
116+
{!isLast && href ? (
117+
<Link to={href} className={styles.breadcrumbItem}>
118+
{item.label}
119+
</Link>
120+
) : (
121+
<span className={clsx(styles.breadcrumbItem, isLast && styles.breadcrumbItemActive)}>{item.label}</span>
122+
)}
123+
</React.Fragment>
124+
);
125+
})}
126+
</nav>
127+
128+
<VersionDropdown pluginId={pluginId} />
129+
</div>
130+
</div>
131+
);
132+
}
133+
134+
export default function DocRootLayout({ children }: Props): ReactNode {
135+
const sidebar = useDocsSidebar();
136+
const [hiddenSidebarContainer, setHiddenSidebarContainer] = useState(false);
137+
138+
return (
139+
<div className={styles.docsWrapper}>
140+
<BackToTopButton />
141+
<SecondaryNavbar />
142+
<div className={styles.docRoot}>
143+
{sidebar && (
144+
<DocRootLayoutSidebar
145+
sidebar={sidebar.items}
146+
hiddenSidebarContainer={hiddenSidebarContainer}
147+
setHiddenSidebarContainer={setHiddenSidebarContainer}
148+
/>
149+
)}
150+
<DocRootLayoutMain hiddenSidebarContainer={hiddenSidebarContainer}>{children}</DocRootLayoutMain>
151+
</div>
152+
</div>
153+
);
154+
}

0 commit comments

Comments
 (0)