This site provides a machine-readable index at /llms.txt.

Skip to main content Skip to navigation

Hydrate a Razor component as a client island

To hydrate a Razor component as a client island — a chart, a live counter, a comment thread that needs to be server-rendered on first load and re-rendered from JSON on SPA navigation — subclass RazorIslandRenderer<T> and register it on IslandsOptions. The region's content depends on the current route. When every page needs the same chrome, reach for an IResponseProcessor or a layout slot instead.

Before you begin

  • A working Pennington site (see Create your first Pennington site if not).
  • SPA navigation already wired: builder.Services.AddSpaNavigation() plus app.UseSpaNavigation(), and the layout emits the data-spa-* attributes the client script expects.
  • A Razor component (or plain HTML string) ready to render into the island slot.
  • Familiarity with the content pipeline at a conceptual level (The content pipeline and union types).

A working reference: examples/ExtensibilityLabExampleChartIslandRenderer, Components/ChartIsland.razor, and Content/chart-demo.md together form the minimal island.

Build the Razor component

Create a Razor component whose [Parameter] surface matches the dictionary the renderer will produce. The component should be pure presentation — it takes its data through parameters and fetches nothing itself. Every value it touches needs to be passable through the IDictionary<string, object?> parameters payload, because that is what RazorIslandRenderer<T> hands to the ComponentRenderer.

@namespace ExtensibilityLabExample.Components
  
<figure class="chart-island" data-extensibility-lab="chart-island-render">
    <figcaption>@Label</figcaption>
    <ul class="chart-bars">
        @foreach (var value in Values ?? Array.Empty<int>())
        {
            <li><span class="chart-bar" style="--bar-value:@value">@value</span></li>
        }
    </ul>
</figure>
  
@code {
    [Parameter] public string Label { get; set; } = "Chart";
    [Parameter] public IReadOnlyList<int>? Values { get; set; }
}

Implement the renderer

Derive from RazorIslandRenderer<T> rather than implementing IIslandRenderer directly. The base class wires the ComponentRenderer call, leaving IslandName and BuildParametersAsync as the only members to override. Reach for IIslandRenderer.RenderAsync only to emit a non-Razor fragment — a pre-rendered string, a cached snippet, or a remote include.

/// <summary>
/// Implements <see cref="IIslandRenderer"/> by subclassing
/// <see cref="RazorIslandRenderer{TComponent}"/>. Renders the
/// <see cref="ChartIsland"/> Razor component for the
/// <c>data-spa-island="chart"</c> slot on any content page that carries
/// one.
/// <para>
/// Registered via
/// <c>options.Islands.Register&lt;ChartIslandRenderer&gt;("chart")</c>
/// in <c>Program.cs</c>. The name string matches the <c>data-spa-island</c>
/// attribute value; <see cref="IslandName"/> exposes the same value so
/// <c>SpaPageDataService</c> can key the JSON envelope.
/// </para>
/// <para>
/// <see cref="BuildParametersAsync"/> returns a parameter dictionary
/// for every route under <c>/chart-demo/</c> (or the per-release pages)
/// and returns <see langword="null"/> everywhere else — a null return
/// tells the base class to skip this island for that page.
/// </para>
/// <para>
/// Backs how-to 2.3.60 <c>/how-to/extensibility/island-renderer</c>.
/// </para>
/// </summary>
public sealed class ChartIslandRenderer : RazorIslandRenderer<ChartIsland>
{
    public ChartIslandRenderer(ComponentRenderer renderer) : base(renderer) { }
  
    public override string IslandName => "chart";
  
    protected override Task<IDictionary<string, object?>?> BuildParametersAsync(ContentRoute route)
    {
        // Only render a chart on pages that advertise one. Saves work on
        // every other page in the site.
        var path = route.CanonicalPath.Value;
        if (!path.Contains("/chart-demo", StringComparison.OrdinalIgnoreCase))
            return Task.FromResult<IDictionary<string, object?>?>(null);
  
        IDictionary<string, object?> parameters = new Dictionary<string, object?>
        {
            ["Label"] = "Quarterly widgets",
            ["Values"] = new[] { 12, 19, 7, 24 },
        };
        return Task.FromResult<IDictionary<string, object?>?>(parameters);
    }
}

