Heimdall Docs

Assets & Templates

Heimdall pages and actions produce HTML, but real applications also need reusable content that does not always belong inline in page code. This page explains how file-based static content and templates fit into the Heimdall rendering model.

The goal is not to introduce a second UI architecture. It is to support HTML-first rendering with the right tool for the kind of content you are producing.

1. What This Layer Solves

Not every piece of rendered output belongs directly in FluentHtml code. Some content is easier to keep as a file, some should be cached and injected as trusted markup, and some should be generated from a template engine such as Scriban.

Heimdall rendering inputs can include:
- FluentHtml for interactive structure
- StaticAssets for file-based markup fragments
- Templates for generated content

2. StaticAssets Is Not Just File Serving

The StaticAssets helper shown here is not primarily about serving CSS or JavaScript to the browser. It is a server-side mechanism for discovering files, loading them from disk, caching them, and returning them as IHtmlContent so they can be injected directly into rendered output.

StaticAssets.Discover(app.Environment.WebRootPath);

var fragment = StaticAssets.Get("fragments/hero.html");

3. Startup Setup

The helper is initialized once during application startup. It scans the web root, builds a path lookup, and prepares those files for later retrieval.

var app = builder.Build();

StaticAssets.Discover(app.Environment.WebRootPath);

app.UseAntiforgery();

This makes the discovered files available to server-side rendering code without needing to reopen and re-resolve paths everywhere they are used.

4. What StaticAssets Actually Returns

StaticAssets.Get(...) returns IHtmlContent, not a file path and not an encoded string. That means the loaded file content can be inserted directly into a FluentHtml tree as already-trusted markup.

container.Add(StaticAssets.Get("fragments/marketing/hero.html"));

5. Why That Matters

This is what makes the helper more than a convenience wrapper. It allows file-based markup fragments to participate directly in server rendering without being HTML-encoded on output.

Markup can live in files instead of inline strings.
Fragments can be reused across pages and components.
The loaded markup can be inserted directly into the render tree.
Repeated reads are avoided through caching.

6. Trusted Markup Is a Deliberate Choice

The helper intentionally returns a custom IHtmlContent implementation that writes raw markup directly to the response. That means these files are treated as trusted content.

private sealed class TrustedMarkup : IHtmlContent
{
    private readonly string _markup;

    public TrustedMarkup(string markup)
    {
        _markup = markup;
    }

    public void WriteTo(TextWriter writer, HtmlEncoder encoder)
    {
        writer.Write(_markup);
    }
}

7. What 'Trusted' Means Here

Because the content is written directly, this pattern is appropriate for files you own and ship with the application. It is not a general-purpose way to render untrusted user input. The value of the helper comes from treating application-owned fragments as safe server-side assets.

Good fit:
- app-owned HTML fragments
- known partial markup
- packaged UI snippets

Not a fit:
- user-submitted HTML
- untrusted external markup

8. File-Based Markup Fragments

One of the clearest uses of StaticAssets is storing reusable HTML fragments as files and injecting them where needed.

container.Add(StaticAssets.Get("fragments/home/hero.html"));
container.Add(StaticAssets.Get("fragments/home/value-props.html"));

This can be a very clean fit for marketing sections, static content blocks, documentation callouts, or large fragments that would feel noisy if embedded directly in C#.

9. Why Caching Helps

The helper caches loaded fragments in memory. That means the first read loads from disk, and later reads reuse the same IHtmlContent instance. This is useful when the same fragment appears in shared layouts, repeated sections, or multiple pages.

private static readonly ConcurrentDictionary<string, IHtmlContent> _cache = new();

10. FluentHtml vs StaticAssets

FluentHtml and StaticAssets are complementary. FluentHtml is best when structure, composition, and Heimdall behavior are central. StaticAssets is useful when the content is already a finished markup fragment.

Use FluentHtml when:
- building interactive UI
- composing server-rendered components
- attaching Heimdall behavior
- expressing structure in C#

Use StaticAssets when:
- a fragment already exists as markup
- content is mostly static
- large HTML blocks would be noisy inline
- reuse is file-oriented rather than compositional

11. Where Templates Fit

Templates solve a different problem. StaticAssets loads an existing file as-is. Templates generate output by combining a source template with runtime data. That makes templates a better fit when the structure stays similar but the content changes.

StaticAssets -> load existing markup
Templates    -> generate markup or text from data

12. Using Scriban

Scriban is a strong fit when you want file-based templates with placeholders and data binding. It works especially well for emails, text-heavy HTML output, and content where a template is easier to manage than a long fluent builder.

var template = Template.Parse("Hello {{ name }}");
var result = template.Render(new { name = "Brad" });

This is not a replacement for FluentHtml. It is another rendering input that is useful when the output is template-shaped rather than component-shaped.

13. FluentHtml vs Templates

Templates are best when the output is document-like, content-heavy, or driven by placeholders. FluentHtml is best when you want typed structure, composition, and close integration with Heimdall behavior.

Use Templates when:
- generating emails
- generating text-heavy HTML
- filling placeholders from data
- content authorship matters more than UI composition

Use FluentHtml when:
- interactive structure matters
- composition is component-driven
- Heimdall behavior is attached directly
- the output is application UI

14. How These Tools Work Together

In a real application, these tools are often combined rather than chosen exclusively. FluentHtml can define the page structure, StaticAssets can inject stable file-based fragments, and templates can generate dynamic content blocks where placeholder-driven rendering is the better fit.

FluentHtml   -> page structure and interaction
StaticAssets -> reusable file-based fragments
Templates    -> generated content from data

15. A Useful Architecture Pattern

A practical pattern is to let FluentHtml own the interactive shell, let StaticAssets provide trusted static fragments, and let templates produce content-oriented output where strong typing would add little value.

Page shell      -> FluentHtml
Static fragment -> StaticAssets.Get(...)
Generated block -> Scriban template

16. Packaging and Reuse

This approach also helps when building reusable libraries or shared application features. File-based fragments and templates can be organized with the application or a package, while the page and action code stays focused on composition and behavior.

Reusable feature may include:
- server rendering code
- HTML fragment files
- templates
- supporting JS / CSS

17. Common Mental Model

A good way to think about this layer is that it expands Heimdall’s rendering toolbox without changing the architectural model.

FluentHtml   = interactive structure
StaticAssets = trusted file-based markup
Templates    = generated content
Runtime      = browser-side interaction layer

18. Why This Fits Heimdall

Heimdall stays HTML-first and server-driven because these tools still end in server-rendered output. The browser is not being asked to invent the UI. It is receiving HTML that was composed, loaded, or generated on the server with the tool best suited to that content.

Server composes HTML
-> from builders, assets, or templates
-> runtime makes interactive boundaries work
-> server remains the source of truth

Next Steps

With rendering inputs, assets, and templates in place, the next step is to see how Heimdall concepts combine into larger real-world interaction patterns.