See Agility CMS in action. Watch a product demo
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
AgilityPiccomponent 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:
| Mode | Description | Use Case |
|---|---|---|
| Static SSR | Server renders HTML, no client JavaScript | Most content pages |
| Interactive Server | Real-time updates via SignalR | Forms, 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
| Parameter | Type | Description |
|---|---|---|
Image | ImageAttachment | The Agility image object |
FallbackWidth | int? | Width for the fallback <img> tag |
Alt | string? | Alt text (defaults to Image.Label) |
Class | string? | CSS classes for the <img> element |
Priority | bool | If 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:
- Go to Settings > Webhooks in Agility CMS
- 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:
Requirements:
- B1 SKU or higher - WebSockets required for Interactive Server mode
- WebSockets and AlwaysOn are enabled automatically
Post-Deployment
- Configure webhooks in Agility CMS
- Set up preview URLs
- Configure custom domain (optional)
See Deploy to Azure for detailed instructions.
Troubleshooting
SignalR Connection Issues
- Verify WebSockets are enabled in Azure
- Check
/_blazor/negotiaterequest in browser DevTools - Ensure
AddInteractiveServerComponents()is inProgram.cs
403 Errors After Restart
Clear browser cookies or hard refresh (Cmd/Ctrl+Shift+R).
Styles Not Applying
- Run
npm install - Check
@sourcedirective intailwind.css - Verify
output.cssis being served
Next Steps
- How Components Work - Component development details
- Fetching Content - REST and GraphQL queries
- Configuration - All configuration options
- Deploy to Azure - Production deployment