Blazor SSR Starter

The Blazor SSR Starter is a modern, production-ready template for building Agility CMS websites with Blazor Server-Side Rendering.

Features

  • Blazor SSR - Server-rendered Blazor components for optimal performance and SEO
  • Interactive Server Components - Opt-in interactivity via SignalR for dynamic features
  • GraphQL Support - Query content using Agility's GraphQL API
  • Responsive Images - Built-in AgilityPic component for optimized images
  • Tag-Based Caching - Webhook-driven cache invalidation
  • CDN-Ready Headers - Stale-while-revalidate cache control
  • Tailwind CSS v4 - Modern utility-first styling
  • One-Click Azure Deployment - Pre-configured ARM template

Project Structure

Agility.NET.Blazor.Starter/
├── Components/
│   ├── AgilityComponents/           # Content modules
│   │   ├── FeaturedPost.razor
│   │   ├── Heading.razor
│   │   ├── PostDetails.razor
│   │   ├── PostsListing/            # Server/Client pattern example
│   │   │   ├── PostsListing.razor
│   │   │   └── PostsListingClient.razor
│   │   ├── RichTextArea.razor
│   │   └── TextBlockWithImage.razor
│   ├── Layout/
│   │   └── MainLayout.razor         # Main page layout
│   ├── Pages/
│   │   └── AgilityPage.razor        # Dynamic page handler
│   └── Shared/
│       ├── AgilityComponent.razor   # Component router
│       ├── AgilityPic.razor         # Responsive images
│       ├── AgilitySource.razor      # Picture source element
│       ├── PreviewBar.razor         # Preview mode indicator
│       ├── SEO.razor                # SEO meta tags
│       ├── SiteFooter.razor
│       └── SiteHeader.razor
├── Endpoints/
│   └── RevalidateEndpoint.cs        # Webhook handler
├── Middleware/
│   ├── AgilityRedirectMiddleware.cs # URL redirects
│   └── CacheControlMiddleware.cs    # CDN cache headers
├── Models/
│   ├── AgilityModels.cs             # Content models
│   └── PostDisplayItem.cs           # Display models
├── Services/
│   ├── Agility/
│   │   ├── IAgilityService.cs       # Service interface
│   │   ├── AgilityService.cs        # Main service
│   │   ├── AgilityService.Content.cs
│   │   ├── AgilityService.Posts.cs
│   │   └── AgilityService.Sitemap.cs
│   └── Cache/
│       ├── AgilityCacheKeys.cs
│       ├── AgilityCacheService.cs   # Tag-based caching
│       └── IAgilityCacheService.cs
├── Styles/
│   └── tailwind.css                 # Tailwind v4 config
├── wwwroot/
│   └── css/output.css               # Generated CSS
├── appsettings.json                 # Default configuration
├── azuredeploy.json                 # Azure ARM template
└── Program.cs                       # Application setup

Render Modes

Blazor SSR supports two render modes:

ModeDescriptionUse Case
Static SSRServer renders HTML, no client JavaScriptMost content pages
Interactive ServerReal-time updates via SignalRForms, infinite scroll, dynamic UI

By default, all components render as Static SSR. This provides the best performance and SEO.

Enabling Interactive Mode

Add the render mode directive to components that need interactivity:

@rendermode @(new InteractiveServerRenderMode(prerender: true))

The prerender: true option ensures the component is still server-rendered initially for SEO, then becomes interactive when SignalR connects.

Server/Client Component Pattern

For components requiring both server-side data fetching and client-side interactivity, use a two-component pattern:

Server Component (Data Fetching)

@* PostsListing.razor *@
@inject IAgilityService AgilityService

@if (posts != null)
{
    <PostsListingClient InitialPosts="@posts" Locale="@Locale" />
}

@code {
    [Parameter] public ModuleModel ComponentData { get; set; } = new();

    private List<PostDisplayItem>? posts;
    private string Locale => ComponentData.Locale ?? "en-us";

    protected override async Task OnInitializedAsync()
    {
        posts = await AgilityService.GetPostsGraphQLAsync(Locale, take: 10);
    }
}

Client Component (Interactivity)

@* PostsListingClient.razor *@
@rendermode @(new InteractiveServerRenderMode(prerender: true))
@inject IAgilityService AgilityService
@inject IJSRuntime JSRuntime

<div class="posts-grid">
    @foreach (var post in allPosts)
    {
        <PostCard Post="@post" />
    }
</div>

<div @ref="loadMoreTrigger" class="load-more-trigger"></div>

@code {
    [Parameter] public List<PostDisplayItem> InitialPosts { get; set; } = new();
    [Parameter] public string Locale { get; set; } = "en-us";

    private List<PostDisplayItem> allPosts = new();
    private ElementReference loadMoreTrigger;
    private bool isLoading = false;

    protected override void OnInitialized()
    {
        allPosts = InitialPosts.ToList();
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            // Set up IntersectionObserver for infinite scroll
            await JSRuntime.InvokeVoidAsync("setupInfiniteScroll",
                loadMoreTrigger, DotNetObjectReference.Create(this));
        }
    }

    [JSInvokable]
    public async Task LoadMore()
    {
        if (isLoading) return;
        isLoading = true;

        var morePosts = await AgilityService.GetPostsGraphQLAsync(
            Locale, take: 10, skip: allPosts.Count);

        allPosts.AddRange(morePosts);
        isLoading = false;
        StateHasChanged();
    }
}

AgilityPic Component

The AgilityPic component generates responsive <picture> elements with optimized images:

