9.9 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
posts → post (correct), but series → serie, news → new. 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 isisStarredAGENTS.md:101saysis_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.
Cross-cutting themes
-
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. -
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.
-
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.