Skip to documentation
Heimdall Docs
Heimdall Docs

Blazor Integration

Blazor and Heimdall can live in the same application, but they solve different ownership problems. Decide who owns the route, the component tree, and the form or event lifecycle before choosing the integration shape.

There are two useful directions: a Blazor application that adds Heimdall around selected HTML boundaries, or a Heimdall application that uses Razor components as non-interactive server-side renderers.

1. The Two Ownership Models

The same .razor file can participate in two very different systems. In a Blazor-first application, Blazor maps the route and may keep a live circuit or interactive WebAssembly runtime. In a Heimdall-first application, IHeimdallBlazorRenderer renders a component tree to HTML for a page or content action; the returned markup is not a live Blazor component.

Text
Blazor-first application
    route -> Blazor endpoint -> component lifecycle
          -> optional interactive circuit

Heimdall-first application
    route/action -> Heimdall -> static component render
                 -> HTML fragment -> DOM target

2. Choose By Request Ownership

The boundary is per route or endpoint, not necessarily per project. A hybrid application can let Blazor own /editor, MVC own /admin, and Heimdall own /dashboard. The important question is which pipeline receives the request that follows the browser event.

Text
Route or boundary       Owner                 Form/action contract
-----------------------  --------------------  --------------------------
/editor                   Blazor static SSR     SupplyParameterFromForm
/dashboard                Heimdall page/action  ContentPayload + HTML
/admin                    MVC/Razor             controller or PageModel
component fragment        caller's endpoint     caller's binding contract

3. Heimdall-First: Render Components As HTML

Use this path when Heimdall owns the page or interaction and a .razor component is a useful server-side rendering primitive. AddHeimdallBlazor registers the component services and IHeimdallBlazorRenderer, but it does not map Razor component endpoints or enable interactive render modes.

C#
builder.Services.AddAntiforgery();
builder.Services.AddHeimdall();
builder.Services.AddHeimdallBlazor();

var app = builder.Build();
app.UseAntiforgery();
app.UseHeimdall();

app.MapHeimdallPage("/orders", () =>
    OrdersPage.Render());

4. A Heimdall Action Can Return A Component Tree

The action loads the current model, renders the component tree, and returns its HTML to a Heimdall target. Parameters, dependency injection, child components, RenderFragment values, and asynchronous initialization participate in the render pass.

Code
[ContentInvocation("orders.summary")]
public static async Task<IHtmlContent> Summary(
    [FromServices] IHeimdallBlazorRenderer components,
    OrderService orders,
    CancellationToken ct)
{
    var model = await orders.GetSummaryAsync(ct);

    return await components.RenderAsync<OrderSummary>(
        new { Model = model },
        ct);
}

5. What The Static Adapter Supports

Static rendering is a complete server-side render pass, not a partial parser. A parent component and its descendants render together. Component parameters, injected services, RenderFragment content, child components, and asynchronous lifecycle work are included in the returned HTML.

Text
Supported during the render pass:
- [Parameter] values
- component and child-component composition
- ChildContent and RenderFragment values
- dependency injection
- asynchronous initialization
- normal Razor markup and encoding

The output is HTML. It does not create a circuit.

6. What The Static Adapter Does Not Activate

The static adapter does not hydrate the result or preserve a component instance between requests. @onclick, @bind, EventCallback, @ref, JavaScript interop, and interactive lifecycle behavior require a Blazor-owned interactive render mode or an explicit Heimdall interaction in the rendered HTML.

Text
Static component output:
<button @onclick="Save">Save</button>

The markup can be rendered, but there is no active
Blazor event callback after it reaches the browser.

For a Heimdall interaction, render an HTML boundary:
<button heimdall-content-click="orders.save"
        heimdall-content-target="#order-result">
  Save
</button>

7. Forms: SupplyParameterFromForm Versus ContentPayload

A native form emitted by a static component still behaves like a native form. However, [SupplyParameterFromForm] only binds when the POST is handled by Blazor's static SSR form-mapping pipeline. A component rendered through IHeimdallBlazorRenderer does not become a Blazor endpoint just because its source is a .razor file.

Text
Blazor-owned static SSR form:

<EditForm Model="Model" FormName="orders" method="post">
  <InputText @bind-Value="Model.Search" />
  <button type="submit">Search</button>
</EditForm>

@code {
    [SupplyParameterFromForm]
    private OrderForm Model { get; set; } = new();
}

Heimdall-managed form:

<form heimdall-content-submit="orders.search"
      heimdall-payload-from="closest-form">
  <input name="Search" />
  <button type="submit">Search</button>
</form>

The corresponding content action uses [ContentPayload].

8. Blazor-First: Static SSR With Heimdall

Use this path when Blazor owns the page routes and component tree, but you want Heimdall for HTML-first interactions and server-rendered updates. The Heimdall Blazor template uses static SSR: it registers Razor components, adds Heimdall's Blazor renderer, and maps component endpoints without enabling Interactive Server or a Blazor circuit.

C#
builder.Services
    .AddRazorComponents();
builder.Services.AddHeimdallBlazor();
builder.Services.AddHeimdall();

var app = builder.Build();
app.UseAntiforgery();
app.UseHeimdall();

app.MapRazorComponents<App>();

9. Keep Live Blazor Islands And Heimdall Targets Separate

The static template does not create live Blazor islands. If a separate Blazor application explicitly enables an interactive render mode, a page can contain both systems, but a DOM subtree should still have one owner. Let Blazor reconcile the live component subtree and let Heimdall swap its own target. Avoid asking Heimdall to replace nodes that an interactive Blazor component still owns.

Text
Page shell (only when interactive Blazor is enabled)
|- Blazor-owned <LiveEditor @rendermode="InteractiveServer" />
|- Heimdall-owned #activity-feed
`- Heimdall-owned #notifications

Blazor updates its component subtree.
Heimdall updates its declared HTML targets.

10. Forms Can Still Be Native

Choosing Heimdall-first does not remove normal HTML forms. A component can emit a form with method and action, and the browser will navigate normally. The destination can be a Blazor endpoint, MVC action, Razor Page handler, minimal API, or another route. The binding behavior belongs to that destination, not to the component that emitted the markup.

HTML
<form method="post" action="/checkout">
  <input name="Email" />
  <button type="submit">Continue</button>
</form>

No Heimdall submit attribute means the browser owns
the submit and follows the normal form contract.

11. When To Use Each Path

Use the smallest ownership model that matches the behavior you need. The goal is not to choose one framework for the whole repository; it is to avoid two runtimes fighting over the same DOM boundary.

Text
Choose Blazor-first when:
- the page needs live component state or callbacks
- the route is naturally a Blazor endpoint
- Blazor form mapping and component lifecycle are central

Choose Heimdall-first when:
- HTML and server actions are the primary interaction model
- a component is useful for composing a static fragment
- you want no circuit or hydration cost
- forms should bind through Heimdall content actions

Choose a hybrid when:
- one page has a clearly isolated live Blazor island
- neighboring regions are better as HTML swaps
- route ownership is explicit for each boundary