diff --git a/docs/features/breadcrumbs.md b/docs/features/breadcrumbs.md index f3545059d..b8aed34db 100644 --- a/docs/features/breadcrumbs.md +++ b/docs/features/breadcrumbs.md @@ -33,3 +33,59 @@ Want to customize it even more? - Component: `quartz/components/Breadcrumbs.tsx` - Style: `quartz/components/styles/breadcrumbs.scss` - Script: inline at `quartz/components/Breadcrumbs.tsx` + +## Using A Frontmatter Prop + +ParentBreadcrumbs` is an alternative breadcrumbs component that derives its hierarchy **entirely from frontmatter-defined parent relationships**, rather than folder structure. This is useful for knowledge-base–style sites, wikis, or any content where pages may belong to multiple logical hierarchies. + +Unlike the default `Breadcrumbs` component, `ParentBreadcrumbs` supports: + +- Explicit parent chains via frontmatter +- Multiple parents per level +- Wiki-style links (`[[Page Name]]`) +- Customizable frontmatter keys + +### How It Works + +`ParentBreadcrumbs` walks upward through a parent chain starting from the current page, following a configurable frontmatter field. +At each level, **all parents are rendered**, while one unvisited parent is chosen to continue the chain upward. + +Example frontmatter: + +```yaml +--- +title: "Advanced Topics" +parent: Basics +--- +``` + +Wiki links are supported: + +```yaml +parent: [[Basics]] +``` + +Multiple parents: + +```yaml +parent: +- [[Basics]] +- [[Reference]] +``` + +### Configuration + +You can configure `ParentBreadcrumbs` by passing options into `Component.ParentBreadcrumbs()`. + +Default configuration: + +```ts +Component.ParentBreadcrumbs({ + spacerSymbol: "❯", // symbol displayed between breadcrumb levels + rootName: "Home", // label for the root (index) page + resolveFrontmatterTitle: true, // use frontmatter.title instead of slug + parentKey: "parent", // frontmatter key used to resolve parents +}) +``` + +All options are optional; omitted values fall back to the defaults. diff --git a/quartz/components/ParentBreadcrumbs.tsx b/quartz/components/ParentBreadcrumbs.tsx index e501f4850..cb3878e0b 100644 --- a/quartz/components/ParentBreadcrumbs.tsx +++ b/quartz/components/ParentBreadcrumbs.tsx @@ -8,16 +8,19 @@ interface ParentBreadcrumbsOptions { spacerSymbol?: string; rootName?: string; resolveFrontmatterTitle?: boolean; + frontmatterProp?: string, } const defaultOptions: ParentBreadcrumbsOptions = { spacerSymbol: "❯", rootName: "Home", resolveFrontmatterTitle: true, + frontmatterProp: "parent", }; export default ((opts?: ParentBreadcrumbsOptions) => { const options = { ...defaultOptions, ...opts }; + const parentKey = options.frontmatterProp; const ParentBreadcrumbs: QuartzComponent = ({ fileData, @@ -34,41 +37,61 @@ export default ((opts?: ParentBreadcrumbsOptions) => { const findFile = (name: string) => { const targetSlug = simplifySlug(name as FullSlug); - return allFiles.find((f: QuartzPluginData) => { const fSlug = simplifySlug(f.slug!); return fSlug === targetSlug || fSlug.endsWith(targetSlug) || f.frontmatter?.title === name; }); }; - const crumbs: Array<{ displayName: string; path: string; }> = []; + type BreadcrumbNode = { displayName: string; path: string; }; + const crumbs: Array = []; + let current = fileData; const visited = new Set(); if (current.slug) visited.add(current.slug); - while (current && current.frontmatter?.parent) { - const parentLink = parseWikiLink(current.frontmatter.parent as string); - const parentFile = findFile(parentLink); + while (current && current.frontmatter?.[parentKey!]) { + const rawParent = current.frontmatter[parentKey!]; + const parentList = Array.isArray(rawParent) ? rawParent : [rawParent]; - if (parentFile && parentFile.slug && !visited.has(parentFile.slug)) { - visited.add(parentFile.slug); - crumbs.push({ - displayName: options.resolveFrontmatterTitle - ? parentFile.frontmatter?.title ?? parentFile.slug - : parentFile.slug, - path: resolveRelative(fileData.slug!, parentFile.slug!) - }); - current = parentFile; + const currentLevelNodes: BreadcrumbNode[] = []; + let nextParent: QuartzPluginData | undefined = undefined; + + for (const p of parentList) { + const linkStr = parseWikiLink(p as string); + const parentFile = findFile(linkStr); + + if (parentFile && parentFile.slug) { + currentLevelNodes.push({ + displayName: options.resolveFrontmatterTitle + ? parentFile.frontmatter?.title ?? parentFile.slug + : parentFile.slug, + path: resolveRelative(fileData.slug!, parentFile.slug!) + }); + + if (!nextParent && !visited.has(parentFile.slug)) { + nextParent = parentFile; + } + } + } + + if (currentLevelNodes.length > 0) { + crumbs.push(currentLevelNodes); + } + + if (nextParent) { + visited.add(nextParent.slug!); + current = nextParent; } else { break; } } if (current.slug !== "index") { - crumbs.push({ + crumbs.push([{ displayName: options.rootName!, path: resolveRelative(fileData.slug!, "index" as SimpleSlug) - }); + }]); } crumbs.reverse(); @@ -79,10 +102,15 @@ export default ((opts?: ParentBreadcrumbsOptions) => { return (