# 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: ```bash nix develop ``` All `npm` commands below must be run inside the dev shell. ## Build, Lint, and Test Commands ```bash # 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`) ```json { "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 ```typescript // 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: ```typescript type SlugLike = 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: ```typescript export default ((userOpts?: Partial) => { const opts: Options = { ...defaultOptions, ...userOpts } const ComponentName: QuartzComponent = ({ cfg, displayClass }: QuartzComponentProps) => { return
...
} ComponentName.css = style // SCSS styles ComponentName.afterDOMLoaded = script // Client-side JS return ComponentName }) satisfies QuartzComponentConstructor ``` ### Plugin Pattern Three plugin types: transformers, filters, and emitters. ```typescript export const PluginName: QuartzTransformerPlugin> = (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`: ```typescript 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](https://github.com/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 ```bash 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 ```bash 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 ```bash # 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-hugo` → `content/**/*.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` ```elixir 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