Files
thor/PROBLEMS.md
T

11 KiB

Thor — UX Problems Catalog

Adversarial review of error messages, behavioral inconsistencies, and user frustration points. Established as a baseline on commit 314cab2.

Each entry cites the source location so it can be tracked to a fix.


Severity legend

  • Critical — user mistake produces silent wrong output or an unhelpful fatal error with no path forward.
  • High — error or warning is emitted but missing "where" or "how to fix."
  • Medium — inconsistency or gotcha that causes confusion or rework.
  • Low — polish / minor frustration.

A. Silent wrong output (no error, wrong result)

These are the most dangerous — the user gets no signal that something is wrong.

A1. Non-JSON frontmatter silently treated as body content — Critical

frontmatter.odin:26

Thor expects JSON frontmatter delimited by bare { / } lines. A user coming from Hugo/Jekyll writes YAML (---) or TOML (+++) frontmatter. It is silently swallowed into the markdown body. No title, no date, no draft flag — and no error. Likely the #1 onboarding trap.

A2. Unknown thor.json keys silently ignored — Critical

site.odin:162

json.unmarshal_string skips unknown fields. A typo like "tittle" instead of "title" produces a silently-empty title. No warning. (TODOS.md already wants a JSON schema.)

A3. Draft pages silently excluded — High

content.odin:64

When -drafts isn't passed, draft pages vanish with no log. User adds a page, forgets the flag, page doesn't appear — zero feedback.

A4. Naive singularization for layout inference — High

content.odin:202

postspost (correct), but seriesserie, newsnew. The layout silently falls through the fallback chain to page/base. No "layout 'serie' not found for section 'series'" message — only a debug log that's off by default.

A5. base_url defaults to localhost:8080 — Critical

site.odin:100

Forgetting to set it means every canonical URL, OG tag, and RSS link points to localhost. No warning. Devastating in production builds.

A6. Missing content/ produces empty build — High

content.odin:82

scan_content_files logs a warnf, the build proceeds with zero pages, then log.infof("Rendered 0 pages"). No fatal error, no "did you create content/?" guidance.

A7. RSS emits sentinel epoch date silently — Medium

feed.odin:33

Pages without a date get "Mon, 01 Jan 0001 00:00:00 +0000" in <pubDate>. No warning that a page is dateless in the feed.

A8. format_rfc822 returns raw ISO on parse failure — Medium

feed.odin:122-125

// TODO: should indicate error somehow — short/malformed dates get embedded verbatim in <pubDate>, producing invalid RSS with no warning.


B. Error messages missing "where" or "how to fix"

B1. render_template blanks the entire page on error — Critical

render.odin:119-133

A single bad tag/pipe anywhere produces log.errorf + return "". The output file is silently written empty. In a nix build (no visible terminal), the user sees a blank page with zero clue why. Already noted in TODOS.md.

B2. Malformed thor.json degrades to defaults — Critical

site.odin:162-168

A JSON syntax error is a warnf, then site_apply_path_defaults kicks in. The site builds with wrong paths and produces a confusing empty result — the cause is two hops removed from the symptom.

B3. Frontmatter parse error has no file location — Critical

frontmatter.odin:41

"failed to parse frontmatter JSON: %v" — no filename. On a 100-post site the user can't find the bad file. Worse: ok=false silently drops the page entirely.

B4. get_template returns empty Template{} on missing base — High

render.odin:88-89

"base.html not found in VFS" — no guidance on how to fix (create the file, check modules, etc.).

B5. dlopen failures lack the OS reason and fix guidance — High

treesitter/treesitter.odin:200-219

"cannot load grammar %s (%s)" shows the path but not why (no dlerror()). No guidance: "set the 'grammars' key in thor.json" or "this .so may be for a different tree-sitter ABI."

B6. Menu-mix fatal lacks location — High

menus.odin:42

"cannot mix config menus with frontmatter menus" — doesn't name which pages have frontmatter menus.

B7. Minify error doesn't name the page — Medium

minify.odin:33

"minify: HTML parse errors, skipping minification" — across 50 pages, which one?

B8. Timezone load failure is a warning with no guidance — Medium

site.odin:148

Doesn't state impact (dates render in UTC) or suggest valid names. Already in TODOS.md.

B9. No "config not found" message — Medium

site.odin:113

Silently falls back to ./thor.json. Wrong-directory runs produce a confusing default build.


C. Silent skip of invalid user input

C1. Unknown markdown extensions silently ignored (CLI) — High

markdown/markdown.odin:49-68

parse_extension_list has a switch with no default case. -ext:higlight (typo for highlight) is silently a no-op.

C2. Unknown markdown extensions silently ignored (config) — High

markdown/markdown.odin:71-90

apply_extension_config has a // TODO: Silently discards invalid values. Unknown keys in thor.json's markdown_extensions are silently dropped. Non-boolean values are or_continued.


D. Naming inconsistencies

D1. Markdown extensions have 3+ names — Medium

