quartz build

This commit is contained in:
Ignacio Ballesteros
2026-02-14 18:00:32 +01:00
parent 23aa2f4dc4
commit 79e93c167e
15 changed files with 710 additions and 19 deletions

60
README.md Normal file
View File

@@ -0,0 +1,60 @@
# org-to-quartz
Convert org-mode notes to a Quartz static site.
## Quick Start
### Build pages from your notes
```bash
nix build .#example-pages
# Output in ./result/
```
### Serve locally (development)
```bash
nix run .#serve ./path/to/your/notes
# Opens http://localhost:8080
```
### Convert org to markdown only
```bash
nix run . -- ./input-notes ./output-dir
```
## Use in your own flake
```nix
{
inputs.org-notes-quartz.url = "github:your-user/org-notes-quartz";
outputs = { self, nixpkgs, org-notes-quartz, ... }:
let
system = "x86_64-linux";
buildQuartzPages = org-notes-quartz.lib.${system}.buildQuartzPages;
in {
packages.${system}.site = buildQuartzPages {
contentDir = ./notes;
# quartzConfig = ./quartz.config.ts; # optional
# quartzLayout = ./quartz.layout.ts; # optional
};
};
}
```
## Available packages
| Package | Description |
|-----------------------------|---------------------------------------------|
| `default` / `org-to-quartz` | CLI to convert org files to Quartz markdown |
| `quartz` | Quartz v4.5.2 static site generator |
| `example-pages` | Example build from `test-notes/` |
## Available apps
| App | Description |
|-----------|-------------------------------|
| `default` | Run org-to-quartz converter |
| `serve` | Serve notes with live preview |

View File

@@ -0,0 +1,38 @@
#+title: Atomic Notes
#+date: 2024-01-11
#+filetags: :concept:writing:
An atomic note contains exactly one idea, fully expressed.
* Characteristics
- Self-contained
- Single focus
- Linkable
- Reusable
* Benefits
When notes are atomic:
- Easy to link from multiple contexts
- Simple to reorganize
- Clear to understand
- Quick to write
* Examples
Good atomic note titles:
- "Spaced repetition improves long-term retention"
- "Links create unexpected connections"
- "Writing clarifies thinking"
Bad titles:
- "Notes about learning"
- "Stuff I read"
* Connection to Other Concepts
Atomic notes are the foundation of [[file:zettelkasten.org][Zettelkasten]] and enable effective [[file:linking.org][Linking]].
This principle comes from software engineering - see cite:martin2008 on the Single Responsibility Principle.

View File

@@ -0,0 +1,26 @@
#+title: Knowledge Management
#+date: 2024-01-10
#+filetags: :concept:learning:
Knowledge management is the process of creating, sharing, using, and managing information.
* Core Principles
1. *Capture* - Record ideas as they come
2. *Organize* - Structure for retrieval
3. *Connect* - Link related concepts
4. *Review* - Revisit and refine
* Related Concepts
- [[file:zettelkasten.org][Zettelkasten]] - A specific method for knowledge management
- [[file:atomic-notes.org][Atomic Notes]] - The building blocks
- [[file:linking.org][Linking]] - How connections create value
* Tools
See [[file:../projects/website.org][Website Project]] for how to publish your knowledge base.
* References
The field draws from library science cite:weinberger2007 and cognitive psychology cite:ahrens2017.

View File

@@ -0,0 +1,43 @@
#+title: Linking
#+date: 2024-01-13
#+filetags: :concept:structure:
Links between notes create a network of knowledge.
* Types of Links
** Hierarchical
Parent-child relationships (folders, outlines)
** Associative
Related concepts ([[file:zettelkasten.org][Zettelkasten]] style)
** Sequential
Reading order, workflows
* Why Links Matter
#+begin_src text
[Note A] --- related to --- [Note B]
| |
+--- supports --- [Note C] -+
#+end_src
Links reveal:
- Hidden connections
- Knowledge gaps
- Entry points
* Backlinks
When [[file:atomic-notes.org][Atomic Notes]] links here, this page knows about it. Backlinks show:
- Who references this concept
- How the idea is used
- Related contexts
* Tools Supporting Links
See [[file:../projects/website.org][Website Project]] for publishing linked notes.
The web itself is built on links cite:bernerslee1999.

View File

