Overview

Quartz components are Preact function components rendered to static HTML at build time. quartz.layout.ts decides which components fill which slot on which page type. The rules below cover the eight slots, the responsive wrappers, ConditionalRender, and custom components, checked against the Quartz 4.5.2 source in this repo.

Know the eight slots

SlotWhereSet in
head<head>: one component, which owns title, canonical, and Open Graph tagsshared
headerTop barshared
beforeBodyAbove the article bodyper page type
left, rightSidebarsper page type
pageBodyThe article itselfthe page emitter, not the layout file
afterBodyBelow the bodyshared
footerPage footer, one componentshared

pageBody is Content() for content pages, set inside the emitter, so it never appears in quartz.layout.ts.

Export the three names the emitters import

export const sharedPageComponents: SharedLayout = { head, header, afterBody, footer }
export const defaultContentPageLayout: PageLayout = { beforeBody, left, right }
export const defaultListPageLayout: PageLayout = { beforeBody, left, right }

The page emitters import exactly these names, so renaming an export breaks the build. Content pages use defaultContentPageLayout; folder and tag pages use defaultListPageLayout. A single emitter can override slots through its options, for example Plugin.ContentPage({ right: [] }).

Use the built-in components you need

ComponentNote
ArticleTitle, ContentMeta, TagList, BreadcrumbsbeforeBody pieces
PageTitle, Search, Darkmode, ReaderMode, ExplorerLeft sidebar
TableOfContents, Backlinks, GraphRight sidebar
FlexLays components out in a row or column, with grow and basis per child
RecentNotes, Comments, Footer, SpacerOptional extras
ConditionalRenderRenders a component only when a predicate passes

This site adds CustomHead, MarkdownSource, AgentBrief, and SponsorSlot.

Keep heavy components off every page

Component scripts are bundled into postscript.js, which every page loads. This site removed Graph from the right sidebar because its pixi.js and d3 imports were 598 KB of a 705 KB postscript.js, and its simulation kept repainting after content loaded. The comment in quartz.layout.ts records the measurements. Re-enable it only with a plan to load it lazily.

Use the responsive wrappers correctly

MobileOnly(Component) and DesktopOnly(Component) pass displayClass="mobile-only" or "desktop-only" to the child, and the child must apply it to its root element. A custom component that ignores displayClass stays visible everywhere. The wrappers only toggle CSS: the hidden child still renders and its scripts still ship, so they do not make an expensive component cheaper.

Breakpoints: mobile is 800 px and below, desktop is 1200 px and above, and tablet is between. The desktop grid is fixed at a 320 px left column, a flexible center, and a 320 px right column, so an empty right array leaves an empty column and does not widen the body. On tablet the right sidebar moves below the content.

Show a component on some pages with ConditionalRender

The predicate receives the full component props, not just the file data, and runs at build time per page.

Component.ConditionalRender({
  component: Component.Breadcrumbs(),
  condition: (page) => page.fileData.slug !== "index",
})

Use it sparingly; many conditionals usually mean a separate page type is warranted.

Write a custom component

A component is a constructor that takes options and returns a Preact component. Static properties carry CSS and scripts.

// quartz/components/Banner.tsx
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import { classNames } from "../util/lang"
 
interface Options { message: string }
 
export default ((opts: Options) => {
  const Banner: QuartzComponent = ({ fileData, displayClass }: QuartzComponentProps) => {
    if (!fileData.frontmatter?.banner) return null
    return <div class={classNames(displayClass, "banner")}>{opts.message}</div>
  }
  Banner.css = `.banner { padding: 0.5rem 1rem; border-left: 4px solid var(--secondary); }`
  return Banner
}) satisfies QuartzComponentConstructor<Options>
  • css is bundled with every other component’s CSS into index.css. Use theme variables (var(--secondary), var(--light)) so dark mode works.
  • beforeDOMLoaded and afterDOMLoaded carry client scripts.
  • Props: fileData (frontmatter, dates, slug), allFiles (every page), cfg, ctx, tree, externalResources, children, displayClass.
  • Avoid scanning allFiles in a component that renders on every page; compute once in a transformer and attach it to fileData.

Import it in quartz.layout.ts and place it in a slot; Quartz does not auto-discover components.

import Banner from "./quartz/components/Banner"
beforeBody: [Component.ArticleTitle(), Banner({ message: "Under revision." })],