diff --git a/AGENTS.md b/AGENTS.md index b8aedada9..044b15136 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,6 +17,16 @@ Built with TypeScript, Preact, and unified/remark/rehype for markdown processing | 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 @@ -234,6 +244,36 @@ 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 + +```bash +# 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 diff --git a/README.md b/README.md index 01e2c5889..eca6d847b 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,71 @@ -# Quartz v4 +# Quartz v4 — org-roam edition -> “[One] who works with the door open gets all kinds of interruptions, but [they] also occasionally gets clues as to what the world is and what might be important.” — Richard Hamming +> "[One] who works with the door open gets all kinds of interruptions, but [they] also occasionally gets clues as to what the world is and what might be important." — Richard Hamming Quartz is a set of tools that helps you publish your [digital garden](https://jzhao.xyz/posts/networked-thought) and notes as a website for free. -🔗 Read the documentation and get started: https://quartz.jzhao.xyz/ +This fork adds first-class support for [org-roam](https://www.orgroam.com/) notes via [ox-hugo](https://ox-hugo.scripter.co/). + +🔗 Upstream documentation: https://quartz.jzhao.xyz/ [Join the Discord Community](https://discord.gg/cRFFHYye7t) +## Quick Start + +### Prerequisites + +This project uses Nix. Enter the development shell, which provides Node.js 22, Elixir, and Emacs with ox-hugo: + +```bash +nix develop +``` + +All commands below must be run inside this shell. + +```bash +npm install +``` + +### Building from org-roam notes + +Your org-roam notes live in a separate directory. Point `NOTES_DIR` at it: + +```bash +# Export notes to content/ and build the site +NOTES_DIR=/path/to/notes npm run build:notes + +# Export, build, and serve with hot reload +NOTES_DIR=/path/to/notes npm run serve:notes + +# Export only (wipes content/ and re-exports all .org files) +NOTES_DIR=/path/to/notes npm run export +``` + +The export script mirrors the subdirectory structure of your notes into `content/` +and generates a fallback `index.md` if your notes don't include one. + +### Building without org-roam notes + +If you manage `content/` directly with Markdown files: + +```bash +# Build the site +npx quartz build + +# Build and serve with hot reload +npx quartz build --serve +``` + +The site is generated in `public/`. When serving, visit http://localhost:8080. + +### Development + +```bash +npm run check # type check + format check +npm run format # auto-format with Prettier +npm run test # run tests +``` + ## Sponsors