<AgilityPic Image="@Fields.Image"
            Class="object-cover w-full"
            FallbackWidth="800"
            Priority="true">
    <AgilitySource Media="(min-width: 1280px)" Width="1200" />
    <AgilitySource Media="(min-width: 768px)" Width="800" />
    <AgilitySource Width="400" />
</AgilityPic>

Parameters

ParameterTypeDescription
ImageImageAttachmentThe Agility image object
FallbackWidthint?Width for the fallback <img> tag
Altstring?Alt text (defaults to Image.Label)
Classstring?CSS classes for the <img> element
PriorityboolIf true, disables lazy loading

Generated Output

<picture>
    <source media="(min-width: 1280px)"
            srcset="https://cdn.agilitycms.com/image.jpg?format=auto&w=1200" />
    <source media="(min-width: 768px)"
            srcset="https://cdn.agilitycms.com/image.jpg?format=auto&w=800" />
    <source srcset="https://cdn.agilitycms.com/image.jpg?format=auto&w=400" />
    <img src="https://cdn.agilitycms.com/image.jpg?format=auto&w=800"
         alt="Image description"
         loading="eager"
         class="object-cover w-full" />
</picture>

Adding a New Component

1. Create the Model

// Models/AgilityModels.cs
public class CallToAction
{
    public string? Heading { get; set; }
    public string? Description { get; set; }
    public Link? Button { get; set; }
    public string? BackgroundColor { get; set; }
}

2. Create the Component

@* Components/AgilityComponents/CallToAction.razor *@

<section class="py-16 @GetBackgroundClass()">
    <div class="container mx-auto px-4 text-center">
        @if (!string.IsNullOrEmpty(Fields?.Heading))
        {
            <h2 class="text-3xl font-bold mb-4">@Fields.Heading</h2>
        }

        @if (!string.IsNullOrEmpty(Fields?.Description))
        {
            <p class="text-xl mb-8 max-w-2xl mx-auto">@Fields.Description</p>
        }

        @if (Fields?.Button?.Href != null)
        {
            <a href="@Fields.Button.Href"
               class="inline-block px-8 py-4 bg-white text-primary-600 font-semibold rounded-lg">
                @(Fields.Button.Text ?? "Get Started")
            </a>
        }
    </div>
</section>

@code {
    [Parameter] public CallToAction? Fields { get; set; }
    [Parameter] public dynamic? CustomData { get; set; }

    private string GetBackgroundClass() => Fields?.BackgroundColor switch
    {
        "primary" => "bg-primary-500 text-white",
        "secondary" => "bg-secondary-500 text-white",
        _ => "bg-gray-100"
    };
}

3. Register the Component

Add to Components/Shared/AgilityComponent.razor:

case "CallToAction":
    <CallToAction Fields="@GetFields<CallToAction>()" CustomData="@CustomData" />
    break;

4. Add the Import (if in subfolder)

If your component is in a subfolder, add to _Imports.razor:

@using Agility.NET.Blazor.Starter.Components.AgilityComponents.CallToAction

Caching

Server-Side Cache

The starter uses tag-based caching for Agility API responses:

// Cache is automatically managed by AgilityCacheService
var posts = await AgilityService.GetPostsGraphQLAsync("en-us");

Cache settings in appsettings.json:

{
  "AppSettings": {
    "CacheInMinutes": 5
  }
}

CDN Cache Headers

The CacheControlMiddleware adds optimized headers:

Cache-Control: public, max-age=30, stale-while-revalidate=86400

Configure in appsettings.json:

{
  "CacheControl": {
    "MaxAgeSeconds": 30,
    "StaleWhileRevalidateSeconds": 86400,
    "StaleIfErrorSeconds": 86400
  }
}

Webhook Revalidation

Set up webhooks in Agility CMS to invalidate cache on publish:

  1. Go to Settings > Webhooks in Agility CMS
  2. Add webhook: https://your-site.com/api/revalidate

The webhook handler automatically invalidates relevant cache entries based on the content type.

Preview Mode

Preview mode is automatically enabled:

  • Development: Always on
  • Production: Via ?agilitypreviewkey=... parameter

When active:

  • Draft content is fetched
  • Caching is disabled
  • Preview bar is displayed

Tailwind CSS v4

Configuration is in Styles/tailwind.css:

@import "tailwindcss";
@plugin "@tailwindcss/typography";

@theme {
  --color-primary-500: #6415FF;
  --color-secondary-500: #243E63;
}

@source "../Components/**/*.razor";

Tailwind automatically rebuilds during dotnet watch.

Manual rebuild:

npx @tailwindcss/cli -i ./Styles/tailwind.css -o ./wwwroot/css/output.css

Deployment

Deploy to Azure

Click the button to deploy:

Deploy to Azure

Requirements:

  • B1 SKU or higher - WebSockets required for Interactive Server mode
  • WebSockets and AlwaysOn are enabled automatically

Post-Deployment

  1. Configure webhooks in Agility CMS
  2. Set up preview URLs
  3. Configure custom domain (optional)

See Deploy to Azure for detailed instructions.

Troubleshooting

SignalR Connection Issues

  1. Verify WebSockets are enabled in Azure
  2. Check /_blazor/negotiate request in browser DevTools
  3. Ensure AddInteractiveServerComponents() is in Program.cs

403 Errors After Restart

Clear browser cookies or hard refresh (Cmd/Ctrl+Shift+R).

Styles Not Applying

  1. Run npm install
  2. Check @source directive in tailwind.css
  3. Verify output.css is being served

Next Steps