Files
thor/HUGO.md
T
2026-07-14 11:59:21 -04:00

3.4 KiB

Hugo vs Thor: Why Thor Doesn't Need Shortcode Context Isolation

Hugo's Shortcode Isolation

Hugo strictly separates shortcode context from layout template context. Shortcodes run during markdown rendering, before layouts. They get a limited context (.Page, .Site, .Params), not the full layout rendering state.

Why Hugo does this

  1. Circular dependencies — Hugo shortcodes output into .Content. Layouts read .Content and compute derived properties (.TableOfContents, .WordCount, .ReadingTime). If shortcodes could access these computed properties, you'd get circular dependencies (shortcode output → computed property → shortcode reads it).

  2. Multiple layouts/themes — Hugo supports user-selectable themes and multiple layouts per section type. A shortcode must produce identical output regardless of which layout renders it. Isolating context guarantees portability.

  3. Shared mutable page object — Hugo builds a rich Page object that both shortcodes and layouts access. Context isolation prevents shortcodes from mutating layout-visible state.

  4. Security — Shortcodes live in content files (potentially user-authored). Full template access would let content authors inject arbitrary logic, access sensitive config, or break site structure.

  5. Caching — Hugo caches shortcode output independently. Predictable context = predictable cache behavior.

Why this doesn't apply to thor

  • No computed page properties — The content Mustache pass runs on raw markdown (before cmark). The layout Mustache pass runs on finished HTML (after cmark). There's no shared .Content object or derived properties. The two passes are at different pipeline stages with no shared mutable state.

  • Single layout set — Thor has one set of templates (base.html + page templates). No theme switching, no multiple layout variants per section. Portability across layouts isn't a concern.

  • Explicit data passing — Each mustache.render call receives its own data struct. Content pass gets frontmatter + params. Layout pass gets page data + site config. No shared mutable object between them.

  • Single author — Content is authored by the site owner. No untrusted user-generated content.

  • No caching — Thor rebuilds from scratch every time. No cache invalidation concerns.

Thor's approach

Thor runs Mustache on content before cmark as a pre-processing step, then runs Mustache on layout templates after cmark as a post-processing step. These are two independent mustache.render calls:

content (markdown)
  → mustache.render(content, content_data, partials)   ← content pass
  → cmark markdown_to_html
  → inject_sidenotes / inject_alerts / highlight_code
  → mustache.render_in_layout(template, page_data, layout, partials)  ← layout pass
  → final HTML

The content pass can use Mustache variables, partials ({{> ./file}}), sections, and conditionals freely. The layout pass wraps the rendered content in the page chrome. They share nothing except the data we explicitly choose to pass.

When this WOULD matter for thor

If thor ever adds:

  • Computed page properties (table of contents, reading time, word count)
  • Multiple selectable themes/layouts
  • User-generated content / multi-author support
  • Partial template caching

...then context isolation between the content and layout passes would become relevant. Until then, it's unnecessary complexity.