# Blazor SSR Starter

> Source: https://agilitycms.com/docs/dotNet/blazor-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:

| 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:

```razor
@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)

```razor
@* 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)

```razor
@* 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:

```razor
<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

```html
<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

```csharp
// 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

```razor
@* 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`:

```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`:

```razor
@using Agility.NET.Blazor.Starter.Components.AgilityComponents.CallToAction
```

## Caching

### Server-Side Cache

The starter uses tag-based caching for Agility API responses:

```csharp
// Cache is automatically managed by AgilityCacheService
var posts = await AgilityService.GetPostsGraphQLAsync("en-us");
```

Cache settings in `appsettings.json`:

```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`:

```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`:

```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:
```bash
npx @tailwindcss/cli -i ./Styles/tailwind.css -o ./wwwroot/css/output.css
```

## Deployment

### Deploy to Azure

Click the button to deploy:

[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fagility%2Fagilitycms-dotnet-starter%2Fmain%2FAgility.NET.Blazor.Starter%2Fazuredeploy.json)

**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](/docs/dotNet/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

- [How Components Work](/docs/dotNet/how-components-work) - Component development details
- [Fetching Content](/docs/dotNet/fetching-content) - REST and GraphQL queries
- [Configuration](/docs/dotNet/configuration) - All configuration options
- [Deploy to Azure](/docs/dotNet/deploy-to-azure) - Production deployment
