Skip to main content
Filter plugins determine which content should be published. They run after transformers process the content but before emitters generate output files.

How Filters Work

Each filter implements a shouldPublish function that receives the processed content and returns:
  • true - Publish the content
  • false - Exclude from build
export type QuartzFilterPluginInstance = {
  name: string
  shouldPublish(ctx: BuildCtx, content: ProcessedContent): boolean
}
All filters must return true for content to be published. If any filter returns false, the content is excluded.

Built-in Filters

RemoveDrafts

Excludes content marked as draft in frontmatter.
Configuration
Plugin.RemoveDrafts()
Implementation:
quartz/plugins/filters/draft.ts
import { QuartzFilterPlugin } from "../types"

export const RemoveDrafts: QuartzFilterPlugin<{}> = () => ({
  name: "RemoveDrafts",
  shouldPublish(_ctx, [_tree, vfile]) {
    const draftFlag: boolean =
      vfile.data?.frontmatter?.draft === true || 
      vfile.data?.frontmatter?.draft === "true"
    return !draftFlag
  },
})
Usage: Mark any page as draft to exclude it from the build:
---
title: Work in Progress
draft: true
---

This page won't be published.
Both draft: true and draft: "true" (string) are recognized as draft.

ExplicitPublish

Only publishes content explicitly marked for publication. This is the opposite of RemoveDrafts - content is excluded by default unless marked as publish: true.
Configuration
Plugin.ExplicitPublish()
Implementation:
quartz/plugins/filters/explicit.ts
import { QuartzFilterPlugin } from "../types"

export const ExplicitPublish: QuartzFilterPlugin = () => ({
  name: "ExplicitPublish",
  shouldPublish(_ctx, [_tree, vfile]) {
    return (
      vfile.data?.frontmatter?.publish === true || 
      vfile.data?.frontmatter?.publish === "true"
    )
  },
})
Usage:
---
title: Public Content
publish: true
---

Only this page will be published.
Use ExplicitPublish instead of RemoveDrafts, not in addition to it. Using both will require content to have both publish: true and draft: false.

Choosing Between Filters

Best for: Most sites where content is public by default
filters: [Plugin.RemoveDrafts()]
Behavior:
  • ✅ Content published by default
  • ❌ Explicitly mark drafts with draft: true
  • 👍 Less frontmatter required
  • 📝 Good for blogs and documentation

Creating Custom Filters

Basic Custom Filter

plugins/filters/myFilter.ts
import { QuartzFilterPlugin } from "../types"

export const PublishAfterDate: QuartzFilterPlugin = () => ({
  name: "PublishAfterDate",
  shouldPublish(_ctx, [_tree, vfile]) {
    const publishDate = vfile.data?.frontmatter?.publishDate
    
    if (!publishDate) return true // No date = publish
    
    const date = new Date(publishDate)
    const now = new Date()
    
    return date <= now // Only publish if date has passed
  },
})
Usage:
---
title: Future Post
publishDate: 2026-12-25
---

This won't be published until Christmas 2026.

Filter with Options

plugins/filters/categoryFilter.ts
import { QuartzFilterPlugin } from "../types"

export interface Options {
  allowedCategories: string[]
  defaultAllow: boolean
}

const defaultOptions: Options = {
  allowedCategories: [],
  defaultAllow: true,
}

export const CategoryFilter: QuartzFilterPlugin<Partial<Options>> = (userOpts) => {
  const opts = { ...defaultOptions, ...userOpts }
  
  return {
    name: "CategoryFilter",
    shouldPublish(_ctx, [_tree, vfile]) {
      const category = vfile.data?.frontmatter?.category
      
      if (!category) return opts.defaultAllow
      
      return opts.allowedCategories.includes(category)
    },
  }
}
Configuration:
quartz.config.ts
import { CategoryFilter } from "./quartz/plugins/filters/categoryFilter"

const config: QuartzConfig = {
  plugins: {
    filters: [
      CategoryFilter({
        allowedCategories: ["blog", "tutorial"],
        defaultAllow: false,
      }),
    ],
  },
}

