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.
Blazor-first application
route -> Blazor endpoint -> component lifecycle
-> optional interactive circuit
Heimdall-first application
route/action -> Heimdall -> static component render
-> HTML fragment -> DOM target2. 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.
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 contract3. 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.
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.
[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.
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.
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.
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.
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.
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.
<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.
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 boundaryNext Steps
After choosing ownership, use the focused guides for the interaction itself. Keep route setup here, then keep actions, forms, state, and frontend boundaries in their own pages.