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-listlanguage 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 (
localeComparewith{ numeric: true, sensitivity: "base" }) - Output format (bulleted list of wikilinks with optional descriptions from frontmatter)
Consequences
Benefits
- Zero content duplication — the same
page-listcode 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.jsonso 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.metadataCachefor 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
dataviewblock 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-listautomation (eliminating MECE violations)
Implementation
Files Created
| File | Purpose |
|---|---|
content/.obsidian/plugins/obsidian-page-list/manifest.json | Plugin metadata (id, version, description) |
content/.obsidian/plugins/obsidian-page-list/main.js | Plugin implementation (~160 lines, plain JS) |
Files Modified
| File | Change |
|---|---|
content/.obsidian/community-plugins.json | Added "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.
Related
- 01 Site Rendering Technology — Quartz 4 selection
- 02 Site Hosting Platform — GitHub Pages deployment
quartz/plugins/transformers/pageList.ts— Quartz-side implementationcontent/.obsidian/plugins/obsidian-page-list/main.js— Obsidian-side implementationcontent/00 Governance/05 Knowledge Standards.md— Documents thepage-listconvention under “Auto-Populated Listings”