Files
quartz-org-roam/AGENTS.md
Ignacio Ballesteros 692f23bc36 Add org-roam workflow: ox-hugo export pipeline and example notes
- Add flake.nix dev shell with Node.js 22, Elixir, and Emacs+ox-hugo
- Add scripts/export.exs: exports org-roam notes to content/ via ox-hugo,
  mirroring subdirectory structure and generating a fallback index.md
- Add npm scripts: export, build:notes, serve:notes
- Configure FrontMatter plugin for TOML (ox-hugo default output)
- Replace ObsidianFlavoredMarkdown with OxHugoFlavouredMarkdown
- Add example notes: Madrid public transport (metro, bus, roads)
- Update README and AGENTS.md with org-roam workflow instructions
2026-02-19 18:20:43 +01:00

8.3 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 script converts them to Markdown via ox-hugo, then Quartz builds the site.

Tooling

The dev shell (nix develop) provides:

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

Export and build

# Export only (wipes content/, exports all .org files)
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

The export script (scripts/export.exs) runs Emacs in batch mode, calling org-hugo-export-to-md per file (per-file mode, not per-subtree). It uses YAML frontmatter (ox-hugo default). content/ is wiped before each export.

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