How Pages Work in .NET

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.

The Sitemap

Every Agility CMS instance has a sitemap that defines:

  • Which pages exist on the site
  • The URL path for each page
  • Which Page Model (layout) each page uses
  • Which components appear on each page

Editors manage the sitemap directly in Agility CMS. Your .NET application reads this sitemap and renders pages accordingly.

Example Sitemap

A typical blog site might have this sitemap:

PageURLPage Model
Home/Main Template
Blog/blogMain Template
Blog Posts/blog/{slug}Main Template
About/aboutMain Template

The "Blog Posts" page is a dynamic page - it generates individual URLs for each blog post (like /blog/my-first-post).

Dynamic Page Routing

Both starters use a catch-all route that intercepts all incoming requests and matches them against the Agility sitemap.

Blazor Implementation

@* 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);
    }
}

MVC Implementation

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;
    }
}

Page Data Structure

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
}

Rendering a Page

The page rendering flow is:

  1. Match URL to sitemap - Find the page definition
  2. Fetch page data - Get zones and modules from Agility API
  3. Select Page Model - Choose the layout based on TemplateName
  4. Render zones - Iterate through each zone in the page
  5. Render components - For each module in a zone, render the matching component

Blazor Example

@* 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>
}

MVC Example

@* Views/PageTemplates/MainTemplate.cshtml *@
@await Html.RenderZoneAsync("MainContentZone")

The RenderZoneAsync helper iterates through modules and invokes the appropriate ViewComponent for each one.

Dynamic Pages

Dynamic pages generate URLs from content items. For example, a "Blog Posts" dynamic page creates individual pages for each blog post.

How Dynamic Pages Work

  1. In Agility CMS, create a dynamic page linked to a content list (e.g., "Posts")
  2. Set the URL pattern (e.g., /blog/{slug})
  3. The sitemap includes entries for each content item:
    • /blog/my-first-post
    • /blog/announcing-new-features
    • /blog/tips-and-tricks

Accessing Dynamic Page Content

When 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>

Caching the Sitemap

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.

Preview Mode

In preview mode, the application fetches draft content instead of published content. This allows editors to preview changes before publishing.

Preview mode is enabled:

  • Automatically in development (ASPNETCORE_ENVIRONMENT=Development)
  • Via query parameter in production (?agilitypreviewkey=...)

When preview mode is active:

  • Draft/staging content is fetched
  • Caching is disabled
  • A preview indicator bar is shown

URL Redirects

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);
}

Multi-Language Support

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.

Next Steps