Heimdall Docs

Pages

A Heimdall page is a route that returns HTML. You map it with MapHeimdallPage(...), render with your existing HTML helpers, and choose the overload that matches how much context your page needs.

Pages are route-level HTML endpoints. Content invocations are for triggered interactions that return fragment HTML.

1. What a Page Is

Pages are the route-level entry points of a Heimdall app. They handle a URL and return HTML for that request.

app.MapHeimdallPage("/", () =>
{
    return Html.Div(d =>
    {
        d.Text("Hello from Heimdall");
    });
});

2. The Available Overloads

Heimdall provides several MapHeimdallPage overloads so you can keep simple pages simple while still supporting pages that need HttpContext, services, or async work.

Func<IHtmlContent> — for fully static pages.
Func<HttpContext, IHtmlContent> — for synchronous pages that need request context.
Func<HttpContext, Task<IHtmlContent>> — for asynchronous pages that need request context.
Func<IServiceProvider, HttpContext, IHtmlContent> — for synchronous pages that need both services and context.
Func<IServiceProvider, HttpContext, Task<IHtmlContent>> — the full async overload and core primitive.

3. Fully Static Pages

Use the simplest overload when the page does not need HttpContext or services.

app.MapHeimdallPage("/about", () =>
{
    return Html.Div(d =>
    {
        d.H1(h => h.Text("About"));
        d.P(p => p.Text("This page is fully static."));
    });
});

4. Pages That Need HttpContext

Use the HttpContext overload when rendering depends on request information like the current user, headers, route values, query string, or request path.

app.MapHeimdallPage("/profile", ctx =>
{
    var name = ctx.User.Identity?.Name ?? "Anonymous";

    return Html.Div(d =>
    {
        d.H1(h => h.Text("Profile"));
        d.P(p => p.Text($"Hello, {name}."));
    });
});

5. Async Pages with HttpContext

Use the async HttpContext overload when the page needs asynchronous work but does not need direct access to IServiceProvider.

app.MapHeimdallPage("/dashboard", async ctx =>
{
    await Task.Delay(10);

    return Html.Div(d =>
    {
        d.H1(h => h.Text("Dashboard"));
        d.P(p => p.Text("Rendered asynchronously."));
    });
});

6. Pages That Need Services and Context

Use the IServiceProvider + HttpContext overload when the page needs access to registered services while still remaining an explicit route-level function.

app.MapHeimdallPage("/account", (sp, ctx) =>
{
    var clock = sp.GetRequiredService<ISystemClock>();

    return Html.Div(d =>
    {
        d.H1(h => h.Text("Account"));
        d.P(p => p.Text($"Rendered at {clock.UtcNow:u}."));
    });
});

7. The Full Async Overload

This is the core primitive behind the other overloads. Use it when you need both request context and asynchronous service-driven rendering.

app.MapHeimdallPage("/reports", async (sp, ctx) =>
{
    var repository = sp.GetRequiredService<IReportRepository>();
    var reports = await repository.GetRecentAsync(ctx.RequestAborted);

    return Html.Div(d =>
    {
        d.H1(h => h.Text("Reports"));

        d.Ul(ul =>
        {
            foreach (var report in reports)
            {
                ul.Li(li => li.Text(report.Title));
            }
        });
    });
});

8. Pages Return HTML

Every overload ultimately returns IHtmlContent. Heimdall renders that content to an HTML response with a text/html content type.

Page Method

public static class RoutingPage
{
    public static IHtmlContent Render()
    {
        return FluentHtml.Main(main =>
        {
            main.Div(d =>
            {
                d.H1(h => h.Text("Pages"));
                d.P(p => p.Text("Rendered on the server."));
            });
        });
    }
}

Mapped Route

app.MapHeimdallPage("/pages", () => RoutingPage.Render());

9. Pages Usually Use a Layout

In a real app, pages usually render inside a shared layout. The layout provides the document shell, navigation, scripts, styles, and global targets like toast containers.

public static IHtmlContent Render(HttpContext ctx)
{
    return MainLayout.Render(ctx, body =>
    {
        body.Main(main =>
        {
            main.Class(Bootstrap.Spacing.Py(5));

            main.Div(container =>
            {
                container.Class(Bootstrap.Layout.Container);
                container.H1(h1 => h1.Text("Pages"));
            });
        });
    });
}

10. Pages vs Actions

This is one of the most important distinctions in Heimdall.

Pages

Mapped with MapHeimdallPage(...). Used for route-level rendering when a browser requests a page.

  • Handle URLs like /, /pages, or /forms
  • Return HTML for the request
  • Usually wrapped in a layout

Actions

Marked with [ContentInvocation]. Used for triggered interactions that return fresh HTML fragments.

  • Triggered by Heimdall attributes
  • Return fragment HTML
  • Typically swapped into an existing target

11. Typical Page Pattern

A common pattern is to keep each route in its own page class with a static Render(...) method. This keeps route mapping simple and page composition easy to follow.

app.MapHeimdallPage("/", () => IndexPage.Render());
app.MapHeimdallPage("/pages", () => RoutingPage.Render());
app.MapHeimdallPage("/actions", () => ActionsPage.Render());

Next Steps

Once pages make sense, the next thing to learn is how interactions work.