diff --git a/docs/features/breadcrumbs.md b/docs/features/breadcrumbs.md index f3545059d..41fece114 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/docs/showcase.md b/docs/showcase.md index 20fc3253b..6e1c56174 100644 --- a/docs/showcase.md +++ b/docs/showcase.md @@ -21,3 +21,4 @@ Want to see what Quartz can do? Here are some cool community gardens: - [Ellie's Notes](https://ellie.wtf) - [Eledah's Crystalline](https://blog.eledah.ir/) - [🌓 Projects & Privacy - FOSS, tech, law](https://be-far.com) +- [🌲Stefan Genov's Garden](https://garden.sgenov.dev) diff --git a/quartz/components/ParentBreadcrumbs.tsx b/quartz/components/ParentBreadcrumbs.tsx new file mode 100644 index 000000000..d7b516b4f --- /dev/null +++ b/quartz/components/ParentBreadcrumbs.tsx @@ -0,0 +1,130 @@ +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { QuartzPluginData } from "../plugins/vfile" +import { classNames } from "../util/lang" +import { resolveRelative, simplifySlug, FullSlug, SimpleSlug } from "../util/path" +import style from "./styles/breadcrumbs.scss" + +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, + allFiles, + displayClass, + }: QuartzComponentProps) => { + const parseWikiLink = (content: string): string => { + if (!content) return "" + let clean = content.trim().replace(/^["']|["']$/g, "") + clean = clean.replace(/^\[\[|\]\]$/g, "") + return clean.split("|")[0] + } + + const findFile = (name: string) => { + const targetSlug = simplifySlug(name as FullSlug) + return allFiles.find((f: QuartzPluginData) => { + const fSlug = simplifySlug(f.slug!) + return ( + fSlug === targetSlug || + fSlug.normalize() == targetSlug.normalize() || + f.frontmatter?.title === name + ) + }) + } + + 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?.[parentKey!]) { + const rawParent = current.frontmatter[parentKey!] + const parentList = Array.isArray(rawParent) ? rawParent : [rawParent] + + 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([ + { + displayName: options.rootName!, + path: resolveRelative(fileData.slug!, "index" as SimpleSlug), + }, + ]) + } + + crumbs.reverse() + + if (crumbs.length === 0 && fileData.slug === "index") { + return <> + } + + return ( + + ) + } + + ParentBreadcrumbs.css = style + return ParentBreadcrumbs +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/index.ts b/quartz/components/index.ts index cece8e614..c7cb712da 100644 --- a/quartz/components/index.ts +++ b/quartz/components/index.ts @@ -23,8 +23,10 @@ import Breadcrumbs from "./Breadcrumbs" import Comments from "./Comments" import Flex from "./Flex" import ConditionalRender from "./ConditionalRender" +import ParentBreadcrumbs from "./ParentBreadcrumbs" export { + ParentBreadcrumbs, ArticleTitle, Content, TagContent,