@@ -0,0 +1,41 @@
#+title: Spaced Repetition
#+date: 2024-01-14
#+filetags: :concept:learning:memory:
Spaced repetition is a learning technique that involves reviewing material at increasing intervals.
* The Forgetting Curve
Without review, we forget:
- 50% within an hour
- 70% within 24 hours
- 90% within a week
* How Spacing Helps
Review schedule example:
| Review | Interval |
|--------|----------|
| 1st | 1 day |
| 2nd | 3 days |
| 3rd | 1 week |
| 4th | 2 weeks |
| 5th | 1 month |
* Software
- Anki
- Org-drill (Emacs)
- RemNote
* Connection to Note-Taking
[[file:zettelkasten.org][Zettelkasten]] provides natural spaced repetition through:
- Random encounters while linking
- Review during searches
- Connections triggering recall
* Research
Based on memory research cite:ebbinghaus1885 and modern applications cite:pimsleur1967.

View File

@@ -0,0 +1,38 @@
#+title: Zettelkasten
#+date: 2024-01-12
#+filetags: :concept:methodology:productivity:
The Zettelkasten (German for "slip box") is a note-taking method developed by sociologist Niklas Luhmann.
* Key Features
- Each note contains one idea ([[file:atomic-notes.org][Atomic Notes]])
- Notes link to each other extensively ([[file:linking.org][Linking]])
- No strict hierarchy - emergence through connections
* How It Works
#+begin_quote
"One cannot think without writing." - Niklas Luhmann
#+end_quote
1. Write fleeting notes
2. Convert to permanent notes
3. Add to the slip box with links
4. Review connections
* Implementation
| Analog | Digital |
|--------|---------|
| Index cards | Org-roam |
| Physical box | Obsidian |
| Manual links | Wiki links |
* Why It Works
The method leverages [[file:spaced-repetition.org][Spaced Repetition]] through organic review and builds on [[file:knowledge-management.org][Knowledge Management]] principles.
* Further Reading
See cite:ahrens2017 for the definitive guide.

View File

@@ -0,0 +1,25 @@
#+title: 2024-01-20
#+date: 2024-01-20
#+filetags: :daily:journal:
* Tasks
- [X] Review [[file:../concepts/zettelkasten.org][Zettelkasten]] notes
- [X] Update [[file:../projects/website.org][Website Project]] status
- [ ] Read more of cite:ahrens2017
* Notes
Realized that [[file:../concepts/atomic-notes.org][Atomic Notes]] principle applies beyond note-taking:
- Commit messages
- Documentation sections
- Email paragraphs
* Ideas
Could [[file:../concepts/spaced-repetition.org][Spaced Repetition]] work for code review? Flag old code for periodic review.
* References
- Discussed [[file:../concepts/knowledge-management.org][Knowledge Management]] with colleague
- Found new source on [[file:../concepts/linking.org][Linking]] theory

17
example-notes/index.org Normal file
View File

@@ -0,0 +1,17 @@
#+title: Welcome
#+date: 2024-01-15
#+filetags: :home:
This is a digital garden built from org-mode notes.
* Getting Started
Explore the different sections:
- [[file:concepts/knowledge-management.org][Knowledge Management]] - Core concepts
- [[file:projects/website.org][Website Project]] - An example project
- [[file:references/index.org][References]] - Books and papers
* Recent Updates
Check out [[file:concepts/zettelkasten.org][Zettelkasten]] for note-taking methodology.

View File

@@ -0,0 +1,34 @@
#+title: Learning System
#+date: 2024-01-18
#+filetags: :project:planning:
A personal system for continuous learning.
* Components
1. *Capture* - Quick notes, highlights, ideas
2. *Process* - Convert to [[file:../concepts/atomic-notes.org][Atomic Notes]]
3. *Connect* - Add [[file:../concepts/linking.org][Links]] to existing notes
4. *Review* - [[file:../concepts/spaced-repetition.org][Spaced Repetition]] for retention
* Daily Practice
| Time | Activity |
|------|----------|
| Morning | Review queue |
| Anytime | Capture ideas |
| Evening | Process inbox |
* Tools
- Org-mode for capture
- Org-roam for [[file:../concepts/zettelkasten.org][Zettelkasten]]
- Org-drill for review
* Inspiration
Based on [[file:../concepts/knowledge-management.org][Knowledge Management]] research and cite:forte2022 Building a Second Brain methodology.
* Status
Currently implementing the capture phase. See [[file:website.org][Website Project]] for publishing component.

