summary:

  • Nested folder/file explorer for site navigation.
  • Customizable.

Info

status:

  • Community plugin. purpose:
  • External-plugin reference implementation.

Installation

command:

npm install github:quartz-community/explorer --legacy-peer-deps

config:

quartz.config.yaml
plugins:
  - source: github:quartz-community/explorer
    enabled: true
    layout:
      position: left
      priority: 50

Features

navigation:

  • Shows all folders/files.
  • Reposition via layout. display_names:
  • Primary: title in folder/index.md; see Authoring Content.
  • Fallback: folder name.

Info

state:

  • Saved to local storage key fileTree. clear:
  • Use browser devtools. disable:
  • Set useSavedState: false.

Customization

yaml_options:

  • Configure serializable Explorer() options in quartz.config.yaml:
quartz.config.yaml
plugins:
  - source: github:quartz-community/explorer
    enabled: true
    options:
      title: Explorer
      folderClickBehavior: collapse # "link" = navigate; "collapse" = toggle
      folderDefaultState: collapsed # "collapsed" | "open"
      useSavedState: true
    layout:
      position: left
      priority: 50

ts_overrides:

  • Configure callback options in quartz.ts:
quartz.ts
import { loadQuartzConfig, loadQuartzLayout } from "./quartz/plugins/loader/config-loader"
import * as ExternalPlugin from "./.quartz/plugins"
 
// Advanced: callback functions cannot be expressed in YAML
ExternalPlugin.Explorer({
  sortFn: (a, b) => {
    /* ... */
  },
  filterFn: (node) => {
    /* ... */
  },
  mapFn: (node) => {
    /* ... */
  },
  order: ["filter", "map", "sort"],
})
 
const config = await loadQuartzConfig()
export default config
export const layout = await loadQuartzLayout()

How overrides work

merge:

  • quartz.ts overrides merge with YAML config during build. precedence:
  • defaults < YAML < TS overrides name_conflicts:
  • Use plugins map:
quartz.ts
import * as ExternalPlugin from "./.quartz/plugins"
ExternalPlugin.plugins["my-explorer"].Explorer({ mapFn: ... })

disable:

Advanced customization

callbacks:

  • Mutate FileTrieNode in place.
  • Semantics mirror Array.prototype sort/filter/map.
@quartz-community/explorer
class FileTrieNode {
  isFolder: boolean
  children: Array<FileTrieNode>
  data: ContentDetails | null
}
export type ContentDetails = {
  slug: FullSlug
  title: string
  links: SimpleSlug[]
  tags: string[]
  content: string
}

default_sort:

  • Folders first.
  • Alphabetical within folders/files.
Default sort function
// Sort order: folders first, then files. Sort folders and files alphabetically
ExternalPlugin.Explorer({
  sortFn: (a, b) => {
    if ((!a.isFolder && !b.isFolder) || (a.isFolder && b.isFolder)) {
      return a.displayName.localeCompare(b.displayName, undefined, {
        numeric: true,
        sensitivity: "base",
      })
    }
 
    if (!a.isFolder && b.isFolder) {
      return 1
    } else {
      return -1
    }
  },
})

execution:

  • Sequence: order.
type SortFn = (a: FileTrieNode, b: FileTrieNode) => number
type FilterFn = (node: FileTrieNode) => boolean
type MapFn = (node: FileTrieNode) => void

Basic examples

Sort all nodes alphabetically

yaml_options:

quartz.config.yaml
plugins:
  - source: github:quartz-community/explorer
    enabled: true
    options:
      # Simple options go in YAML
      title: Explorer
      folderDefaultState: collapsed

ts_override:

quartz.ts (override)
ExternalPlugin.Explorer({
  sortFn: (a, b) => {
    return a.displayName.localeCompare(b.displayName)
  },
})

Change display names (map)

ts_override:

quartz.ts (override)
ExternalPlugin.Explorer({
  mapFn: (node) => {
    node.displayName = node.displayName.toUpperCase()
    return node
  },
})

Note

callbacks:

  • JS callbacks (mapFn, filterFn, sortFn) require TS overrides.

Omit by display name (filter)

ts_override:

quartz.ts (override)
ExternalPlugin.Explorer({
  filterFn: (node) => {
    // Names to omit
    const omit = new Set(["authoring content", "tags", "advanced"])
 
    // Can also use node.slug or node.data fields
    // node.data exists only for files present on disk
    // Implicit folder nodes may lack index.md and node.data
    return !omit.has(node.displayName.toLowerCase())
  },
})

Remove files by tag

ts_override:

quartz.ts (override)
ExternalPlugin.Explorer({
  filterFn: (node) => {
    // Exclude files tagged "explorerexclude"
    return node.data?.tags?.includes("explorerexclude") !== true
  },
})

Show every element

ts_override:

quartz.ts (override)
ExternalPlugin.Explorer({
  filterFn: undefined, // no filter; show every file/folder
})

Advanced examples

Tip

structure:

  • Define callbacks outside component to keep quartz.ts small:
quartz.ts
import * as ExternalPlugin from "./.quartz/plugins"
import type { ExplorerOptions } from "./.quartz/plugins"
 
const mapFn: ExplorerOptions["mapFn"] = (node) => {
  // ...
}
const filterFn: ExplorerOptions["filterFn"] = (node) => {
  // ...
}
const sortFn: ExplorerOptions["sortFn"] = (a, b) => {
  // ...
}
 
ExternalPlugin.Explorer({
  // ...
  mapFn,
  filterFn,
  sortFn,
})

Add emoji prefix

ts_override:

quartz.ts (override)
ExternalPlugin.Explorer({
  mapFn: (node) => {
    if (node.isFolder) {
      node.displayName = "📁 " + node.displayName
    } else {
      node.displayName = "📄 " + node.displayName
    }
  },
})