Introduction
One of Agility CMS's core principles is that editors should have full control over the pages on their website. They shouldn't need a developer to create new pages, change URLs, or reorganize content.
One of Agility CMS's core principles is that editors should have full control over the pages on their website. They shouldn't need a developer to create new pages, change URLs, or reorganize content.
This guide explains how .NET applications integrate with Agility's page management system to enable this editor-driven experience.
Every Agility CMS instance has a sitemap that defines:
Editors manage the sitemap directly in Agility CMS. Your .NET application reads this sitemap and renders pages accordingly.
A typical blog site might have this sitemap:
| Page | URL | Page Model |
|---|---|---|
| Home | / | Main Template |
| Blog | /blog | Main Template |
| Blog Posts | /blog/{slug} | Main Template |
| About | /about | Main Template |
The "Blog Posts" page is a dynamic page - it generates individual URLs for each blog post (like /blog/my-first-post).
Both starters use a catch-all route that intercepts all incoming requests and matches them against the Agility sitemap.
@* Components/Pages/AgilityPage.razor *@
@page "/{*slug}"
@code {
[Parameter] public string? Slug { get; set; }
protected override async Task OnInitializedAsync()
{
// Get the sitemap
var sitemap = await AgilityService.GetSitemapPagesAsync();
// Find the page matching this URL
var page = sitemap.FirstOrDefault(p =>
p.Path.Equals($"/{Slug ?? ""}", StringComparison.OrdinalIgnoreCase));
// Fetch full page data
PageData = await AgilityService.GetPageAsync(page.PageID, locale);
}
}
The MVC starter uses a DynamicRouteValueTransformer for routing:
// Middleware/AgilityRouteTransformer.cs
public class AgilityRouteTransformer : DynamicRouteValueTransformer
{
public override async ValueTask<RouteValueDictionary> TransformAsync(
HttpContext httpContext, RouteValueDictionary values)
{
var path = httpContext.Request.Path.Value ?? "/";
// Get sitemap pages (cached)
var sitemapPages = await GetSitemapPages();
// 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;
}
}
When you fetch a page from Agility, you receive a structured response containing:
public class PageResponse
{
public int PageID { get; set; }
public string Name { get; set; }
public string Path { get; set; }
public string TemplateName { get; set; } // The Page Model name
public Dictionary<string, Zone> Zones { get; set; } // Content zones
public SEO Seo { get; set; } // SEO metadata
}
public class Zone
{
public string Name { get; set; }
public List<Module> Modules { get; set; } // Components in this zone
}
public class Module
{
public string ModuleName { get; set; } // Component type
public dynamic Fields { get; set; } // Component content
}
The page rendering flow is:
TemplateName@* AgilityPage.razor *@
@foreach (var zone in PageData.Zones)
{
<div class="zone zone-@zone.Key.ToLower()">
@foreach (var module in zone.Value.Modules)
{
<AgilityComponent
ComponentName="@module.ModuleName"
Fields="@module.Fields" />
}
</div>
}
@* Views/PageTemplates/MainTemplate.cshtml *@
@await Html.RenderZoneAsync("MainContentZone")
The RenderZoneAsync helper iterates through modules and invokes the appropriate ViewComponent for each one.
Dynamic pages generate URLs from content items. For example, a "Blog Posts" dynamic page creates individual pages for each blog post.
/blog/{slug})/blog/my-first-post/blog/announcing-new-features/blog/tips-and-tricksWhen rendering a dynamic page, you have access to the linked content item:
// The dynamic page includes the content item
var post = page.DynamicPageItem; // The specific blog post
// Use it in your component
<h1>@post.Fields.Title</h1>
<div>@((MarkupString)post.Fields.Content)</div>
For performance, both starters cache the sitemap in memory:
// Sitemap is cached to avoid API calls on every request
var sitemap = await _cache.GetOrCreateAsync("sitemap", async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
return await _fetchApi.GetSitemapFlat(channelName, locale);
});
The cache is invalidated via webhooks when content is published in Agility CMS.
In preview mode, the application fetches draft content instead of published content. This allows editors to preview changes before publishing.
Preview mode is enabled:
ASPNETCORE_ENVIRONMENT=Development)?agilitypreviewkey=...)When preview mode is active:
Agility CMS supports URL redirects managed by editors. Both starters include middleware to handle these redirects:
// Middleware/AgilityRedirectMiddleware.cs
public async Task InvokeAsync(HttpContext context)
{
var redirects = await GetRedirects();
var redirect = redirects.FirstOrDefault(r =>
r.OriginUrl.Equals(context.Request.Path, StringComparison.OrdinalIgnoreCase));
if (redirect != null)
{
context.Response.Redirect(redirect.DestinationUrl, permanent: true);
return;
}
await _next(context);
}
Agility CMS supports multi-language sites. Each locale has its own sitemap with localized content.
Configure supported locales in appsettings.json:
{
"AppSettings": {
"Locales": "en-us,fr-ca,es-mx"
}
}
The starters automatically detect the locale from the URL or use the default locale.