IslandName is the key the SPA envelope uses for this island, and it has to match the data-spa-island attribute on the markup. BuildParametersAsync receives the ContentRoute for the page being rendered — inspect CanonicalPath and return null for any route that does not carry this island so the base class skips rendering. Returning parameters on every route wastes work and produces orphan HTML in pages with no slot to hold it.

// Only render a chart on pages that advertise one. Saves work on
// every other page in the site.
var path = route.CanonicalPath.Value;
if (!path.Contains("/chart-demo", StringComparison.OrdinalIgnoreCase))
    return Task.FromResult<IDictionary<string, object?>?>(null);
  
IDictionary<string, object?> parameters = new Dictionary<string, object?>
{
    ["Label"] = "Quarterly widgets",
    ["Values"] = new[] { 12, 19, 7, 24 },
};
return Task.FromResult<IDictionary<string, object?>?>(parameters);

Author the slot in your content

Wrap the server-rendered region in an element that carries data-spa-island="islandName". Nothing else is required — the SPA runtime replaces the element's innerHTML on navigation, and on first load the renderer's output is already there. Keep a <noscript> fallback or a plain-markup default inside the slot so the page still reads sensibly before the island hydrates.

---
title: Chart island demo
description: Content page embedding a data-spa-island="chart" region rendered by ChartIslandRenderer.
---
  
# Chart island demo
  
The `<div>` below is marked as an SPA island. On first render the
server-side `ChartIslandRenderer` returns the HTML inside it. On
subsequent in-site navigation, `/_spa-data/chart-demo.json` carries the
same HTML in the `islands.chart` slot so the SPA engine can swap it
without a full reload.
  
<div data-spa-island="chart" data-extensibility-lab="chart-island">
  <noscript>Chart placeholder — JavaScript required for client navigation.</noscript>
</div>
  
The server-rendered markup is wired up in
`ChartIslandRenderer.BuildParametersAsync`.

Register the implementation

Call options.Islands.Register<TRenderer>("islandName") inside the AddPennington configuration. The generic type argument is the renderer; the string is both the data-spa-island attribute value and the key SpaPageDataService writes into the islands slot of the JSON envelope — the two have to agree exactly. Register one entry per island. The dictionary is keyed by name, so registering twice with the same name replaces the earlier entry.

builder.Services.AddPennington(penn =>
{
    penn.Islands.Register<ChartIslandRenderer>("chart");
    // ...
});

Islands run because SpaNavigationContentService emits per-page envelopes at /_spa-data/{slug}.json, and the ComponentRenderer the renderer depends on is registered as a scoped service alongside it. If either line is missing from Program.cs the renderer never runs — even on first load — because the DocSite content island short-circuits without its services.

builder.Services.AddScoped<ComponentRenderer>();
builder.Services.AddSpaNavigation();
  
// ...
  
app.UseSpaNavigation();

Result

On first load of /chart-demo/, the ChartIsland component is rendered directly into the page inside the data-spa-island="chart" element — the chart <figure> appears in view-source, not only in devtools. On client-side navigation back to /chart-demo/ from another page, the SPA runtime fetches /_spa-data/chart-demo.json, reads the islands.chart HTML from the envelope, and swaps it into the same slot without a full page reload. Routes whose CanonicalPath does not match /chart-demo get a null parameters dictionary and no chart HTML in their envelope.

Verify

  • Run dotnet run --project examples/ExtensibilityLabExample and visit /chart-demo/ — the chart <figure> is present in the initial HTML (view source, not devtools).
  • Request /_spa-data/chart-demo.json directly — the response contains an islands object with a chart key whose value is the rendered HTML.
  • Navigate to /chart-demo/ from another page via a link click — the region updates without a full page reload, and routes that do not carry data-spa-island="chart" show no chart HTML in their envelope.