Context

The Calab.ai Handbook is authored in Obsidian and published via Quartz 4 (see Decision 01). To eliminate manually-maintained child page listings (a MECE violation where the same information was duplicated across parent and child pages), we introduced a custom page-list code block syntax processed by a Quartz transformer plugin at build time.

The page-list code block allows authors to write:

```page-list
path: 02 Guilds/GL03 Delivery Guild/Practices
sort: filename
```

At build time, the Quartz PageList transformer scans the specified folder, reads frontmatter from each child page, and replaces the code block with an auto-generated bulleted list of wikilinks. This ensures listings stay in sync automatically when pages are added, removed, or renamed — eliminating a class of MECE violations entirely.

However, this syntax is Quartz-specific. In Obsidian’s reading view, the page-list code block renders as a raw fenced code block showing the directive text, providing no navigation value to authors working locally. Since the Handbook is a dual-environment system (Obsidian for authoring, Quartz for publishing), a decision was needed on how to handle this rendering gap.

Decision

We will maintain rendering parity between Obsidian and Quartz for page-list code blocks by implementing a custom Obsidian plugin (obsidian-page-list) that mirrors the Quartz PageList transformer.

Both implementations share the same:

  • Code block syntax (page-list language fence)
  • Directive format (path:, sort:, depth:)
  • Folder scanning logic (overview file priority: README.md > index.md > first .md)
  • Self-exclusion (current file is omitted from its own listing)
  • Sorting behaviour (localeCompare with { numeric: true, sensitivity: "base" })
  • Output format (bulleted list of wikilinks with optional descriptions from frontmatter)

Consequences

Benefits

  • Zero content duplication — the same page-list code block works in both environments with no additional markup needed
  • Consistent authoring experience — authors see the same navigable listing whether browsing in Obsidian or on the published site
  • No dependency on external plugins — the Obsidian plugin is first-party, ~160 lines of plain JavaScript, with no build step required
  • No BRAT dependency — the plugin is installed directly in the vault’s plugin directory, tracked in git alongside other plugin files
  • Automatic enablement — the plugin ID is registered in community-plugins.json so it activates for all vault users

Trade-offs

  • Custom plugin maintenance — the Obsidian plugin must be kept in sync with the Quartz transformer if directives or scanning logic change. Both implementations are small (~160 lines each) and share identical logic, so drift risk is low.
  • No live-reload on file changes — the Obsidian plugin renders the listing when the reading view loads. Adding a new page requires switching away and back (or toggling edit/read mode) to see the updated listing. The Quartz build always produces a fresh listing.
  • Metadata cache dependency — the Obsidian plugin uses app.metadataCache for frontmatter. If the cache hasn’t indexed a newly-created file yet, it may briefly show the filename instead of the frontmatter title. This resolves automatically within seconds.

Considered Alternatives

A. Accept Status Quo (No Obsidian Rendering)

Leave page-list code blocks as raw code blocks in Obsidian. Authors use the file explorer sidebar for navigation.

Why not chosen: Degrades the authoring experience. Index pages — the primary navigation entry points — would show unhelpful code blocks instead of navigable listings. The vault is actively used for both authoring and reading.

B. Dual Blocks (page-list + Dataview)

Place both a page-list block (for Quartz) and a dataview block (for Obsidian) in each file. Each system renders its own block and ignores the other.

Why not chosen:

  • Dataview is not installed in the vault, and Quartz has no Dataview support (noted in Decision 01)
  • Content duplication: two blocks expressing the same intent must be kept in sync
  • Quartz would render the raw dataview block as a visible code block unless a second stripping transformer is added
  • Higher maintenance burden for 14+ files

C. Dataview Syntax Everywhere + Custom Quartz Parser

Standardise on Dataview query syntax and write a Quartz transformer that parses a subset of Dataview queries.

Why not chosen:

  • High implementation effort — requires building a Dataview query parser (even for a subset)
  • Risk of subtle behavioural differences between the two rendering engines
  • Dataview’s query language is more complex than needed for simple folder listings
  • Still requires installing Dataview in Obsidian

D. Templater-Based Static Generation

Use the already-installed Templater plugin to “bake” static wikilink lists into the markdown files.

Why not chosen:

  • Lists become static once generated — they do not auto-update when pages are added or removed
  • Requires manually re-running the template for each affected file
  • Defeats the purpose of the page-list automation (eliminating MECE violations)

Implementation

Files Created

FilePurpose
content/.obsidian/plugins/obsidian-page-list/manifest.jsonPlugin metadata (id, version, description)
content/.obsidian/plugins/obsidian-page-list/main.jsPlugin implementation (~160 lines, plain JS)

Files Modified

FileChange
content/.obsidian/community-plugins.jsonAdded "obsidian-page-list" to enable the plugin

Architecture

Obsidian (authoring)                    Quartz (publishing)
========================               ========================
registerMarkdownCodeBlockProcessor     markdownPlugins() remark plugin
  └─ "page-list" language                └─ visit(tree, "code")
     │                                      │
     ├─ parseDirectives(source)             ├─ parseDirectives(node.value)
     ├─ vault.getFolderByPath(path)         ├─ fs.readdirSync(targetDir)
     ├─ metadataCache.getFileCache()        ├─ matter(fs.readFileSync())
     ├─ findOverviewFile(folder)            ├─ findOverviewFile(dir)
     ├─ sort by title/filename/created      ├─ sort by title/filename/created
     └─ MarkdownRenderer.render()           └─ emit text nodes with [[wikilinks]]
        (renders wikilinks as HTML links)       (OFM converts to link AST nodes)

No Content Changes Required

All 14 existing page-list code blocks work identically in both environments without modification.

  • 01 Site Rendering Technology — Quartz 4 selection
  • 02 Site Hosting Platform — GitHub Pages deployment
  • quartz/plugins/transformers/pageList.ts — Quartz-side implementation
  • content/.obsidian/plugins/obsidian-page-list/main.js — Obsidian-side implementation
  • content/00 Governance/05 Knowledge Standards.md — Documents the page-list convention under “Auto-Populated Listings”