See Agility CMS in action. Watch a product demo
MVC Starter
The MVC Starter is a traditional ASP.NET Core Razor Pages template for building Agility CMS websites with server-side rendering.
Features
- Razor Pages - Server-rendered pages with C# code-behind
- ViewComponents - Reusable content modules matching Agility components
- Dynamic Routing - Automatic page routing based on Agility sitemap
- GraphQL Support - Query content using Agility's GraphQL API
- URL Redirects - Middleware for Agility-managed redirects
- Tailwind CSS - Utility-first styling with PostCSS
- One-Click Azure Deployment - Pre-configured ARM template
Project Structure
Agility.NET.MVC.Starter/
├── Pages/
│ ├── AgilityPage.cshtml # Dynamic page view
│ ├── AgilityPage.cshtml.cs # Page model with content loading
│ ├── Error.cshtml
│ └── _ViewImports.cshtml
├── ViewComponents/
│ ├── PageModules/ # Content modules
│ │ ├── FeaturedPost.cs
│ │ ├── Heading.cs
│ │ ├── PostDetails.cs
│ │ ├── PostsListing.cs
│ │ ├── RichTextArea.cs
│ │ └── TextBlockWithImage.cs
│ └── Shared/ # Global components
│ ├── PreviewBar.cs
│ ├── SEO.cs
│ ├── SiteFooter.cs
│ └── SiteHeader.cs
├── Views/
│ ├── PageModules/ # Component views
│ │ ├── FeaturedPost.cshtml
│ │ ├── Heading.cshtml
│ │ ├── PostDetails.cshtml
│ │ ├── PostsListing.cshtml
│ │ ├── RichTextArea.cshtml
│ │ └── TextBlockWithImage.cshtml
│ ├── PageTemplates/ # Page Model layouts
│ │ └── MainTemplate.cshtml
│ └── Shared/
│ ├── Components/ # Shared component views
│ └── _Layout.cshtml
├── Models/
│ ├── AgilityModels.cs # Content models
│ ├── ModelExtensions.cs
│ └── ViewComponents/
├── Middleware/
│ ├── AgilityRouteTransformer.cs # Dynamic routing
│ └── AgilityRedirectMiddleware.cs # URL redirects
├── Helpers/
│ ├── PageHelpers.cs
│ ├── PreviewHelper.cs
│ └── TransformerMiddlewareHelpers.cs
├── Util/
│ ├── Constants.cs
│ └── Utility.cs
├── wwwroot/
│ └── css/
│ ├── site.css # Tailwind input
│ └── output.css # Generated CSS
├── appsettings.json # Default configuration
├── azuredeploy.json # Azure ARM template
├── Program.cs # Application host
├── Startup.cs # DI configuration
├── tailwind.config.js # Tailwind configuration
└── postcss.config.js # PostCSS configuration
How It Works
Request Flow
- Request arrives at the application
- AgilityRouteTransformer intercepts the request
- Sitemap lookup finds the matching page
- AgilityPage.cshtml.cs fetches page data from Agility API
- Page Model template is selected based on
TemplateName - ViewComponents render each module in page zones
Route Transformation
The AgilityRouteTransformer converts URLs to Agility pages:
// Middleware/AgilityRouteTransformer.cs
public override async ValueTask<RouteValueDictionary> TransformAsync(
HttpContext httpContext, RouteValueDictionary values)
{
var path = httpContext.Request.Path.Value ?? "/";
// Get cached sitemap
var sitemapPages = await GetSitemapPages(httpContext);
// Find matching page
var page = sitemapPages.FirstOrDefault(p =>
p.Path.Equals(path, StringComparison.OrdinalIgnoreCase));
if (page != null)
{
values["page"] = "/AgilityPage";
values["agilityPage"] = page;
}
return values;
}
Page Rendering
The AgilityPage fetches and renders page content:
// Pages/AgilityPage.cshtml.cs
public class AgilityPageModel : PageModel
{
private readonly FetchApiService _fetchApiService;
public PageResponse? PageResponse { get; set; }
public Dictionary<string, Zone>? ContentZones { get; set; }
public async Task<IActionResult> OnGetAsync()
{
// Get the sitemap page from route data
var sitemapPage = RouteData.Values["agilityPage"] as SitemapPage;
if (sitemapPage == null)
return NotFound();
// Fetch full page data
PageResponse = await _fetchApiService.GetTypedPage(
pageId: sitemapPage.PageID,
locale: sitemapPage.Locale,
contentLinkDepth: 3
);
ContentZones = PageResponse.Zones;
return Page();
}
}
ViewComponents
ViewComponents are the MVC equivalent of Blazor components. Each has a C# class and Razor view.
Creating a ViewComponent
1. Create the C# Class
// ViewComponents/PageModules/CallToAction.cs
public class CallToAction : ViewComponent
{
public IViewComponentResult Invoke(ModuleModel moduleModel)
{
var fields = moduleModel.Model.Fields.ToObject<CallToActionFields>();
return View("/Views/PageModules/CallToAction.cshtml", fields);
}
}
// Model class
public class CallToActionFields
{
public string? Heading { get; set; }
public string? Description { get; set; }
public Link? Button { get; set; }
public string? BackgroundColor { get; set; }
}
2. Create the View
@* Views/PageModules/CallToAction.cshtml *@
@model CallToActionFields
@{
var bgClass = Model?.BackgroundColor switch
{
"primary" => "bg-primary-500 text-white",
"secondary" => "bg-secondary-500 text-white",
_ => "bg-gray-100"
};
}
<section class="py-16 @bgClass">
<div class="container mx-auto px-4 text-center">
@if (!string.IsNullOrEmpty(Model?.Heading))
{
<h2 class="text-3xl font-bold mb-4">@Model.Heading</h2>
}
@if (!string.IsNullOrEmpty(Model?.Description))
{
<p class="text-xl mb-8 max-w-2xl mx-auto">@Model.Description</p>
}
@if (Model?.Button?.Href != null)
{
<a href="@Model.Button.Href"
class="inline-block px-8 py-4 bg-white text-primary-600 font-semibold rounded-lg">
@(Model.Button.Text ?? "Get Started")
</a>
}
</div>
</section>
Components with Data Fetching
For components that need to fetch additional data:
// ViewComponents/PageModules/PostsListing.cs
public class PostsListing : ViewComponent
{
private readonly FetchApiService _fetchApiService;
public PostsListing(FetchApiService fetchApiService)
{
_fetchApiService = fetchApiService;
}
public async Task<IViewComponentResult> InvokeAsync(ModuleModel moduleModel)
{
var isPreview = PreviewHelpers.IsPreviewMode(HttpContext);
// Fetch posts using GraphQL
var posts = await _fetchApiService.GetContentByGraphQL<Post>(
query: @"
query {
posts(take: 10, sort: ""fields.date"", direction: ""desc"") {
contentID
fields {
title
slug
date
excerpt
image {
url
label
}
}
}
}
",
objName: "posts",
locale: moduleModel.Locale,
isPreview: isPreview
);
return View("/Views/PageModules/PostsListing.cshtml", posts);
}
}
Page Model Templates
Page Models map to view files in Views/PageTemplates/:
| Page Model Name | View File |
|---|---|
| Main Template | MainTemplate.cshtml |
| Two Column | TwoColumn.cshtml |
| Blog Post | BlogPost.cshtml |
Main Template Example
@* Views/PageTemplates/MainTemplate.cshtml *@
@model AgilityPageModel
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
@await Component.InvokeAsync("SEO", Model.PageResponse)
<link href="~/css/output.css" rel="stylesheet" />
</head>
<body class="min-h-screen flex flex-col">
@await Component.InvokeAsync("PreviewBar")
@await Component.InvokeAsync("SiteHeader")
<main class="flex-grow">
@* Render the main content zone *@
@await Html.RenderZoneAsync("MainContentZone", Model)
</main>
@await Component.InvokeAsync("SiteFooter")
<script src="~/js/site.js"></script>
</body>
</html>
Zone Rendering Helper
The RenderZoneAsync helper renders all modules in a zone:
// Extension method
public static async Task RenderZoneAsync(
this IHtmlHelper html,
string zoneName,
AgilityPageModel pageModel)
{
var zone = pageModel.ContentZones?.GetValueOrDefault(zoneName);
if (zone?.Modules == null) return;
foreach (var module in zone.Modules)
{
var moduleModel = new ModuleModel
{
Module = module.ModuleName,
Model = module,
Locale = pageModel.Locale,
SitemapPage = pageModel.SitemapPage
};
await html.RenderComponentAsync(
module.ModuleName,
new { moduleModel });
}
}
Images
Use Agility's image API for optimized images:
@if (Model?.Image?.Url != null)
{
<picture>
<source media="(min-width: 1024px)"
srcset="@(Model.Image.Url)?format=auto&w=1200" />
<source media="(min-width: 768px)"
srcset="@(Model.Image.Url)?format=auto&w=800" />
<img src="@(Model.Image.Url)?format=auto&w=400"
alt="@Model.Image.Label"
loading="lazy"
class="w-full h-auto" />
</picture>
}
Preview Mode
Preview mode detection:
// Helpers/PreviewHelper.cs
public static bool IsPreviewMode(HttpContext context)
{
// Always preview in development
if (Environment.IsDevelopment())
return true;
// Check for preview cookie
if (context.Request.Cookies.TryGetValue("agilitypreviewkey", out var cookieKey))
{
return ValidatePreviewKey(cookieKey);
}
// Check for preview query parameter
if (context.Request.Query.TryGetValue("agilitypreviewkey", out var queryKey))
{
return ValidatePreviewKey(queryKey);
}
return false;
}
Tailwind CSS
Configuration in tailwind.config.js:
module.exports = {
content: [
'./Pages/**/*.cshtml',
'./Views/**/*.cshtml',
'./ViewComponents/**/*.cs'
],
theme: {
extend: {
colors: {
primary: {
500: '#6415FF',
600: '#5011CC'
},
secondary: {
500: '#243E63',
600: '#1A2E4A'
}
}
}
},
plugins: [
require('@tailwindcss/typography')
]
}
Development
Run both Tailwind watch and .NET watch:
npm run dev & dotnet watch
Or separately:
# Terminal 1: Tailwind CSS watch
npm run dev
# Terminal 2: .NET watch
dotnet watch
Production Build
npm run build
dotnet publish -c Release
Deployment
Deploy to Azure
Post-Deployment
- Configure webhooks in Agility CMS for cache invalidation
- Set up preview URLs
- Configure custom domain (optional)
See Deploy to Azure for detailed instructions.
Configuration
appsettings.json
{
"AppSettings": {
"InstanceGUID": "",
"SecurityKey": "",
"WebsiteName": "",
"FetchAPIKey": "",
"PreviewAPIKey": "",
"Locales": "en-us",
"ChannelName": "website",
"CacheInMinutes": 5
}
}
Local Development
Create appsettings.local.json (gitignored):
{
"AppSettings": {
"InstanceGUID": "your-guid",
"SecurityKey": "your-key",
"FetchAPIKey": "defaultlive.your-key",
"PreviewAPIKey": "defaultpreview.your-key"
}
}
Comparison with Blazor Starter
| Feature | MVC Starter | Blazor Starter |
|---|---|---|
| Rendering | Server-only | Server + optional SignalR |
| Interactivity | JavaScript required | C# via SignalR |
| Component Model | ViewComponent | Razor Component |
| File Structure | Separate .cs/.cshtml | Single .razor |
| State Management | ViewData/TempData | Component state |
| Learning Curve | Traditional ASP.NET | Modern Blazor |
Choose MVC if:
- You prefer traditional ASP.NET patterns
- Your team is experienced with Razor Pages
- You don't need real-time interactivity
- You want simpler deployment (no WebSockets required)
Choose Blazor if:
- You want modern C# component patterns
- You need interactive features without JavaScript
- You're building a new project with no legacy constraints
- Your team is comfortable with Blazor
Troubleshooting
ViewComponent Not Found
- Verify the class name matches the module name exactly
- Check the class inherits from
ViewComponent - Ensure it has an
InvokeorInvokeAsyncmethod
CSS Not Loading
- Run
npm install - Run
npm run build - Check
output.cssexists inwwwroot/css/
404 on Pages
- Verify sitemap is being fetched (check logs)
- Ensure
ChannelNamematches your Agility channel - Check page is published in Agility CMS
Next Steps
- How Components Work - Component patterns
- Fetching Content - REST and GraphQL
- Configuration - All settings
- Deploy to Azure - Production deployment