Emitter plugins generate the final output files for your Quartz site. They run after transformers process content and filters determine what to publish.
How Emitters Work
Emitters receive all processed content and generate output files:
export type QuartzEmitterPluginInstance = {
name : string
emit : (
ctx : BuildCtx ,
content : ProcessedContent [],
resources : StaticResources ,
) => Promise < FilePath []> | AsyncGenerator < FilePath >
partialEmit ?: (...) => Promise < FilePath []> | AsyncGenerator < FilePath > | null
getQuartzComponents ?: ( ctx : BuildCtx ) => QuartzComponent []
externalResources ?: ( ctx : BuildCtx ) => Partial < StaticResources >
}
Emitters can be run in parallel, so they should not depend on each other’s output.
Core Emitters
ContentPage
Generates individual HTML pages for each Markdown file.
Plugin . ContentPage ({
// Optional: customize layout components
head: MyCustomHead ,
header: [ MyHeader ],
beforeBody: [ Breadcrumbs ],
pageBody: Content (),
left: [ DesktopOnly ( TableOfContents ())],
right: [ Graph (), Backlinks ()],
footer: Footer (),
})
Features:
Renders Markdown content to HTML
Applies page layout and components
Generates SEO metadata
Handles internal links
Output:
{slug}.html for each content file
Skips tag pages and folder indexes (handled by other emitters)
ContentPage is essential - your site needs this to display content!
Assets
Copies non-Markdown files (images, PDFs, videos) to the output directory.
Implementation:
quartz/plugins/emitters/assets.ts
export const Assets : QuartzEmitterPlugin = () => {
return {
name: "Assets" ,
async * emit ({ argv , cfg }) {
// Glob all non-MD files
const fps = await glob ( "**" , argv . directory , [
"**/*.md" ,
... cfg . configuration . ignorePatterns
])
for ( const fp of fps ) {
yield copyFile ( argv , fp )
}
},
}
}
Behavior:
Copies images, PDFs, videos, etc.
Preserves directory structure
Respects ignorePatterns in config
Slugifies output filenames
Preserves .html filenames
Assets runs in parallel with other emitters for faster builds.
Static
Copies static files from quartz/static/ directory.
Usage:
Place files in quartz/static/ to include them in the output:
quartz/static/
├── robots.txt
├── favicon.ico
├── images/
│ └── logo.png
└── files/
└── download.pdf
These will be copied to the root of your output directory.
ComponentResources
Generates CSS and JavaScript bundles for Quartz components.
Plugin . ComponentResources ()
Output:
index.css - Compiled styles
prescript.js - Runs before page load
postscript.js - Runs after DOM ready
Features:
Bundles component styles
Minifies JavaScript
Handles theme variables
Includes component dependencies
Required for proper styling and functionality. Don’t remove this emitter!
ContentIndex
Generates search index, sitemap, and RSS feed.
Plugin . ContentIndex ({
enableSiteMap: true ,
enableRSS: true ,
rssLimit: 10 ,
rssFullHtml: false ,
rssSlug: "index" ,
includeEmptyFiles: true ,
})
Generate XML sitemap for SEO
Maximum number of items in RSS feed
Include full HTML content in RSS (vs. description only)
Include files with no content in index
Output:
sitemap.xml - Site structure for search engines
index.xml - RSS feed (or custom slug)
static/contentIndex.json - Search index
FolderPage
Generates index pages for folders.
Features:
Lists all pages in a folder
Shows subfolder structure
Sortable by date or title
Customizable layout
Example:
For folder structure:
content/
└── blog/
├── post1.md
└── post2.md
Generates: blog/index.html with list of posts.
TagPage
Generates index pages for each tag.
Features:
Creates page for each unique tag
Lists all content with that tag
Sortable by date
Tag hierarchy support
Example:
Content with tags: [tutorial, quartz] creates:
tags/tutorial/index.html
tags/quartz/index.html
AliasRedirects
Generates redirect pages for aliases defined in frontmatter.
Usage:
---
title : My Page
aliases :
- old-url
- previous-name
---
Generates redirect pages:
old-url.html → redirects to my-page.html
previous-name.html → redirects to my-page.html
Great for maintaining old URLs when you rename pages!
NotFoundPage
Generates custom 404 error page.
Output:
404.html - Custom not found page
Features:
Uses your site theme
Shows navigation
Provides search functionality
Favicon
Copies favicon to output directory.
Place favicon files in quartz/static/ and they’ll be copied to the root.
CNAME
Generates CNAME file for custom domains (GitHub Pages).
Uses baseUrl from configuration to create CNAME file.
CustomOgImages
Generates custom Open Graph images for social media previews.
Significantly increases build time. Comment out for faster development builds.
Emitter Configuration Example
const config : QuartzConfig = {
plugins: {
emitters: [
// Redirects first (fast)
Plugin . AliasRedirects (),
// Core resources
Plugin . ComponentResources (),
// Content pages
Plugin . ContentPage (),
Plugin . FolderPage (),
Plugin . TagPage (),
Plugin . NotFoundPage (),
// Indexes and feeds
Plugin . ContentIndex ({
enableSiteMap: true ,
enableRSS: true ,
rssLimit: 20 ,
}),
// Assets (runs in parallel)
Plugin . Assets (),
Plugin . Static (),
Plugin . Favicon (),
Plugin . CNAME (),
// Slow emitters last (comment out in dev)
// Plugin.CustomOgImages(),
],
},
}
Creating Custom Emitters
Basic Custom Emitter
plugins/emitters/myEmitter.ts
import { QuartzEmitterPlugin } from "../types"
import { write } from "./helpers"
import { FilePath , FullSlug } from "../../util/path"
export const CustomSitemap : QuartzEmitterPlugin = () => {
return {
name: "CustomSitemap" ,
async * emit ( ctx , content ) {
const urls = content
. map (([ _ , file ]) => file . data . slug ! )
. join ( " \n " )
yield write ({
ctx ,
content: urls ,
slug: "sitemap" as FullSlug ,
ext: ".txt" ,
})
},
}
}
Emitter with File Generation
plugins/emitters/jsonExport.ts
import { QuartzEmitterPlugin } from "../types"
import { write } from "./helpers"
import { FilePath , FullSlug } from "../../util/path"
export interface Options {
includeContent : boolean
}
const defaultOptions : Options = {
includeContent: false ,
}
export const JSONExport : QuartzEmitterPlugin < Partial < Options >> = ( userOpts ) => {
const opts = { ... defaultOptions , ... userOpts }
return {
name: "JSONExport" ,
async * emit ( ctx , content ) {
const data = content . map (([ tree , file ]) => ({
slug: file . data . slug ,
title: file . data . frontmatter ?. title ,
tags: file . data . frontmatter ?. tags ,
content: opts . includeContent ? file . data . text : undefined ,
}))
yield write ({
ctx ,
content: JSON . stringify ( data , null , 2 ),
slug: "export" as FullSlug ,
ext: ".json" ,
})
},
}
}
Emitter with Multiple Files
plugins/emitters/perTagPages.ts
import { QuartzEmitterPlugin } from "../types"
import { write } from "./helpers"
import { FullSlug } from "../../util/path"
export const PerTagJSON : QuartzEmitterPlugin = () => {
return {
name: "PerTagJSON" ,
async * emit ( ctx , content ) {
// Group content by tag
const tagMap = new Map < string , any []>()
for ( const [ _ , file ] of content ) {
const tags = file . data . frontmatter ?. tags || []
for ( const tag of tags ) {
if ( ! tagMap . has ( tag )) {
tagMap . set ( tag , [])
}
tagMap . get ( tag ) ! . push ({
title: file . data . frontmatter ?. title ,
slug: file . data . slug ,
})
}
}
// Emit one JSON file per tag
for ( const [ tag , items ] of tagMap ) {
yield write ({
ctx ,
content: JSON . stringify ( items , null , 2 ),
slug: `tags/ ${ tag } /data` as FullSlug ,
ext: ".json" ,
})
}
},
}
}
Partial Emit (Incremental Builds)
Emitters can implement partialEmit for faster development rebuilds:
export const MyEmitter : QuartzEmitterPlugin = () => {
return {
name: "MyEmitter" ,
// Full build
async * emit ( ctx , content , resources ) {
for ( const [ tree , file ] of content ) {
yield processFile ( ctx , tree , file )
}
},
// Incremental rebuild (dev mode)
async * partialEmit ( ctx , content , resources , changeEvents ) {
// Only process changed files
const changedSlugs = new Set (
changeEvents
. filter ( e => e . type !== "delete" && e . file )
. map ( e => e . file ! . data . slug ! )
)
for ( const [ tree , file ] of content ) {
if ( changedSlugs . has ( file . data . slug ! )) {
yield processFile ( ctx , tree , file )
}
}
},
}
}
partialEmit is called during quartz build --serve for faster hot reloads.
Using Helper Functions
Quartz provides helper functions for emitters:
import { write } from "./helpers"
import path from "path"
import fs from "fs"
// Write HTML/text files
yield write ({
ctx ,
content: htmlString ,
slug: "my-page" as FullSlug ,
ext: ".html" ,
})
// Write to specific path
const dest = path . join ( ctx . argv . output , "custom" , "path.txt" )
await fs . promises . mkdir ( path . dirname ( dest ), { recursive: true })
await fs . promises . writeFile ( dest , content )
yield dest as FilePath
Accessing Components
Emitters can specify which components they use:
import { Content } from "../../components"
import { QuartzComponent } from "../../components/types"
export const MyEmitter : QuartzEmitterPlugin = () => {
return {
name: "MyEmitter" ,
getQuartzComponents ( ctx ) {
return [
Content (),
// ... other components
]
},
async * emit ( ctx , content , resources ) {
// ...
},
}
}
This helps Quartz optimize resource loading by only including needed components.
External Resources
Emitters can inject CSS/JS like transformers:
export const MyEmitter : QuartzEmitterPlugin = () => {
return {
name: "MyEmitter" ,
externalResources ( ctx ) {
return {
css: [{ content: "https://example.com/style.css" }],
js: [
{
src: "https://example.com/script.js" ,
loadTime: "afterDOMReady" ,
contentType: "external" ,
},
],
additionalHead: [
< meta name = "custom" content = "value" /> ,
],
}
},
async * emit ( ctx , content , resources ) {
// ...
},
}
}
Common Emitter Patterns
Generate Aggregation Pages
async * emit ( ctx , content ) {
// Group by year
const byYear = new Map ()
for ( const [ _ , file ] of content ) {
const year = new Date ( file . data . frontmatter ?. date ). getFullYear ()
if ( ! byYear . has ( year )) byYear . set ( year , [])
byYear . get ( year ). push ( file )
}
// Emit page per year
for ( const [ year , files ] of byYear ) {
yield write ({
ctx ,
content: renderYearPage ( year , files ),
slug: `archive/ ${ year } ` as FullSlug ,
ext: ".html" ,
})
}
}
async * emit ( ctx , content ) {
for ( const [ _ , file ] of content ) {
// Only copy files with specific tag
const tags = file . data . frontmatter ?. tags || []
if ( ! tags . includes ( "special" )) continue
const src = file . data . filePath !
const dest = path . join ( ctx . argv . output , "special" , path . basename ( src ))
await fs . promises . mkdir ( path . dirname ( dest ), { recursive: true })
await fs . promises . copyFile ( src , dest )
yield dest as FilePath
}
}
Use async generators - Allows parallel processing
Implement partialEmit - Faster development rebuilds
Cache expensive operations - Store results between builds
Comment out slow emitters - Skip CustomOgImages during development
Debugging Emitters
export const DebugEmitter : QuartzEmitterPlugin = () => {
return {
name: "DebugEmitter" ,
async * emit ( ctx , content ) {
console . log ( `Processing ${ content . length } files` )
for ( const [ tree , file ] of content ) {
console . log ( `- ${ file . data . slug } ` )
console . log ( ` Title: ${ file . data . frontmatter ?. title } ` )
console . log ( ` Tags: ${ file . data . frontmatter ?. tags ?. join ( ", " ) } ` )
}
// Don't actually emit anything
return []
},
}
}
See Also
Plugin Overview Understanding the plugin system
Transformer Plugins Process content before emission
Filter Plugins Control what gets emitted
VFile Documentation Understanding file data structure