Files
quartz-org-roam/AGENTS.md

12 KiB

AGENTS.md - Coding Agent Instructions

This document provides essential information for AI coding agents working in this repository.

Project Overview

Quartz is a static site generator for publishing digital gardens and notes as websites. Built with TypeScript, Preact, and unified/remark/rehype for markdown processing.

Stack Technology
Language TypeScript 5.x (strict mode)
Runtime Node.js >=22 (v22.16.0 pinned)
Package Mgr npm >=10.9.2
Module System ES Modules ("type": "module")
UI Framework Preact 10.x (JSX with react-jsx pragma)
Build Tool esbuild
Styling SCSS via esbuild-sass-plugin

Environment

This is a Nix project. Use the provided flake.nix to enter a dev shell with Node.js 22 and npm:

nix develop

All npm commands below must be run inside the dev shell.

Build, Lint, and Test Commands

# Type check and format check (CI validation)
npm run check

# Auto-format code with Prettier
npm run format

# Run all tests
npm run test

# Run a single test file
npx tsx --test quartz/util/path.test.ts

# Run tests matching a pattern (use --test-name-pattern)
npx tsx --test --test-name-pattern="typeguards" quartz/util/path.test.ts

# Build the static site
npx quartz build

# Build and serve with hot reload
npx quartz build --serve

# Profile build performance
npm run profile

Test Files Location

Tests use Node.js native test runner via tsx. Test files follow the *.test.ts pattern:

  • quartz/util/path.test.ts
  • quartz/util/fileTrie.test.ts
  • quartz/components/scripts/search.test.ts

Code Style Guidelines

Prettier Configuration (.prettierrc)

{
  "printWidth": 100,
  "tabWidth": 2,
  "semi": false,
  "trailingComma": "all",
  "quoteProps": "as-needed"
}

No ESLint - only Prettier for formatting. Run npm run format before committing.

TypeScript Configuration

  • Strict mode enabled (strict: true)
  • noUnusedLocals: true - no unused variables
  • noUnusedParameters: true - no unused function parameters
  • JSX configured for Preact (jsxImportSource: "preact")

Import Conventions

// 1. External packages first
import { PluggableList } from "unified"
import { visit } from "unist-util-visit"

// 2. Internal utilities/types (relative paths)
import { QuartzTransformerPlugin } from "../types"
import { FilePath, slugifyFilePath } from "../../util/path"
import { i18n } from "../../i18n"

Naming Conventions

Element Convention Example
Files (utils) camelCase path.ts, fileTrie.ts
Files (comps) PascalCase TableOfContents.tsx, Search.tsx
Types/Interfaces PascalCase QuartzComponent, FullSlug
Type Guards is* prefix isFilePath(), isFullSlug()
Constants UPPER_CASE QUARTZ, UPSTREAM_NAME
Options types Options interface Options { ... }

Branded Types Pattern

This codebase uses branded types for type-safe path handling:

type SlugLike<T> = string & { __brand: T }
export type FilePath = SlugLike<"filepath">
export type FullSlug = SlugLike<"full">
export type SimpleSlug = SlugLike<"simple">

// Always validate with type guards before using
export function isFilePath(s: string): s is FilePath { ... }

Component Pattern (Preact)

Components use a factory function pattern with attached static properties:

export default ((userOpts?: Partial<Options>) => {
  const opts: Options = { ...defaultOptions, ...userOpts }

  const ComponentName: QuartzComponent = ({ cfg, displayClass }: QuartzComponentProps) => {
    return <div class={classNames(displayClass, "component-name")}>...</div>
  }

  ComponentName.css = style           // SCSS styles
  ComponentName.afterDOMLoaded = script  // Client-side JS
  return ComponentName
}) satisfies QuartzComponentConstructor

Plugin Pattern

Three plugin types: transformers, filters, and emitters.

export const PluginName: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
  const opts = { ...defaultOptions, ...userOpts }
  return {
    name: "PluginName",
    markdownPlugins(ctx) { return [...] },
    htmlPlugins(ctx) { return [...] },
    externalResources(ctx) { return { js: [], css: [] } },
  }
}

Testing Pattern

Use Node.js native test runner with assert:

import test, { describe, beforeEach } from "node:test"
import assert from "node:assert"

describe("FeatureName", () => {
  test("should do something", () => {
    assert.strictEqual(actual, expected)
    assert.deepStrictEqual(actualObj, expectedObj)
    assert(condition) // truthy assertion
    assert(!condition) // falsy assertion
  })
})

Error Handling

  • Use try/catch for critical operations (file I/O, parsing)
  • Custom trace utility for error reporting with stack traces
  • process.exit(1) for fatal errors
  • console.warn() for non-fatal issues

Async Patterns

  • Prefer async/await over raw promises
  • Use async generators (async *emit()) for streaming file output
  • Use async-mutex for concurrent build protection

Project Structure