View File

@@ -0,0 +1,40 @@
#+title: Website Project
#+date: 2024-01-20
#+filetags: :project:active:
Building a digital garden from org-mode notes.
* Overview
Transform [[file:../concepts/knowledge-management.org][Knowledge Management]] practices into a public website.
* Goals
- [X] Convert org files to markdown
- [X] Preserve [[file:../concepts/linking.org][Linking]] between notes
- [ ] Add search functionality
- [ ] Deploy to hosting
* Technical Stack
| Component | Tool |
|-----------|------|
| Source | Org-mode |
| Converter | org-to-quartz |
| Generator | Quartz |
| Hosting | TBD |
* Architecture
#+begin_src text
org files --> org-to-quartz --> markdown --> Quartz --> HTML
#+end_src
* Related
- [[file:../concepts/zettelkasten.org][Zettelkasten]] - The methodology behind the content
- [[file:../references/index.org][References]] - Sources cited throughout
* Notes
Using [[file:../concepts/atomic-notes.org][Atomic Notes]] makes each page focused and linkable.

View File

@@ -0,0 +1,32 @@
#+title: How to Take Smart Notes
#+date: 2024-01-05
#+filetags: :reference:book:
* Citation
Ahrens, S. (2017). /How to Take Smart Notes/. CreateSpace.
* Summary
A guide to the [[file:../concepts/zettelkasten.org][Zettelkasten]] method based on Niklas Luhmann's practice.
* Key Ideas
1. Writing is thinking
2. One idea per note ([[file:../concepts/atomic-notes.org][Atomic Notes]])
3. Links over folders ([[file:../concepts/linking.org][Linking]])
4. Bottom-up organization
* Quotes
#+begin_quote
"The slip-box is designed to present you with ideas you have already forgotten."
#+end_quote
#+begin_quote
"Every intellectual endeavour starts from an already existing preconception."
#+end_quote
* Application
This book directly influenced the [[file:../projects/learning-system.org][Learning System]] project.

View File

@@ -0,0 +1,31 @@
#+title: References
#+date: 2024-01-08
#+filetags: :reference:index:
Collection of books, papers, and resources.
* Books
** Knowledge Management
- cite:ahrens2017 - How to Take Smart Notes
- cite:forte2022 - Building a Second Brain
** Software & Writing
- cite:martin2008 - Clean Code
- cite:weinberger2007 - Everything Is Miscellaneous
* Papers
- cite:ebbinghaus1885 - Memory: A Contribution to Experimental Psychology
- cite:pimsleur1967 - A Memory Schedule
* Web Resources
- cite:bernerslee1999 - Weaving the Web
* How References Are Used
References connect to concepts:
- [[file:../concepts/zettelkasten.org][Zettelkasten]] cites cite:ahrens2017
- [[file:../concepts/spaced-repetition.org][Spaced Repetition]] cites cite:ebbinghaus1885
- [[file:../concepts/linking.org][Linking]] cites cite:bernerslee1999

254
flake.nix
View File

