This commit is contained in:
Stefan Genov 2026-01-29 05:38:38 +01:00 committed by GitHub
commit 73e37a3de1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 189 additions and 0 deletions

View File

@ -33,3 +33,59 @@ Want to customize it even more?
- Component: `quartz/components/Breadcrumbs.tsx` - Component: `quartz/components/Breadcrumbs.tsx`
- Style: `quartz/components/styles/breadcrumbs.scss` - Style: `quartz/components/styles/breadcrumbs.scss`
- Script: inline at `quartz/components/Breadcrumbs.tsx` - 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-basestyle 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.

View File

@ -21,3 +21,4 @@ Want to see what Quartz can do? Here are some cool community gardens:
- [Ellie's Notes](https://ellie.wtf) - [Ellie's Notes](https://ellie.wtf)
- [Eledah's Crystalline](https://blog.eledah.ir/) - [Eledah's Crystalline](https://blog.eledah.ir/)
- [🌓 Projects & Privacy - FOSS, tech, law](https://be-far.com) - [🌓 Projects & Privacy - FOSS, tech, law](https://be-far.com)
- [🌲Stefan Genov's Garden](https://garden.sgenov.dev)

View File

@ -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<BreadcrumbNode[]> = []
let current = fileData
const visited = new Set<string>()
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 (
<nav class={classNames(displayClass, "breadcrumb-container")} aria-label="breadcrumbs">
{crumbs.map((crumbLevel, levelIndex) => (
<div class="breadcrumb-element">
{crumbLevel.map((node, nodeIndex) => (
<>
<a href={node.path}>{node.displayName}</a>
{nodeIndex < crumbLevel.length - 1 && <span style={{ opacity: 0.5 }}> / </span>}
</>
))}
{levelIndex !== crumbs.length && <p>{options.spacerSymbol}</p>}
</div>
))}
<div class="breadcrumb-element">
<p>{fileData.frontmatter?.title}</p>
</div>
</nav>
)
}
ParentBreadcrumbs.css = style
return ParentBreadcrumbs
}) satisfies QuartzComponentConstructor

View File

@ -23,8 +23,10 @@ import Breadcrumbs from "./Breadcrumbs"
import Comments from "./Comments" import Comments from "./Comments"
import Flex from "./Flex" import Flex from "./Flex"
import ConditionalRender from "./ConditionalRender" import ConditionalRender from "./ConditionalRender"
import ParentBreadcrumbs from "./ParentBreadcrumbs"
export { export {
ParentBreadcrumbs,
ArticleTitle, ArticleTitle,
Content, Content,
TagContent, TagContent,