Fix index.md not being generated

- Generate index even when no .org files are found
- Don't halt CLI early when no org files - let pipeline complete
- Index will be empty but present, allowing Quartz to serve something
This commit is contained in:
Ignacio Ballesteros
2026-02-22 11:45:56 +01:00
parent 11ab8336e4
commit 38b4e0b341
3 changed files with 582 additions and 3 deletions

View File

@@ -10171,4 +10171,582 @@ Generated org_garden app
```
## Agent (2026-02-21 21:50:09)
Now let's commit and push:
Now let's commit and push:
### Tool Call [completed]: git add -A && git commit -m "Fix Quartz port not being passed correctly
When CLI options --port/--ws-port were not provided, nil values were
being explicitly passed to Server.start_link, which caused Keyword.get
to return nil instead of the default. Now we only include port options
in the keyword list if they have actual values."
**Tool:** execute
**Timestamp:** 2026-02-21 21:50:13
```
[main 11ab833] Fix Quartz port not being passed correctly
2 files changed, 1147 insertions(+), 7 deletions(-)
```
### Tool Call [completed]: git push
**Tool:** execute
**Timestamp:** 2026-02-21 21:50:17
```
remote: . Processing 1 references
remote: Processed 1 references in total
To gitea.bueso.eu:ignacio.ballesteros/org-garden.git
87fd311..11ab833 main -> main
```
## Agent (2026-02-21 21:50:19)
Fixed. The issue was that when CLI options `--port` and `--ws-port` weren't provided, the code was passing `port: nil` and `ws_port: nil` explicitly. `Keyword.get(opts, :port, default)` returns `nil` (not the default) when the key exists with a `nil` value.
Now only non-nil port values are included in the options passed to `Server.start_link`.
## User (2026-02-22 11:44:42)
Now index.md is missing, review the pipeline and transformations
➜ org-garden git:(main) ✗
## Agent (2026-02-22 11:44:46)
Let me investigate the index generation issue by reviewing the pipeline flow:
### Tool Call [completed]: read
**Tool:** read
**Timestamp:** 2026-02-22 11:44:47
```
<path>/home/ignaciobll/code/org-garden/lib/org_garden/index.ex</path>
<type>file</type>
<content>1: defmodule OrgGarden.Index do
2: @moduledoc """
3: Generates a fallback `index.md` in the content directory if none was
4: exported from an `.org` file.
5:
6: The generated index lists all markdown pages alphabetically with links.
7: """
8:
9: @doc """
10: Generate `content_dir/index.md` if it does not already exist.
11:
12: If an `index.md` was already created by ox-hugo (from an `index.org`),
13: it is left untouched.
14: """
15: @spec generate(String.t()) :: :ok
16: def generate(content_dir) do
17: index_path = Path.join(content_dir, "index.md")
18:
19: unless File.exists?(index_path) do
20: IO.puts(" generating default index.md")
21:
22: pages =
23: Path.join(content_dir, "**/*.md")
24: |> Path.wildcard()
25: |> Enum.map(fn path ->
26: slug = Path.relative_to(path, content_dir) |> Path.rootname()
27:
28: title =
29: path
30: |> File.read!()
31: |> then(fn content ->
32: case Regex.run(~r/^title\s*=\s*"(.+)"/m, content) do
33: [_, t] -> t
34: _ -> slug
35: end
36: end)
37:
38: {slug, title}
39: end)
40: |> Enum.sort_by(fn {_, title} -> title end)
41: |> Enum.map(fn {slug, title} -> "- [#{title}](#{slug})" end)
42: |> Enum.join("\n")
43:
44: File.write!(index_path, """
45: ---
46: title: Index
47: ---
48:
49: #{pages}
50: """)
51: end
52:
53: :ok
54: end
55:
56: @doc """
57: Regenerate the index by removing any previously generated one first.
58:
59: Only removes the index if it was generated by us (contains `title: Index`).
60: User-exported index files (from `index.org`) are left untouched.
61: """
62: @spec regenerate(String.t()) :: :ok
63: def regenerate(content_dir) do
64: index_path = Path.join(content_dir, "index.md")
65:
66: if File.exists?(index_path) do
67: content = File.read!(index_path)
68:
69: if generated_index?(content) do
70: File.rm!(index_path)
71: end
72: end
73:
74: generate(content_dir)
75: end
76:
77: defp generated_index?(content) do
78: # Our generated index uses "title: Index" in YAML frontmatter.
79: # ox-hugo uses TOML frontmatter (title = "..."), so this won't
80: # match user-exported files.
81: String.contains?(content, "title: Index")
82: end
83: end
84:
(End of file - total 84 lines)
</content>
```
## Agent (2026-02-22 11:44:49)
The index module looks fine. Let me check the server.ex to see how the pipeline is called:
### Tool Call [completed]: read
**Tool:** read
**Timestamp:** 2026-02-22 11:44:51
```
<path>/home/ignaciobll/code/org-garden/lib/org_garden/server.ex</path>
<type>file</type>
<content>136: defp run_initial_pipeline(state) do
137: %{
138: notes_dir: notes_dir,
139: output_dir: output_dir,
140: content_dir: content_dir,
141: pipeline_opts: pipeline_opts
142: } = state
143:
144: Logger.info("Running initial export pipeline...")
145:
146: # Wipe content directory
147: wipe(content_dir)
148:
149: # Export all org files
150: case OrgGarden.Export.export_all(notes_dir, output_dir) do
151: {:ok, 0} ->
152: Logger.warning("No .org files found in #{notes_dir}")
153: :ok
154:
155: {:ok, count} ->
156: Logger.info("Exported #{count} file(s)")
157:
158: # Run transforms
159: {:ok, stats} = OrgGarden.run(content_dir, @transforms, pipeline_opts)
160:
161: Enum.each(stats, fn {mod, c} ->
162: Logger.info("#{inspect(mod)}: #{c} file(s) modified")
163: end)
164:
165: # Generate index
166: OrgGarden.Index.generate(content_dir)
167: :ok
168:
169: {:error, failures} ->
170: Logger.error("Failed to export #{length(failures)} file(s)")
171: {:error, {:export_failed, failures}}
172: end
173: end
174:
175: defp start_supervisor(state) do
(File has more lines. Use 'offset' parameter to read beyond line 175)
</content>
```
## Agent (2026-02-22 11:44:53)
I see the issue! When there are 0 org files (line 151-153), we return `:ok` early without generating an index. But more importantly, let me check what happens with the parallel export - maybe it's returning early on the `{:ok, 0}` case incorrectly.
Let me also check the export.ex parallel code more carefully:
### Tool Call [completed]: read
**Tool:** read
**Timestamp:** 2026-02-22 11:44:57
```
<path>/home/ignaciobll/code/org-garden/lib/org_garden/export.ex</path>
<type>file</type>
<content>74: @default_max_concurrency 8
75:
76: @doc """
77: Export all `.org` files found under `notes_dir`.
78:
79: Exports files in parallel for improved performance. The concurrency level
80: can be configured via the `:export_concurrency` application config or
81: the `EXPORT_CONCURRENCY` environment variable. Defaults to #{@default_max_concurrency}.
82:
83: Returns `{:ok, count}` where `count` is the number of successfully
84: exported files, or `{:error, failures}` if any files failed.
85: """
86: @spec export_all(String.t(), String.t()) :: {:ok, non_neg_integer()} | {:error, list()}
87: def export_all(notes_dir, output_dir) do
88: org_files =
89: Path.join(notes_dir, "**/*.org")
90: |> Path.wildcard()
91:
92: if org_files == [] do
93: Logger.warning("No .org files found in #{notes_dir}")
94: {:ok, 0}
95: else
96: max_concurrency = get_concurrency()
97: Logger.info("Exporting #{length(org_files)} org file(s) from #{notes_dir} (concurrency: #{max_concurrency})")
98:
99: results =
100: org_files
101: |> Task.async_stream(
102: fn orgfile ->
103: Logger.info(" exporting: #{orgfile}")
104: {orgfile, export_file(orgfile, notes_dir, output_dir)}
105: end,
106: max_concurrency: max_concurrency,
107: timeout: :infinity,
108: ordered: false
109: )
110: |> Enum.map(fn {:ok, result} -> result end)
111:
112: failures =
113: Enum.filter(results, fn
114: {_, {:ok, _}} -> false
115: {_, {:error, _}} -> true
116: end)
117:
118: if failures == [] do
119: {:ok, length(results)}
120: else
121: {:error, failures}
122: end
123: end
124: end
125:
126: defp get_concurrency do
127: Application.get_env(:org_garden, :export_concurrency, @default_max_concurrency)
128: end
129:
130: @doc """
131: Compute the expected `.md` path for a given `.org` file.
132:
133: Uses the same section-mapping logic as ox-hugo: the relative directory
(File has more lines. Use 'offset' parameter to read beyond line 133)
</content>
```
## Agent (2026-02-22 11:45:00)
The export looks fine. Let me run the actual build to see what's happening. First let me check what the actual content_dir is and if index.md exists there:
### Tool Call [completed]: ls -la content/ 2>/dev/null || echo "content/ doesn't exist"
**Tool:** execute
**Timestamp:** 2026-02-22 11:45:02
```
total 8
drwxr-xr-x 2 ignaciobll users 4096 Feb 21 21:48 .
drwxr-xr-x 15 ignaciobll users 4096 Feb 21 21:48 ..
```
## Agent (2026-02-22 11:45:06)
Content directory is empty. Let me check what notes are available:
### Tool Call [completed]: ls -la notes/ 2>/dev/null || echo "notes/ doesn't exist"
**Tool:** execute
**Timestamp:** 2026-02-22 11:45:07
```
total 20
drwxr-xr-x 5 ignaciobll users 4096 Feb 21 00:35 .
drwxr-xr-x 15 ignaciobll users 4096 Feb 21 21:48 ..
drwxr-xr-x 2 ignaciobll users 4096 Feb 21 00:35 bus
drwxr-xr-x 2 ignaciobll users 4096 Feb 21 00:35 metro
drwxr-xr-x 2 ignaciobll users 4096 Feb 21 00:35 roads
```
### Tool Call [completed]: find notes/ -name "*.org" 2>/dev/null
**Tool:** execute
**Timestamp:** 2026-02-22 11:45:10
```
```
## Agent (2026-02-22 11:45:12)
There are no `.org` files in `notes/`. Let me check if there are any org files elsewhere, or if the content was previously in a different form:
### Tool Call [completed]: find . -name "*.org" -not -path "./.git/*" 2>/dev/null | head -20
**Tool:** execute
**Timestamp:** 2026-02-22 11:45:13
```
./README.org
```
## Agent (2026-02-22 11:45:15)
There's only the README.org. So the actual issue is that when there are no org files to export, the index.md isn't being generated. Let me trace through the pipeline:
1. In `server.ex:run_initial_pipeline`, when `export_all` returns `{:ok, 0}`, we just return `:ok` without calling `OrgGarden.Index.generate`
Let me also check the CLI flow since you might be running the CLI directly:
### Tool Call [completed]: read
**Tool:** read
**Timestamp:** 2026-02-22 11:45:19
```
<path>/home/ignaciobll/code/org-garden/lib/org_garden/cli.ex</path>
<type>file</type>
<content>300: # ---------------------------------------------------------------------------
301: # Phase 2: Export org files via Emacs + ox-hugo
302: # ---------------------------------------------------------------------------
303:
304: defp export_all(notes_dir, output_dir) do
305: IO.puts("==> Exporting org files from #{notes_dir}")
306:
307: case OrgGarden.Export.export_all(notes_dir, output_dir) do
308: {:ok, 0} ->
309: IO.puts("No .org files found in #{notes_dir}")
310: System.halt(0)
311:
312: {:ok, count} ->
313: IO.puts(" exported #{count} file(s)")
314:
315: {:error, failures} ->
316: IO.puts(:stderr, "\nFailed to export #{length(failures)} file(s):")
317:
318: Enum.each(failures, fn {f, {:error, reason}} ->
319: IO.puts(:stderr, " #{f}: #{inspect(reason)}")
320: end)
321:
322: System.halt(1)
323: end
324: end
325:
326: # ---------------------------------------------------------------------------
327: # Phase 3: Markdown transformation pipeline
328: # ---------------------------------------------------------------------------
329:
(File has more lines. Use 'offset' parameter to read beyond line 329)
</content>
```
## Agent (2026-02-22 11:45:22)
I see the problem! In the CLI's `export_all` function (line 308-310), when there are 0 org files, it calls `System.halt(0)` immediately - it doesn't even get to run transforms or generate the index!
But wait, you said index.md is missing. Let me check if there ARE org files being exported but the index isn't being generated. Let me trace the full serve flow:
### Tool Call [completed]: read
**Tool:** read
**Timestamp:** 2026-02-22 11:45:25
```
<path>/home/ignaciobll/code/org-garden/lib/org_garden/cli.ex</path>
<type>file</type>
<content>118: ]
119: )
120:
121: notes_dir = extract_notes_dir(positional, "serve")
122: output_dir = extract_output_dir(opts)
123: content_dir = extract_content_dir(opts, output_dir)
124:
125: {notes_dir, output_dir, content_dir, opts}
126: end
127:
128: # ---------------------------------------------------------------------------
129: # Command: build
130: # ---------------------------------------------------------------------------
131:
132: def handle_build(argv) do
133: quartz_path = require_quartz_env()
134: {notes_dir, output_dir, content_dir, _opts} = parse_build_args(argv)
135: pipeline_opts = Config.pipeline_opts()
136:
137: # Full batch export
138: wipe(content_dir)
139: export_all(notes_dir, output_dir)
140: run_pipeline(content_dir, pipeline_opts)
141: generate_index(content_dir)
142:
143: node_path = Config.get(:node_path, "node")
144:
145: IO.puts("==> Building static site with Quartz...")
146:
147: {output, status} =
148: System.cmd(
149: node_path,
150: [
151: Path.join(quartz_path, "quartz/bootstrap-cli.mjs"),
152: "build",
153: "--directory",
154: content_dir,
155: "--output",
156: Path.join(output_dir, "public")
157: ],
158: cd: quartz_path,
159: stderr_to_stdout: true
160: )
161:
162: IO.puts(output)
163:
164: if status != 0 do
165: abort("Quartz build failed with status #{status}")
166: end
167:
(File has more lines. Use 'offset' parameter to read beyond line 167)
</content>
```
## Agent (2026-02-22 11:45:27)
I see - for `build` command, `generate_index` is called (line 141), but the problem is `export_all` on line 139 might call `System.halt(0)` before we get there if no org files exist.
Let me look at where serve mode handles this:
### Tool Call [completed]: read
**Tool:** read
**Timestamp:** 2026-02-22 11:45:29
```
<path>/home/ignaciobll/code/org-garden/lib/org_garden/cli.ex</path>
<type>file</type>
<content>68: # ---------------------------------------------------------------------------
69: # Command: serve
70: # ---------------------------------------------------------------------------
71:
72: def handle_serve(argv) do
73: require_quartz_env()
74: {notes_dir, output_dir, content_dir, opts} = parse_serve_args(argv)
75:
76: IO.puts("==> Starting development server...")
77:
78: server_opts =
79: [
80: notes_dir: notes_dir,
81: output_dir: output_dir,
82: content_dir: content_dir
83: ]
84: |> maybe_put(:port, opts[:port])
85: |> maybe_put(:ws_port, opts[:ws_port])
86:
87: case OrgGarden.Server.start_link(server_opts) do
88: {:ok, pid} ->
89: IO.puts("==> Server running at http://localhost:#{opts[:port] || Config.get(:http_port, 8080)}")
90: IO.puts("==> Watching #{notes_dir} for changes (Ctrl+C to stop)")
91:
92: # Wait for server to exit
93: ref = Process.monitor(pid)
94:
95: receive do
96: {:DOWN, ^ref, :process, ^pid, reason} ->
97: case reason do
98: :normal -> :ok
99: :shutdown -> :ok
100: {:shutdown, _} -> :ok
101: _ -> abort("Server crashed: #{inspect(reason)}")
102: end
(File has more lines. Use 'offset' parameter to read beyond line 102)
</content>
```
## Agent (2026-02-22 11:45:32)
For `serve`, it delegates to `OrgGarden.Server` which calls `run_initial_pipeline`. The problem there is that when `{:ok, 0}` is returned, index is not generated.
Let me fix both issues:
1. In `server.ex`: Generate index even when there are 0 org files
2. In `cli.ex`: Don't halt immediately when 0 org files - let the pipeline continue
### Tool Call [completed]: edit
**Tool:** edit
**Timestamp:** 2026-02-22 11:45:39
```
Edit applied successfully.
╭─────────╮
│ changes │
╰─────────╯
@@ -2,6 +2,8 @@
case OrgGarden.Export.export_all(notes_dir, output_dir) do
{:ok, 0} ->
Logger.warning("No .org files found in #{notes_dir}")
+ # Still generate index (will be empty or have default content)
+ OrgGarden.Index.generate(content_dir)
:ok
{:ok, count} ->
```
## Agent (2026-02-22 11:45:41)
Now let me fix the CLI as well - it shouldn't halt immediately when there are no files. That's overly aggressive. The pipeline should still complete (even if empty):
### Tool Call [completed]: edit
**Tool:** edit
**Timestamp:** 2026-02-22 11:45:46
```
Edit applied successfully.
╭─────────╮
│ changes │
╰─────────╯
@@ -3,8 +3,7 @@
case OrgGarden.Export.export_all(notes_dir, output_dir) do
{:ok, 0} ->
- IO.puts("No .org files found in #{notes_dir}")
- System.halt(0)
+ IO.puts(" no .org files found")
{:ok, count} ->
IO.puts(" exported #{count} file(s)")
```
## Agent (2026-02-22 11:45:49)
Now let's verify compilation:
### Tool Call [completed]: nix develop --command mix compile --warnings-as-errors
**Tool:** execute
**Timestamp:** 2026-02-22 11:45:52
```
warning: Git tree '/home/ignaciobll/code/org-garden' is dirty
Compiling 2 files (.ex)
Generated org_garden app
```

View File

@@ -306,8 +306,7 @@ defmodule OrgGarden.CLI do
case OrgGarden.Export.export_all(notes_dir, output_dir) do
{:ok, 0} ->
IO.puts("No .org files found in #{notes_dir}")
System.halt(0)
IO.puts(" no .org files found")
{:ok, count} ->
IO.puts(" exported #{count} file(s)")

View File

@@ -150,6 +150,8 @@ defmodule OrgGarden.Server do
case OrgGarden.Export.export_all(notes_dir, output_dir) do
{:ok, 0} ->
Logger.warning("No .org files found in #{notes_dir}")
# Still generate index (will be empty or have default content)
OrgGarden.Index.generate(content_dir)
:ok
{:ok, count} ->