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
This commit is contained in:
136
scripts/export.exs
Normal file
136
scripts/export.exs
Normal file
@@ -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}")
|
||||
Reference in New Issue
Block a user