Skip to main content
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.
Configuration
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.
Configuration
Plugin.Assets()
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.
Configuration
Plugin.Static()
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.
Configuration
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.
Configuration
Plugin.ContentIndex({
  enableSiteMap: true,
  enableRSS: true,
  rssLimit: 10,
  rssFullHtml: false,
  rssSlug: "index",
  includeEmptyFiles: true,
})
enableSiteMap
boolean
default:true
Generate XML sitemap for SEO
enableRSS
boolean
default:true
Generate RSS feed
rssLimit
number
default:10
Maximum number of items in RSS feed
rssFullHtml
boolean
default:false
Include full HTML content in RSS (vs. description only)
rssSlug
string
default:"index"
Slug for RSS feed file
includeEmptyFiles
boolean
default:true
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.
Configuration
Plugin.FolderPage()
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.
Configuration
Plugin.TagPage()
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.
Configuration
Plugin.AliasRedirects()
Usage:
content/my-page.md
---
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.
Configuration
Plugin.NotFoundPage()
Output:
  • 404.html - Custom not found page
Features:
  • Uses your site theme
  • Shows navigation
  • Provides search functionality

Favicon

Copies favicon to output directory.
Configuration
Plugin.Favicon()
Place favicon files in quartz/static/ and they’ll be copied to the root.

CNAME

Generates CNAME file for custom domains (GitHub Pages).
Configuration
Plugin.CNAME()
Uses baseUrl from configuration to create CNAME file.

CustomOgImages

Generates custom Open Graph images for social media previews.
Configuration
Plugin.CustomOgImages()
Significantly increases build time. Comment out for faster development builds.

Emitter Configuration Example

quartz.config.ts
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

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) {
  const data = content.map(([_, file]) => ({
    title: file.data.frontmatter?.title,
    slug: file.data.slug,
  }))
  
  // JSON
  yield write({
    ctx,
    content: JSON.stringify(data),
    slug: "data" as FullSlug,
    ext: ".json",
  })
  
  // CSV
  const csv = data.map(d => `${d.title},${d.slug}`).join("\n")
  yield write({
    ctx,
    content: csv,
    slug: "data" as FullSlug,
    ext: ".csv",
  })
}
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
  }
}

Performance Tips

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

Build docs developers (and LLMs) love