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
| Slot | Where | Set in |
|---|---|---|
head | <head>: one component, which owns title, canonical, and Open Graph tags | shared |
header | Top bar | shared |
beforeBody | Above the article body | per page type |
left, right | Sidebars | per page type |
pageBody | The article itself | the page emitter, not the layout file |
afterBody | Below the body | shared |
footer | Page footer, one component | shared |
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
| Component | Note |
|---|---|
ArticleTitle, ContentMeta, TagList, Breadcrumbs | beforeBody pieces |
PageTitle, Search, Darkmode, ReaderMode, Explorer | Left sidebar |
TableOfContents, Backlinks, Graph | Right sidebar |
Flex | Lays components out in a row or column, with grow and basis per child |
RecentNotes, Comments, Footer, Spacer | Optional extras |
ConditionalRender | Renders 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>cssis bundled with every other component’s CSS intoindex.css. Use theme variables (var(--secondary),var(--light)) so dark mode works.beforeDOMLoadedandafterDOMLoadedcarry client scripts.- Props:
fileData(frontmatter, dates, slug),allFiles(every page),cfg,ctx,tree,externalResources,children,displayClass. - Avoid scanning
allFilesin a component that renders on every page; compute once in a transformer and attach it tofileData.
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." })],