Quartz
7 min read
Open-source static site generator that turns a collection of Markdown notes (e.g. an Obsidian vault) into a fully-featured, interactive website. Features graph view, full-text search, backlinks, and wikilink support out of the box.
Setup
sh
git clone https://github.com/jackyzha0/quartz.git
cd quartz
npm i
npx quartz create # scaffold / initialize content directory
npx quartz build --serve # dev server at http://localhost:8080
npx quartz build # build static output into ./public
npx quartz sync --commit # sync content + commit + push
Point Quartz at an existing Obsidian vault by symlinking or placing the vault inside content/, or set contentFolder in the build step.
Project Layout
quartz/
├── quartz.config.ts # site configuration
├── quartz.layout.ts # page layout (components)
├── content/ # your Markdown files
│ └── index.md # homepage
├── quartz/static/ # static assets copied verbatim to /public
├── quartz/styles/ # custom CSS (custom.scss is your entry point)
└── public/ # generated output (git-ignored)
Configuration (quartz.config.ts)
ts
import { QuartzConfig } from "./quartz/cfg";
import * as Plugin from "./quartz/plugins";
const config: QuartzConfig = {
configuration: {
pageTitle: "My Knowledge Base",
pageTitleSuffix: " – My Site",
enableSPA: true, // single-page app routing
enablePopovers: true, // hover preview popups
baseUrl: "example.com", // no protocol, no trailing slash
ignorePatterns: ["private", "templates", ".obsidian"],
defaultDateType: "modified", // "created" | "modified" | "published"
locale: "en-US", // affects date formatting
analytics: null,
// analytics: { provider: "plausible" }
// analytics: { provider: "google", tagId: "G-XXXXXXX" }
// analytics: { provider: "umami", websiteId: "...", host: "..." }
// analytics: { provider: "goatcounter", websiteId: "..." }
theme: {
fontOrigin: "googleFonts", // "googleFonts" | "local"
cdnCaching: true,
typography: {
header: "Schibsted Grotesk",
body: "Source Sans Pro",
code: "IBM Plex Mono",
},
colors: {
lightMode: {
light: "#faf8f8",
lightgray: "#e5e5e5",
gray: "#b8b8b8",
darkgray: "#4e4e4e",
dark: "#2b2b2b",
secondary: "#284b63",
tertiary: "#84a59d",
highlight: "rgba(143, 159, 169, 0.15)",
textHighlight: "#fff23688",
},
darkMode: {
light: "#161618",
lightgray: "#393639",
gray: "#646464",
darkgray: "#d4d4d4",
dark: "#ebebec",
secondary: "#7b97aa",
tertiary: "#84a59d",
highlight: "rgba(143, 159, 169, 0.15)",
textHighlight: "#b3aa0288",
},
},
},
},
plugins: {
transformers: [
Plugin.FrontMatter(),
Plugin.CreatedModifiedDate({
priority: ["frontmatter", "git", "filesystem"],
}),
Plugin.SyntaxHighlighting({
theme: { light: "github-light", dark: "github-dark" },
keepBackground: false,
}),
Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }),
Plugin.GitHubFlavoredMarkdown(),
Plugin.TableOfContents(),
Plugin.CrawlLinks({
markdownLinkResolution: "shortest",
prettyLinks: true,
}),
Plugin.Description(),
Plugin.Latex({ renderEngine: "katex" }),
],
filters: [Plugin.RemoveDrafts()],
emitters: [
Plugin.AliasRedirects(),
Plugin.ComponentResources(),
Plugin.ContentPage(),
Plugin.FolderPage(),
Plugin.TagPage(),
Plugin.ContentIndex({ enableSiteMap: true, enableRSS: true }),
Plugin.Assets(),
Plugin.NotFoundPage(),
],
},
};
export default config;
Layout (quartz.layout.ts)
Controls which components appear where on the page.
ts
import { PageLayout, SharedLayout } from "./quartz/cfg";
import * as Component from "./quartz/components";
// Shared across all page types
export const sharedPageComponents: SharedLayout = {
head: Component.Head(),
header: [],
afterBody: [],
footer: Component.Footer({
links: { GitHub: "https://github.com/you/repo" },
}),
};
// Layout for regular content pages
export const defaultContentPageLayout: PageLayout = {
beforeBody: [
Component.Breadcrumbs(),
Component.ArticleTitle(),
Component.ContentMeta(),
Component.TagList(),
],
left: [
Component.PageTitle(),
Component.MobileOnly(Component.Spacer()),
Component.Search(),
Component.Darkmode(),
Component.ReaderMode(),
Component.DesktopOnly(Component.Explorer()),
],
right: [
Component.Graph(),
Component.DesktopOnly(Component.TableOfContents()),
Component.DesktopOnly(Component.Backlinks()),
Component.RecentNotes({ title: "Recent", limit: 5 }),
],
};
// Layout for folder/tag index pages
export const defaultListPageLayout: PageLayout = {
beforeBody: [
Component.Breadcrumbs(),
Component.ArticleTitle(),
Component.ContentMeta(),
],
left: [
Component.PageTitle(),
Component.MobileOnly(Component.Spacer()),
Component.Search(),
Component.Darkmode(),
Component.DesktopOnly(Component.Explorer()),
],
right: [],
};
Available Components
| Component | Description |
|---|---|
Component.ArticleTitle() | Page h1 title |
Component.Backlinks() | Pages that link to this page |
Component.Breadcrumbs() | Navigation breadcrumb trail |
Component.Comments() | Comment section (Giscus, Utterances, Cusdis) |
Component.ContentMeta() | Date, reading time, word count |
Component.Darkmode() | Light/dark toggle |
Component.Explorer() | Collapsible file-tree sidebar |
Component.Footer({ links }) | Site footer with links |
Component.Graph() | Interactive local/global graph view |
Component.Head() | <head> metadata and resources |
Component.PageTitle() | Site title (links to home) |
Component.ReaderMode() | Toggle to strip sidebar/decorations |
Component.RecentNotes({ title, limit, filter, sort }) | List of recently modified notes |
Component.Search() | Full-text search bar |
Component.Spacer() | Empty flex spacer |
Component.TableOfContents() | In-page TOC from headings |
Component.TagList() | Tags from front matter |
Component.DesktopOnly(C) | Wrap to show only on desktop |
Component.MobileOnly(C) | Wrap to show only on mobile |
Plugins Reference
Transformers
Process and transform Markdown content before rendering.
| Plugin | Key Options | Description |
|---|---|---|
FrontMatter() | delimiters, language | Parse YAML/TOML front matter |
CreatedModifiedDate() | priority: ["frontmatter","git","filesystem"] | Set created/modified dates |
SyntaxHighlighting() | theme: {light, dark}, keepBackground | Code block highlighting via Shiki |
ObsidianFlavoredMarkdown() | enableInHtmlEmbed, parseTags, parseBlockReferences, enableYouTubeEmbed | Wikilinks, callouts, comments, embeds |
GitHubFlavoredMarkdown() | linkHeadings | Tables, task lists, strikethrough, autolinks |
TableOfContents() | maxDepth, minEntries, showByDefault, collapseByDefault | Auto-generate TOC from headings |
CrawlLinks() | markdownLinkResolution, prettyLinks, openLinksInNewTab, externalLinkIcon | Resolve internal links, add external icons |
Description() | descriptionLength | Auto-generate page descriptions |
Latex() | renderEngine: "katex"|"mathjax", customMacros | Math rendering |
HardLineBreaks() | — | Treat single newlines as <br> |
Citations() | bibliographyFile, csl | BibTeX citation support |
RoamFlavoredMarkdown() | — | Roam Research {{roam/links}} compatibility |
OxHugoFlavoredMarkdown() | wikilinks, removePredefinedAnchor, replaceFigureWithMdImg | OxHugo Hugo export compatibility |
Filters
Exclude pages from output.
| Plugin | Description |
|---|---|
RemoveDrafts() | Exclude pages with draft: true in front matter |
ExplicitPublish() | Only include pages with publish: true in front matter |
Emitters
Generate output files.
| Plugin | Description |
|---|---|
ContentPage() | Render each note as an HTML page |
FolderPage() | Index page for each folder |
TagPage() | Index page for each tag |
ContentIndex({ enableSiteMap, enableRSS }) | Search index, sitemap.xml, rss.xml |
AliasRedirects() | Redirect pages for aliases front matter |
ComponentResources() | Bundle and emit CSS/JS |
Assets() | Copy static assets from quartz/static/ |
NotFoundPage() | 404 page |
CNAME() | CNAME file for GitHub Pages custom domain |
Front Matter
yaml
---
title: "Override Page Title"
description: "Short description for SEO and social previews"
date: 2024-01-15 # created date (ISO 8601)
created: 2024-01-15
modified: 2024-06-01
published: 2024-06-01 # use with defaultDateType: "published"
tags:
- tag1
- tag2
aliases: # create redirect pages from these slugs
- old-page-name
- another-alias
draft: true # excluded by RemoveDrafts filter
publish: false # excluded by ExplicitPublish filter
cssclasses: # add classes to <body>
- wide-page
- no-toc
socialImage: "images/cover.png" # Open Graph / Twitter card image
lang: en # language for i18n
---
Features
Wikilinks & Obsidian Compat
Enabled by ObsidianFlavoredMarkdown(). Supports:
• [[Page Name]] and [[Page Name|Display Text]]
• [[#Heading]] anchor links
• ![[image.png]] and ![[note.md]] embeds
• %%Comments%% (stripped from output)
• Callouts: > [!note], > [!warning], > [!tip], etc.
• Block references: [[page#^blockid]]
Graph View
Interactive node graph showing note connections. Configure in layout:
ts
Component.Graph({
localGraph: {
drag: true,
zoom: true,
depth: 1,
scale: 1.1,
repelForce: 0.5,
centerForce: 0.3,
linkDistance: 30,
fontSize: 0.6,
opacityScale: 1,
showTags: true,
removeSelfLinks: true,
},
globalGraph: {
drag: true,
zoom: true,
depth: -1,
scale: 0.9,
repelForce: 0.5,
},
});
Full-text Search
Powered by FlexSearch. Works offline (index is embedded in the page). Enabled by ContentIndex() emitter + Search() component. Supports fuzzy matching.
Explorer
Collapsible file-tree sidebar. Configure sort order, depth, and which folders are open by default:
ts
Component.Explorer({
folderClickBehavior: "collapse", // "collapse" | "link"
folderDefaultState: "collapsed", // "collapsed" | "open"
useSavedState: true,
sortFn: (a, b) => {
/* custom sort */
},
filterFn: (node) => node.name !== "private",
mapFn: (node) => node,
});
Popover Previews
Hover over a wikilink to see a preview of the linked page. Enabled by enablePopovers: true in config.
Backlinks
Shows all notes that link to the current page. Add Component.Backlinks() to the layout.
LaTeX
Add Plugin.Latex({ renderEngine: "katex" }) to transformers. Use $inline$ and $$block$$ math.
Mermaid Diagrams
Supported via GitHubFlavoredMarkdown(). Use fenced code blocks with mermaid language tag.
Syntax Highlighting
Via Shiki. Supports 100+ languages and themes. Configure light/dark themes separately:
ts
Plugin.SyntaxHighlighting({
theme: { light: "github-light", dark: "one-dark-pro" },
keepBackground: false,
});
RSS Feed
Enabled by ContentIndex({ enableRSS: true }). Feed published at /index.xml.
Sitemap
Enabled by ContentIndex({ enableSiteMap: true }). Published at /sitemap.xml.
Set socialImage in front matter or Quartz will auto-generate an OG image from the page title and description.
Comments
Add Component.Comments() to afterBody. Supports Giscus (GitHub Discussions), Utterances, and Cusdis:
ts
Component.Comments({
provider: "giscus",
options: {
repo: "you/repo",
repoId: "R_xxx",
category: "Announcements",
categoryId: "DIC_xxx",
},
});
Private Pages
Two approaches:
• Add draft: true + use RemoveDrafts() filter
• Use ExplicitPublish() filter and only add publish: true to pages you want public
• Add folder names to ignorePatterns in config
Internationalization
Set locale in config. Controls date formatting. Per-page language via lang in front matter.
Reader Mode
Add Component.ReaderMode() to layout. Hides sidebars and decorations for focused reading.
Recent Notes
ts
Component.RecentNotes({
title: "Recent Notes",
limit: 5,
filter: (f) => f.slug !== "index",
sort: (f1, f2) =>
(f1.dates?.modified ?? 0) > (f2.dates?.modified ?? 0) ? -1 : 1,
});
Custom Styles
Edit quartz/styles/custom.scss:
scss
// Override CSS variables
:root[saved-theme="light"] {
--light: #fafafa;
--secondary: #4a6fa5;
}
// Wide page layout for specific pages
.wide-page article {
max-width: 900px;
}
// Style callouts
.callout[data-callout="custom"] {
--color: #5b9bd5;
--border: #5b9bd5;
}
Deployment
GitHub Pages
Create .github/workflows/deploy.yml:
yaml
name: Deploy Quartz to Pages
on:
push:
branches: [v4]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # needed for git-based dates
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npx quartz build
- uses: actions/upload-pages-artifact@v3
with:
path: public
deploy:
needs: build
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- uses: actions/deploy-pages@v4
id: deployment
Cloudflare Pages
Build command: npx quartz build
Output directory: public
Node version: 22 (set in environment variables)
Vercel / Netlify
Build command: npx quartz build
Publish directory: public