# MVC Starter

> Source: https://agilitycms.com/docs/dotNet/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

1. **Request arrives** at the application
2. **AgilityRouteTransformer** intercepts the request
3. **Sitemap lookup** finds the matching page
4. **AgilityPage.cshtml.cs** fetches page data from Agility API
5. **Page Model template** is selected based on `TemplateName`
6. **ViewComponents** render each module in page zones

### Route Transformation

The `AgilityRouteTransformer` converts URLs to Agility pages:

```csharp
// 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:

```csharp
// 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

```csharp
// 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

```razor
@* 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:

```csharp
// 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

```razor
@* 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:

```csharp
// 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:

```razor
@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:

```csharp
// 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`:

```javascript
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:

```bash
npm run dev & dotnet watch
```

Or separately:

```bash
# Terminal 1: Tailwind CSS watch
npm run dev

# Terminal 2: .NET watch
dotnet watch
```

### Production Build

```bash
npm run build
dotnet publish -c Release
```

## Deployment

### Deploy to Azure

[![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.MVC.Starter%2Fazuredeploy.json)

### Post-Deployment

1. Configure webhooks in Agility CMS for cache invalidation
2. Set up preview URLs
3. Configure custom domain (optional)

See [Deploy to Azure](/docs/dotNet/deploy-to-azure) for detailed instructions.

## Configuration

### appsettings.json

```json
{
  "AppSettings": {
    "InstanceGUID": "",
    "SecurityKey": "",
    "WebsiteName": "",
    "FetchAPIKey": "",
    "PreviewAPIKey": "",
    "Locales": "en-us",
    "ChannelName": "website",
    "CacheInMinutes": 5
  }
}
```

### Local Development

Create `appsettings.local.json` (gitignored):

```json
{
  "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

1. Verify the class name matches the module name exactly
2. Check the class inherits from `ViewComponent`
3. Ensure it has an `Invoke` or `InvokeAsync` method

### CSS Not Loading

1. Run `npm install`
2. Run `npm run build`
3. Check `output.css` exists in `wwwroot/css/`

### 404 on Pages

1. Verify sitemap is being fetched (check logs)
2. Ensure `ChannelName` matches your Agility channel
3. Check page is published in Agility CMS

## Next Steps

- [How Components Work](/docs/dotNet/how-components-work) - Component patterns
- [Fetching Content](/docs/dotNet/fetching-content) - REST and GraphQL
- [Configuration](/docs/dotNet/configuration) - All settings
- [Deploy to Azure](/docs/dotNet/deploy-to-azure) - Production deployment
