Starter
The MVC Starter is a traditional ASP.NET Core Razor Pages template for building Agility CMS websites with server-side rendering.
The MVC Starter is a traditional ASP.NET Core Razor Pages template for building Agility CMS websites with server-side rendering.
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
TemplateNameThe 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;
}
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 are the MVC equivalent of Blazor components. Each has a C# class and Razor view.
// 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; }
}
@* 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>
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 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 |
@* 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>
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 });
}
}
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 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;
}
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')
]
}
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
npm run build
dotnet publish -c Release
See Deploy to Azure for detailed instructions.
{
"AppSettings": {
"InstanceGUID": "",
"SecurityKey": "",
"WebsiteName": "",
"FetchAPIKey": "",
"PreviewAPIKey": "",
"Locales": "en-us",
"ChannelName": "website",
"CacheInMinutes": 5
}
}
Create appsettings.local.json (gitignored):
{
"AppSettings": {
"InstanceGUID": "your-guid",
"SecurityKey": "your-key",
"FetchAPIKey": "defaultlive.your-key",
"PreviewAPIKey": "defaultpreview.your-key"
}
}
| 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:
Choose Blazor if:
ViewComponentInvoke or InvokeAsync methodnpm installnpm run buildoutput.css exists in wwwroot/css/ChannelName matches your Agility channel