@@ -44,35 +44,267 @@
}; };
}; };
# Script to serve quartz with converted notes # Quartz package - builds the Quartz static site generator
quartz = pkgs.buildNpmPackage {
pname = "quartz";
version = "4.5.2";
src = pkgs.fetchFromGitHub {
owner = "jackyzha0";
repo = "quartz";
rev = "v4.5.2";
hash = "sha256-A6ePeNmcsbtKVnm7hVFOyjyc7gRYvXuG0XXQ6fvTLEw=";
};
npmDepsHash = "sha256-xxK9qy04m1olekOJIyYJHfdkYFzpjsgcfyFPuKsHpKE=";
# Quartz doesn't have a build step in the traditional sense
# It's a CLI tool that builds sites at runtime
dontNpmBuild = true;
# Install the quartz CLI
installPhase = ''
runHook preInstall
mkdir -p $out/lib/quartz
cp -r . $out/lib/quartz
mkdir -p $out/bin
cat > $out/bin/quartz <<'WRAPPER'
#!/usr/bin/env bash
# Quartz CLI wrapper - runs quartz from its library directory
# or from current directory if it contains a quartz config
QUARTZ_LIB="$out/lib/quartz"
if [[ -f "./quartz.config.ts" ]]; then
# Run from current directory (user's project)
exec ${pkgs.nodejs}/bin/node "$QUARTZ_LIB/quartz/bootstrap-cli.mjs" "$@"
else
# Run from quartz lib directory
cd "$QUARTZ_LIB"
exec ${pkgs.nodejs}/bin/node quartz/bootstrap-cli.mjs "$@"
fi
WRAPPER
# Replace $out with actual path
substituteInPlace $out/bin/quartz --replace-quiet '$out' "$out"
chmod +x $out/bin/quartz
runHook postInstall
'';
meta = {
description = "A fast, batteries-included static-site generator for digital gardens";
homepage = "https://quartz.jzhao.xyz";
mainProgram = "quartz";
};
};
# Default quartz config that works in Nix sandbox (no network fetches)
defaultQuartzConfig = pkgs.writeText "quartz.config.ts" ''
import { QuartzConfig } from "./quartz/cfg"
import * as Plugin from "./quartz/plugins"
const config: QuartzConfig = {
configuration: {
pageTitle: "Quartz Notes",
pageTitleSuffix: "",
enableSPA: true,
enablePopovers: true,
analytics: null,
locale: "en-US",
baseUrl: "localhost",
ignorePatterns: ["private", "templates", ".obsidian"],
defaultDateType: "modified",
theme: {
fontOrigin: "local",
cdnCaching: false,
typography: {
header: "sans-serif",
body: "sans-serif",
code: "monospace",
},
colors: {
lightMode: {
light: "#faf8f8",
lightgray: "#e5e5e5",
gray: "#b8b8b8",
darkgray: "#4e4e4e",
dark: "#2b2b2b",
secondary: "#284b63",
tertiary: "#84a59d",
highlight: "rgba(143, 159, 169, 0.15)",
textHighlight: "#fff23688",
},
darkMode: {
light: "#161618",
lightgray: "#393639",
gray: "#646464",
darkgray: "#d4d4d4",
dark: "#ebebec",
secondary: "#7b97aa",
tertiary: "#84a59d",
highlight: "rgba(143, 159, 169, 0.15)",
textHighlight: "#b3aa0288",
},
},
},
},
plugins: {
transformers: [
Plugin.FrontMatter(),
Plugin.CreatedModifiedDate({
priority: ["frontmatter", "git", "filesystem"],
}),
Plugin.SyntaxHighlighting({
theme: {
light: "github-light",
dark: "github-dark",
},
keepBackground: false,
}),
Plugin.OxHugoFlavouredMarkdown(),
Plugin.GitHubFlavoredMarkdown(),
Plugin.TableOfContents(),
Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }),
Plugin.Description(),
Plugin.Latex({ renderEngine: "katex" }),
],
filters: [Plugin.RemoveDrafts()],
emitters: [
Plugin.AliasRedirects(),
Plugin.ComponentResources(),
Plugin.ContentPage(),
Plugin.FolderPage(),
Plugin.TagPage(),
Plugin.ContentIndex({
enableSiteMap: true,
enableRSS: true,
}),
Plugin.Assets(),
Plugin.Static(),
Plugin.Favicon(),
Plugin.NotFoundPage(),
// CustomOgImages disabled - requires network access for fonts
],
},
}
export default config
'';
# Function to build Quartz pages from a content directory
# Usage: buildQuartzPages { contentDir = ./my-org-notes; }
# contentDir: directory containing org files or markdown files
# quartzConfig: optional path to quartz.config.ts (uses sandbox-friendly default if not provided)
# quartzLayout: optional path to quartz.layout.ts
buildQuartzPages = {
contentDir,
quartzConfig ? null,
quartzLayout ? null,
name ? "quartz-pages"
}:
pkgs.stdenv.mkDerivation {
inherit name;
# Don't use src, we copy everything in buildPhase
dontUnpack = true;
nativeBuildInputs = [
pkgs.nodejs
org-to-quartz
];
buildPhase = ''
runHook preBuild
# Set up a writable quartz directory
cp -r ${quartz}/lib/quartz/* .
chmod -R u+w .
# Remove default content
rm -rf content
mkdir -p content
# Convert org files to markdown, or copy markdown directly
if ls ${contentDir}/*.org >/dev/null 2>&1; then
echo "Converting org notes from ${contentDir}..."
org-to-quartz ${contentDir} content
else
echo "Copying content from ${contentDir}..."
cp -r ${contentDir}/* content/
fi
# Apply quartz config (use sandbox-friendly default if not provided)
${if quartzConfig != null then ''
echo "Using custom quartz.config.ts..."
cp ${quartzConfig} quartz.config.ts
'' else ''
echo "Using default sandbox-friendly quartz.config.ts..."
cp ${defaultQuartzConfig} quartz.config.ts
''}
${pkgs.lib.optionalString (quartzLayout != null) ''
echo "Using custom quartz.layout.ts..."
cp ${quartzLayout} quartz.layout.ts
''}
# Build the static site
export HOME=$(mktemp -d)
echo "Building Quartz site..."
node quartz/bootstrap-cli.mjs build
runHook postBuild
'';
installPhase = ''
runHook preInstall
mv public $out
runHook postInstall
'';
};
# Script to serve quartz with converted notes (for development)
quartz-serve = pkgs.writeShellScriptBin "quartz-serve" '' quartz-serve = pkgs.writeShellScriptBin "quartz-serve" ''
set -e set -e
NOTES_DIR="''${1:-.}" NOTES_DIR="''${1:-.}"
PORT="''${2:-8080}" PORT="''${2:-8080}"
WORK_DIR=$(mktemp -d) WORK_DIR=$(mktemp -d)
echo "Cloning Quartz..." echo "Setting up Quartz..."
${pkgs.git}/bin/git clone --depth 1 https://github.com/jackyzha0/quartz.git "$WORK_DIR/quartz" 2>/dev/null cp -r ${quartz}/lib/quartz/* "$WORK_DIR/"
chmod -R u+w "$WORK_DIR"
echo "Installing dependencies..."
cd "$WORK_DIR/quartz"
${pkgs.nodejs}/bin/npm install --silent
echo "Converting org notes from $NOTES_DIR..." echo "Converting org notes from $NOTES_DIR..."
${org-to-quartz}/bin/org-to-quartz "$NOTES_DIR" "$WORK_DIR/quartz/content" -v rm -rf "$WORK_DIR/content"
mkdir -p "$WORK_DIR/content"
${org-to-quartz}/bin/org-to-quartz "$NOTES_DIR" "$WORK_DIR/content" -v
# Enable OxHugo plugin cd "$WORK_DIR"
${pkgs.gnused}/bin/sed -i 's/Plugin.GitHubFlavoredMarkdown()/Plugin.OxHugoFlavouredMarkdown(),\n Plugin.GitHubFlavoredMarkdown()/' quartz.config.ts
echo "" echo ""
echo "Starting Quartz on http://localhost:$PORT" echo "Starting Quartz on http://localhost:$PORT"
${pkgs.nodejs}/bin/npx quartz build --serve --port "$PORT" ${pkgs.nodejs}/bin/node quartz/bootstrap-cli.mjs build --serve --port "$PORT"
''; '';
# Example: build pages from example-notes directory
example-pages = buildQuartzPages {
contentDir = ./example-notes;
name = "example-quartz-pages";
};
in { in {
packages = { packages = {
default = org-to-quartz; default = org-to-quartz;
org-to-quartz = org-to-quartz; org-to-quartz = org-to-quartz;
quartz = quartz;
example-pages = example-pages;
};
# Export the buildQuartzPages function for use in other flakes
lib = {
inherit buildQuartzPages;
}; };
devShells.default = pkgs.mkShell { devShells.default = pkgs.mkShell {

View File

@@ -11,18 +11,26 @@ from .citations import CitationResolver
def find_org_files(input_dir: Path) -> list[Path]: def find_org_files(input_dir: Path) -> list[Path]:
"""Find all .org files in directory (non-recursive).""" """Find all .org files in directory (recursive)."""
return list(input_dir.glob("*.org")) return list(input_dir.rglob("*.org"))
def convert_file( def convert_file(
org_path: Path, org_path: Path,
input_dir: Path,
output_dir: Path, output_dir: Path,
citation_resolver: CitationResolver | None, citation_resolver: CitationResolver | None,
verbose: bool = False, verbose: bool = False,
) -> Path | None: ) -> Path | None:
"""Convert a single org file to Quartz markdown. """Convert a single org file to Quartz markdown.
Args:
org_path: Path to the org file
input_dir: Root input directory (for computing relative paths)
output_dir: Root output directory
citation_resolver: Optional citation resolver
verbose: Enable verbose output
Returns: Returns:
Path to created note directory, or None on error Path to created note directory, or None on error
""" """
@@ -33,8 +41,21 @@ def convert_file(
if verbose: if verbose:
print(f" Parsed: {doc.title or org_path.stem}") print(f" Parsed: {doc.title or org_path.stem}")
# Compute relative path from input dir to preserve directory structure
relative_path = org_path.relative_to(input_dir)
# Special case: root index.org becomes content/index.md directly
if relative_path.stem == "index" and relative_path.parent == Path("."):
note_dir = output_dir
output_path = output_dir / "index.md"
else:
# Replace .org with directory containing index.md
# e.g., concepts/zettelkasten.org -> concepts/zettelkasten/index.md
note_relative_dir = relative_path.parent / relative_path.stem
note_dir = output_dir / note_relative_dir
output_path = note_dir / "index.md"
# Create output directory for this note # Create output directory for this note
note_dir = output_dir / doc.slug
note_dir.mkdir(parents=True, exist_ok=True) note_dir.mkdir(parents=True, exist_ok=True)
# Copy images first (before conversion, to get path mapping) # Copy images first (before conversion, to get path mapping)
@@ -50,7 +71,6 @@ def convert_file(
md_content = update_image_paths(md_content, image_mapping) md_content = update_image_paths(md_content, image_mapping)
# Write output # Write output
output_path = note_dir / "index.md"
output_path.write_text(md_content, encoding="utf-8") output_path.write_text(md_content, encoding="utf-8")
return note_dir return note_dir
@@ -123,7 +143,7 @@ def main() -> int:
for org_path in org_files: for org_path in org_files:
print(f"Converting: {org_path.name}") print(f"Converting: {org_path.name}")
result = convert_file(org_path, args.output_dir, citation_resolver, args.verbose) result = convert_file(org_path, args.input_dir, args.output_dir, citation_resolver, args.verbose)
if result: if result:
success_count += 1 success_count += 1
if args.verbose: if args.verbose:

View File

@@ -57,7 +57,8 @@ def process_wikilinks(content: str) -> str:
Pandoc converts org links to markdown links: Pandoc converts org links to markdown links:
- [[roam:Title]] -> [roam:Title](roam:Title) -> [[Title]] - [[roam:Title]] -> [roam:Title](roam:Title) -> [[Title]]
- [[id:uuid][Desc]] -> [Desc](id:uuid) -> [[Desc]] - [[id:uuid][Desc]] -> [Desc](id:uuid) -> [[Desc]]
- [[file:x.org][Desc]] -> [Desc](file:x.org) -> [[Desc]] - [[file:x.org][Desc]] -> [Desc](x) (relative link)
- [[file:../dir/x.org][Desc]] -> [Desc](../dir/x) (relative link)
""" """
# [roam:Title](roam:Title) -> [[Title]] # [roam:Title](roam:Title) -> [[Title]]
content = re.sub(r"\[roam:([^\]]+)\]\(roam:[^)]+\)", r"[[\1]]", content) content = re.sub(r"\[roam:([^\]]+)\]\(roam:[^)]+\)", r"[[\1]]", content)
@@ -65,8 +66,21 @@ def process_wikilinks(content: str) -> str:
# [Description](id:uuid) -> [[Description]] # [Description](id:uuid) -> [[Description]]
content = re.sub(r"\[([^\]]+)\]\(id:[a-f0-9-]+\)", r"[[\1]]", content) content = re.sub(r"\[([^\]]+)\]\(id:[a-f0-9-]+\)", r"[[\1]]", content)
# [Description](file:something.org) -> [[Description]] # [Description](file:path/to/something.org) -> [Description](/path/to/something)
content = re.sub(r"\[([^\]]+)\]\(file:[^)]+\.org\)", r"[[\1]]", content) # Convert org file links to proper Quartz paths
def convert_file_link(match):
description = match.group(1)
path = match.group(2)
# Remove file: prefix and .org suffix, convert to Quartz path
# ../concepts/foo.org -> ../concepts/foo
clean_path = re.sub(r"\.org$", "", path)
return f"[{description}]({clean_path})"
content = re.sub(r"\[([^\]]+)\]\(file:([^)]+\.org)\)", convert_file_link, content)
# Also handle relative paths without file: prefix (pandoc sometimes drops it)
# [Description](../something.org) -> [Description](../something)
content = re.sub(r"\[([^\]]+)\]\(([^):]+\.org)\)", convert_file_link, content)
return content return content