# Thor β Odin Static Site Generator Thor is a static site generator written in [Odin](https://odin-lang.org), replacing Hugo for the `sbrow.github.io` blog. It lives at `./thor/` as a git subtree with its own `flake.nix`. ## π« DO NOT EDIT THE DOCS β BY HUMANS, FOR HUMANS > **THE DOCUMENTATION UNDER `thor/site/` IS HANDWRITTEN, BY HUMANS, FOR HUMANS.** > > **NO AI, AGENT, BOT, ASSISTANT, OR OTHER NON-HUMAN MAY EDIT, REWRITE, > REPHRASE, REFORMAT, "IMPROVE," SUMMARIZE, OR GENERATE ANY FILE UNDER > `thor/site/` β EVER.** > > These are not machine artifacts. A human wrote every word. AI may be > consulted as a sanity check, but the prose stays human. If you are not a > human, do not touch these files. See `thor/site/content/ai.md`. ## Architecture ``` thor.json β site config (title, base_url, params, modules, og) content/ β markdown and HTML content files layouts/ β Mustache templates + partials (user overrides) assets/ β CSS (Tufte-based), JS, fonts, images thor/defaults/ β bundled default templates (embedded via #directory) public/ β build output (generated) ``` ### Package structure ``` thor/ βββ treesitter/ # FFI types + grammar management (standalone package) βββ markdown/ # Content transformation pipeline (imports ../treesitter) βββ mustache/ # Template engine with lambdas + pipe filters + diagnostics βββ content.odin # Page struct, Pending_File, scan_content_files, collect_languages, load_page βββ render.odin # Template rendering, Template_Context, sort_pages, RSS, sitemap βββ menus.odin # Menu_Entry, DEFAULT_WEIGHT, build_menus, collect_auto_menus, merge_page_menus, parse_page_menus, parse_config_menus βββ site.odin # Config (Flags, Config_File, Site, Site_Context), init_site βββ minify.odin # HTML/CSS minification (imports treesitter) βββ feed.odin # RSS + sitemap generation βββ vfs.odin # Union file system (defaults β modules β site) βββ assets.odin # VFS-based asset copying βββ html.odin # HTML helpers: strip_html_tags, unescape_html, generate_summary, generate_description βββ opengraph.odin # Open_Graph struct + og_for_site/og_for_page βββ frontmatter.odin # JSON frontmatter parser (supports nested og + lastmod + weight + menus) βββ defaults.odin # DEFAULTS_PATH constant (#directory) βββ main.odin # Entry point βββ bench/ # Template rendering benchmark βββ defaults/layouts/ # Bundled default templates ``` ### Source files (main package) | File | Responsibility | |---|---| | `main.odin` | Entry point. Parses CLI flags via `core:flags`, sets logger level from `-verbose`/`-quiet`, calls `init_site`, `build_vfs`, wires `treesitter.grammar_dir`/`query_dir` from config, `site_load_content`, `render_site`. Optional Spall profiling via `SPALL` config flag. | | `site.odin` | `Flags` (CLI, includes `-verbose`/`-quiet`), `Config_File` (thor.json), `Site_Context` (template-facing: `title`, `description`, `base_url`, `params`, `og`, `menus`), `Site` (runtime state + arena + VFS + pages + `og`). `Feature` enum. `init_site(site, flags)` β takes pre-parsed `Flags`. Config menu parsing in `site_apply_config`. | | `content.odin` | `Page` struct (includes `weight`, `menus`, `params: json.Value`, `toc: string`, `og`), `Pending_File` struct, `scan_content_files` (section-aware walk that handles leaf bundles), `collect_languages` (pre-scan for code fence languages), `load_page` (falls back to file mtime, generates TOC via `md.generate_toc` when frontmatter `"toc": true`), `infer_layout`. Calls `md.process()` for the markdown pipeline. | | `render.odin` | Template rendering: `render_site`, `render_page_html`, `render_home_html`, `render_section`. `Template_Context` (unified render struct with `site: Site_Context`, `page: Page`, `menus`, `params`, `posts`, `pages`). 3-frame context stack via `[]any{ctx.site, ctx.page, ctx}`. `merge_params(site, page)` β shallow merge of site + page params. Error deduplication via `seen: ^map[string]bool` passed through render chain. `sort_pages` (weight primary, date secondary). `to_title_case` for section display names. VFS-based template loading with fallback chain (`get_template`). | | `menus.odin` | Menu system: `Menu_Entry {name, url, weight: Maybe(int)}`, `DEFAULT_WEIGHT = 10`. `build_menus` (priority chain: config β auto + page frontmatter, then `warn_all_duplicate_weights`). `collect_auto_menus` (sections + root-level pages, skips pages with explicit `"menus": "main"` frontmatter). `merge_page_menus` (frontmatter entries with effective weight fallback via nil check). `parse_page_menus` (string/array/object forms). `parse_config_menus` (from thor.json). `sort_menu_entries` / `compare_menu_entries` (weight primary via `.? or_else DEFAULT_WEIGHT`, name secondary). `warn_duplicate_weights` / `warn_all_duplicate_weights` (log when two entries in same menu have same explicitly-set weight). | | `minify.odin` | HTML/CSS minification via tree-sitter. Imports `ts "treesitter"`. | | `feed.odin` | RSS feed + sitemap XML. Uses `page.url` for canonical URLs. | | `vfs.odin` | Union file system: `VFS`, `build_vfs`, `mount_dir`, `mount_subdir`, `mount_recursive`, `vfs_get`, `vfs_get_entry`, `vfs_entry_data`. Layers defaults β modules β site. | | `assets.odin` | `copy_assets_dir` β iterates VFS entries with `assets/` prefix, minifies CSS, copies verbatim or via `os.copy_file`. | | `html.odin` | `strip_html_tags`, `unescape_html`, `generate_summary` (word-count truncation, zero-alloc), `generate_description` (HTMLβplain text: strip tags, decode entities, collapse whitespace). | | `opengraph.odin` | `Open_Graph` struct (fields ordered per OGP spec, `is_article: Maybe(bool)`). `og_for_site(site)` for site defaults (from config + derived), `og_for_page(site_og, page)` for page-specific (overlay page.og + derive from page data). Description falls back to `generate_description(generate_summary(body_html))`. | | `frontmatter.odin` | JSON frontmatter parser (`{ }` delimited). Supports `layout`, `lastmod`, `weight: Maybe(int)`, `menus`, `params: json.Value`, `toc: bool`, and nested `og` object (via `json_get_open_graph`). Helpers: `json_get_string`, `json_get_bool`, `json_get_int` (returns `Maybe(int)`, nil for absent/invalid). | | `defaults.odin` | `DEFAULTS_PATH` constant, resolved at compile time via `#directory` so bundled templates ship in the binary. | ### Subpackages | Package | Files | Responsibility | |---|---|---| | `treesitter/` | `treesitter.odin` | FFI types (`Parser`, `Node`, `Query`, etc.), `@(link_prefix="ts_")` foreign bindings, grammar management (`Grammar_Store` with persistent allocator, `load_language`/`compile_query` building blocks, `ensure_parser`/`load_grammar` lazy loading, `preload_grammar`/`preload_grammars` for parallel loading with `sync.Mutex` cache protection), statically-linked HTML/CSS grammars | | `markdown/` | `markdown.odin` | `Extension` enum, `DEFAULT_EXTENSIONS`, `process(body, ext, file_path, allocator)` β full pipeline (clones cmark output, frees original), `parse_extension_list`, `apply_extension_config` | | | `footnotes.odin` | `strip_definitions` (pre-cmark), `inject_notes` (post-cmark). cmark output freed via `defer cm.free_string(raw_html)` on separate variable. | | | `alerts.odin` | `inject_alerts` β GitHub alert blocks (`> [!NOTE]`) β styled blockquotes with semantic class names (`alert-note` etc.) | | | `emoji.odin` | `expand_emoji` β `:shortcode:` β unicode emoji | | | `sectionate.odin` | `wrap_sections` β splits HTML at `
`/``/`