---
title: Render a Razor component as a page on a bare host
description: "Use HtmlRenderer.RenderComponentAsync inside a MapGet to make a Razor component the entire response body, no DocSite layout pipeline required."
canonical_url: https://usepennington.net/how-to/response-pipeline/razor-page-on-bare-host/
sidecar_url: https://usepennington.net/how-to/response-pipeline/razor-page-on-bare-host.md
content_hash: sha256:b2a3bc83f02d0f647c4f0f75fbedb14ab365a7316c2fa4cd9bcd8ca6c361f235
tokens: 1971
uid: how-to.response-pipeline.razor-page-on-bare-host
reading_time_minutes: 3
---

Guides
# Render a Razor component as a page on a bare host

Use HtmlRenderer.RenderComponentAsync inside a MapGet to make a Razor component the entire response body, no DocSite layout pipeline required.

 
To render a Razor component as the whole response body for a custom route on a bare `AddPennington` host, render it through Blazor's server-side `HtmlRenderer` from inside a `MapGet`. The component owns the document — `<html>`, `<head>`, `<body>` — so the response is a complete HTML page without DocSite or BlogSite layout machinery. Use this pattern when a custom `IContentService` discovers per-record routes (`/instructors/{slug}/`, `/status/{slug}/`) and the rendered output is too complex for inline HTML strings.

 
## Before you begin

 
 - A working Pennington site on bare `AddPennington` (see [Create your first Pennington site](https://usepennington.net/tutorials/getting-started/first-site.md) if not).
 - A reference to `Microsoft.AspNetCore.Components.Web` — already transitive through `Pennington`.
 - Familiarity with `IContentService` for publishing the routes you'll render against ([Source content from outside the markdown pipeline](https://usepennington.net/how-to/content-services/custom-content-service.md)).
 
 
A working reference: `examples/BareHostRazorPageExample` — one Razor component plus a single `MapGet` that renders it.

 
## Author the page component

 
Write a Razor component whose `[Parameter]` properties are everything the page needs — there is no ambient `HttpContext`, layout, or cascading state from a parent. The component renders the entire document so it includes `<!DOCTYPE html>` and the `<link rel="stylesheet" href="/styles.css">` tag for [MonorailCSS](https://monorailcss.github.io/MonorailCss.Framework/) output.

 
```razor:symbol
@* StatusPage — a Razor component used as the entire page body for routes like
   /status/{slug}. Program.cs renders it through HtmlRenderer.RenderComponentAsync
   inside a MapGet, so the component owns the whole document including <html>,
   <head>, and <body>. *@
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>@Title</title>
    <link rel="stylesheet" href="/styles.css" />
</head>
<body class="bg-base-50 text-base-900 dark:bg-base-950 dark:text-base-50">
    <main class="mx-auto max-w-2xl px-6 py-12">
        <header class="mb-8">
            <p class="text-xs font-semibold uppercase tracking-wide text-accent-500">@Slug</p>
            <h1 class="mt-1 font-display text-3xl font-bold">@Title</h1>
        </header>
  
        <p class="text-base-700 dark:text-base-300">@Summary</p>
  
        <dl class="mt-8 grid grid-cols-1 gap-x-6 gap-y-3 sm:grid-cols-[10rem_1fr]">
            @foreach (var item in Facts)
            {
                <dt class="text-xs font-semibold uppercase tracking-wide text-base-500 dark:text-base-400">@item.Key</dt>
                <dd class="text-sm text-base-700 dark:text-base-300">@item.Value</dd>
            }
        </dl>
    </main>
</body>
</html>
  
@code {
    [Parameter, EditorRequired] public string Slug { get; set; } = string.Empty;
    [Parameter, EditorRequired] public string Title { get; set; } = string.Empty;
    [Parameter, EditorRequired] public string Summary { get; set; } = string.Empty;
    [Parameter] public IReadOnlyList<KeyValuePair<string, string>> Facts { get; set; } = [];
}
```

 
## Register the Blazor renderer services

 
`HtmlRenderer` needs Blazor's component services and an `IHttpContextAccessor` so cascading values can resolve. Register both alongside the `AddPennington` and `AddMonorailCss` hosts:

 
```csharp
builder.Services.AddRazorComponents();
builder.Services.AddHttpContextAccessor();
```

 
`AddRazorComponents` registers `HtmlRenderer` and its dispatcher; `AddHttpContextAccessor` lets a rendered component resolve cascading values. There is no `MapRazorComponents`, no `App.razor`, and no `_Host` page — the bare host never starts the Blazor router. Components reach the response only through the `MapGet` below.

 
## Render the component inside a `MapGet`

 
The route handler turns a slug into the component's `[Parameter]` values and hands them to a render helper. A missing record returns null parameters, which the helper turns into a 404:

 
```csharp:symbol
public static async Task<IResult> RenderRazorPageAsync<TComponent>(
    HtmlRenderer renderer,
    IDictionary<string, object?>? parameters)
    where TComponent : IComponent
{
    if (parameters is null)
    {
        return Results.NotFound();
    }
  
    var html = await renderer.Dispatcher.InvokeAsync(async () =>
    {
        var output = await renderer.RenderComponentAsync<TComponent>(
            ParameterView.FromDictionary(parameters));
        return output.ToHtmlString();
    });
    return Results.Content(html, "text/html");
}
```

 
`RenderRazorPageAsync<TComponent>` is the only Blazor-specific code the host needs: it dispatches the render onto the renderer's dispatcher, materializes the output with `ToHtmlString`, and hands the complete HTML string to `Results.Content`. Reuse it for any other component-as-page route. The route wiring itself is a plain minimal-API endpoint:

 
```csharp
app.MapGet("/status/{slug}/", (string slug, StatusPagesContentService statuses, HtmlRenderer renderer)
    => BareHostRenderer.RenderRazorPageAsync<StatusPage>(renderer, statuses.TryGet(slug) is { } entry
        ? new Dictionary<string, object?>
        {
            [nameof(StatusPage.Slug)] = entry.Slug,
            [nameof(StatusPage.Title)] = entry.Title,
            [nameof(StatusPage.Summary)] = entry.Summary,
            [nameof(StatusPage.Facts)] = entry.Facts,
        }
        : null));
```

 
### Why not a Blazor `@page`?

 
A routed `@page` component needs the Blazor router, an `App.razor`, and `MapRazorComponents` — the machinery [Serve markdown through Blazor Pages](https://usepennington.net/tutorials/getting-started/first-page.md) stands up. A bare `AddPennington` host runs none of that, so a `@page` directive would never be routed. Rendering through `HtmlRenderer` inside a `MapGet` keeps the host minimal: the component is a render target, not a routed endpoint, and your `IContentService` owns route discovery.

 
## Publish the routes through `IContentService`

 
A custom `IContentService` yields one `EndpointSource` per route so the build crawler discovers each URL and fetches it through the live pipeline — your `MapGet` produces the HTML the same way at build time as at request time. See [Source content from outside the markdown pipeline](https://usepennington.net/how-to/content-services/custom-content-service.md) for the per-record discovery pattern, including a worked `EndpointSource` example.

 
## Verify

 
 - Run `dotnet run --project examples/BareHostRazorPageExample` and open `/status/intro/` and `/status/verify/` at the URL the console prints (the `Now listening on:` line). Each renders the `StatusPage` component as a full HTML page styled by `/styles.css`.
 - Confirm the static build picks up both routes: `dotnet run --project examples/BareHostRazorPageExample -- build` writes `output/status/intro/index.html` and `output/status/verify/index.html`.
 
 
## Related

 
 - How-to: [Source content from outside the file system](https://usepennington.net/how-to/content-services/custom-content-service.md)
 - Background: [The content pipeline and union types](https://usepennington.net/explanation/core/content-pipeline.md)
 
 
[Previous
                
                Customize the DocSite chrome through DocSiteOptions](https://usepennington.net/how-to/response-pipeline/override-docsite-components.md)[Next
                    
                Add tags to the document head](https://usepennington.net/how-to/response-pipeline/head-contributor.md)