Context Name
thor.json key markdown_extensions
CLI flag -ext / -no-ext
Struct fields md_enable / md_disable
JSON/CLI values emoji, sidenotes (lowercase)
Enum members .Emoji, .Sidenotes (PascalCase)

D2. -ext usage string omits heading_ids — Medium

site.odin:91

The help text lists emoji,sidenotes,alerts,highlight,sections but the enum also has HeadingIDs. Users can't discover it from --help.

D3. Starred field has three names — Low

  • Page.starred (content.odin:28)
  • Frontmatter.isStarred (frontmatter.odin:17) — so the JSON key is isStarred
  • AGENTS.md:101 says is_starred (stale)

D4. Inconsistent error severity for similar failures — Medium

  • Template parse error → log.errorf + os.exit(1) (fatal) — render.odin:37-49
  • Template render error → log.errorf + return "" (non-fatal, blank page) — render.odin:126-131
  • Config parse error → warnf + fallback to defaults — site.odin:163-165

Same category of failure (user wrote something wrong) with wildly different consequences.

D5. Dead os.exit(1) after log.fatalf — Low

render.odin:33, menus.odin:43

fatalf already exits; the following line is dead code.


E. Configuration gotchas

E1. "menus": {} is a stealth opt-out — Medium

menus.odin:31-38

An empty object silently disables all auto-menus. A user who adds the key intending to configure later quietly loses their nav. The semantics (absent ≠ empty) are undocumented outside code comments.

E2. Config precedence is invisible — Medium

site.odin

CLI > JSON > defaults, but there's no "resolved config" log. Debugging "why is my base_url wrong?" requires reading source.

E3. format pipe logs ERROR but still renders — Medium

pipes.odin:261-265

Missing date.format produces log.errorf but falls back to DEFAULT_DATE_FORMAT. The severity says "error" but the behavior says "warning."


F. File/directory behavior surprises

F1. Root dirs = sections, nested dirs = leaf bundles — Medium

content.odin:108-127

This meaningful semantic distinction is entirely implicit. content/about/team.md is a leaf bundle (page "about" with body from team.md), not a section "about" with page "team". No error or guidance when the user's mental model differs.

F2. Missing layouts/ silently uses defaults — High

vfs.odin:38-40

mount_dir returns silently if the directory doesn't exist. Wrong path → all user templates missing → defaults used. No "layouts directory X not found" message.

F3. Missing section index silently synthesized — Medium

render.odin:316-326

A section with pages but no index.md gets a synthetic Page with only a title. No warning. User expecting an error gets a mostly-blank page.

F4. Windows line endings silently break frontmatter — Medium

frontmatter.odin:26

has_prefix(content, "{\n") fails on \r\n; the JSON is treated as body. Zero feedback.


G. Template authoring frustrations

G1. Template fallback chain is silent at Info level — Medium

render.odin:84

Only log.debugf, which is off by default (main.odin:48 sets .Info). User's custom layout silently ignored, defaults used.

G2. Render error blanks entire page — Critical

render.odin:126-131

(Same as B1 — restated here for the template-authoring perspective.) One bad tag → whole page "". The most impactful silent failure in the system.

G3. Pipe errors lack "did you mean?" suggestions — High

mustache/pipes.odin:241

Unknown key errors ({{tittle}}), missing partials, and unmatched block overrides all get Levenshtein "did you mean?" hints via suggest_correction. But unknown pipe operations ({{date | formats}}) get only "unknown pipe op 'formats'" with no suggestion. The known filter names ("format", "group_by") are a small fixed set — perfect for suggestions.

Structural gap: Error_Body has no hint field, and format_render_error doesn't pass hint to format_error (defaults to ""). So even if a suggestion were computed, there's nowhere to put it without either appending to msg or adding hint to Error_Body.

G4. Triple-mustache {{{ mishandled by tag_content_base — Medium

mustache/mustache.odin:230

tag_content_base skips {{ and sigils (#^/&><$!) to find where tag content begins. But triple-mustache {{{key}}} is common — after {{, the next char is {, which is not in the sigil list, so base points at { instead of the actual key content. Any pipe position calculation for {{{key | format}}} will be off by one byte.


Cross-cutting themes

  1. Debug-level logging masks important fallbacks. Layout fallbacks, template misses, and grammar skips are all debugf — invisible at the default Info level. Users never learn their customizations were ignored.

  2. The system fails open, not closed. Missing files, missing directories, missing config — all silently fall back to defaults rather than surfacing the problem. Friendly until it isn't.

  3. No "resolved state" visibility. There's no way for a user to see what thor actually loaded: which layouts, which config values, which pages were skipped as drafts. The build is a black box.


Gold-standard examples to emulate

These are the parts of the codebase that already do it right:

  • Mustache diagnostics (mustache/diagnostic.odin + suggest.odin): rust-style multi-line context, caret underlines, Levenshtein "did you mean?" hints, file:line:col.
  • Treesitter query version-mismatch (treesitter/treesitter.odin:299-315): explains the likely cause, shows both grammar/query versions, flags mismatches explicitly.