Advanced Filter: Tag-based Publishing

plugins/filters/tagFilter.ts
import { QuartzFilterPlugin } from "../types"

export interface Options {
  requiredTags: string[]
  excludedTags: string[]
  mode: "any" | "all" // any = OR, all = AND
}

const defaultOptions: Options = {
  requiredTags: [],
  excludedTags: [],
  mode: "any",
}

export const TagFilter: QuartzFilterPlugin<Partial<Options>> = (userOpts) => {
  const opts = { ...defaultOptions, ...userOpts }
  
  return {
    name: "TagFilter",
    shouldPublish(_ctx, [_tree, vfile]) {
      const tags = vfile.data?.frontmatter?.tags || []
      
      // Exclude if has any excluded tag
      const hasExcluded = opts.excludedTags.some(tag => tags.includes(tag))
      if (hasExcluded) return false
      
      // If no required tags, allow
      if (opts.requiredTags.length === 0) return true
      
      // Check required tags based on mode
      if (opts.mode === "any") {
        return opts.requiredTags.some(tag => tags.includes(tag))
      } else {
        return opts.requiredTags.every(tag => tags.includes(tag))
      }
    },
  }
}
Usage:
quartz.config.ts
filters: [
  TagFilter({
    requiredTags: ["public", "published"],
    mode: "any",
  }),
]
Publishes content with either public OR published tag.

Multiple Filters

You can combine multiple filters. Content must pass all filters to be published.
quartz.config.ts
const config: QuartzConfig = {
  plugins: {
    filters: [
      Plugin.RemoveDrafts(),
      PublishAfterDate(),
      TagFilter({
        excludedTags: ["private"],
      }),
    ],
  },
}
With multiple filters, content is excluded if any filter returns false.

Filter Access to Data

Filters have access to processed content data:
shouldPublish(_ctx, [tree, vfile]) {
  // vfile.data contains:
  const frontmatter = vfile.data.frontmatter  // Parsed frontmatter
  const slug = vfile.data.slug                // Page slug
  const filePath = vfile.data.filePath        // Original file path
  const text = vfile.data.text                // Processed text content
  const tags = vfile.data.frontmatter?.tags   // Tags
  
  // tree contains the HTML AST
  // ctx contains build context
  
  return true
}

Common Filter Patterns

shouldPublish(_ctx, [_tree, vfile]) {
  const path = vfile.data.filePath!
  return !path.includes("/private/")
}
shouldPublish(_ctx, [_tree, vfile]) {
  const text = vfile.data.text || ""
  const wordCount = text.split(/\s+/).length
  return wordCount >= 100 // Only publish substantial content
}
shouldPublish(_ctx, [_tree, vfile]) {
  const status = vfile.data.frontmatter?.status
  return status === "published" || status === "public"
}
shouldPublish(ctx, [_tree, vfile]) {
  const isDev = ctx.argv.serve
  const isInternal = vfile.data.frontmatter?.internal
  
  // Show internal content in dev, hide in production
  return isDev || !isInternal
}

Best Practices

Keep filters simple - Complex logic can slow down builds
Fail safe - Return true by default if data is missing
Log filtered content - Help users understand what’s excluded
Document frontmatter - Explain required fields to users

Debugging Filters

Add logging to understand filter behavior:
export const DebugFilter: QuartzFilterPlugin = () => ({
  name: "DebugFilter",
  shouldPublish(_ctx, [_tree, vfile]) {
    const title = vfile.data.frontmatter?.title
    const draft = vfile.data.frontmatter?.draft
    
    console.log(`Checking: ${title}`)    console.log(`  Draft: ${draft}`)
    console.log(`  Decision: ${!draft}`)
    
    return !draft
  },
})

See Also

Plugin Overview

Understanding the plugin system

Transformer Plugins

Process content before filtering

Emitter Plugins

Generate output after filtering

VFile Documentation

Understanding file data structure

Build docs developers (and LLMs) love