---
title: "Quartz Plugins"
slug: "quartz-plugins"
category: "tooling"
tags: ["tooling", "quartz", "plugins", "transformers", "ssg", "typescript", "pipeline"]
status: "stable"
last_updated: 2026-05-14
summary: "Quartz Transformer, Filter, and Emitter plugin types; the built-in plugin set; writing a custom transformer; ordering and naming pitfalls."
related: ["[[tooling/quartz]]", "[[tooling/quartz-config]]", "[[tooling/quartz-components]]", "[[tooling/quartz-layout]]", "[[tooling/quartz-debugging]]", "[[howto/deploy-quartz-site]]", "[[ops/github-pages]]"]
---

## Overview

Quartz plugins sit in three categories that match three pipeline stages: transformers parse and enrich the content graph, filters remove pages before they emit, and emitters write files to the `public/` directory. Every build runs the full pipeline in order. Understanding the stage boundaries prevents the most common configuration mistakes.

## Transformers run first and mutate the content graph

A transformer receives every page's hast tree and its associated `QuartzPluginData` after markdown parsing and before any output. Use transformers to extract frontmatter, resolve links, add syntax highlighting, and inject computed metadata.

Key built-in transformers and what they own:

| Plugin | Responsibility |
|---|---|
| `FrontMatter` | Parses YAML frontmatter into `fileData.frontmatter` |
| `CreatedModifiedDate` | Attaches `created` and `modified` dates from frontmatter or git |
| `ObsidianFlavoredMarkdown` | Resolves `[[wikilinks]]`, callouts, Mermaid, and `![[embeds]]` |
| `GitHubFlavoredMarkdown` | Tables, strikethrough, task lists, autolinks |
| `SyntaxHighlighting` | Shiki-based code block highlighting |
| `CrawlLinks` | Rewrites internal links to routed paths and records link graph |
| `TableOfContents` | Extracts headings and attaches the ToC tree to `fileData` |
| `Latex` | Renders math blocks via KaTeX or MathJax |

`FrontMatter` must be first in the transformers array. Every other transformer that reads `fileData.frontmatter` depends on it.

## Filters run second and remove pages from the graph

A filter is a predicate that returns `true` to keep a page and `false` to drop it. Filtered pages do not appear in any emitter output, including the link graph and backlinks.

```ts
filters: [Plugin.RemoveDrafts()],
```

`RemoveDrafts` drops any page where `frontmatter.draft === true` or `frontmatter.status === "draft"` (depending on configuration). Add it in production. Remove it in development when reviewing draft pages is needed. A custom filter looks like:

```ts
// Drop any page tagged "internal"
const RemoveInternal: QuartzFilterPlugin = () => ({
  name: "RemoveInternal",
  shouldPublish(_ctx, [_tree, vfile]) {
    const tags: string[] = vfile.data.frontmatter?.tags ?? []
    return !tags.includes("internal")
  },
})
```

## Emitters run last and write output files

An emitter takes the filtered graph and produces one or more files in `public/`. Each emitter owns a specific output type.

| Plugin | Output |
|---|---|
| `ContentPage` | One HTML file per content page |
| `FolderPage` | Index pages for each folder |
| `TagPage` | Index pages for each tag |
| `ContentIndex` | `/sitemap.xml` and `/index.xml` (RSS) |
| `Assets` | Copies `content/` static assets |
| `Static` | Copies `quartz/static/` assets |
| `CNAME` | Writes `public/CNAME` from `baseUrl` |

`CNAME` must be in the emitters list for custom-domain deploys on GitHub Pages. See [[tooling/quartz-deployment]] for the full deploy flow.

## Write a custom transformer by implementing `QuartzTransformerPlugin`

A transformer plugin returns an object with optional `remark` and `rehype` hooks plus a `name` string.

```ts
import { QuartzTransformerPlugin } from "../types"

export const ReadingTimeEstimate: QuartzTransformerPlugin = () => ({
  name: "ReadingTimeEstimate",
  markdownPlugins() {
    return [
      () => (tree, vfile) => {
        const words = vfile.value.toString().split(/\s+/).length
        vfile.data.frontmatter ??= {}
        vfile.data.frontmatter.readingTime = Math.ceil(words / 200)
      },
    ]
  },
})
```

Return `markdownPlugins()` for remark (pre-hast) transforms and `htmlPlugins()` for rehype (post-hast) transforms. Both methods return arrays of unified plugins.

## Respect transformer ordering or watch silently wrong output appear

Transformers run in declaration order. The rules:

1. `FrontMatter` is always first. Every other plugin that reads frontmatter fields fails silently if it runs before `FrontMatter`.
2. `ObsidianFlavoredMarkdown` must run before `CrawlLinks`. `CrawlLinks` resolves links that `ObsidianFlavoredMarkdown` has already normalized to standard markdown syntax. Swapping them leaves wikilinks unresolved.
3. `SyntaxHighlighting` runs after `GitHubFlavoredMarkdown`. GFM wraps fenced code blocks in the right AST nodes; syntax highlighting colors them. The reverse order colors nothing.
4. `TableOfContents` should run after `ObsidianFlavoredMarkdown` so that heading IDs account for any transformations applied to heading text.

See [[tooling/quartz-debugging]] for how to trace which transformer mutated a page.

## Avoid the `name` collision pitfall in custom plugins

Each plugin's `name` field must be unique across transformers, filters, and emitters. Quartz uses `name` to identify plugins in its cache and error messages. If two plugins share a name, the second one silently overwrites the cache entry of the first, producing stale output that persists across rebuilds until `.quartz-cache/` is cleared.

```ts
// Wrong: generic name risks collision
const MyPlugin: QuartzTransformerPlugin = () => ({ name: "Plugin" })

// Correct: scoped, specific name
const MyPlugin: QuartzTransformerPlugin = () => ({ name: "InjectReadingTime" })
```

## Related

- [[tooling/quartz]]
- [[tooling/quartz-config]]
- [[tooling/quartz-components]]
- [[tooling/quartz-layout]]
- [[tooling/quartz-debugging]]
- [[howto/deploy-quartz-site]]
- [[ops/github-pages]]
