| docs/features/breadcrumbs.md | ●●●●● patch | view | raw | blame | history | |
| quartz.layout.ts | ●●●●● patch | view | raw | blame | history | |
| quartz/components/Breadcrumbs.tsx | ●●●●● patch | view | raw | blame | history | |
| quartz/components/index.ts | ●●●●● patch | view | raw | blame | history | |
| quartz/components/styles/breadcrumbs.scss | ●●●●● patch | view | raw | blame | history | |
| quartz/plugins/transformers/ofm.ts | ●●●●● patch | view | raw | blame | history | |
| quartz/util/lang.ts | ●●●●● patch | view | raw | blame | history |
docs/features/breadcrumbs.md
New file @@ -0,0 +1,35 @@ --- title: "Breadcrumbs" tags: - component --- Breadcrumbs provide a way to navigate a hierarchy of pages within your site using a list of its parent folders. By default, the element at the very top of your page is the breadcrumb navigation bar (can also be seen at the top on this page!). ## Customization Most configuration can be done by passing in options to `Component.Breadcrumbs()`. For example, here's what the default configuration looks like: ```typescript title="quartz.layout.ts" Component.Breadcrumbs({ spacerSymbol: ">", // symbol between crumbs rootName: "Home", // name of first/root element resolveFrontmatterTitle: false, // wether to resolve folder names through frontmatter titles (more computationally expensive) hideOnRoot: true, // wether to hide breadcrumbs on root `index.md` page }) ``` When passing in your own options, you can omit any or all of these fields if you'd like to keep the default value for that field. You can also adjust where the breadcrumbs will be displayed by adjusting the [[layout]] (moving `Component.Breadcrumbs()` up or down) Want to customize it even more? - Removing graph view: delete all usages of `Component.Breadcrumbs()` from `quartz.layout.ts`. - Component: `quartz/components/Breadcrumbs.tsx` - Style: `quartz/components/styles/breadcrumbs.scss` - Script: inline at `quartz/components/Breadcrumbs.tsx` quartz.layout.ts
@@ -15,7 +15,12 @@ // components for pages that display a single page (e.g. a single note) export const defaultContentPageLayout: PageLayout = { beforeBody: [Component.ArticleTitle(), Component.ContentMeta(), Component.TagList()], beforeBody: [ Component.Breadcrumbs(), Component.ArticleTitle(), Component.ContentMeta(), Component.TagList(), ], left: [ Component.PageTitle(), Component.MobileOnly(Component.Spacer()), quartz/components/Breadcrumbs.tsx
New file @@ -0,0 +1,118 @@ import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import breadcrumbsStyle from "./styles/breadcrumbs.scss" import { FullSlug, SimpleSlug, resolveRelative } from "../util/path" import { capitalize } from "../util/lang" import { QuartzPluginData } from "../plugins/vfile" type CrumbData = { displayName: string path: string } interface BreadcrumbOptions { /** * Symbol between crumbs */ spacerSymbol: string /** * Name of first crumb */ rootName: string /** * wether to look up frontmatter title for folders (could cause performance problems with big vaults) */ resolveFrontmatterTitle: boolean /** * Wether to display breadcrumbs on root `index.md` */ hideOnRoot: boolean } const defaultOptions: BreadcrumbOptions = { spacerSymbol: ">", rootName: "Home", resolveFrontmatterTitle: false, hideOnRoot: true, } function formatCrumb(displayName: string, baseSlug: FullSlug, currentSlug: SimpleSlug): CrumbData { return { displayName, path: resolveRelative(baseSlug, currentSlug) } } // given a folderName (e.g. "features"), search for the corresponding `index.md` file function findCurrentFile(allFiles: QuartzPluginData[], folderName: string) { return allFiles.find((file) => { if (file.slug?.endsWith("index")) { const folderParts = file.filePath?.split("/") if (folderParts) { const name = folderParts[folderParts?.length - 2] if (name === folderName) { return true } } } }) } export default ((opts?: Partial<BreadcrumbOptions>) => { // Merge options with defaults const options: BreadcrumbOptions = { ...defaultOptions, ...opts } function Breadcrumbs({ fileData, allFiles }: QuartzComponentProps) { // Hide crumbs on root if enabled if (options.hideOnRoot && fileData.slug === "index") { return <></> } // Format entry for root element const firstEntry = formatCrumb(capitalize(options.rootName), fileData.slug!, "/" as SimpleSlug) const crumbs: CrumbData[] = [firstEntry] // Get parts of filePath (every folder) const parts = fileData.filePath?.split("/")?.splice(1) if (parts) { // full path until current part let current = "" for (let i = 0; i < parts.length - 1; i++) { const folderName = parts[i] let currentTitle = folderName // TODO: performance optimizations/memoizing // Try to resolve frontmatter folder title if (options?.resolveFrontmatterTitle) { // try to find file for current path const currentFile = findCurrentFile(allFiles, folderName) if (currentFile) { currentTitle = currentFile.frontmatter!.title } } // Add current path to full path current += folderName + "/" // Format and add current crumb const crumb = formatCrumb(capitalize(currentTitle), fileData.slug!, current as SimpleSlug) crumbs.push(crumb) } // Add current file to crumb (can directly use frontmatter title) if (parts.length > 0) { crumbs.push({ displayName: capitalize(fileData.frontmatter!.title), path: "", }) } } return ( <nav class="breadcrumb-container" aria-label="breadcrumbs"> {crumbs.map((crumb, index) => ( <div class="breadcrumb-element"> <a href={crumb.path}>{crumb.displayName}</a> {index !== crumbs.length - 1 && <p>{` ${options.spacerSymbol} `}</p>} </div> ))} </nav> ) } Breadcrumbs.css = breadcrumbsStyle return Breadcrumbs }) satisfies QuartzComponentConstructor quartz/components/index.ts
@@ -18,6 +18,7 @@ import DesktopOnly from "./DesktopOnly" import MobileOnly from "./MobileOnly" import RecentNotes from "./RecentNotes" import Breadcrumbs from "./Breadcrumbs" export { ArticleTitle, @@ -40,4 +41,5 @@ MobileOnly, RecentNotes, NotFound, Breadcrumbs, } quartz/components/styles/breadcrumbs.scss
New file @@ -0,0 +1,22 @@ .breadcrumb-container { margin: 0; margin-top: 0.75rem; padding: 0; display: flex; flex-direction: row; flex-wrap: wrap; gap: 0.5rem; } .breadcrumb-element { p { margin: 0; margin-left: 0.5rem; padding: 0; line-height: normal; } display: flex; flex-direction: row; align-items: center; justify-content: center; } quartz/plugins/transformers/ofm.ts
@@ -14,6 +14,7 @@ import { toHast } from "mdast-util-to-hast" import { toHtml } from "hast-util-to-html" import { PhrasingContent } from "mdast-util-find-and-replace/lib" import { capitalize } from "../../util/lang" export interface Options { comments: boolean @@ -104,10 +105,6 @@ return calloutMapping[callout] ?? "note" } const capitalize = (s: string): string => { return s.substring(0, 1).toUpperCase() + s.substring(1) } // !? -> optional embedding // \[\[ -> open brace // ([^\[\]\|\#]+) -> one or more non-special characters ([,],|, or #) (name) quartz/util/lang.ts
@@ -5,3 +5,7 @@ return `${count} ${s}s` } } export function capitalize(s: string): string { return s.substring(0, 1).toUpperCase() + s.substring(1) }