quartz/
├── bootstrap-cli.mjs    # CLI entry point
├── build.ts             # Build orchestration
├── cfg.ts               # Configuration types
├── components/          # Preact UI components
│   ├── *.tsx            # Components
│   ├── scripts/         # Client-side scripts (*.inline.ts)
│   └── styles/          # Component SCSS
├── plugins/
│   ├── transformers/    # Markdown AST transformers
│   ├── filters/         # Content filters
│   ├── emitters/        # Output generators
│   └── types.ts         # Plugin type definitions
├── processors/          # Build pipeline (parse/filter/emit)
├── util/                # Utility functions
└── i18n/                # Internationalization (30+ locales)

Branch Workflow

This is a fork of jackyzha0/quartz with org-roam customizations.

Branch Purpose
main Clean mirror of upstream quartz — no custom code
org-roam Default branch — all customizations live here
feature/* Short-lived branches off org-roam

Pulling Upstream Updates

git checkout main
git fetch upstream
git merge upstream/main
git checkout org-roam
git merge main
# Resolve conflicts if any, then commit

Working on Features

git checkout org-roam
git checkout -b feature/my-feature
# ... work ...
git checkout org-roam
git merge feature/my-feature
git branch -d feature/my-feature

Merge direction: upstream → main → org-roam → feature/*

Org-Roam Workflow

Notes live in a separate directory outside this repo. The export pipeline converts them to Markdown via ox-hugo, applies post-processing transforms, then Quartz builds the site.

Tooling

The dev shell (nix develop) provides:

  • nodejs_22 — Quartz build
  • elixir — runs the export script and pipeline
  • emacs + ox-hugo — performs the org → markdown conversion

Export and build

# Export only (wipes content/, exports all .org files, runs pipeline)
NOTES_DIR=/path/to/notes npm run export

# Export then build the site
NOTES_DIR=/path/to/notes npm run build:notes

# Positional arg also works
elixir scripts/export.exs /path/to/notes

Optional env vars for the pipeline:

Var Default Purpose
BIBTEX_FILE Path to .bib file for citation fallback
ZOTERO_URL http://localhost:23119 Zotero Better BibTeX base URL
CITATION_MODE warn silent / warn / strict

Export pipeline phases

scripts/export.exs runs four phases in sequence:

  1. Wipe content/ (preserving .gitkeep)
  2. Export each .org file via emacs --batch + ox-hugocontent/**/*.md
  3. Pipeline — run Elixir transform modules over every .md file
  4. Index — generate a fallback content/index.md if none was exported

The export uses TOML frontmatter (+++) and per-file mode (not per-subtree).

Markdown pipeline (scripts/pipeline/)

A standalone Mix project that post-processes content/*.md after ox-hugo. It is compiled automatically on first run; subsequent runs use the _build/ cache and are fast.

Architecture:

scripts/pipeline/
├── mix.exs                          # deps: req, jason
└── lib/
    ├── pipeline.ex                  # Generic runner (fold transforms over .md files)
    ├── pipeline/
    │   ├── application.ex           # OTP app — starts Finch HTTP pool
    │   ├── transform.ex             # Behaviour: init/1, apply/3, teardown/1
    │   ├── transforms/
    │   │   └── citations.ex         # Resolves cite:key → [Label](url)
    │   └── resolvers/
    │       ├── zotero.ex            # JSON-RPC to Zotero Better BibTeX
    │       ├── bibtex.ex            # Parses local .bib file
    │       └── doi.ex               # Bare-key fallback (always succeeds)

Adding a new transform:

  1. Create scripts/pipeline/lib/pipeline/transforms/my_transform.ex
  2. Implement the Pipeline.Transform behaviour (init/1, apply/3)
  3. Append the module to transforms in scripts/export.exs
transforms = [
  Pipeline.Transforms.Citations,
  Pipeline.Transforms.MyTransform,  # new
]

Citation resolution (Pipeline.Transforms.Citations)

Handles org-citar syntax that passes through ox-hugo unchanged:

Syntax Example
org-cite / citar [cite:@key]
multiple keys [cite:@key1;@key2]
bare (legacy) cite:key

Resolution chain (first success wins):

  1. Zotero — JSON-RPC to localhost:23119/better-bibtex/json-rpc
    • Calls item.search to find the item, then item.attachments to get the PDF link (zotero://open-pdf/library/items/KEY)
    • Falls back to zotero://select/library/items/KEY if no PDF attachment
    • Probe uses a JSON-RPC call, not /better-bibtex/cayw (that endpoint blocks waiting for interactive input)
  2. BibTeX — parses BIBTEX_FILE; extracts authors, year, DOI/URL
  3. DOI fallback — always succeeds; renders bare key or https://doi.org/...

Zotero JSON-RPC gotcha: Req 0.5 does not allow combining :finch and :connect_options in the same call. Use :receive_timeout only.

Important Notes

  • Client-side scripts: Use .inline.ts suffix, bundled via esbuild
  • Isomorphic code: quartz/util/path.ts must not use Node.js APIs
  • Incremental builds: Plugins can implement partialEmit for efficiency
  • Markdown flavors: Supports Obsidian (ofm.ts) and Roam (roam.ts) syntax
  • Pipeline build artifacts: scripts/pipeline/_build/ and scripts/pipeline/deps/ are gitignored — run mix deps.get inside scripts/pipeline/ after a fresh clone