diff --git a/flake.lock b/flake.lock new file mode 100644 index 000000000..43f833f07 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1771008912, + "narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a82ccc39b39b621151d6732718e3e250109076fa", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000..49feffe2e --- /dev/null +++ b/flake.nix @@ -0,0 +1,30 @@ +{ + description = "Quartz org-roam dev shell"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { inherit system; }; + in + { + devShells.default = pkgs.mkShell { + buildInputs = [ + pkgs.nodejs_22 + pkgs.elixir + ((pkgs.emacsPackagesFor pkgs.emacs-nox).emacsWithPackages (epkgs: [ epkgs.ox-hugo ])) + pkgs.mcp-nixos + ]; + + shellHook = '' + echo "Node $(node --version) / npm $(npm --version)" + elixir --version 2>/dev/null | head -1 || true + echo "Emacs $(emacs --version | head -1)" + ''; + }; + }); +} diff --git a/notes/bus/emt-madrid.org b/notes/bus/emt-madrid.org new file mode 100644 index 000000000..5449e2c60 --- /dev/null +++ b/notes/bus/emt-madrid.org @@ -0,0 +1,16 @@ +:PROPERTIES: +:ID: emt-madrid +:END: +#+title: EMT Madrid (urban bus) + +Empresa Municipal de Transportes (EMT) operates the urban bus network +within the municipality of Madrid — around 200 lines. + +* Notable lines +- *Line 27* — connects Embajadores with Barrio de la Concepción, one of the + oldest routes in the network. +- *Line 34* — Argüelles to Carabanchel, crossing the city centre via Gran Vía. +- *Búho (owl) lines* — night buses running from Cibeles from midnight to 6 am. + +* See also +- [[id:madrid-transport][Madrid Public Transport]] diff --git a/notes/madrid-transport.org b/notes/madrid-transport.org new file mode 100644 index 000000000..c491e0d31 --- /dev/null +++ b/notes/madrid-transport.org @@ -0,0 +1,17 @@ +:PROPERTIES: +:ID: madrid-transport +:END: +#+title: Madrid Public Transport + +Madrid has one of the most extensive public transport networks in Europe, +operated primarily by [[id:crtm][Consorcio Regional de Transportes de Madrid]] (CRTM). + +* Modes +- [[id:metro-madrid][Metro de Madrid]] — 13 lines, ~300 km of track +- [[id:emt-madrid][EMT Bus]] — urban buses within the city +- Cercanías — suburban rail run by Renfe +- Interurbano — regional buses to the wider Community of Madrid + +* Ticketing +A single [[https://www.crtm.es][tarjeta transporte]] (transport card) works across all modes. +The Multi card covers zones A–C and is topped up at any metro station. diff --git a/notes/metro/metro-madrid.org b/notes/metro/metro-madrid.org new file mode 100644 index 000000000..f149711cc --- /dev/null +++ b/notes/metro/metro-madrid.org @@ -0,0 +1,18 @@ +:PROPERTIES: +:ID: metro-madrid +:END: +#+title: Metro de Madrid + +The Madrid Metro is the main rapid transit network in the city, opened in 1919. +It is the second oldest metro in the Iberian Peninsula after Barcelona. + +* Key Lines +| Line | Name | Colour | Terminals | +|------+-----------------+--------+------------------------------| +| L1 | Pinar de Chamartín–Valdecarros | Blue | Pinar de Chamartín / Valdecarros | +| L6 | Circular | Grey | Circular (loop) | +| L10 | — | Dark blue | Hospital Infanta Sofía / Tres Olivos | + +* See also +- [[id:madrid-transport][Madrid Public Transport]] +- [[id:sol-interchange][Sol interchange]] diff --git a/notes/metro/sol-interchange.org b/notes/metro/sol-interchange.org new file mode 100644 index 000000000..bade5b5c0 --- /dev/null +++ b/notes/metro/sol-interchange.org @@ -0,0 +1,12 @@ +:PROPERTIES: +:ID: sol-interchange +:END: +#+title: Sol (interchange) + +Sol is the busiest interchange station in the Madrid Metro, sitting beneath +Puerta del Sol in the city centre. + +Lines serving Sol: [[id:metro-madrid][L1]], L2, L3. + +It also connects to the Cercanías hub underneath, making it the de-facto +zero point of Madrid's public transport. diff --git a/notes/roads/crtm.org b/notes/roads/crtm.org new file mode 100644 index 000000000..09d17ceff --- /dev/null +++ b/notes/roads/crtm.org @@ -0,0 +1,22 @@ +:PROPERTIES: +:ID: crtm +:END: +#+title: CRTM — Consorcio Regional de Transportes de Madrid + +The CRTM is the regional authority that coordinates public transport across +the Community of Madrid. It does not operate services directly but sets +fares, zones, and integration policy. + +* Fare zones +| Zone | Coverage | +|-------+-----------------------------| +| A | Municipality of Madrid | +| B1 | Inner ring municipalities | +| B2 | Outer ring municipalities | +| B3 | Further suburban area | +| C1–C2 | Commuter belt | + +* Related +- [[id:madrid-transport][Madrid Public Transport]] +- [[id:metro-madrid][Metro de Madrid]] +- [[id:emt-madrid][EMT Madrid]] diff --git a/notes/roads/m30.org b/notes/roads/m30.org new file mode 100644 index 000000000..4954904a3 --- /dev/null +++ b/notes/roads/m30.org @@ -0,0 +1,19 @@ +:PROPERTIES: +:ID: m30 +:END: +#+title: M-30 + +The M-30 is Madrid's innermost ring road, circling the city centre at a +radius of roughly 3–5 km from Puerta del Sol. + +It runs mostly underground through the Madrid Río tunnel section along the +Manzanares river, built during the 2004–2007 renovation that reclaimed the +riverbank as a public park. + +* Key junctions +- Nudo Norte — connects to A-1 (Burgos) and A-6 (La Coruña) +- Nudo Sur — connects to A-4 (Cádiz) and A-42 (Toledo) + +* See also +- [[id:crtm][CRTM]] +- [[id:madrid-transport][Madrid Public Transport]] diff --git a/opencode.json b/opencode.json new file mode 100644 index 000000000..a40710832 --- /dev/null +++ b/opencode.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "nixos": { + "type": "local", + "command": ["mcp-nixos"], + "enabled": true + } + } +} diff --git a/package.json b/package.json index e1d138a52..69cc15666 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,10 @@ "check": "tsc --noEmit && npx prettier . --check", "format": "npx prettier . --write", "test": "tsx --test", - "profile": "0x -D prof ./quartz/bootstrap-cli.mjs build --concurrency=1" + "profile": "0x -D prof ./quartz/bootstrap-cli.mjs build --concurrency=1", + "export": "elixir scripts/export.exs", + "build:notes": "elixir scripts/export.exs && npx quartz build", + "serve:notes": "elixir scripts/export.exs && npx quartz build --serve" }, "engines": { "npm": ">=10.9.2", diff --git a/quartz.config.ts b/quartz.config.ts index b3db3d60d..a8540f766 100644 --- a/quartz.config.ts +++ b/quartz.config.ts @@ -55,7 +55,7 @@ const config: QuartzConfig = { }, plugins: { transformers: [ - Plugin.FrontMatter(), + Plugin.FrontMatter({ delimiters: "+++", language: "toml" }), Plugin.CreatedModifiedDate({ priority: ["frontmatter", "git", "filesystem"], }), @@ -66,7 +66,11 @@ const config: QuartzConfig = { }, keepBackground: false, }), - Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }), + // OxHugoFlavouredMarkdown must come before GitHubFlavoredMarkdown. + // Note: not compatible with ObsidianFlavoredMarkdown — use one or the other. + // If ox-hugo exports TOML frontmatter, change FrontMatter to: + // Plugin.FrontMatter({ delims: "+++", language: "toml" }) + Plugin.OxHugoFlavouredMarkdown(), Plugin.GitHubFlavoredMarkdown(), Plugin.TableOfContents(), Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }), diff --git a/scripts/export.exs b/scripts/export.exs new file mode 100644 index 000000000..bf9be3255 --- /dev/null +++ b/scripts/export.exs @@ -0,0 +1,136 @@ +#!/usr/bin/env elixir +# Export org-roam notes (per-file) to content/ via ox-hugo. +# +# Usage: +# NOTES_DIR=~/notes elixir scripts/export.exs +# elixir scripts/export.exs /path/to/notes +# +# The positional argument takes precedence over the NOTES_DIR env var. + +notes_dir = + case System.argv() do + [dir | _] -> dir + [] -> + System.get_env("NOTES_DIR") || + (IO.puts(:stderr, "Usage: NOTES_DIR=/path/to/notes elixir scripts/export.exs"); System.halt(1)) + end + +notes_dir = Path.expand(notes_dir) +repo_root = __DIR__ |> Path.join("..") |> Path.expand() +content_dir = Path.join(repo_root, "content") + +unless File.dir?(notes_dir) do + IO.puts(:stderr, "Error: notes directory does not exist: #{notes_dir}") + System.halt(1) +end + +# Wipe content/, preserving .gitkeep +IO.puts("==> Wiping #{content_dir}") + +content_dir +|> File.ls!() +|> Enum.reject(&(&1 == ".gitkeep")) +|> Enum.each(fn entry -> + Path.join(content_dir, entry) |> File.rm_rf!() +end) + +# Collect all .org files +IO.puts("==> Exporting org files from #{notes_dir}") + +org_files = + Path.join(notes_dir, "**/*.org") + |> Path.wildcard() + +if org_files == [] do + IO.puts("No .org files found in #{notes_dir}") + System.halt(0) +end + +# Export each file via emacs --batch +results = + Enum.map(org_files, fn orgfile -> + IO.puts(" exporting: #{orgfile}") + + # Mirror the notes subdirectory structure under content/ + section = + orgfile + |> Path.dirname() + |> Path.relative_to(notes_dir) + + {output, exit_code} = + System.cmd( + "emacs", + [ + "--batch", + "--eval", "(require 'ox-hugo)", + "--eval", ~s[(setq org-hugo-base-dir "#{repo_root}")], + "--eval", ~s[(setq org-hugo-default-section-directory "#{section}")], + "--visit", orgfile, + "--funcall", "org-hugo-export-to-md" + ], + stderr_to_stdout: true + ) + + # Filter noisy emacs startup lines, same as the shell script + filtered = + output + |> String.split("\n") + |> Enum.reject(&String.match?(&1, ~r/^Loading|^ad-handle|^For information/)) + |> Enum.join("\n") + + if filtered != "", do: IO.puts(filtered) + + {orgfile, exit_code} + end) + +failures = Enum.filter(results, fn {_, code} -> code != 0 end) + +if failures != [] do + IO.puts(:stderr, "\nFailed to export #{length(failures)} file(s):") + Enum.each(failures, fn {f, code} -> IO.puts(:stderr, " [exit #{code}] #{f}") end) + System.halt(1) +end + +md_count = + Path.join(content_dir, "**/*.md") + |> Path.wildcard() + |> length() + +# Generate a default index.md if none was exported +index_path = Path.join(content_dir, "index.md") + +unless File.exists?(index_path) do + IO.puts("==> Generating default index.md") + + pages = + Path.join(content_dir, "**/*.md") + |> Path.wildcard() + |> Enum.map(fn path -> + slug = Path.relative_to(path, content_dir) |> Path.rootname() + + title = + path + |> File.read!() + |> then(fn content -> + case Regex.run(~r/^title\s*=\s*"(.+)"/m, content) do + [_, t] -> t + _ -> slug + end + end) + + {slug, title} + end) + |> Enum.sort_by(fn {_, title} -> title end) + |> Enum.map(fn {slug, title} -> "- [#{title}](#{slug})" end) + |> Enum.join("\n") + + File.write!(index_path, """ + --- + title: Index + --- + + #{pages} + """) +end + +IO.puts("==> Done. #{md_count} markdown files in #{content_dir}")